fix: polylan sync

This commit is contained in:
2026-09-16 18:42:37 +02:00
parent 9ec9a54649
commit 4392122cfc
6 changed files with 107 additions and 25 deletions
@@ -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:
@@ -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
@@ -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=(
"<h3>Submit your result</h3>"
f'<a href="{_submission_url(scope)}" target="_blank" rel="noopener">'
"<h2>Submit your result</h2>"
f'<a href="{_submission_url(scope)}" target="_blank" rel="noopener" class="btn btn-info">'
"Submit your proof here</a>"
),
)