icpc-api 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- icpc/__init__.py +32 -0
- icpc/api/__init__.py +18 -0
- icpc/api/common.py +97 -0
- icpc/api/contest.py +253 -0
- icpc/api/person.py +139 -0
- icpc/api/public.py +65 -0
- icpc/api/staff.py +83 -0
- icpc/api/team.py +335 -0
- icpc/auth/__init__.py +28 -0
- icpc/auth/cognito.py +142 -0
- icpc/auth/flows.py +314 -0
- icpc/auth/provider.py +29 -0
- icpc/auth/srp.py +185 -0
- icpc/auth/store.py +223 -0
- icpc/auth/tokens.py +86 -0
- icpc/cli/__init__.py +20 -0
- icpc/cli/columns.py +555 -0
- icpc/cli/main.py +1156 -0
- icpc/cli/render.py +160 -0
- icpc/config.py +57 -0
- icpc/errors.py +159 -0
- icpc/facade/__init__.py +6 -0
- icpc/facade/client.py +606 -0
- icpc/facade/domain.py +198 -0
- icpc/models/__init__.py +60 -0
- icpc/models/_generated.py +566 -0
- icpc/models/base.py +41 -0
- icpc/models/blobs.py +81 -0
- icpc/models/common.py +61 -0
- icpc/models/entities.py +522 -0
- icpc/models/enums.py +192 -0
- icpc/models/mixins.py +44 -0
- icpc/py.typed +0 -0
- icpc/search/__init__.py +99 -0
- icpc/search/_generated.py +1814 -0
- icpc/search/dsl.py +124 -0
- icpc/search/endpoint.py +173 -0
- icpc/search/fields.py +59 -0
- icpc/transport/__init__.py +29 -0
- icpc/transport/_shared.py +121 -0
- icpc/transport/async_client.py +120 -0
- icpc/transport/operation.py +139 -0
- icpc/transport/sync_client.py +121 -0
- icpc_api-0.1.0.dist-info/METADATA +143 -0
- icpc_api-0.1.0.dist-info/RECORD +50 -0
- icpc_api-0.1.0.dist-info/WHEEL +5 -0
- icpc_api-0.1.0.dist-info/entry_points.txt +2 -0
- icpc_api-0.1.0.dist-info/licenses/LICENSE +21 -0
- icpc_api-0.1.0.dist-info/licenses/THIRD-PARTY-LICENSES.md +220 -0
- icpc_api-0.1.0.dist-info/top_level.txt +1 -0
icpc/facade/domain.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""The joined object model and the pure function that builds it.
|
|
2
|
+
|
|
3
|
+
Search endpoints hand back four disconnected tables — teams, team members,
|
|
4
|
+
institutions, participants — keyed by ids. :func:`join` stitches them into one
|
|
5
|
+
navigable structure. It touches no network, so it is testable from recorded rows
|
|
6
|
+
alone, and the client is free to fetch the four tables concurrently.
|
|
7
|
+
|
|
8
|
+
:class:`Team` and :class:`Member` *are* their grid rows: they subclass
|
|
9
|
+
:class:`~icpc.models._generated.TeamRow` and
|
|
10
|
+
:class:`~icpc.models._generated.TeamMemberRow`, so every column of a grid is a
|
|
11
|
+
typed attribute on them directly, with the join's own fields alongside.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Iterable, Sequence
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from pydantic import Field
|
|
21
|
+
|
|
22
|
+
from icpc.models._generated import (
|
|
23
|
+
ContestParticipantRow,
|
|
24
|
+
InstitutionRow,
|
|
25
|
+
TeamMemberRow,
|
|
26
|
+
TeamRow,
|
|
27
|
+
)
|
|
28
|
+
from icpc.models.base import Row
|
|
29
|
+
from icpc.models.blobs import TeamMemberBlob
|
|
30
|
+
from icpc.models.common import NamedRef
|
|
31
|
+
from icpc.models.entities import Contest
|
|
32
|
+
from icpc.models.enums import TeamStatus, coach_roles, contestant_roles
|
|
33
|
+
|
|
34
|
+
__all__ = ["ContestView", "Member", "Team", "join"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Member(TeamMemberRow):
|
|
38
|
+
"""One person's membership of one team.
|
|
39
|
+
|
|
40
|
+
Every column of the ``teammember`` grid is an attribute here. When that
|
|
41
|
+
table was not fetched the roster comes from each team row's embedded blob
|
|
42
|
+
instead, and only :attr:`person_id`, :attr:`username`, :attr:`role` and
|
|
43
|
+
:attr:`registration_complete` are populated — the rest are ``None``, as they
|
|
44
|
+
are for any unprojected column.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
#: Whether registration is complete, from whichever source built this
|
|
48
|
+
#: membership. The inherited :attr:`complete_registration` is the grid's own
|
|
49
|
+
#: column, and is ``None`` when the roster came from the blob.
|
|
50
|
+
registration_complete: bool | None = None
|
|
51
|
+
#: The person's contest-wide record, when the participant table was fetched.
|
|
52
|
+
participant: ContestParticipantRow | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Team(TeamRow):
|
|
56
|
+
"""A team with its roster and institution attached.
|
|
57
|
+
|
|
58
|
+
Every column of the ``team`` grid is an attribute here; :attr:`members` and
|
|
59
|
+
:attr:`institution` are what the join adds.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
#: The joined roster. The row's embedded blob is still on
|
|
63
|
+
#: :attr:`~icpc.models.mixins.HasTeamMembers.member_blobs`.
|
|
64
|
+
members: list[Member] = Field(default_factory=list)
|
|
65
|
+
#: The institution row, when that table was fetched.
|
|
66
|
+
institution: InstitutionRow | None = None
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def contestants(self) -> list[Member]:
|
|
70
|
+
"""Members who compete.
|
|
71
|
+
|
|
72
|
+
``STUDENT_COACH`` and ``CONTESTANT_COACH`` appear here *and* in
|
|
73
|
+
:attr:`coaches` — they hold both roles, and the ICPC UI counts them twice.
|
|
74
|
+
"""
|
|
75
|
+
roles = contestant_roles()
|
|
76
|
+
return [m for m in self.members if m.role in roles]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def coaches(self) -> list[Member]:
|
|
80
|
+
"""Members who coach. Overlaps :attr:`contestants`; see there."""
|
|
81
|
+
roles = coach_roles()
|
|
82
|
+
return [m for m in self.members if m.role in roles]
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def other(self) -> list[Member]:
|
|
86
|
+
"""Members who neither compete nor coach: reserves, attendees, staff.
|
|
87
|
+
|
|
88
|
+
The complement of :attr:`contestants` and :attr:`coaches` together, so a
|
|
89
|
+
contestant coach is not in here despite holding two roles.
|
|
90
|
+
"""
|
|
91
|
+
roles = contestant_roles() | coach_roles()
|
|
92
|
+
return [m for m in self.members if m.role not in roles]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(slots=True)
|
|
96
|
+
class ContestView:
|
|
97
|
+
"""Everything fetched about one contest, joined up."""
|
|
98
|
+
|
|
99
|
+
contest: Contest | None = None
|
|
100
|
+
sites: list[NamedRef] = field(default_factory=list)
|
|
101
|
+
teams: list[Team] = field(default_factory=list)
|
|
102
|
+
institutions: dict[int, InstitutionRow] = field(default_factory=dict)
|
|
103
|
+
people: dict[int, ContestParticipantRow] = field(default_factory=dict)
|
|
104
|
+
|
|
105
|
+
def team(self, team_id: int) -> Team | None:
|
|
106
|
+
return next((t for t in self.teams if t.id == team_id), None)
|
|
107
|
+
|
|
108
|
+
def by_site(self) -> dict[str | None, list[Team]]:
|
|
109
|
+
"""Teams grouped by site name."""
|
|
110
|
+
grouped: dict[str | None, list[Team]] = {}
|
|
111
|
+
for team in self.teams:
|
|
112
|
+
grouped.setdefault(team.site, []).append(team)
|
|
113
|
+
return grouped
|
|
114
|
+
|
|
115
|
+
def by_status(self) -> dict[TeamStatus | str | None, list[Team]]:
|
|
116
|
+
grouped: dict[TeamStatus | str | None, list[Team]] = {}
|
|
117
|
+
for team in self.teams:
|
|
118
|
+
grouped.setdefault(team.status, []).append(team)
|
|
119
|
+
return grouped
|
|
120
|
+
|
|
121
|
+
def members(self) -> list[Member]:
|
|
122
|
+
"""Every membership across every team, in team order."""
|
|
123
|
+
return [member for team in self.teams for member in team.members]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _columns(row: Row) -> dict[str, Any]:
|
|
127
|
+
"""A fetched row's fields and its unknown extras, ready to re-seed a subclass.
|
|
128
|
+
|
|
129
|
+
Taken straight off the instance rather than through ``model_dump``: the rows
|
|
130
|
+
are already validated, and a contest is thousands of them.
|
|
131
|
+
"""
|
|
132
|
+
return {**row.__dict__, **(row.__pydantic_extra__ or {})}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _member_from_row(row: TeamMemberRow) -> Member:
|
|
136
|
+
return Member.model_construct(
|
|
137
|
+
**_columns(row),
|
|
138
|
+
registration_complete=row.complete_registration,
|
|
139
|
+
participant=None,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _member_from_blob(blob: TeamMemberBlob) -> Member:
|
|
144
|
+
return Member.model_construct(
|
|
145
|
+
person_id=blob.person_id,
|
|
146
|
+
username=blob.username,
|
|
147
|
+
role=blob.role,
|
|
148
|
+
registration_complete=blob.reg_complete,
|
|
149
|
+
participant=None,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def join(
|
|
154
|
+
teams: Sequence[TeamRow],
|
|
155
|
+
*,
|
|
156
|
+
contest: Contest | None = None,
|
|
157
|
+
sites: Sequence[NamedRef] = (),
|
|
158
|
+
members: Iterable[TeamMemberRow] = (),
|
|
159
|
+
institutions: Iterable[InstitutionRow] = (),
|
|
160
|
+
participants: Iterable[ContestParticipantRow] = (),
|
|
161
|
+
) -> ContestView:
|
|
162
|
+
"""Build a :class:`ContestView` from raw search rows.
|
|
163
|
+
|
|
164
|
+
When ``members`` is empty the roster falls back to each team row's embedded
|
|
165
|
+
``teamMembers`` blob, which is thinner but costs no extra request.
|
|
166
|
+
"""
|
|
167
|
+
by_inst = {row.inst_id: row for row in institutions if row.inst_id is not None}
|
|
168
|
+
by_person = {row.person_id: row for row in participants if row.person_id is not None}
|
|
169
|
+
|
|
170
|
+
rosters: dict[int, list[Member]] = {}
|
|
171
|
+
for row in members:
|
|
172
|
+
if row.team_id is None:
|
|
173
|
+
continue
|
|
174
|
+
rosters.setdefault(row.team_id, []).append(_member_from_row(row))
|
|
175
|
+
|
|
176
|
+
built: list[Team] = []
|
|
177
|
+
for row in teams:
|
|
178
|
+
roster = rosters.get(row.id) if row.id is not None else None
|
|
179
|
+
if roster is None:
|
|
180
|
+
roster = [_member_from_blob(blob) for blob in row.member_blobs]
|
|
181
|
+
for member in roster:
|
|
182
|
+
if member.person_id is not None:
|
|
183
|
+
member.participant = by_person.get(member.person_id)
|
|
184
|
+
built.append(
|
|
185
|
+
Team.model_construct(
|
|
186
|
+
**_columns(row),
|
|
187
|
+
members=roster,
|
|
188
|
+
institution=by_inst.get(row.inst_id) if row.inst_id is not None else None,
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return ContestView(
|
|
193
|
+
contest=contest,
|
|
194
|
+
sites=list(sites),
|
|
195
|
+
teams=built,
|
|
196
|
+
institutions=by_inst,
|
|
197
|
+
people=by_person,
|
|
198
|
+
)
|
icpc/models/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Typed wire models: generated search rows plus hand-written entity DTOs."""
|
|
2
|
+
|
|
3
|
+
from icpc.models._generated import (
|
|
4
|
+
CertificateRow,
|
|
5
|
+
ContestParticipantRow,
|
|
6
|
+
InstitutionRow,
|
|
7
|
+
PromoteRow,
|
|
8
|
+
StaffFundingRow,
|
|
9
|
+
StaffMemberRow,
|
|
10
|
+
StaffRow,
|
|
11
|
+
StaffTshirtRow,
|
|
12
|
+
StandingsRow,
|
|
13
|
+
TeamMemberRow,
|
|
14
|
+
TeamRow,
|
|
15
|
+
TeamSummaryRow,
|
|
16
|
+
Top20Row,
|
|
17
|
+
TransportationMissingRow,
|
|
18
|
+
TshirtRow,
|
|
19
|
+
)
|
|
20
|
+
from icpc.models.base import Row
|
|
21
|
+
from icpc.models.blobs import TeamMemberBlob, parse_extra_fields, parse_members
|
|
22
|
+
from icpc.models.common import FileRef, NamedRef
|
|
23
|
+
from icpc.models.enums import (
|
|
24
|
+
ExportType,
|
|
25
|
+
MemberRole,
|
|
26
|
+
ParticipantRole,
|
|
27
|
+
TeamStatus,
|
|
28
|
+
coach_roles,
|
|
29
|
+
contestant_roles,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"CertificateRow",
|
|
34
|
+
"ContestParticipantRow",
|
|
35
|
+
"ExportType",
|
|
36
|
+
"FileRef",
|
|
37
|
+
"InstitutionRow",
|
|
38
|
+
"MemberRole",
|
|
39
|
+
"NamedRef",
|
|
40
|
+
"ParticipantRole",
|
|
41
|
+
"PromoteRow",
|
|
42
|
+
"Row",
|
|
43
|
+
"StaffFundingRow",
|
|
44
|
+
"StaffMemberRow",
|
|
45
|
+
"StaffRow",
|
|
46
|
+
"StaffTshirtRow",
|
|
47
|
+
"StandingsRow",
|
|
48
|
+
"TeamMemberBlob",
|
|
49
|
+
"TeamMemberRow",
|
|
50
|
+
"TeamRow",
|
|
51
|
+
"TeamStatus",
|
|
52
|
+
"TeamSummaryRow",
|
|
53
|
+
"Top20Row",
|
|
54
|
+
"TransportationMissingRow",
|
|
55
|
+
"TshirtRow",
|
|
56
|
+
"coach_roles",
|
|
57
|
+
"contestant_roles",
|
|
58
|
+
"parse_extra_fields",
|
|
59
|
+
"parse_members",
|
|
60
|
+
]
|