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.
Files changed (50) hide show
  1. icpc/__init__.py +32 -0
  2. icpc/api/__init__.py +18 -0
  3. icpc/api/common.py +97 -0
  4. icpc/api/contest.py +253 -0
  5. icpc/api/person.py +139 -0
  6. icpc/api/public.py +65 -0
  7. icpc/api/staff.py +83 -0
  8. icpc/api/team.py +335 -0
  9. icpc/auth/__init__.py +28 -0
  10. icpc/auth/cognito.py +142 -0
  11. icpc/auth/flows.py +314 -0
  12. icpc/auth/provider.py +29 -0
  13. icpc/auth/srp.py +185 -0
  14. icpc/auth/store.py +223 -0
  15. icpc/auth/tokens.py +86 -0
  16. icpc/cli/__init__.py +20 -0
  17. icpc/cli/columns.py +555 -0
  18. icpc/cli/main.py +1156 -0
  19. icpc/cli/render.py +160 -0
  20. icpc/config.py +57 -0
  21. icpc/errors.py +159 -0
  22. icpc/facade/__init__.py +6 -0
  23. icpc/facade/client.py +606 -0
  24. icpc/facade/domain.py +198 -0
  25. icpc/models/__init__.py +60 -0
  26. icpc/models/_generated.py +566 -0
  27. icpc/models/base.py +41 -0
  28. icpc/models/blobs.py +81 -0
  29. icpc/models/common.py +61 -0
  30. icpc/models/entities.py +522 -0
  31. icpc/models/enums.py +192 -0
  32. icpc/models/mixins.py +44 -0
  33. icpc/py.typed +0 -0
  34. icpc/search/__init__.py +99 -0
  35. icpc/search/_generated.py +1814 -0
  36. icpc/search/dsl.py +124 -0
  37. icpc/search/endpoint.py +173 -0
  38. icpc/search/fields.py +59 -0
  39. icpc/transport/__init__.py +29 -0
  40. icpc/transport/_shared.py +121 -0
  41. icpc/transport/async_client.py +120 -0
  42. icpc/transport/operation.py +139 -0
  43. icpc/transport/sync_client.py +121 -0
  44. icpc_api-0.1.0.dist-info/METADATA +143 -0
  45. icpc_api-0.1.0.dist-info/RECORD +50 -0
  46. icpc_api-0.1.0.dist-info/WHEEL +5 -0
  47. icpc_api-0.1.0.dist-info/entry_points.txt +2 -0
  48. icpc_api-0.1.0.dist-info/licenses/LICENSE +21 -0
  49. icpc_api-0.1.0.dist-info/licenses/THIRD-PARTY-LICENSES.md +220 -0
  50. 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
+ )
@@ -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
+ ]