diff --git a/polylan_submitter/polylan_submitter/settings.py b/polylan_submitter/polylan_submitter/settings.py index 2998408..8861d84 100644 --- a/polylan_submitter/polylan_submitter/settings.py +++ b/polylan_submitter/polylan_submitter/settings.py @@ -155,7 +155,7 @@ AANDT_API_BASE_URL = os.environ.get("AANDT_API_BASE_URL", "https://polylan.ch") # Base URL of this site, used to build submission links pushed back to the # legacy AAndT match messages (see submissions.services.aandt_sync) -SUBMITTER_BASE_URL = os.environ.get("SUBMITTER_BASE_URL", "http://127.0.0.1:7777") +SUBMITTER_BASE_URL = os.environ.get("SUBMITTER_BASE_URL", "http://localhost:7777") # File Upload Settings MEDIA_URL = "/media/" diff --git a/polylan_submitter/submissions/services/aandt_client/models/match.py b/polylan_submitter/submissions/services/aandt_client/models/match.py index 953ae9c..40fbbd6 100644 --- a/polylan_submitter/submissions/services/aandt_client/models/match.py +++ b/polylan_submitter/submissions/services/aandt_client/models/match.py @@ -31,7 +31,7 @@ class Match: rounds_count (int | Unset): use_points (bool | Unset): points_are_positions (bool | Unset): - multiround_modes (MatchMultiroundModes | Unset): + multiround_modes (MatchMultiroundModes | None | Unset): multiround_modes_display (None | str | Unset): creation_date (str | Unset): Example: 2026-08-01 10:00:00+00:00. playing_date (str | Unset): @@ -49,7 +49,7 @@ class Match: rounds_count: int | Unset = UNSET use_points: bool | Unset = UNSET points_are_positions: bool | Unset = UNSET - multiround_modes: MatchMultiroundModes | Unset = UNSET + multiround_modes: MatchMultiroundModes | None | Unset = UNSET multiround_modes_display: None | str | Unset = UNSET creation_date: str | Unset = UNSET playing_date: str | Unset = UNSET @@ -78,9 +78,13 @@ class Match: points_are_positions = self.points_are_positions - multiround_modes: str | Unset = UNSET - if not isinstance(self.multiround_modes, Unset): + multiround_modes: None | str | Unset + if isinstance(self.multiround_modes, Unset): + multiround_modes = UNSET + elif isinstance(self.multiround_modes, MatchMultiroundModes): multiround_modes = self.multiround_modes.value + else: + multiround_modes = self.multiround_modes multiround_modes_display: None | str | Unset if isinstance(self.multiround_modes_display, Unset): @@ -172,12 +176,24 @@ class Match: points_are_positions = d.pop("points_are_positions", UNSET) - _multiround_modes = d.pop("multiround_modes", UNSET) - multiround_modes: MatchMultiroundModes | Unset - if isinstance(_multiround_modes, Unset): - multiround_modes = UNSET - else: - multiround_modes = MatchMultiroundModes(_multiround_modes) + def _parse_multiround_modes( + data: object, + ) -> MatchMultiroundModes | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + multiround_modes_type_1 = MatchMultiroundModes(data) + + return multiround_modes_type_1 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(MatchMultiroundModes | None | Unset, data) + + multiround_modes = _parse_multiround_modes(d.pop("multiround_modes", UNSET)) def _parse_multiround_modes_display(data: object) -> None | str | Unset: if data is None: diff --git a/polylan_submitter/submissions/services/aandt_openapi.yaml b/polylan_submitter/submissions/services/aandt_openapi.yaml index ec5e190..46f3df3 100644 --- a/polylan_submitter/submissions/services/aandt_openapi.yaml +++ b/polylan_submitter/submissions/services/aandt_openapi.yaml @@ -125,6 +125,10 @@ components: items: $ref: "#/components/schemas/MatchRoundResult" + MatchMultiroundModes: + type: string + enum: [PA, PS, AP] + Match: type: object properties: @@ -146,9 +150,9 @@ components: points_are_positions: type: boolean multiround_modes: - type: string + allOf: + - $ref: "#/components/schemas/MatchMultiroundModes" nullable: true - enum: [PA, PS, AP] multiround_modes_display: type: string nullable: true diff --git a/polylan_submitter/submissions/services/aandt_sync.py b/polylan_submitter/submissions/services/aandt_sync.py index f76e1cc..26961db 100644 --- a/polylan_submitter/submissions/services/aandt_sync.py +++ b/polylan_submitter/submissions/services/aandt_sync.py @@ -14,11 +14,13 @@ from games.models import EventGame from ..models import Scope from .aandt_client import AuthenticatedClient from .aandt_client.api.default import ( + aandt_api_infos, aandt_api_match_messages_set, aandt_api_matches, aandt_api_members, ) from .aandt_client.models import ( + AandtApiInfosResponse200, AandtApiMatchesResponse200, AandtApiMatchMessagesSetBody, AandtApiMembersResponse200, @@ -33,6 +35,11 @@ from .aandt_client.types import Unset # uvx openapi-python-client generate --path aandt_openapi.yaml --meta none \ # --output-path aandt_client --overwrite +# AAndTInfosStatus value for "matches in progress" (see the / endpoint). Compared +# as a plain string rather than the generated enum, whose member names +# (VALUE_0, VALUE_1, ...) aren't stable across regenerations. +AANDT_STATUS_IN_PROGRESS = "7inprog" + class ScopeSyncError(Exception): """Raised when an EventGame cannot be synced against the AAndT API.""" @@ -43,6 +50,7 @@ class ScopeSyncResult: created: int = 0 updated: int = 0 warnings: list[str] = field(default_factory=list) + status: str | None = None _T = TypeVar("_T") @@ -109,11 +117,16 @@ def sync_event_game_scopes(event_game: EventGame, /) -> ScopeSyncResult: submission page's URL pushed back as the match's player-facing message, so participants browsing the legacy site get a direct link here. + Scopes whose match is no longer returned by the API (deleted, or from a + previous AAndT run reusing the same token) are deactivated. + Args: event_game: The EventGame to sync; must have `apibot_token` set. Returns: - A summary of created/updated scopes and any unresolved participants. + A summary of created/updated scopes and any unresolved participants, + plus the AAndT's current status (see `AANDT_STATUS_IN_PROGRESS`), so + callers can decide whether to keep polling. Raises: ScopeSyncError: When the EventGame has no token, or the API call fails. @@ -124,6 +137,7 @@ def sync_event_game_scopes(event_game: EventGame, /) -> ScopeSyncResult: client = _client(event_game) try: + infos_response = aandt_api_infos.sync_detailed(client=client) members_response = aandt_api_members.sync_detailed(client=client) matches_response = aandt_api_matches.sync_detailed(client=client) except (httpx.HTTPError, ValueError) as exc: @@ -134,32 +148,52 @@ def sync_event_game_scopes(event_game: EventGame, /) -> ScopeSyncResult: # resync this copy and regenerate. raise ScopeSyncError(f"Failed to reach AAndT API: {exc}") from exc + infos_parsed = infos_response.parsed members_parsed = members_response.parsed matches_parsed = matches_response.parsed - if not isinstance(members_parsed, AandtApiMembersResponse200) or not isinstance( - matches_parsed, AandtApiMatchesResponse200 + if ( + not isinstance(infos_parsed, AandtApiInfosResponse200) + or not isinstance(members_parsed, AandtApiMembersResponse200) + or not isinstance(matches_parsed, AandtApiMatchesResponse200) ): raise ScopeSyncError( "AAndT API returned " - f"{members_response.status_code.value}/{matches_response.status_code.value}" + f"{infos_response.status_code.value}/{members_response.status_code.value}" + f"/{matches_response.status_code.value}" ) + aandt_infos = _unwrap(infos_parsed.aandt) + status = _unwrap(aandt_infos.status) if aandt_infos is not None else None + members = _unwrap(members_parsed.members, []) matches = _unwrap(matches_parsed.matches, []) userids_by_member_id = _userids_by_member_id(members) - result = ScopeSyncResult() + result = ScopeSyncResult(status=status.value if status is not None else None) now = timezone.now() + synced_match_ids: set[int] = set() for match in matches: - created, warnings = _sync_match(event_game, client, match, userids_by_member_id, now) + match_id = _unwrap(match.id) + created, warnings = _sync_match( + event_game, client, match, userids_by_member_id, now + ) if created is None: continue + synced_match_ids.add(match_id) result.created += created result.updated += not created result.warnings.extend(warnings) + ( + Scope.objects.filter( + event_game=event_game, is_active=True, external_id__isnull=False + ) + .exclude(external_id__in=synced_match_ids) + .update(is_active=False) + ) + return result @@ -210,11 +244,13 @@ def _sync_match( return created, warnings -def _push_submission_link(client: AuthenticatedClient, match_id: int, scope: Scope) -> list[str]: +def _push_submission_link( + client: AuthenticatedClient, match_id: int, scope: Scope +) -> list[str]: body = AandtApiMatchMessagesSetBody( players=( - "