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
« 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.
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"""
9from typing import Protocol
11from ..domain.models import (
12 Club,
13 Event,
14 EventOverview,
15 Match,
16 Standings,
17 Team,
18)
21class DiscoveryPort(Protocol):
22 """Contract for resolving human queries to events.
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 """
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 ...
38class ECNLAPIPort(Protocol):
39 """Contract for the upstream ECNL data source (AthleteOne API)."""
41 async def get_event(self, event_id: int) -> Event:
42 """Return event metadata (name, location, dates) by event ID.
44 Raises:
45 ECNLNotFoundError: If no event with that ID exists.
46 """
47 ...
49 async def get_event_overview(self, event_id: int) -> EventOverview:
50 """Return the division/flight navigation tree for an event.
52 Raises:
53 ECNLNotFoundError: If no event with that ID exists.
54 """
55 ...
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 ...
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 ...
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 ...
69 async def get_flight_teams(self, flight_id: int) -> list[Team]:
70 """Return the teams competing in a flight."""
71 ...
73 async def get_event_teams(self, event_id: int) -> list[Team]:
74 """Return every team registered for an event."""
75 ...
77 async def get_clubs(self, event_id: int) -> list[Club]:
78 """Return the clubs participating in an event."""
79 ...
81 async def get_match(self, match_token: str) -> dict:
82 """Return raw match-detail / box-score data for a match token.
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 ...
89 async def get_brackets(self, event_id: int, flight_id: int) -> dict:
90 """Return raw playoff-bracket data for a flight.
92 Brackets are tree-shaped and event-specific, so the decoded ``data``
93 object is returned directly.
94 """
95 ...
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.
100 Used by discovery to enumerate the events for a league/gender org
101 without a dedicated event-list endpoint.
102 """
103 ...