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

1"""Wire-format -> domain-model parsers for the AthleteOne API. 

2 

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. 

6 

7The feed is permissive with field presence and casing, so parsers use ``.get`` 

8with sensible fallbacks rather than assuming keys exist. 

9""" 

10 

11from typing import Any 

12 

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) 

23 

24 

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 

33 

34 

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 

38 

39 

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 

46 

47 

48def parse_event(raw: dict[str, Any]) -> Event: 

49 """Parse an event-details ``data`` object into an Event. 

50 

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 ) 

66 

67 

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 ) 

78 

79 

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 ) 

90 

91 

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 ) 

113 

114 

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 ) 

133 

134 

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 ) 

144 

145 

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 )