ctfy.sdk.competition.team
client.competition(id)ctfy.sdk.competition.team — my team within one competition.
Captain + member management for the caller's team in a single competition:
rename / leave / kick, plus the outgoing invite codes (team.invites) and
the incoming join-request queue (team.requests).
1"""``client.competition(id).team`` — my team within one competition. 2 3Captain + member management for the caller's team in a single competition: 4rename / leave / kick, plus the outgoing invite codes (``team.invites``) and 5the incoming join-request queue (``team.requests``). 6""" 7 8from __future__ import annotations 9 10from functools import cached_property 11 12from ctfy.sdk._helpers import PagedList, _extract_items, _raise_for_status 13from ctfy.sdk.base import BaseHttpClient 14from ctfy.server.models import ( 15 DirectInviteSent, 16 RiskAppealView, 17 RiskNotice, 18 TeamAffiliationSelection, 19 TeamDetail, 20 TeamInviteInfo, 21 TeamMachineLogin, 22 TeamMachineStatus, 23 TeamPracticeSummary, 24) 25 26 27class CompetitionTeam: 28 """The caller's team in this competition.""" 29 30 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 31 self._http = http 32 self._cid = competition_id 33 34 def rename(self, *, name: str | None = None, description: str | None = None) -> TeamDetail: 35 """Captain-only: rename / re-describe the team.""" 36 body: dict[str, str] = {} 37 if name is not None: 38 body["name"] = name 39 if description is not None: 40 body["description"] = description 41 resp = self._http.request("PATCH", f"/competitions/{self._cid}/team", json=body) 42 _raise_for_status(resp) 43 return TeamDetail.model_validate(resp.json()) 44 45 def affiliation(self) -> TeamAffiliationSelection: 46 """Captain-only: the squad's institution badge and its options. 47 48 Three facts in one answer — what the captain chose, what it 49 resolves to *now*, and every institution a current member 50 publishes. The middle one can be ``None`` while the first is 51 set: that is a choice whose backing has gone, and it reads 52 differently from having chosen nothing. 53 """ 54 resp = self._http.request("GET", f"/competitions/{self._cid}/team/affiliation") 55 _raise_for_status(resp) 56 return TeamAffiliationSelection.model_validate(resp.json()) 57 58 def set_affiliation(self, domain: str) -> TeamAffiliationSelection: 59 """Captain-only: publish ``domain`` beside the squad's name. 60 61 Refused (``409 affiliation_not_published``) unless a current 62 member already publishes that institution on their own profile — 63 a team cannot claim one nobody on it has shown. 64 """ 65 resp = self._http.request( 66 "PUT", f"/competitions/{self._cid}/team/affiliation", json={"domain": domain} 67 ) 68 _raise_for_status(resp) 69 return TeamAffiliationSelection.model_validate(resp.json()) 70 71 def clear_affiliation(self) -> TeamAffiliationSelection: 72 """Captain-only: stop publishing a badge. Never refused, and 73 takes no domain — so it also escapes the state where the choice 74 outlived the member who backed it.""" 75 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/affiliation") 76 _raise_for_status(resp) 77 return TeamAffiliationSelection.model_validate(resp.json()) 78 79 def machine(self) -> TeamMachineStatus: 80 """Whether the team has a machine and whether *you* may open it. 81 82 Any member may ask; the answer carries no host and no username. 83 """ 84 resp = self._http.request("GET", f"/competitions/{self._cid}/team/machine") 85 _raise_for_status(resp) 86 return TeamMachineStatus.model_validate(resp.json()) 87 88 def practice(self) -> TeamPracticeSummary: 89 """What your team answered right in practice here, after a challenge 90 closed (#2183): ``challenge_count`` and each first right answer. 91 Never a solve and never on a board; ``404 team_not_found`` without a 92 team here.""" 93 resp = self._http.request("GET", f"/competitions/{self._cid}/my-team/practice") 94 _raise_for_status(resp) 95 return TeamPracticeSummary.model_validate(resp.json()) 96 97 def view_machine(self) -> TeamMachineLogin: 98 """Captain-only: open the team's machine login (``403 not_captain`` 99 for anyone else, ``404 machine_not_assigned`` when there is none). 100 101 Each call writes an audit row naming you. The login is a secret: 102 do not log or store the result. 103 """ 104 resp = self._http.request("POST", f"/competitions/{self._cid}/team/machine/view") 105 _raise_for_status(resp) 106 return TeamMachineLogin.model_validate(resp.json()) 107 108 def risk_notices(self) -> list[RiskNotice]: 109 """What the organisers did about your team here, newest first (#2068), 110 with whether you may appeal each. Empty for a team nothing was done to.""" 111 resp = self._http.request("GET", f"/competitions/{self._cid}/risk/notices") 112 _raise_for_status(resp) 113 return [RiskNotice.model_validate(row) for row in resp.json()] 114 115 def appeal(self, action_id: str, body: str) -> RiskAppealView: 116 """Captain-only: appeal an action. One appeal per action, within seven 117 days of it or until the report locks (whichever is earlier). **Plain 118 text only**, at most 2000 characters: no Markdown, no HTML, no 119 attachments, and URLs are not made into links.""" 120 resp = self._http.request( 121 "POST", 122 f"/competitions/{self._cid}/risk/actions/{action_id}/appeal", 123 json={"body": body}, 124 ) 125 _raise_for_status(resp) 126 return RiskAppealView.model_validate(resp.json()) 127 128 def supplement_appeal(self, action_id: str, body: str) -> RiskAppealView: 129 """Captain-only: add the one supplement to your appeal, on the same rules.""" 130 resp = self._http.request( 131 "POST", 132 f"/competitions/{self._cid}/risk/actions/{action_id}/appeal/supplement", 133 json={"body": body}, 134 ) 135 _raise_for_status(resp) 136 return RiskAppealView.model_validate(resp.json()) 137 138 def leave(self) -> None: 139 """Drop your membership for this competition. If captain and others 140 remain, captaincy transfers to the longest-tenured member; if you 141 were the lone member the team is deleted.""" 142 resp = self._http.request("POST", f"/competitions/{self._cid}/team/leave") 143 _raise_for_status(resp) 144 145 def kick(self, user_id: str) -> None: 146 """Captain-only: remove ``user_id`` from the team. Self-kick is 147 rejected (use :meth:`leave` instead).""" 148 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/members/{user_id}") 149 _raise_for_status(resp) 150 151 @cached_property 152 def invites(self) -> CompetitionInvites: 153 """Invite codes + direct email invites the captain hands out.""" 154 return CompetitionInvites(self._http, self._cid) 155 156 @cached_property 157 def requests(self) -> CompetitionRequests: 158 """Pending join requests the captain approves / rejects.""" 159 return CompetitionRequests(self._http, self._cid) 160 161 162class CompetitionInvites: 163 """Captain-only: the team's outgoing invites for this competition.""" 164 165 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 166 self._http = http 167 self._cid = competition_id 168 169 def create(self, *, max_uses: int = 1, ttl_seconds: int = 24 * 3600) -> TeamInviteInfo: 170 """Mint an invite code scoped to this comp. ``max_uses=0`` is 171 unlimited; ``ttl_seconds=0`` never expires. Others redeem it via 172 ``client.competition(id).register(mode="join", code=…)``.""" 173 resp = self._http.request( 174 "POST", 175 f"/competitions/{self._cid}/invites", 176 json={"max_uses": max_uses, "ttl_seconds": ttl_seconds}, 177 ) 178 _raise_for_status(resp) 179 return TeamInviteInfo.model_validate(resp.json()) 180 181 def list(self) -> PagedList[TeamInviteInfo]: 182 """All outstanding invites for the caller's team in this comp.""" 183 resp = self._http.request("GET", f"/competitions/{self._cid}/invites") 184 _raise_for_status(resp) 185 return _extract_items(resp.json(), TeamInviteInfo) 186 187 def revoke(self, invite_id: str) -> None: 188 """Invalidate a previously-minted invite.""" 189 resp = self._http.request("DELETE", f"/competitions/{self._cid}/invites/{invite_id}") 190 _raise_for_status(resp) 191 192 def send(self, email: str) -> DirectInviteSent: 193 """Invite one exact email address. The lookup is case-insensitive 194 but otherwise unforgiving (no fuzzy match). 195 196 ⚠️ The answer is the same whether or not the address has an account, 197 is already on a team, or was already invited (#2863): it says the 198 invite was sent *if the address can receive one*. The recipient 199 sees it in their inbox and accepts via 200 :meth:`Competition.accept_invite`.""" 201 resp = self._http.request( 202 "POST", 203 f"/competitions/{self._cid}/invites/direct", 204 json={"email": email}, 205 ) 206 _raise_for_status(resp) 207 return DirectInviteSent.model_validate(resp.json()) 208 209 210class CompetitionRequests: 211 """Captain-only: incoming join requests for this competition.""" 212 213 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 214 self._http = http 215 self._cid = competition_id 216 217 def approve(self, invite_id: str) -> TeamDetail: 218 """Approve a pending join request → the requester joins the team.""" 219 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/approve") 220 _raise_for_status(resp) 221 return TeamDetail.model_validate(resp.json()) 222 223 def reject(self, invite_id: str) -> None: 224 """Reject a pending join request → invite revoked. The requester 225 can re-open a fresh request afterwards.""" 226 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/reject") 227 _raise_for_status(resp)
28class CompetitionTeam: 29 """The caller's team in this competition.""" 30 31 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 32 self._http = http 33 self._cid = competition_id 34 35 def rename(self, *, name: str | None = None, description: str | None = None) -> TeamDetail: 36 """Captain-only: rename / re-describe the team.""" 37 body: dict[str, str] = {} 38 if name is not None: 39 body["name"] = name 40 if description is not None: 41 body["description"] = description 42 resp = self._http.request("PATCH", f"/competitions/{self._cid}/team", json=body) 43 _raise_for_status(resp) 44 return TeamDetail.model_validate(resp.json()) 45 46 def affiliation(self) -> TeamAffiliationSelection: 47 """Captain-only: the squad's institution badge and its options. 48 49 Three facts in one answer — what the captain chose, what it 50 resolves to *now*, and every institution a current member 51 publishes. The middle one can be ``None`` while the first is 52 set: that is a choice whose backing has gone, and it reads 53 differently from having chosen nothing. 54 """ 55 resp = self._http.request("GET", f"/competitions/{self._cid}/team/affiliation") 56 _raise_for_status(resp) 57 return TeamAffiliationSelection.model_validate(resp.json()) 58 59 def set_affiliation(self, domain: str) -> TeamAffiliationSelection: 60 """Captain-only: publish ``domain`` beside the squad's name. 61 62 Refused (``409 affiliation_not_published``) unless a current 63 member already publishes that institution on their own profile — 64 a team cannot claim one nobody on it has shown. 65 """ 66 resp = self._http.request( 67 "PUT", f"/competitions/{self._cid}/team/affiliation", json={"domain": domain} 68 ) 69 _raise_for_status(resp) 70 return TeamAffiliationSelection.model_validate(resp.json()) 71 72 def clear_affiliation(self) -> TeamAffiliationSelection: 73 """Captain-only: stop publishing a badge. Never refused, and 74 takes no domain — so it also escapes the state where the choice 75 outlived the member who backed it.""" 76 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/affiliation") 77 _raise_for_status(resp) 78 return TeamAffiliationSelection.model_validate(resp.json()) 79 80 def machine(self) -> TeamMachineStatus: 81 """Whether the team has a machine and whether *you* may open it. 82 83 Any member may ask; the answer carries no host and no username. 84 """ 85 resp = self._http.request("GET", f"/competitions/{self._cid}/team/machine") 86 _raise_for_status(resp) 87 return TeamMachineStatus.model_validate(resp.json()) 88 89 def practice(self) -> TeamPracticeSummary: 90 """What your team answered right in practice here, after a challenge 91 closed (#2183): ``challenge_count`` and each first right answer. 92 Never a solve and never on a board; ``404 team_not_found`` without a 93 team here.""" 94 resp = self._http.request("GET", f"/competitions/{self._cid}/my-team/practice") 95 _raise_for_status(resp) 96 return TeamPracticeSummary.model_validate(resp.json()) 97 98 def view_machine(self) -> TeamMachineLogin: 99 """Captain-only: open the team's machine login (``403 not_captain`` 100 for anyone else, ``404 machine_not_assigned`` when there is none). 101 102 Each call writes an audit row naming you. The login is a secret: 103 do not log or store the result. 104 """ 105 resp = self._http.request("POST", f"/competitions/{self._cid}/team/machine/view") 106 _raise_for_status(resp) 107 return TeamMachineLogin.model_validate(resp.json()) 108 109 def risk_notices(self) -> list[RiskNotice]: 110 """What the organisers did about your team here, newest first (#2068), 111 with whether you may appeal each. Empty for a team nothing was done to.""" 112 resp = self._http.request("GET", f"/competitions/{self._cid}/risk/notices") 113 _raise_for_status(resp) 114 return [RiskNotice.model_validate(row) for row in resp.json()] 115 116 def appeal(self, action_id: str, body: str) -> RiskAppealView: 117 """Captain-only: appeal an action. One appeal per action, within seven 118 days of it or until the report locks (whichever is earlier). **Plain 119 text only**, at most 2000 characters: no Markdown, no HTML, no 120 attachments, and URLs are not made into links.""" 121 resp = self._http.request( 122 "POST", 123 f"/competitions/{self._cid}/risk/actions/{action_id}/appeal", 124 json={"body": body}, 125 ) 126 _raise_for_status(resp) 127 return RiskAppealView.model_validate(resp.json()) 128 129 def supplement_appeal(self, action_id: str, body: str) -> RiskAppealView: 130 """Captain-only: add the one supplement to your appeal, on the same rules.""" 131 resp = self._http.request( 132 "POST", 133 f"/competitions/{self._cid}/risk/actions/{action_id}/appeal/supplement", 134 json={"body": body}, 135 ) 136 _raise_for_status(resp) 137 return RiskAppealView.model_validate(resp.json()) 138 139 def leave(self) -> None: 140 """Drop your membership for this competition. If captain and others 141 remain, captaincy transfers to the longest-tenured member; if you 142 were the lone member the team is deleted.""" 143 resp = self._http.request("POST", f"/competitions/{self._cid}/team/leave") 144 _raise_for_status(resp) 145 146 def kick(self, user_id: str) -> None: 147 """Captain-only: remove ``user_id`` from the team. Self-kick is 148 rejected (use :meth:`leave` instead).""" 149 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/members/{user_id}") 150 _raise_for_status(resp) 151 152 @cached_property 153 def invites(self) -> CompetitionInvites: 154 """Invite codes + direct email invites the captain hands out.""" 155 return CompetitionInvites(self._http, self._cid) 156 157 @cached_property 158 def requests(self) -> CompetitionRequests: 159 """Pending join requests the captain approves / rejects.""" 160 return CompetitionRequests(self._http, self._cid)
The caller's team in this competition.
35 def rename(self, *, name: str | None = None, description: str | None = None) -> TeamDetail: 36 """Captain-only: rename / re-describe the team.""" 37 body: dict[str, str] = {} 38 if name is not None: 39 body["name"] = name 40 if description is not None: 41 body["description"] = description 42 resp = self._http.request("PATCH", f"/competitions/{self._cid}/team", json=body) 43 _raise_for_status(resp) 44 return TeamDetail.model_validate(resp.json())
Captain-only: rename / re-describe the team.
46 def affiliation(self) -> TeamAffiliationSelection: 47 """Captain-only: the squad's institution badge and its options. 48 49 Three facts in one answer — what the captain chose, what it 50 resolves to *now*, and every institution a current member 51 publishes. The middle one can be ``None`` while the first is 52 set: that is a choice whose backing has gone, and it reads 53 differently from having chosen nothing. 54 """ 55 resp = self._http.request("GET", f"/competitions/{self._cid}/team/affiliation") 56 _raise_for_status(resp) 57 return TeamAffiliationSelection.model_validate(resp.json())
Captain-only: the squad's institution badge and its options.
Three facts in one answer — what the captain chose, what it
resolves to now, and every institution a current member
publishes. The middle one can be None while the first is
set: that is a choice whose backing has gone, and it reads
differently from having chosen nothing.
59 def set_affiliation(self, domain: str) -> TeamAffiliationSelection: 60 """Captain-only: publish ``domain`` beside the squad's name. 61 62 Refused (``409 affiliation_not_published``) unless a current 63 member already publishes that institution on their own profile — 64 a team cannot claim one nobody on it has shown. 65 """ 66 resp = self._http.request( 67 "PUT", f"/competitions/{self._cid}/team/affiliation", json={"domain": domain} 68 ) 69 _raise_for_status(resp) 70 return TeamAffiliationSelection.model_validate(resp.json())
Captain-only: publish domain beside the squad's name.
Refused (409 affiliation_not_published) unless a current
member already publishes that institution on their own profile —
a team cannot claim one nobody on it has shown.
72 def clear_affiliation(self) -> TeamAffiliationSelection: 73 """Captain-only: stop publishing a badge. Never refused, and 74 takes no domain — so it also escapes the state where the choice 75 outlived the member who backed it.""" 76 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/affiliation") 77 _raise_for_status(resp) 78 return TeamAffiliationSelection.model_validate(resp.json())
Captain-only: stop publishing a badge. Never refused, and takes no domain — so it also escapes the state where the choice outlived the member who backed it.
80 def machine(self) -> TeamMachineStatus: 81 """Whether the team has a machine and whether *you* may open it. 82 83 Any member may ask; the answer carries no host and no username. 84 """ 85 resp = self._http.request("GET", f"/competitions/{self._cid}/team/machine") 86 _raise_for_status(resp) 87 return TeamMachineStatus.model_validate(resp.json())
Whether the team has a machine and whether you may open it.
Any member may ask; the answer carries no host and no username.
89 def practice(self) -> TeamPracticeSummary: 90 """What your team answered right in practice here, after a challenge 91 closed (#2183): ``challenge_count`` and each first right answer. 92 Never a solve and never on a board; ``404 team_not_found`` without a 93 team here.""" 94 resp = self._http.request("GET", f"/competitions/{self._cid}/my-team/practice") 95 _raise_for_status(resp) 96 return TeamPracticeSummary.model_validate(resp.json())
What your team answered right in practice here, after a challenge
closed (#2183): challenge_count and each first right answer.
Never a solve and never on a board; 404 team_not_found without a
team here.
98 def view_machine(self) -> TeamMachineLogin: 99 """Captain-only: open the team's machine login (``403 not_captain`` 100 for anyone else, ``404 machine_not_assigned`` when there is none). 101 102 Each call writes an audit row naming you. The login is a secret: 103 do not log or store the result. 104 """ 105 resp = self._http.request("POST", f"/competitions/{self._cid}/team/machine/view") 106 _raise_for_status(resp) 107 return TeamMachineLogin.model_validate(resp.json())
Captain-only: open the team's machine login (403 not_captain
for anyone else, 404 machine_not_assigned when there is none).
Each call writes an audit row naming you. The login is a secret: do not log or store the result.
109 def risk_notices(self) -> list[RiskNotice]: 110 """What the organisers did about your team here, newest first (#2068), 111 with whether you may appeal each. Empty for a team nothing was done to.""" 112 resp = self._http.request("GET", f"/competitions/{self._cid}/risk/notices") 113 _raise_for_status(resp) 114 return [RiskNotice.model_validate(row) for row in resp.json()]
What the organisers did about your team here, newest first (#2068), with whether you may appeal each. Empty for a team nothing was done to.
116 def appeal(self, action_id: str, body: str) -> RiskAppealView: 117 """Captain-only: appeal an action. One appeal per action, within seven 118 days of it or until the report locks (whichever is earlier). **Plain 119 text only**, at most 2000 characters: no Markdown, no HTML, no 120 attachments, and URLs are not made into links.""" 121 resp = self._http.request( 122 "POST", 123 f"/competitions/{self._cid}/risk/actions/{action_id}/appeal", 124 json={"body": body}, 125 ) 126 _raise_for_status(resp) 127 return RiskAppealView.model_validate(resp.json())
Captain-only: appeal an action. One appeal per action, within seven days of it or until the report locks (whichever is earlier). Plain text only, at most 2000 characters: no Markdown, no HTML, no attachments, and URLs are not made into links.
129 def supplement_appeal(self, action_id: str, body: str) -> RiskAppealView: 130 """Captain-only: add the one supplement to your appeal, on the same rules.""" 131 resp = self._http.request( 132 "POST", 133 f"/competitions/{self._cid}/risk/actions/{action_id}/appeal/supplement", 134 json={"body": body}, 135 ) 136 _raise_for_status(resp) 137 return RiskAppealView.model_validate(resp.json())
Captain-only: add the one supplement to your appeal, on the same rules.
139 def leave(self) -> None: 140 """Drop your membership for this competition. If captain and others 141 remain, captaincy transfers to the longest-tenured member; if you 142 were the lone member the team is deleted.""" 143 resp = self._http.request("POST", f"/competitions/{self._cid}/team/leave") 144 _raise_for_status(resp)
Drop your membership for this competition. If captain and others remain, captaincy transfers to the longest-tenured member; if you were the lone member the team is deleted.
146 def kick(self, user_id: str) -> None: 147 """Captain-only: remove ``user_id`` from the team. Self-kick is 148 rejected (use :meth:`leave` instead).""" 149 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/members/{user_id}") 150 _raise_for_status(resp)
Captain-only: remove user_id from the team. Self-kick is
rejected (use leave() instead).
152 @cached_property 153 def invites(self) -> CompetitionInvites: 154 """Invite codes + direct email invites the captain hands out.""" 155 return CompetitionInvites(self._http, self._cid)
Invite codes + direct email invites the captain hands out.
157 @cached_property 158 def requests(self) -> CompetitionRequests: 159 """Pending join requests the captain approves / rejects.""" 160 return CompetitionRequests(self._http, self._cid)
Pending join requests the captain approves / rejects.
163class CompetitionInvites: 164 """Captain-only: the team's outgoing invites for this competition.""" 165 166 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 167 self._http = http 168 self._cid = competition_id 169 170 def create(self, *, max_uses: int = 1, ttl_seconds: int = 24 * 3600) -> TeamInviteInfo: 171 """Mint an invite code scoped to this comp. ``max_uses=0`` is 172 unlimited; ``ttl_seconds=0`` never expires. Others redeem it via 173 ``client.competition(id).register(mode="join", code=…)``.""" 174 resp = self._http.request( 175 "POST", 176 f"/competitions/{self._cid}/invites", 177 json={"max_uses": max_uses, "ttl_seconds": ttl_seconds}, 178 ) 179 _raise_for_status(resp) 180 return TeamInviteInfo.model_validate(resp.json()) 181 182 def list(self) -> PagedList[TeamInviteInfo]: 183 """All outstanding invites for the caller's team in this comp.""" 184 resp = self._http.request("GET", f"/competitions/{self._cid}/invites") 185 _raise_for_status(resp) 186 return _extract_items(resp.json(), TeamInviteInfo) 187 188 def revoke(self, invite_id: str) -> None: 189 """Invalidate a previously-minted invite.""" 190 resp = self._http.request("DELETE", f"/competitions/{self._cid}/invites/{invite_id}") 191 _raise_for_status(resp) 192 193 def send(self, email: str) -> DirectInviteSent: 194 """Invite one exact email address. The lookup is case-insensitive 195 but otherwise unforgiving (no fuzzy match). 196 197 ⚠️ The answer is the same whether or not the address has an account, 198 is already on a team, or was already invited (#2863): it says the 199 invite was sent *if the address can receive one*. The recipient 200 sees it in their inbox and accepts via 201 :meth:`Competition.accept_invite`.""" 202 resp = self._http.request( 203 "POST", 204 f"/competitions/{self._cid}/invites/direct", 205 json={"email": email}, 206 ) 207 _raise_for_status(resp) 208 return DirectInviteSent.model_validate(resp.json())
Captain-only: the team's outgoing invites for this competition.
170 def create(self, *, max_uses: int = 1, ttl_seconds: int = 24 * 3600) -> TeamInviteInfo: 171 """Mint an invite code scoped to this comp. ``max_uses=0`` is 172 unlimited; ``ttl_seconds=0`` never expires. Others redeem it via 173 ``client.competition(id).register(mode="join", code=…)``.""" 174 resp = self._http.request( 175 "POST", 176 f"/competitions/{self._cid}/invites", 177 json={"max_uses": max_uses, "ttl_seconds": ttl_seconds}, 178 ) 179 _raise_for_status(resp) 180 return TeamInviteInfo.model_validate(resp.json())
Mint an invite code scoped to this comp. max_uses=0 is
unlimited; ttl_seconds=0 never expires. Others redeem it via
client.competition(id).register(mode="join", code=…).
182 def list(self) -> PagedList[TeamInviteInfo]: 183 """All outstanding invites for the caller's team in this comp.""" 184 resp = self._http.request("GET", f"/competitions/{self._cid}/invites") 185 _raise_for_status(resp) 186 return _extract_items(resp.json(), TeamInviteInfo)
All outstanding invites for the caller's team in this comp.
188 def revoke(self, invite_id: str) -> None: 189 """Invalidate a previously-minted invite.""" 190 resp = self._http.request("DELETE", f"/competitions/{self._cid}/invites/{invite_id}") 191 _raise_for_status(resp)
Invalidate a previously-minted invite.
193 def send(self, email: str) -> DirectInviteSent: 194 """Invite one exact email address. The lookup is case-insensitive 195 but otherwise unforgiving (no fuzzy match). 196 197 ⚠️ The answer is the same whether or not the address has an account, 198 is already on a team, or was already invited (#2863): it says the 199 invite was sent *if the address can receive one*. The recipient 200 sees it in their inbox and accepts via 201 :meth:`Competition.accept_invite`.""" 202 resp = self._http.request( 203 "POST", 204 f"/competitions/{self._cid}/invites/direct", 205 json={"email": email}, 206 ) 207 _raise_for_status(resp) 208 return DirectInviteSent.model_validate(resp.json())
Invite one exact email address. The lookup is case-insensitive but otherwise unforgiving (no fuzzy match).
⚠️ The answer is the same whether or not the address has an account,
is already on a team, or was already invited (#2863): it says the
invite was sent if the address can receive one. The recipient
sees it in their inbox and accepts via
Competition.accept_invite().
211class CompetitionRequests: 212 """Captain-only: incoming join requests for this competition.""" 213 214 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 215 self._http = http 216 self._cid = competition_id 217 218 def approve(self, invite_id: str) -> TeamDetail: 219 """Approve a pending join request → the requester joins the team.""" 220 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/approve") 221 _raise_for_status(resp) 222 return TeamDetail.model_validate(resp.json()) 223 224 def reject(self, invite_id: str) -> None: 225 """Reject a pending join request → invite revoked. The requester 226 can re-open a fresh request afterwards.""" 227 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/reject") 228 _raise_for_status(resp)
Captain-only: incoming join requests for this competition.
218 def approve(self, invite_id: str) -> TeamDetail: 219 """Approve a pending join request → the requester joins the team.""" 220 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/approve") 221 _raise_for_status(resp) 222 return TeamDetail.model_validate(resp.json())
Approve a pending join request → the requester joins the team.
224 def reject(self, invite_id: str) -> None: 225 """Reject a pending join request → invite revoked. The requester 226 can re-open a fresh request afterwards.""" 227 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/reject") 228 _raise_for_status(resp)
Reject a pending join request → invite revoked. The requester can re-open a fresh request afterwards.