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 TeamAffiliationSelection, TeamDetail, TeamInviteInfo 15 16 17class CompetitionTeam: 18 """The caller's team in this competition.""" 19 20 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 21 self._http = http 22 self._cid = competition_id 23 24 def rename(self, *, name: str | None = None, description: str | None = None) -> TeamDetail: 25 """Captain-only: rename / re-describe the team.""" 26 body: dict[str, str] = {} 27 if name is not None: 28 body["name"] = name 29 if description is not None: 30 body["description"] = description 31 resp = self._http.request("PATCH", f"/competitions/{self._cid}/team", json=body) 32 _raise_for_status(resp) 33 return TeamDetail.model_validate(resp.json()) 34 35 def affiliation(self) -> TeamAffiliationSelection: 36 """Captain-only: the squad's institution badge and its options. 37 38 Three facts in one answer — what the captain chose, what it 39 resolves to *now*, and every institution a current member 40 publishes. The middle one can be ``None`` while the first is 41 set: that is a choice whose backing has gone, and it reads 42 differently from having chosen nothing. 43 """ 44 resp = self._http.request("GET", f"/competitions/{self._cid}/team/affiliation") 45 _raise_for_status(resp) 46 return TeamAffiliationSelection.model_validate(resp.json()) 47 48 def set_affiliation(self, domain: str) -> TeamAffiliationSelection: 49 """Captain-only: publish ``domain`` beside the squad's name. 50 51 Refused (``409 affiliation_not_published``) unless a current 52 member already publishes that institution on their own profile — 53 a team cannot claim one nobody on it has shown. 54 """ 55 resp = self._http.request( 56 "PUT", f"/competitions/{self._cid}/team/affiliation", json={"domain": domain} 57 ) 58 _raise_for_status(resp) 59 return TeamAffiliationSelection.model_validate(resp.json()) 60 61 def clear_affiliation(self) -> TeamAffiliationSelection: 62 """Captain-only: stop publishing a badge. Never refused, and 63 takes no domain — so it also escapes the state where the choice 64 outlived the member who backed it.""" 65 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/affiliation") 66 _raise_for_status(resp) 67 return TeamAffiliationSelection.model_validate(resp.json()) 68 69 def leave(self) -> None: 70 """Drop your membership for this competition. If captain and others 71 remain, captaincy transfers to the longest-tenured member; if you 72 were the lone member the team is deleted.""" 73 resp = self._http.request("POST", f"/competitions/{self._cid}/team/leave") 74 _raise_for_status(resp) 75 76 def kick(self, user_id: str) -> None: 77 """Captain-only: remove ``user_id`` from the team. Self-kick is 78 rejected (use :meth:`leave` instead).""" 79 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/members/{user_id}") 80 _raise_for_status(resp) 81 82 @cached_property 83 def invites(self) -> CompetitionInvites: 84 """Invite codes + direct email invites the captain hands out.""" 85 return CompetitionInvites(self._http, self._cid) 86 87 @cached_property 88 def requests(self) -> CompetitionRequests: 89 """Pending join requests the captain approves / rejects.""" 90 return CompetitionRequests(self._http, self._cid) 91 92 93class CompetitionInvites: 94 """Captain-only: the team's outgoing invites for this competition.""" 95 96 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 97 self._http = http 98 self._cid = competition_id 99 100 def create(self, *, max_uses: int = 1, ttl_seconds: int = 24 * 3600) -> TeamInviteInfo: 101 """Mint an invite code scoped to this comp. ``max_uses=0`` is 102 unlimited; ``ttl_seconds=0`` never expires. Others redeem it via 103 ``client.competition(id).register(mode="join", code=…)``.""" 104 resp = self._http.request( 105 "POST", 106 f"/competitions/{self._cid}/invites", 107 json={"max_uses": max_uses, "ttl_seconds": ttl_seconds}, 108 ) 109 _raise_for_status(resp) 110 return TeamInviteInfo.model_validate(resp.json()) 111 112 def list(self) -> PagedList[TeamInviteInfo]: 113 """All outstanding invites for the caller's team in this comp.""" 114 resp = self._http.request("GET", f"/competitions/{self._cid}/invites") 115 _raise_for_status(resp) 116 return _extract_items(resp.json(), TeamInviteInfo) 117 118 def revoke(self, invite_id: str) -> None: 119 """Invalidate a previously-minted invite.""" 120 resp = self._http.request("DELETE", f"/competitions/{self._cid}/invites/{invite_id}") 121 _raise_for_status(resp) 122 123 def send(self, email: str) -> TeamInviteInfo: 124 """Mint an invite addressed to one exact email. The lookup is 125 case-insensitive but otherwise unforgiving (no fuzzy match, no 126 enumeration vectors). The recipient accepts via 127 :meth:`Competition.accept_invite`.""" 128 resp = self._http.request( 129 "POST", 130 f"/competitions/{self._cid}/invites/direct", 131 json={"email": email}, 132 ) 133 _raise_for_status(resp) 134 return TeamInviteInfo.model_validate(resp.json()) 135 136 137class CompetitionRequests: 138 """Captain-only: incoming join requests for this competition.""" 139 140 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 141 self._http = http 142 self._cid = competition_id 143 144 def approve(self, invite_id: str) -> TeamDetail: 145 """Approve a pending join request → the requester joins the team.""" 146 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/approve") 147 _raise_for_status(resp) 148 return TeamDetail.model_validate(resp.json()) 149 150 def reject(self, invite_id: str) -> None: 151 """Reject a pending join request → invite revoked. The requester 152 can re-open a fresh request afterwards.""" 153 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/reject") 154 _raise_for_status(resp)
18class CompetitionTeam: 19 """The caller's team in this competition.""" 20 21 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 22 self._http = http 23 self._cid = competition_id 24 25 def rename(self, *, name: str | None = None, description: str | None = None) -> TeamDetail: 26 """Captain-only: rename / re-describe the team.""" 27 body: dict[str, str] = {} 28 if name is not None: 29 body["name"] = name 30 if description is not None: 31 body["description"] = description 32 resp = self._http.request("PATCH", f"/competitions/{self._cid}/team", json=body) 33 _raise_for_status(resp) 34 return TeamDetail.model_validate(resp.json()) 35 36 def affiliation(self) -> TeamAffiliationSelection: 37 """Captain-only: the squad's institution badge and its options. 38 39 Three facts in one answer — what the captain chose, what it 40 resolves to *now*, and every institution a current member 41 publishes. The middle one can be ``None`` while the first is 42 set: that is a choice whose backing has gone, and it reads 43 differently from having chosen nothing. 44 """ 45 resp = self._http.request("GET", f"/competitions/{self._cid}/team/affiliation") 46 _raise_for_status(resp) 47 return TeamAffiliationSelection.model_validate(resp.json()) 48 49 def set_affiliation(self, domain: str) -> TeamAffiliationSelection: 50 """Captain-only: publish ``domain`` beside the squad's name. 51 52 Refused (``409 affiliation_not_published``) unless a current 53 member already publishes that institution on their own profile — 54 a team cannot claim one nobody on it has shown. 55 """ 56 resp = self._http.request( 57 "PUT", f"/competitions/{self._cid}/team/affiliation", json={"domain": domain} 58 ) 59 _raise_for_status(resp) 60 return TeamAffiliationSelection.model_validate(resp.json()) 61 62 def clear_affiliation(self) -> TeamAffiliationSelection: 63 """Captain-only: stop publishing a badge. Never refused, and 64 takes no domain — so it also escapes the state where the choice 65 outlived the member who backed it.""" 66 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/affiliation") 67 _raise_for_status(resp) 68 return TeamAffiliationSelection.model_validate(resp.json()) 69 70 def leave(self) -> None: 71 """Drop your membership for this competition. If captain and others 72 remain, captaincy transfers to the longest-tenured member; if you 73 were the lone member the team is deleted.""" 74 resp = self._http.request("POST", f"/competitions/{self._cid}/team/leave") 75 _raise_for_status(resp) 76 77 def kick(self, user_id: str) -> None: 78 """Captain-only: remove ``user_id`` from the team. Self-kick is 79 rejected (use :meth:`leave` instead).""" 80 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/members/{user_id}") 81 _raise_for_status(resp) 82 83 @cached_property 84 def invites(self) -> CompetitionInvites: 85 """Invite codes + direct email invites the captain hands out.""" 86 return CompetitionInvites(self._http, self._cid) 87 88 @cached_property 89 def requests(self) -> CompetitionRequests: 90 """Pending join requests the captain approves / rejects.""" 91 return CompetitionRequests(self._http, self._cid)
The caller's team in this competition.
25 def rename(self, *, name: str | None = None, description: str | None = None) -> TeamDetail: 26 """Captain-only: rename / re-describe the team.""" 27 body: dict[str, str] = {} 28 if name is not None: 29 body["name"] = name 30 if description is not None: 31 body["description"] = description 32 resp = self._http.request("PATCH", f"/competitions/{self._cid}/team", json=body) 33 _raise_for_status(resp) 34 return TeamDetail.model_validate(resp.json())
Captain-only: rename / re-describe the team.
36 def affiliation(self) -> TeamAffiliationSelection: 37 """Captain-only: the squad's institution badge and its options. 38 39 Three facts in one answer — what the captain chose, what it 40 resolves to *now*, and every institution a current member 41 publishes. The middle one can be ``None`` while the first is 42 set: that is a choice whose backing has gone, and it reads 43 differently from having chosen nothing. 44 """ 45 resp = self._http.request("GET", f"/competitions/{self._cid}/team/affiliation") 46 _raise_for_status(resp) 47 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.
49 def set_affiliation(self, domain: str) -> TeamAffiliationSelection: 50 """Captain-only: publish ``domain`` beside the squad's name. 51 52 Refused (``409 affiliation_not_published``) unless a current 53 member already publishes that institution on their own profile — 54 a team cannot claim one nobody on it has shown. 55 """ 56 resp = self._http.request( 57 "PUT", f"/competitions/{self._cid}/team/affiliation", json={"domain": domain} 58 ) 59 _raise_for_status(resp) 60 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.
62 def clear_affiliation(self) -> TeamAffiliationSelection: 63 """Captain-only: stop publishing a badge. Never refused, and 64 takes no domain — so it also escapes the state where the choice 65 outlived the member who backed it.""" 66 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/affiliation") 67 _raise_for_status(resp) 68 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.
70 def leave(self) -> None: 71 """Drop your membership for this competition. If captain and others 72 remain, captaincy transfers to the longest-tenured member; if you 73 were the lone member the team is deleted.""" 74 resp = self._http.request("POST", f"/competitions/{self._cid}/team/leave") 75 _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.
77 def kick(self, user_id: str) -> None: 78 """Captain-only: remove ``user_id`` from the team. Self-kick is 79 rejected (use :meth:`leave` instead).""" 80 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/members/{user_id}") 81 _raise_for_status(resp)
Captain-only: remove user_id from the team. Self-kick is
rejected (use leave() instead).
83 @cached_property 84 def invites(self) -> CompetitionInvites: 85 """Invite codes + direct email invites the captain hands out.""" 86 return CompetitionInvites(self._http, self._cid)
Invite codes + direct email invites the captain hands out.
88 @cached_property 89 def requests(self) -> CompetitionRequests: 90 """Pending join requests the captain approves / rejects.""" 91 return CompetitionRequests(self._http, self._cid)
Pending join requests the captain approves / rejects.
94class CompetitionInvites: 95 """Captain-only: the team's outgoing invites for this competition.""" 96 97 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 98 self._http = http 99 self._cid = competition_id 100 101 def create(self, *, max_uses: int = 1, ttl_seconds: int = 24 * 3600) -> TeamInviteInfo: 102 """Mint an invite code scoped to this comp. ``max_uses=0`` is 103 unlimited; ``ttl_seconds=0`` never expires. Others redeem it via 104 ``client.competition(id).register(mode="join", code=…)``.""" 105 resp = self._http.request( 106 "POST", 107 f"/competitions/{self._cid}/invites", 108 json={"max_uses": max_uses, "ttl_seconds": ttl_seconds}, 109 ) 110 _raise_for_status(resp) 111 return TeamInviteInfo.model_validate(resp.json()) 112 113 def list(self) -> PagedList[TeamInviteInfo]: 114 """All outstanding invites for the caller's team in this comp.""" 115 resp = self._http.request("GET", f"/competitions/{self._cid}/invites") 116 _raise_for_status(resp) 117 return _extract_items(resp.json(), TeamInviteInfo) 118 119 def revoke(self, invite_id: str) -> None: 120 """Invalidate a previously-minted invite.""" 121 resp = self._http.request("DELETE", f"/competitions/{self._cid}/invites/{invite_id}") 122 _raise_for_status(resp) 123 124 def send(self, email: str) -> TeamInviteInfo: 125 """Mint an invite addressed to one exact email. The lookup is 126 case-insensitive but otherwise unforgiving (no fuzzy match, no 127 enumeration vectors). The recipient accepts via 128 :meth:`Competition.accept_invite`.""" 129 resp = self._http.request( 130 "POST", 131 f"/competitions/{self._cid}/invites/direct", 132 json={"email": email}, 133 ) 134 _raise_for_status(resp) 135 return TeamInviteInfo.model_validate(resp.json())
Captain-only: the team's outgoing invites for this competition.
101 def create(self, *, max_uses: int = 1, ttl_seconds: int = 24 * 3600) -> TeamInviteInfo: 102 """Mint an invite code scoped to this comp. ``max_uses=0`` is 103 unlimited; ``ttl_seconds=0`` never expires. Others redeem it via 104 ``client.competition(id).register(mode="join", code=…)``.""" 105 resp = self._http.request( 106 "POST", 107 f"/competitions/{self._cid}/invites", 108 json={"max_uses": max_uses, "ttl_seconds": ttl_seconds}, 109 ) 110 _raise_for_status(resp) 111 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=…).
113 def list(self) -> PagedList[TeamInviteInfo]: 114 """All outstanding invites for the caller's team in this comp.""" 115 resp = self._http.request("GET", f"/competitions/{self._cid}/invites") 116 _raise_for_status(resp) 117 return _extract_items(resp.json(), TeamInviteInfo)
All outstanding invites for the caller's team in this comp.
119 def revoke(self, invite_id: str) -> None: 120 """Invalidate a previously-minted invite.""" 121 resp = self._http.request("DELETE", f"/competitions/{self._cid}/invites/{invite_id}") 122 _raise_for_status(resp)
Invalidate a previously-minted invite.
124 def send(self, email: str) -> TeamInviteInfo: 125 """Mint an invite addressed to one exact email. The lookup is 126 case-insensitive but otherwise unforgiving (no fuzzy match, no 127 enumeration vectors). The recipient accepts via 128 :meth:`Competition.accept_invite`.""" 129 resp = self._http.request( 130 "POST", 131 f"/competitions/{self._cid}/invites/direct", 132 json={"email": email}, 133 ) 134 _raise_for_status(resp) 135 return TeamInviteInfo.model_validate(resp.json())
Mint an invite addressed to one exact email. The lookup is
case-insensitive but otherwise unforgiving (no fuzzy match, no
enumeration vectors). The recipient accepts via
Competition.accept_invite().
138class CompetitionRequests: 139 """Captain-only: incoming join requests for this competition.""" 140 141 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 142 self._http = http 143 self._cid = competition_id 144 145 def approve(self, invite_id: str) -> TeamDetail: 146 """Approve a pending join request → the requester joins the team.""" 147 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/approve") 148 _raise_for_status(resp) 149 return TeamDetail.model_validate(resp.json()) 150 151 def reject(self, invite_id: str) -> None: 152 """Reject a pending join request → invite revoked. The requester 153 can re-open a fresh request afterwards.""" 154 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/reject") 155 _raise_for_status(resp)
Captain-only: incoming join requests for this competition.
145 def approve(self, invite_id: str) -> TeamDetail: 146 """Approve a pending join request → the requester joins the team.""" 147 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/approve") 148 _raise_for_status(resp) 149 return TeamDetail.model_validate(resp.json())
Approve a pending join request → the requester joins the team.
151 def reject(self, invite_id: str) -> None: 152 """Reject a pending join request → invite revoked. The requester 153 can re-open a fresh request afterwards.""" 154 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/reject") 155 _raise_for_status(resp)
Reject a pending join request → invite revoked. The requester can re-open a fresh request afterwards.