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