1"""Module defines the main entry point for the Football Fixtures Scraper Actor.
2
3This Actor fetches football fixtures and results using the Football-Data.org API.
4"""
5
6from __future__ import annotations
7
8from datetime import datetime, date
9from apify import Actor
10import httpx
11
12
13async def find_team_id(api_key: str, team_name: str) -> int | None:
14 """Find team ID by searching for team name.
15
16 Args:
17 api_key: Football-Data.org API key
18 team_name: Name of the team to search for
19
20 Returns:
21 Team ID if found, None otherwise
22 """
23 async with httpx.AsyncClient() as client:
24 try:
25
26 Actor.log.info(f'Searching for team: {team_name}')
27
28
29 response = await client.get(
30 'https://api.football-data.org/v4/competitions/PL/teams',
31 headers={'X-Auth-Token': api_key},
32 timeout=30.0
33 )
34
35 if response.status_code == 200:
36 data = response.json()
37 for team in data.get('teams', []):
38 if team_name.lower() in team['name'].lower() or team_name.lower() in team.get('shortName', '').lower():
39 Actor.log.info(f'Found team: {team["name"]} (ID: {team["id"]})')
40 return team['id']
41
42
43 Actor.log.info('Team not found in Premier League, trying broader search...')
44 response = await client.get(
45 f'https://api.football-data.org/v4/teams',
46 headers={'X-Auth-Token': api_key},
47 params={'name': team_name},
48 timeout=30.0
49 )
50
51 if response.status_code == 200:
52 data = response.json()
53 teams = data.get('teams', [])
54 if teams:
55 team = teams[0]
56 Actor.log.info(f'Found team: {team["name"]} (ID: {team["id"]})')
57 return team['id']
58
59 except Exception as e:
60 Actor.log.error(f'Error searching for team: {str(e)}')
61
62 return None
63
64
65async def fetch_team_fixtures(
66 api_key: str,
67 team_id: int,
68 date_from: str | None,
69 date_to: str | None,
70 competition: str | None,
71 max_items: int
72) -> list[dict]:
73 """Fetch fixtures and results for a specific team from Football-Data.org API.
74
75 Args:
76 api_key: Football-Data.org API key
77 team_id: ID of the team
78 date_from: Start date for matches (YYYY-MM-DD)
79 date_to: End date for matches (YYYY-MM-DD)
80 competition: Competition code filter (e.g., 'PL', 'CL')
81 max_items: Maximum number of matches to return
82
83 Returns:
84 List of match dictionaries (both upcoming fixtures and past results)
85 """
86 fixtures = []
87
88 async with httpx.AsyncClient() as client:
89 try:
90 Actor.log.info(f'Fetching matches for team ID {team_id}')
91
92
93 params = {}
94 if date_from:
95 params['dateFrom'] = date_from
96 if date_to:
97 params['dateTo'] = date_to
98 if competition:
99 params['competitions'] = competition
100
101
102 response = await client.get(
103 f'https://api.football-data.org/v4/teams/{team_id}/matches',
104 headers={'X-Auth-Token': api_key},
105 params=params,
106 timeout=30.0
107 )
108
109 if response.status_code == 200:
110 data = response.json()
111 matches = data.get('matches', [])
112 Actor.log.info(f'API returned {len(matches)} matches')
113
114 for match in matches:
115 fixture_data = {
116 'matchDate': match['utcDate'],
117 'homeTeam': match['homeTeam']['name'],
118 'awayTeam': match['awayTeam']['name'],
119 'competition': match['competition']['name'],
120 'venue': match.get('venue', ''),
121 'matchUrl': f"https://www.football-data.org/match/{match['id']}",
122 'status': match['status'],
123 'matchday': match.get('matchday'),
124 'score': match.get('score', {})
125 }
126
127 fixtures.append(fixture_data)
128 Actor.log.info(
129 f"Found match: {fixture_data['homeTeam']} vs {fixture_data['awayTeam']} "
130 f"on {fixture_data['matchDate']} - {fixture_data['status']} ({fixture_data['competition']})"
131 )
132
133
134 if max_items > 0 and len(fixtures) >= max_items:
135 break
136
137 elif response.status_code == 429:
138 Actor.log.error('API rate limit exceeded. Free tier allows 10 requests/minute.')
139 raise Exception('API rate limit exceeded')
140 elif response.status_code == 403:
141 Actor.log.error('Invalid API key or access forbidden')
142 raise Exception('Invalid API key')
143 else:
144 Actor.log.error(f'API request failed with status {response.status_code}')
145 raise Exception(f'API error: {response.status_code}')
146
147 except httpx.TimeoutException:
148 Actor.log.error('API request timed out')
149 raise
150 except Exception as e:
151 Actor.log.error(f'Error fetching fixtures: {str(e)}')
152 raise
153
154 return fixtures
155
156
157async def main() -> None:
158 """Define the main entry point for the Apify Actor.
159
160 This Actor fetches football fixtures and results using the Football-Data.org API.
161 """
162 async with Actor:
163 Actor.log.info('Football Fixtures & Results Scraper starting...')
164
165
166 actor_input = await Actor.get_input() or {}
167 api_key = actor_input.get('apiKey')
168 team_name = actor_input.get('teamName', 'Chelsea FC')
169 competition = actor_input.get('competition')
170 date_from = actor_input.get('dateFrom')
171 date_to = actor_input.get('dateTo', '2026-05-31')
172 max_items = actor_input.get('maxItems', 50)
173
174 Actor.log.info(
175 f'Input: Team={team_name}, Competition={competition or "All"}, '
176 f'DateFrom={date_from or "Today"}, DateTo={date_to}, MaxItems={max_items}'
177 )
178
179
180 if not api_key:
181 raise ValueError(
182 'apiKey is required. Get your free API key at: '
183 'https://www.football-data.org/client/register'
184 )
185 if not team_name:
186 raise ValueError('teamName is required')
187
188
189 if not date_from:
190 date_from = date.today().isoformat()
191
192 try:
193
194 team_id = await find_team_id(api_key, team_name)
195
196 if not team_id:
197 Actor.log.error(f'Could not find team: {team_name}')
198 Actor.log.info('Make sure to use the full team name (e.g., "Chelsea FC" not just "Chelsea")')
199 return
200
201
202 fixtures = await fetch_team_fixtures(
203 api_key=api_key,
204 team_id=team_id,
205 date_from=date_from,
206 date_to=date_to,
207 competition=competition,
208 max_items=max_items
209 )
210
211
212 if fixtures:
213
214 charging_manager = Actor.get_charging_manager()
215 pricing_info = charging_manager.get_pricing_info()
216
217
218 if pricing_info and hasattr(pricing_info, 'max_total_charge_usd'):
219 Actor.log.info(f'User spending limit: ${pricing_info.max_total_charge_usd}')
220
221
222
223 await Actor.push_data(fixtures, 'match')
224 Actor.log.info(f'✓ Saved {len(fixtures)} matches to dataset')
225 Actor.log.info(f'💰 Charged for {len(fixtures)} events')
226 else:
227 Actor.log.warning(f'No matches found for {team_name}')
228
229 except Exception as e:
230 Actor.log.error(f'Actor failed: {str(e)}')
231 raise
232
233 Actor.log.info('Actor finished successfully')