Coverage for src/ecnl/ports/outbound.py: 100%

16 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-28 08:19 +0000

1"""Outbound ports — interfaces the application layer depends on. 

2 

3These are the contracts that driven adapters must satisfy. The application 

4layer only imports these protocols; it never references concrete 

5implementations. This is what makes the hexagonal boundary testable and 

6swappable (real HTTP adapter vs. an in-memory stub). 

7""" 

8 

9from typing import Protocol 

10 

11from ..domain.models import ( 

12 Club, 

13 Event, 

14 EventOverview, 

15 Match, 

16 Standings, 

17 Team, 

18) 

19 

20 

21class DiscoveryPort(Protocol): 

22 """Contract for resolving human queries to events. 

23 

24 There is no public event-list endpoint, so discovery walks the league/gender 

25 org club lists to enumerate events and classifies each by name. 

26 """ 

27 

28 async def find_events( 

29 self, 

30 league: str | None = None, 

31 gender: str | None = None, 

32 season: str | None = None, 

33 ) -> list[Event]: 

34 """Return events matching the given filters (None means "any").""" 

35 ... 

36 

37 

38class ECNLAPIPort(Protocol): 

39 """Contract for the upstream ECNL data source (AthleteOne API).""" 

40 

41 async def get_event(self, event_id: int) -> Event: 

42 """Return event metadata (name, location, dates) by event ID. 

43 

44 Raises: 

45 ECNLNotFoundError: If no event with that ID exists. 

46 """ 

47 ... 

48 

49 async def get_event_overview(self, event_id: int) -> EventOverview: 

50 """Return the division/flight navigation tree for an event. 

51 

52 Raises: 

53 ECNLNotFoundError: If no event with that ID exists. 

54 """ 

55 ... 

56 

57 async def get_standings(self, event_id: int, division_id: int, flight_id: int) -> Standings: 

58 """Return the standings table for a flight.""" 

59 ... 

60 

61 async def get_schedule(self, event_id: int, flight_id: int) -> list[Match]: 

62 """Return all matches for a flight, in source order.""" 

63 ... 

64 

65 async def get_team_schedule(self, event_id: int, team_id: int) -> list[Match]: 

66 """Return all matches for a single team within an event.""" 

67 ... 

68 

69 async def get_flight_teams(self, flight_id: int) -> list[Team]: 

70 """Return the teams competing in a flight.""" 

71 ... 

72 

73 async def get_event_teams(self, event_id: int) -> list[Team]: 

74 """Return every team registered for an event.""" 

75 ... 

76 

77 async def get_clubs(self, event_id: int) -> list[Club]: 

78 """Return the clubs participating in an event.""" 

79 ... 

80 

81 async def get_match(self, match_token: str) -> dict: 

82 """Return raw match-detail / box-score data for a match token. 

83 

84 The detail payload is highly variable, so it is returned as the decoded 

85 JSON ``data`` object rather than a fixed domain model. 

86 """ 

87 ... 

88 

89 async def get_brackets(self, event_id: int, flight_id: int) -> dict: 

90 """Return raw playoff-bracket data for a flight. 

91 

92 Brackets are tree-shaped and event-specific, so the decoded ``data`` 

93 object is returned directly. 

94 """ 

95 ... 

96 

97 async def get_org_club_events(self, org_id: int) -> list[int]: 

98 """Return the distinct active event IDs across an org's clubs. 

99 

100 Used by discovery to enumerate the events for a league/gender org 

101 without a dedicated event-list endpoint. 

102 """ 

103 ...