Coverage for src/ecnl/adapters/outbound/athleteone_parsers.py: 75%
40 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-28 08:19 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-28 08:19 +0000
1"""Wire-format -> domain-model parsers for the AthleteOne API.
3Kept separate from the HTTP adapter so that module stays focused on transport.
4Each ``_parse_*`` function takes one raw JSON object (already unwrapped from the
5``{"result": "success", "data": ...}`` envelope) and returns a domain model.
7The feed is permissive with field presence and casing, so parsers use ``.get``
8with sensible fallbacks rather than assuming keys exist.
9"""
11from typing import Any
13from ...domain.classification import classify_event_name
14from ...domain.models import (
15 Club,
16 Division,
17 Event,
18 Flight,
19 Match,
20 StandingRow,
21 Team,
22)
25def _to_int(value: Any) -> int | None:
26 """Coerce a feed value to int, returning None when it isn't numeric."""
27 if value is None or value == "":
28 return None
29 try:
30 return int(value)
31 except (TypeError, ValueError):
32 return None
35def _int0(value: Any) -> int:
36 """Coerce a feed value to int, defaulting to 0 for missing/non-numeric input."""
37 return _to_int(value) or 0
40def _standing_ppg(raw: dict[str, Any], points: int, played: int) -> float:
41 """Return points-per-game from the feed, or derive it from points/games played."""
42 ppg = raw.get("ppg")
43 if ppg is not None:
44 return float(ppg)
45 return round(points / played, 4) if played else 0.0
48def parse_event(raw: dict[str, Any]) -> Event:
49 """Parse an event-details ``data`` object into an Event.
51 Classifies league/gender/conference/season from the event name.
52 """
53 name = raw.get("name", "")
54 league, gender, conference, season = classify_event_name(name)
55 return Event(
56 event_id=_to_int(raw.get("eventID")) or 0,
57 name=name,
58 league=league,
59 gender=gender,
60 conference=conference,
61 season=season,
62 location=raw.get("location") or raw.get("city"),
63 start_date=raw.get("startDate"),
64 end_date=raw.get("endDate"),
65 )
68def _parse_flight(raw: dict[str, Any], division_name: str) -> Flight:
69 """Parse one flight entry from a division's ``flightList``."""
70 return Flight(
71 flight_id=_to_int(raw.get("flightID")) or 0,
72 division_id=_to_int(raw.get("divisionID")) or 0,
73 division_name=division_name,
74 name=raw.get("flightName", ""),
75 teams_count=_to_int(raw.get("teamsCount")) or 0,
76 has_active_schedule=bool(raw.get("hasActiveSchedule", False)),
77 )
80def parse_division_with_flights(raw: dict[str, Any], gender: str) -> Division:
81 """Parse one entry from ``girlsDivAndFlightList`` / ``boysDivAndFlightList``."""
82 name = raw.get("divisionName", "")
83 flights = [_parse_flight(f, name) for f in raw.get("flightList", [])]
84 return Division(
85 division_id=_to_int(raw.get("divisionID")) or 0,
86 name=name,
87 gender=gender,
88 flights=flights,
89 )
92def parse_standing_row(raw: dict[str, Any]) -> StandingRow:
93 """Parse one team row from a standings ``teamStandings`` list."""
94 wins, losses, draws = _int0(raw.get("wins")), _int0(raw.get("losses")), _int0(raw.get("draws"))
95 points = _to_int(raw.get("standingpoints"))
96 if points is None:
97 points = wins * 3 + draws
98 played = wins + losses + draws
99 return StandingRow(
100 team_id=_int0(raw.get("teamID")),
101 team_name=raw.get("name", ""),
102 wins=wins,
103 losses=losses,
104 draws=draws,
105 points=points,
106 points_per_game=_standing_ppg(raw, points, played),
107 goals_for=_to_int(raw.get("goalsfor")),
108 goals_against=_to_int(raw.get("goalsagainst")),
109 goal_differential=_to_int(raw.get("goaldifferential")),
110 rank=_to_int(raw.get("rank")),
111 club_logo=raw.get("clublogo") or raw.get("clubLogo"),
112 )
115def parse_match(raw: dict[str, Any]) -> Match:
116 """Parse one match entry from a flight schedule."""
117 return Match(
118 match_id=_to_int(raw.get("matchID")) or 0,
119 date=raw.get("gameDate"),
120 time=raw.get("gameTime"),
121 home_team_id=_to_int(raw.get("hometeamID")),
122 home_team=raw.get("homeTeam") or raw.get("hometeam") or "",
123 away_team_id=_to_int(raw.get("awayteamID")),
124 away_team=raw.get("awayTeam") or raw.get("awayteam") or "",
125 home_score=_to_int(raw.get("hometeamscore")),
126 away_score=_to_int(raw.get("awayteamscore")),
127 venue=raw.get("venue"),
128 complex=raw.get("complex"),
129 flight_id=_to_int(raw.get("flightID")),
130 division=raw.get("division"),
131 status=raw.get("type"),
132 )
135def parse_team(raw: dict[str, Any]) -> Team:
136 """Parse one team entry from a flight/event team list."""
137 return Team(
138 team_id=_to_int(raw.get("teamID")) or 0,
139 name=raw.get("name", ""),
140 club_id=_to_int(raw.get("clubID")),
141 club_logo=raw.get("clubLogo"),
142 head_coach=raw.get("headCoach"),
143 )
146def parse_club(raw: dict[str, Any]) -> Club:
147 """Parse one club entry from an org/event club list."""
148 return Club(
149 club_id=_to_int(raw.get("clubID")) or 0,
150 name=raw.get("clubName") or raw.get("clubFullName") or "",
151 city=raw.get("city"),
152 state_code=raw.get("stateCode"),
153 logo=raw.get("clubLogo"),
154 )