ctfy.sdk.client
Platform HTTP client — the player-facing client for the ctfy platform.
This is the client agents and players reach for. It drives the platform's
/api/v1/* REST API with typed Pydantic returns and auto-retry on transient
errors. Operations split into two tiers:
- Global / account namespaces on the client itself —
~PlatformClient.auth,~PlatformClient.me,~PlatformClient.users,~PlatformClient.teams,~PlatformClient.challenges(global catalog),~PlatformClient.instances,~PlatformClient.submissions,~PlatformClient.achievements,~PlatformClient.activities,~PlatformClient.scoreboard,~PlatformClient.nodes, and~PlatformClient.competitions(discovery). A competition scope —
competition()returns a~ctfy.sdk.competition.Competitionhandle for the operations that are inherently competition-scoped (register, your team, the comp's challenges, team search, standings), socompetition_idis named once. Instance lifecycle and answer submission stay flat (keyed byinstance_id)::from ctfy.sdk import PlatformClient client = PlatformClient("http://localhost:8100", token="pf_xxx") comp = client.competition(client.competitions.list(phase="running")[0].id) comp.register(mode="solo") ready = client.instances.start(comp.challenges()[0].id, competition_id=comp.id) result = client.submissions.submit(ready.id, "FLAG{...}") board = comp.scoreboard()
The admin / operator surface is a separate
~ctfy.sdk.admin.AdminClient, reached via admin. A few
server-status / realtime / version helpers stay top-level on the client:
health(), get_meta(), cluster_info(), events(),
check_server_compatibility().
1"""Platform HTTP client — the player-facing client for the ctfy platform. 2 3This is the client agents and players reach for. It drives the platform's 4``/api/v1/*`` REST API with typed Pydantic returns and auto-retry on transient 5errors. Operations split into two tiers: 6 7* **Global / account** namespaces on the client itself — 8 :attr:`~PlatformClient.auth`, :attr:`~PlatformClient.me`, 9 :attr:`~PlatformClient.users`, :attr:`~PlatformClient.teams`, 10 :attr:`~PlatformClient.challenges` (global catalog), 11 :attr:`~PlatformClient.instances`, :attr:`~PlatformClient.submissions`, 12 :attr:`~PlatformClient.achievements`, :attr:`~PlatformClient.activities`, 13 :attr:`~PlatformClient.scoreboard`, :attr:`~PlatformClient.nodes`, and 14 :attr:`~PlatformClient.competitions` (discovery). 15* **A competition scope** — :meth:`competition` returns a 16 :class:`~ctfy.sdk.competition.Competition` handle for the operations that are 17 inherently competition-scoped (register, your team, the comp's challenges, 18 team search, standings), so ``competition_id`` is named once. Instance 19 lifecycle and answer submission stay flat (keyed by ``instance_id``):: 20 21 from ctfy.sdk import PlatformClient 22 23 client = PlatformClient("http://localhost:8100", token="pf_xxx") 24 comp = client.competition(client.competitions.list(phase="running")[0].id) 25 comp.register(mode="solo") 26 ready = client.instances.start(comp.challenges()[0].id, competition_id=comp.id) 27 result = client.submissions.submit(ready.id, "FLAG{...}") 28 board = comp.scoreboard() 29 30The admin / operator surface is a separate 31:class:`~ctfy.sdk.admin.AdminClient`, reached via :attr:`admin`. A few 32server-status / realtime / version helpers stay top-level on the client: 33:meth:`health`, :meth:`get_meta`, :meth:`cluster_info`, :meth:`events`, 34:meth:`check_server_compatibility`. 35""" 36 37from __future__ import annotations 38 39from functools import cached_property 40 41import httpx 42 43from ctfy.core.constants import DEFAULT_CLIENT_TIMEOUT 44from ctfy.core.version import VersionCheck, check_compatibility, package_version 45from ctfy.sdk._helpers import ( 46 InstanceReadyResult, 47 _raise_for_status, 48) 49from ctfy.sdk._helpers import ( 50 _extract_items as _extract_items, 51) 52from ctfy.sdk._helpers import ( 53 _poll_instance_ready as _poll_instance_ready, 54) 55from ctfy.sdk.admin import AdminClient 56from ctfy.sdk.base import BaseHttpClient 57from ctfy.sdk.competition import Competition 58from ctfy.sdk.events import EventStream 59from ctfy.sdk.resources.achievements import AchievementsResource 60from ctfy.sdk.resources.activities import ActivitiesResource 61from ctfy.sdk.resources.auth import AuthResource 62from ctfy.sdk.resources.challenges import ChallengesResource 63from ctfy.sdk.resources.competitions import CompetitionsResource 64from ctfy.sdk.resources.instances import InstancesResource 65from ctfy.sdk.resources.me import MeResource 66from ctfy.sdk.resources.nodes import NodesResource 67from ctfy.sdk.resources.scoreboard import ScoreboardResource 68from ctfy.sdk.resources.submissions import SubmissionsResource 69from ctfy.sdk.resources.teams import TeamsResource 70from ctfy.sdk.resources.users import UsersResource 71from ctfy.server.models import ClusterInfo, HealthResponse, MetaResponse 72 73__all__ = ["InstanceReadyResult", "PlatformClient"] 74 75 76class PlatformClient(BaseHttpClient): 77 """Player-facing HTTP client for the ctfy platform. 78 79 Methods are grouped into resource namespaces (``client.teams.list()``, …) 80 plus a per-competition scope via ``client.competition(id)``. The admin 81 surface is a separate :class:`~ctfy.sdk.admin.AdminClient` reached via 82 :attr:`admin`. Used by the CLI, the SDK, the MCP server, and external callers. 83 """ 84 85 def __init__( 86 self, 87 server_url: str, 88 token: str = "", 89 max_retries: int = 3, 90 timeout: int = DEFAULT_CLIENT_TIMEOUT, 91 ): 92 self._url = server_url.rstrip("/") 93 super().__init__(f"{self._url}/api/v1", token, timeout=timeout, max_retries=max_retries) 94 95 # -- resource namespaces ------------------------------------------------ 96 97 @cached_property 98 def teams(self) -> TeamsResource: 99 """Public team discovery + profile reads.""" 100 return TeamsResource(self) 101 102 @cached_property 103 def users(self) -> UsersResource: 104 """Public per-user profile reads.""" 105 return UsersResource(self) 106 107 @cached_property 108 def me(self) -> MeResource: 109 """The calling user's profile, inbox, account, and progress.""" 110 return MeResource(self) 111 112 @cached_property 113 def auth(self) -> AuthResource: 114 """Password auth, OAuth identities, API tokens, sign-in discovery.""" 115 return AuthResource(self) 116 117 @cached_property 118 def achievements(self) -> AchievementsResource: 119 """Public badge catalog, recent-unlock feed, easter eggs.""" 120 return AchievementsResource(self) 121 122 @cached_property 123 def challenges(self) -> ChallengesResource: 124 """Global challenge catalog, attachments, facets, feedback chips.""" 125 return ChallengesResource(self) 126 127 @cached_property 128 def competitions(self) -> CompetitionsResource: 129 """Discover competitions (``list`` / ``get``). To act within one, use 130 :meth:`competition`.""" 131 return CompetitionsResource(self) 132 133 def competition(self, competition_id: str) -> Competition: 134 """Scope to one competition. The inherently competition-scoped 135 operations — register, your team (captain tooling + invites + 136 join-requests), the comp's challenges, team search, and standings — 137 hang off the returned :class:`~ctfy.sdk.competition.Competition` 138 handle, so ``competition_id`` is named once instead of on every call. 139 (Instance lifecycle and answer submission stay on :attr:`instances` / 140 :attr:`submissions`, keyed by ``instance_id``.)""" 141 return Competition(self, competition_id) 142 143 @cached_property 144 def instances(self) -> InstancesResource: 145 """Launch / control / inspect challenge instances by instance id 146 (the competition is inferred server-side).""" 147 return InstancesResource(self) 148 149 @cached_property 150 def submissions(self) -> SubmissionsResource: 151 """Submit / verify answers + list submissions (by instance id), plus 152 the instance-less QA challenges.""" 153 return SubmissionsResource(self) 154 155 @cached_property 156 def scoreboard(self) -> ScoreboardResource: 157 """Global standings, per-challenge stats, persisted snapshots.""" 158 return ScoreboardResource(self) 159 160 @cached_property 161 def activities(self) -> ActivitiesResource: 162 """Platform activity log + histogram.""" 163 return ActivitiesResource(self) 164 165 @cached_property 166 def nodes(self) -> NodesResource: 167 """Public worker-node list (operator node mgmt is on ``admin.nodes``).""" 168 return NodesResource(self) 169 170 @cached_property 171 def admin(self) -> AdminClient: 172 """Admin / operator surface (separate, role-gated server-side). 173 174 Shares this client's transport. See :class:`~ctfy.sdk.admin.AdminClient`. 175 """ 176 return AdminClient(self) 177 178 # -- top-level convenience: server status / realtime / version ---------- 179 180 def health(self) -> HealthResponse: 181 resp = self.request("GET", "/health") 182 _raise_for_status(resp) 183 return HealthResponse.model_validate(resp.json()) 184 185 def get_meta(self) -> MetaResponse: 186 """Server identity + challenge repo SHA + build version. 187 Admin tokens additionally see cluster capacity + team / solve 188 counts in the same payload.""" 189 resp = self.request("GET", "/meta") 190 _raise_for_status(resp) 191 return MetaResponse.model_validate(resp.json()) 192 193 def cluster_info(self) -> ClusterInfo: 194 """Aggregate worker-node capacity / utilisation (the public 195 capacity banner; no per-node detail).""" 196 resp = self.request("GET", "/cluster-info") 197 _raise_for_status(resp) 198 return ClusterInfo.model_validate(resp.json()) 199 200 def check_server_compatibility(self, *, health: HealthResponse | None = None) -> VersionCheck: 201 """Compare this client's ``ctfy`` version against the server's. 202 203 Uses the cheap unauthenticated ``/health`` probe (pass an 204 already-fetched :class:`HealthResponse` to avoid a second round 205 trip). Pure classification — never prints, never raises on a 206 mismatch, never mutates anything. The caller (CLI, MCP, harness) 207 decides what to do with :class:`~ctfy.core.version.VersionCheck` 208 (typically: print ``.message`` to stderr when not ``.quiet``). 209 210 A server too old to report its version yields 211 :attr:`Compatibility.UNKNOWN` (``health.version == ""``), which 212 is ``.quiet`` — so this stays silent against legacy servers 213 rather than crying wolf. 214 """ 215 h = health if health is not None else self.health() 216 return check_compatibility(package_version(), h.version) 217 218 def events(self, *, auto_reconnect: bool = True) -> EventStream: 219 """Open the platform SSE event stream. 220 221 Yields ``{"event": <name>, "data": <dict>}`` for each frame. 222 Filters: team-scoped events for every team the caller's user is 223 on (across every per-comp competition), plus all global events; 224 admin tokens additionally see admin-only frames. 225 226 Usage:: 227 228 with client.events() as stream: 229 for event in stream: 230 if event["event"] == "solve": 231 ... 232 233 Auto-reconnects on transient network errors with exponential 234 backoff (1s → 30s capped). Set ``auto_reconnect=False`` to make 235 the iterator raise instead. 236 """ 237 token = self._token 238 239 def _factory() -> httpx.Client: 240 # New client per session so a reconnect after a stale 241 # connection drops fresh sockets, not warmed-over ones. 242 return httpx.Client(base_url=self._base_url, timeout=None) 243 244 return EventStream( 245 client_factory=_factory, 246 path="/events", 247 token=token, 248 auto_reconnect=auto_reconnect, 249 )
41@dataclass 42class InstanceReadyResult: 43 """Result of polling an instance until ready.""" 44 45 id: str 46 surface: AttackSurface 47 cert_volume: str = ""
Result of polling an instance until ready.
77class PlatformClient(BaseHttpClient): 78 """Player-facing HTTP client for the ctfy platform. 79 80 Methods are grouped into resource namespaces (``client.teams.list()``, …) 81 plus a per-competition scope via ``client.competition(id)``. The admin 82 surface is a separate :class:`~ctfy.sdk.admin.AdminClient` reached via 83 :attr:`admin`. Used by the CLI, the SDK, the MCP server, and external callers. 84 """ 85 86 def __init__( 87 self, 88 server_url: str, 89 token: str = "", 90 max_retries: int = 3, 91 timeout: int = DEFAULT_CLIENT_TIMEOUT, 92 ): 93 self._url = server_url.rstrip("/") 94 super().__init__(f"{self._url}/api/v1", token, timeout=timeout, max_retries=max_retries) 95 96 # -- resource namespaces ------------------------------------------------ 97 98 @cached_property 99 def teams(self) -> TeamsResource: 100 """Public team discovery + profile reads.""" 101 return TeamsResource(self) 102 103 @cached_property 104 def users(self) -> UsersResource: 105 """Public per-user profile reads.""" 106 return UsersResource(self) 107 108 @cached_property 109 def me(self) -> MeResource: 110 """The calling user's profile, inbox, account, and progress.""" 111 return MeResource(self) 112 113 @cached_property 114 def auth(self) -> AuthResource: 115 """Password auth, OAuth identities, API tokens, sign-in discovery.""" 116 return AuthResource(self) 117 118 @cached_property 119 def achievements(self) -> AchievementsResource: 120 """Public badge catalog, recent-unlock feed, easter eggs.""" 121 return AchievementsResource(self) 122 123 @cached_property 124 def challenges(self) -> ChallengesResource: 125 """Global challenge catalog, attachments, facets, feedback chips.""" 126 return ChallengesResource(self) 127 128 @cached_property 129 def competitions(self) -> CompetitionsResource: 130 """Discover competitions (``list`` / ``get``). To act within one, use 131 :meth:`competition`.""" 132 return CompetitionsResource(self) 133 134 def competition(self, competition_id: str) -> Competition: 135 """Scope to one competition. The inherently competition-scoped 136 operations — register, your team (captain tooling + invites + 137 join-requests), the comp's challenges, team search, and standings — 138 hang off the returned :class:`~ctfy.sdk.competition.Competition` 139 handle, so ``competition_id`` is named once instead of on every call. 140 (Instance lifecycle and answer submission stay on :attr:`instances` / 141 :attr:`submissions`, keyed by ``instance_id``.)""" 142 return Competition(self, competition_id) 143 144 @cached_property 145 def instances(self) -> InstancesResource: 146 """Launch / control / inspect challenge instances by instance id 147 (the competition is inferred server-side).""" 148 return InstancesResource(self) 149 150 @cached_property 151 def submissions(self) -> SubmissionsResource: 152 """Submit / verify answers + list submissions (by instance id), plus 153 the instance-less QA challenges.""" 154 return SubmissionsResource(self) 155 156 @cached_property 157 def scoreboard(self) -> ScoreboardResource: 158 """Global standings, per-challenge stats, persisted snapshots.""" 159 return ScoreboardResource(self) 160 161 @cached_property 162 def activities(self) -> ActivitiesResource: 163 """Platform activity log + histogram.""" 164 return ActivitiesResource(self) 165 166 @cached_property 167 def nodes(self) -> NodesResource: 168 """Public worker-node list (operator node mgmt is on ``admin.nodes``).""" 169 return NodesResource(self) 170 171 @cached_property 172 def admin(self) -> AdminClient: 173 """Admin / operator surface (separate, role-gated server-side). 174 175 Shares this client's transport. See :class:`~ctfy.sdk.admin.AdminClient`. 176 """ 177 return AdminClient(self) 178 179 # -- top-level convenience: server status / realtime / version ---------- 180 181 def health(self) -> HealthResponse: 182 resp = self.request("GET", "/health") 183 _raise_for_status(resp) 184 return HealthResponse.model_validate(resp.json()) 185 186 def get_meta(self) -> MetaResponse: 187 """Server identity + challenge repo SHA + build version. 188 Admin tokens additionally see cluster capacity + team / solve 189 counts in the same payload.""" 190 resp = self.request("GET", "/meta") 191 _raise_for_status(resp) 192 return MetaResponse.model_validate(resp.json()) 193 194 def cluster_info(self) -> ClusterInfo: 195 """Aggregate worker-node capacity / utilisation (the public 196 capacity banner; no per-node detail).""" 197 resp = self.request("GET", "/cluster-info") 198 _raise_for_status(resp) 199 return ClusterInfo.model_validate(resp.json()) 200 201 def check_server_compatibility(self, *, health: HealthResponse | None = None) -> VersionCheck: 202 """Compare this client's ``ctfy`` version against the server's. 203 204 Uses the cheap unauthenticated ``/health`` probe (pass an 205 already-fetched :class:`HealthResponse` to avoid a second round 206 trip). Pure classification — never prints, never raises on a 207 mismatch, never mutates anything. The caller (CLI, MCP, harness) 208 decides what to do with :class:`~ctfy.core.version.VersionCheck` 209 (typically: print ``.message`` to stderr when not ``.quiet``). 210 211 A server too old to report its version yields 212 :attr:`Compatibility.UNKNOWN` (``health.version == ""``), which 213 is ``.quiet`` — so this stays silent against legacy servers 214 rather than crying wolf. 215 """ 216 h = health if health is not None else self.health() 217 return check_compatibility(package_version(), h.version) 218 219 def events(self, *, auto_reconnect: bool = True) -> EventStream: 220 """Open the platform SSE event stream. 221 222 Yields ``{"event": <name>, "data": <dict>}`` for each frame. 223 Filters: team-scoped events for every team the caller's user is 224 on (across every per-comp competition), plus all global events; 225 admin tokens additionally see admin-only frames. 226 227 Usage:: 228 229 with client.events() as stream: 230 for event in stream: 231 if event["event"] == "solve": 232 ... 233 234 Auto-reconnects on transient network errors with exponential 235 backoff (1s → 30s capped). Set ``auto_reconnect=False`` to make 236 the iterator raise instead. 237 """ 238 token = self._token 239 240 def _factory() -> httpx.Client: 241 # New client per session so a reconnect after a stale 242 # connection drops fresh sockets, not warmed-over ones. 243 return httpx.Client(base_url=self._base_url, timeout=None) 244 245 return EventStream( 246 client_factory=_factory, 247 path="/events", 248 token=token, 249 auto_reconnect=auto_reconnect, 250 )
Player-facing HTTP client for the ctfy platform.
Methods are grouped into resource namespaces (client.teams.list(), …)
plus a per-competition scope via client.competition(id). The admin
surface is a separate ~ctfy.sdk.admin.AdminClient reached via
admin. Used by the CLI, the SDK, the MCP server, and external callers.
98 @cached_property 99 def teams(self) -> TeamsResource: 100 """Public team discovery + profile reads.""" 101 return TeamsResource(self)
Public team discovery + profile reads.
103 @cached_property 104 def users(self) -> UsersResource: 105 """Public per-user profile reads.""" 106 return UsersResource(self)
Public per-user profile reads.
108 @cached_property 109 def me(self) -> MeResource: 110 """The calling user's profile, inbox, account, and progress.""" 111 return MeResource(self)
The calling user's profile, inbox, account, and progress.
113 @cached_property 114 def auth(self) -> AuthResource: 115 """Password auth, OAuth identities, API tokens, sign-in discovery.""" 116 return AuthResource(self)
Password auth, OAuth identities, API tokens, sign-in discovery.
118 @cached_property 119 def achievements(self) -> AchievementsResource: 120 """Public badge catalog, recent-unlock feed, easter eggs.""" 121 return AchievementsResource(self)
Public badge catalog, recent-unlock feed, easter eggs.
123 @cached_property 124 def challenges(self) -> ChallengesResource: 125 """Global challenge catalog, attachments, facets, feedback chips.""" 126 return ChallengesResource(self)
Global challenge catalog, attachments, facets, feedback chips.
128 @cached_property 129 def competitions(self) -> CompetitionsResource: 130 """Discover competitions (``list`` / ``get``). To act within one, use 131 :meth:`competition`.""" 132 return CompetitionsResource(self)
Discover competitions (list / get). To act within one, use
competition().
134 def competition(self, competition_id: str) -> Competition: 135 """Scope to one competition. The inherently competition-scoped 136 operations — register, your team (captain tooling + invites + 137 join-requests), the comp's challenges, team search, and standings — 138 hang off the returned :class:`~ctfy.sdk.competition.Competition` 139 handle, so ``competition_id`` is named once instead of on every call. 140 (Instance lifecycle and answer submission stay on :attr:`instances` / 141 :attr:`submissions`, keyed by ``instance_id``.)""" 142 return Competition(self, competition_id)
Scope to one competition. The inherently competition-scoped
operations — register, your team (captain tooling + invites +
join-requests), the comp's challenges, team search, and standings —
hang off the returned ~ctfy.sdk.competition.Competition
handle, so competition_id is named once instead of on every call.
(Instance lifecycle and answer submission stay on instances /
submissions, keyed by instance_id.)
144 @cached_property 145 def instances(self) -> InstancesResource: 146 """Launch / control / inspect challenge instances by instance id 147 (the competition is inferred server-side).""" 148 return InstancesResource(self)
Launch / control / inspect challenge instances by instance id (the competition is inferred server-side).
150 @cached_property 151 def submissions(self) -> SubmissionsResource: 152 """Submit / verify answers + list submissions (by instance id), plus 153 the instance-less QA challenges.""" 154 return SubmissionsResource(self)
Submit / verify answers + list submissions (by instance id), plus the instance-less QA challenges.
156 @cached_property 157 def scoreboard(self) -> ScoreboardResource: 158 """Global standings, per-challenge stats, persisted snapshots.""" 159 return ScoreboardResource(self)
Global standings, per-challenge stats, persisted snapshots.
161 @cached_property 162 def activities(self) -> ActivitiesResource: 163 """Platform activity log + histogram.""" 164 return ActivitiesResource(self)
Platform activity log + histogram.
166 @cached_property 167 def nodes(self) -> NodesResource: 168 """Public worker-node list (operator node mgmt is on ``admin.nodes``).""" 169 return NodesResource(self)
Public worker-node list (operator node mgmt is on admin.nodes).
171 @cached_property 172 def admin(self) -> AdminClient: 173 """Admin / operator surface (separate, role-gated server-side). 174 175 Shares this client's transport. See :class:`~ctfy.sdk.admin.AdminClient`. 176 """ 177 return AdminClient(self)
Admin / operator surface (separate, role-gated server-side).
Shares this client's transport. See ~ctfy.sdk.admin.AdminClient.
186 def get_meta(self) -> MetaResponse: 187 """Server identity + challenge repo SHA + build version. 188 Admin tokens additionally see cluster capacity + team / solve 189 counts in the same payload.""" 190 resp = self.request("GET", "/meta") 191 _raise_for_status(resp) 192 return MetaResponse.model_validate(resp.json())
Server identity + challenge repo SHA + build version. Admin tokens additionally see cluster capacity + team / solve counts in the same payload.
194 def cluster_info(self) -> ClusterInfo: 195 """Aggregate worker-node capacity / utilisation (the public 196 capacity banner; no per-node detail).""" 197 resp = self.request("GET", "/cluster-info") 198 _raise_for_status(resp) 199 return ClusterInfo.model_validate(resp.json())
Aggregate worker-node capacity / utilisation (the public capacity banner; no per-node detail).
201 def check_server_compatibility(self, *, health: HealthResponse | None = None) -> VersionCheck: 202 """Compare this client's ``ctfy`` version against the server's. 203 204 Uses the cheap unauthenticated ``/health`` probe (pass an 205 already-fetched :class:`HealthResponse` to avoid a second round 206 trip). Pure classification — never prints, never raises on a 207 mismatch, never mutates anything. The caller (CLI, MCP, harness) 208 decides what to do with :class:`~ctfy.core.version.VersionCheck` 209 (typically: print ``.message`` to stderr when not ``.quiet``). 210 211 A server too old to report its version yields 212 :attr:`Compatibility.UNKNOWN` (``health.version == ""``), which 213 is ``.quiet`` — so this stays silent against legacy servers 214 rather than crying wolf. 215 """ 216 h = health if health is not None else self.health() 217 return check_compatibility(package_version(), h.version)
Compare this client's ctfy version against the server's.
Uses the cheap unauthenticated /health probe (pass an
already-fetched HealthResponse to avoid a second round
trip). Pure classification — never prints, never raises on a
mismatch, never mutates anything. The caller (CLI, MCP, harness)
decides what to do with ~ctfy.core.version.VersionCheck
(typically: print .message to stderr when not .quiet).
A server too old to report its version yields
Compatibility.UNKNOWN (health.version == ""), which
is .quiet — so this stays silent against legacy servers
rather than crying wolf.
219 def events(self, *, auto_reconnect: bool = True) -> EventStream: 220 """Open the platform SSE event stream. 221 222 Yields ``{"event": <name>, "data": <dict>}`` for each frame. 223 Filters: team-scoped events for every team the caller's user is 224 on (across every per-comp competition), plus all global events; 225 admin tokens additionally see admin-only frames. 226 227 Usage:: 228 229 with client.events() as stream: 230 for event in stream: 231 if event["event"] == "solve": 232 ... 233 234 Auto-reconnects on transient network errors with exponential 235 backoff (1s → 30s capped). Set ``auto_reconnect=False`` to make 236 the iterator raise instead. 237 """ 238 token = self._token 239 240 def _factory() -> httpx.Client: 241 # New client per session so a reconnect after a stale 242 # connection drops fresh sockets, not warmed-over ones. 243 return httpx.Client(base_url=self._base_url, timeout=None) 244 245 return EventStream( 246 client_factory=_factory, 247 path="/events", 248 token=token, 249 auto_reconnect=auto_reconnect, 250 )
Open the platform SSE event stream.
Yields {"event": <name>, "data": <dict>} for each frame.
Filters: team-scoped events for every team the caller's user is
on (across every per-comp competition), plus all global events;
admin tokens additionally see admin-only frames.
Usage::
with client.events() as stream:
for event in stream:
if event["event"] == "solve":
...
Auto-reconnects on transient network errors with exponential
backoff (1s → 30s capped). Set auto_reconnect=False to make
the iterator raise instead.