11"""
22Just for common code shared bettwen our modules.
3- Tempting to put the reddit posting stuff here
4- -- maybe overengineering..
53"""
64
5+ import json
6+ import os
77from datetime import datetime
8- import pytz
8+ from zoneinfo import ZoneInfo
9+ from typing import Dict , List , Tuple , Any
910
1011normalised_team_names = {
1112 "St Patrick's Athl." : "St Patrick's Athletic" ,
1516 "Wexford" : "Wexford FC" ,
1617}
1718
18- match_table_headers = ["Home Team" , "Kickoff " , "Away Team" , "Ground" ]
19+ match_table_headers = ["Home Team" , "Score " , "Away Team" , "Ground" , "Status " ]
1920
2021
2122table_headers = [
3334]
3435
3536
36- def normalise_team_name (team_name ):
37- """Normalise team name."""
37+ CACHE_FILE = "match_cache.json"
38+
39+
40+ def normalise_team_name (team_name : str ) -> str :
41+ """Normalise team name.
42+
43+ Args:
44+ team_name: Team name from API
45+
46+ Returns:
47+ Normalized team name or original if no mapping exists
48+ """
3849 return normalised_team_names .get (team_name , team_name )
3950
4051
41- def ordinal_suffix (day ):
42- """Add suffix to date header. Why is this not built into the Python standard library!?!"""
52+ def ordinal_suffix (day : int ) -> str :
53+ """Add suffix to date header.
54+
55+ Args:
56+ day: Day of month (1-31)
57+
58+ Returns:
59+ Day with ordinal suffix (e.g., "1st", "22nd", "3rd")
60+ """
4361 suffixes = {1 : "st" , 2 : "nd" , 3 : "rd" }
4462 if 4 <= day <= 20 or 24 <= day <= 30 :
4563 return f"{ day } th"
4664 return f"{ day } { suffixes .get (day % 10 , 'th' )} "
4765
4866
49- def get_last_matches (team_id , league_table ):
50- """Return last five match results through emojis for a team."""
67+ def get_last_matches (team_id : int , league_table : List [Dict [str , Any ]]) -> str :
68+ """Return last five match results through emojis for a team.
69+
70+ Args:
71+ team_id: Team ID from API
72+ league_table: League standings data
73+
74+ Returns:
75+ String of emoji representing form (e.g., "✅⚪❌✅✅")
76+ """
5177 last_matches = 5
5278 team_form = next (
53- (team ["form" ] for team in league_table if team ["team" ]["id" ] == team_id ), None
79+ (team ["form" ] for team in league_table
80+ if team ["team" ]["id" ] == team_id ),
81+ None
5482 )
5583 if team_form :
5684 return (
@@ -62,13 +90,221 @@ def get_last_matches(team_id, league_table):
6290 return ""
6391
6492
65- def parse_match_datetime (match_date_str , timezone_str = "Europe/Dublin" ):
66- """
67- Parse the match datetime string and adjust for daylight saving time.
93+ def parse_match_datetime (
94+ match_date_str : str ,
95+ timezone_str : str = "Europe/Dublin" ) -> datetime :
96+ """Parse the match datetime string and adjust for daylight saving time.
97+
98+ Args:
99+ match_date_str: ISO format datetime string from API
100+ timezone_str: Target timezone (default: Europe/Dublin IST)
101+
102+ Returns:
103+ datetime object in the specified timezone
68104 """
69- timezone = pytz . timezone (timezone_str )
105+ timezone = ZoneInfo (timezone_str )
70106
71107 # Parse the match datetime in UTC and convert to the local timezone
72- match_datetime_local = datetime .fromisoformat (match_date_str ).astimezone (timezone )
108+ match_datetime_local = datetime .fromisoformat (
109+ match_date_str
110+ ).astimezone (timezone )
73111
74112 return match_datetime_local
113+
114+
115+ def get_match_status_display (fixture : Dict [str , Any ]) -> Tuple [str , str ]:
116+ """Get display text for match status and score.
117+
118+ Args:
119+ fixture: Fixture dictionary from API-Football
120+
121+ Returns:
122+ Tuple of (score_display, status_display)
123+ Examples: ("2-1", "45'"), ("vs", "19:45"), ("1-0", "FT")
124+ """
125+ # Defensive null-checking for API response structure
126+ status = fixture .get ("fixture" , {}).get ("status" , {}).get ("short" , "NS" )
127+ elapsed = fixture .get ("fixture" , {}).get ("status" , {}).get ("elapsed" )
128+ home_score = fixture .get ("goals" , {}).get ("home" , 0 )
129+ away_score = fixture .get ("goals" , {}).get ("away" , 0 )
130+
131+ # Ensure scores are not None (API can return null during pre-match)
132+ home_score = home_score if home_score is not None else 0
133+ away_score = away_score if away_score is not None else 0
134+
135+ score_display = f"{ home_score } -{ away_score } "
136+
137+ # Get kickoff time with null-safety
138+ fixture_date = fixture .get ("fixture" , {}).get ("date" )
139+ if fixture_date :
140+ kickoff_time = parse_match_datetime (fixture_date ).strftime ("%H:%M" )
141+ else :
142+ kickoff_time = "TBD"
143+
144+ # Pre-match
145+ if status in ["TBD" , "NS" ]:
146+ return "vs" , kickoff_time
147+
148+ # Live match statuses - map status codes to display text
149+ status_map = {
150+ "1H" : f"{ elapsed } '" if elapsed else "1H" ,
151+ "HT" : "HT" ,
152+ "2H" : f"{ elapsed } '" if elapsed else "2H" ,
153+ "ET" : "ET" ,
154+ "P" : "Pens" ,
155+ "FT" : "FT" ,
156+ "AET" : "AET" ,
157+ "PEN" : "Pens" ,
158+ }
159+
160+ if status in status_map :
161+ return score_display , status_map [status ]
162+
163+ # Default fallback
164+ return "vs" , status
165+
166+
167+ def format_live_fixture (fixture : Dict [str , Any ]) -> List [str ]:
168+ """Format a fixture for display in match table with live score.
169+
170+ Args:
171+ fixture: Fixture dictionary from API-Football
172+
173+ Returns:
174+ List: [home_team, score, away_team, venue, status]
175+ """
176+ home_team = normalise_team_name (
177+ fixture .get ("teams" , {}).get ("home" , {}).get ("name" , "Unknown" )
178+ )
179+ away_team = normalise_team_name (
180+ fixture .get ("teams" , {}).get ("away" , {}).get ("name" , "Unknown" )
181+ )
182+ venue = fixture .get ("fixture" , {}).get ("venue" , {}).get ("name" , "TBD" )
183+ score , status = get_match_status_display (fixture )
184+
185+ return [home_team , score , away_team , venue , status ]
186+
187+
188+ def load_cache () -> Dict [str , Any ]:
189+ """Load post metadata cache from JSON file.
190+
191+ Returns:
192+ Dictionary containing cached post metadata for each competition
193+ """
194+ if os .path .exists (CACHE_FILE ):
195+ with open (CACHE_FILE , "r" , encoding = "utf-8" ) as f :
196+ return json .load (f )
197+ return {}
198+
199+
200+ def save_cache (cache_data : Dict [str , Any ]) -> None :
201+ """Save post metadata cache to JSON file.
202+
203+ Args:
204+ cache_data: Dictionary containing post metadata to cache
205+ """
206+ with open (CACHE_FILE , "w" , encoding = "utf-8" ) as f :
207+ json .dump (cache_data , f , indent = 2 )
208+
209+
210+ def extract_scorers (
211+ fixture : Dict [str , Any ]) -> Dict [str , List [Dict [str , Any ]]]:
212+ """Extract goal scorers from fixture events.
213+
214+ Args:
215+ fixture: Fixture dictionary containing events
216+
217+ Returns:
218+ Dictionary with 'home' and 'away' lists of scorer dictionaries.
219+ Each scorer dict contains: {name, minute, penalty, own_goal}
220+ """
221+ scorers = {"home" : [], "away" : []}
222+
223+ if "events" not in fixture :
224+ return scorers
225+
226+ for event in fixture ["events" ]:
227+ if event .get ("type" ) != "Goal" :
228+ continue
229+
230+ scorer_info = {
231+ "name" : event .get ("player" , {}).get ("name" , "Unknown" ),
232+ "minute" : event .get ("time" , {}).get ("elapsed" , 0 ),
233+ "penalty" : event .get ("detail" , "" ) == "Penalty" ,
234+ "own_goal" : event .get ("detail" , "" ) == "Own Goal" ,
235+ }
236+
237+ team = event .get ("team" , {}).get ("name" )
238+ home_team = fixture .get ("teams" , {}).get ("home" , {}).get ("name" )
239+
240+ if team and home_team and team == home_team :
241+ scorers ["home" ].append (scorer_info )
242+ elif team :
243+ scorers ["away" ].append (scorer_info )
244+
245+ return scorers
246+
247+
248+ def format_scorers_inline (fixture : Dict [str , Any ]) -> str :
249+ """Format goal scorers as a compact inline string (fully dynamic).
250+
251+ Returns scorers in format: "Team: Player (min'), Player (min') | Team: ..."
252+ Only includes teams with goals. Returns empty string if no goals.
253+
254+ Precedence: Home team first, then away team (only if both have scored).
255+ If only one team scored, shows that team only.
256+
257+ Args:
258+ fixture: Fixture dictionary
259+
260+ Returns:
261+ Formatted inline string, or empty string if no scorers
262+ """
263+ scorers = extract_scorers (fixture )
264+
265+ home_team = normalise_team_name (
266+ fixture .get ("teams" , {}).get ("home" , {}).get ("name" , "Home" )
267+ )
268+ away_team = normalise_team_name (
269+ fixture .get ("teams" , {}).get ("away" , {}).get ("name" , "Away" )
270+ )
271+
272+ # Only show scorers if goals have been scored
273+ if not scorers ["home" ] and not scorers ["away" ]:
274+ return ""
275+
276+ parts = []
277+
278+ # Helper function to format scorer list
279+ def format_scorer_list (scorer_list : List [Dict [str , Any ]]) -> List [str ]:
280+ """Format a list of scorers."""
281+ formatted = []
282+ for scorer in scorer_list :
283+ minute_str = f"{ scorer ['minute' ]} '"
284+ if scorer ["own_goal" ]:
285+ formatted .append (f"{ scorer ['name' ]} (OG { minute_str } )" )
286+ elif scorer ["penalty" ]:
287+ formatted .append (f"{ scorer ['name' ]} (P { minute_str } )" )
288+ else :
289+ formatted .append (f"{ scorer ['name' ]} ({ minute_str } )" )
290+ return formatted
291+
292+ # Home team scorers (show if any goals) - FIRST precedence
293+ if scorers ["home" ]:
294+ scorer_list = format_scorer_list (scorers ["home" ])
295+ parts .append (f"**{ home_team } :** { ', ' .join (scorer_list )} " )
296+
297+ # Away team scorers (show if any goals) - SECOND precedence
298+ if scorers ["away" ]:
299+ scorer_list = format_scorer_list (scorers ["away" ])
300+ parts .append (f"**{ away_team } :** { ', ' .join (scorer_list )} " )
301+
302+ # Dynamically build result
303+ if len (parts ) == 2 :
304+ # Both teams scored: show with pipe separator
305+ return " | " .join (parts )
306+ if len (parts ) == 1 :
307+ # Only one team scored: show just that team
308+ return parts [0 ]
309+
310+ return ""
0 commit comments