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 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 leave(self) -> None: 36 """Drop your membership for this competition. If captain and others 37 remain, captaincy transfers to the longest-tenured member; if you 38 were the lone member the team is deleted.""" 39 resp = self._http.request("POST", f"/competitions/{self._cid}/team/leave") 40 _raise_for_status(resp) 41 42 def kick(self, user_id: str) -> None: 43 """Captain-only: remove ``user_id`` from the team. Self-kick is 44 rejected (use :meth:`leave` instead).""" 45 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/members/{user_id}") 46 _raise_for_status(resp) 47 48 @cached_property 49 def invites(self) -> CompetitionInvites: 50 """Invite codes + direct email invites the captain hands out.""" 51 return CompetitionInvites(self._http, self._cid) 52 53 @cached_property 54 def requests(self) -> CompetitionRequests: 55 """Pending join requests the captain approves / rejects.""" 56 return CompetitionRequests(self._http, self._cid) 57 58 59class CompetitionInvites: 60 """Captain-only: the team's outgoing invites for this competition.""" 61 62 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 63 self._http = http 64 self._cid = competition_id 65 66 def create(self, *, max_uses: int = 1, ttl_seconds: int = 24 * 3600) -> TeamInviteInfo: 67 """Mint an invite code scoped to this comp. ``max_uses=0`` is 68 unlimited; ``ttl_seconds=0`` never expires. Others redeem it via 69 ``client.competition(id).register(mode="join", code=…)``.""" 70 resp = self._http.request( 71 "POST", 72 f"/competitions/{self._cid}/invites", 73 json={"max_uses": max_uses, "ttl_seconds": ttl_seconds}, 74 ) 75 _raise_for_status(resp) 76 return TeamInviteInfo.model_validate(resp.json()) 77 78 def list(self) -> PagedList[TeamInviteInfo]: 79 """All outstanding invites for the caller's team in this comp.""" 80 resp = self._http.request("GET", f"/competitions/{self._cid}/invites") 81 _raise_for_status(resp) 82 return _extract_items(resp.json(), TeamInviteInfo) 83 84 def revoke(self, invite_id: str) -> None: 85 """Invalidate a previously-minted invite.""" 86 resp = self._http.request("DELETE", f"/competitions/{self._cid}/invites/{invite_id}") 87 _raise_for_status(resp) 88 89 def send(self, email: str) -> TeamInviteInfo: 90 """Mint an invite addressed to one exact email. The lookup is 91 case-insensitive but otherwise unforgiving (no fuzzy match, no 92 enumeration vectors). The recipient accepts via 93 :meth:`Competition.accept_invite`.""" 94 resp = self._http.request( 95 "POST", 96 f"/competitions/{self._cid}/invites/direct", 97 json={"email": email}, 98 ) 99 _raise_for_status(resp) 100 return TeamInviteInfo.model_validate(resp.json()) 101 102 103class CompetitionRequests: 104 """Captain-only: incoming join requests for this competition.""" 105 106 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 107 self._http = http 108 self._cid = competition_id 109 110 def approve(self, invite_id: str) -> TeamDetail: 111 """Approve a pending join request → the requester joins the team.""" 112 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/approve") 113 _raise_for_status(resp) 114 return TeamDetail.model_validate(resp.json()) 115 116 def reject(self, invite_id: str) -> None: 117 """Reject a pending join request → invite revoked. The requester 118 can re-open a fresh request afterwards.""" 119 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/reject") 120 _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 leave(self) -> None: 37 """Drop your membership for this competition. If captain and others 38 remain, captaincy transfers to the longest-tenured member; if you 39 were the lone member the team is deleted.""" 40 resp = self._http.request("POST", f"/competitions/{self._cid}/team/leave") 41 _raise_for_status(resp) 42 43 def kick(self, user_id: str) -> None: 44 """Captain-only: remove ``user_id`` from the team. Self-kick is 45 rejected (use :meth:`leave` instead).""" 46 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/members/{user_id}") 47 _raise_for_status(resp) 48 49 @cached_property 50 def invites(self) -> CompetitionInvites: 51 """Invite codes + direct email invites the captain hands out.""" 52 return CompetitionInvites(self._http, self._cid) 53 54 @cached_property 55 def requests(self) -> CompetitionRequests: 56 """Pending join requests the captain approves / rejects.""" 57 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 leave(self) -> None: 37 """Drop your membership for this competition. If captain and others 38 remain, captaincy transfers to the longest-tenured member; if you 39 were the lone member the team is deleted.""" 40 resp = self._http.request("POST", f"/competitions/{self._cid}/team/leave") 41 _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.
43 def kick(self, user_id: str) -> None: 44 """Captain-only: remove ``user_id`` from the team. Self-kick is 45 rejected (use :meth:`leave` instead).""" 46 resp = self._http.request("DELETE", f"/competitions/{self._cid}/team/members/{user_id}") 47 _raise_for_status(resp)
Captain-only: remove user_id from the team. Self-kick is
rejected (use leave() instead).
49 @cached_property 50 def invites(self) -> CompetitionInvites: 51 """Invite codes + direct email invites the captain hands out.""" 52 return CompetitionInvites(self._http, self._cid)
Invite codes + direct email invites the captain hands out.
54 @cached_property 55 def requests(self) -> CompetitionRequests: 56 """Pending join requests the captain approves / rejects.""" 57 return CompetitionRequests(self._http, self._cid)
Pending join requests the captain approves / rejects.
60class CompetitionInvites: 61 """Captain-only: the team's outgoing invites for this competition.""" 62 63 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 64 self._http = http 65 self._cid = competition_id 66 67 def create(self, *, max_uses: int = 1, ttl_seconds: int = 24 * 3600) -> TeamInviteInfo: 68 """Mint an invite code scoped to this comp. ``max_uses=0`` is 69 unlimited; ``ttl_seconds=0`` never expires. Others redeem it via 70 ``client.competition(id).register(mode="join", code=…)``.""" 71 resp = self._http.request( 72 "POST", 73 f"/competitions/{self._cid}/invites", 74 json={"max_uses": max_uses, "ttl_seconds": ttl_seconds}, 75 ) 76 _raise_for_status(resp) 77 return TeamInviteInfo.model_validate(resp.json()) 78 79 def list(self) -> PagedList[TeamInviteInfo]: 80 """All outstanding invites for the caller's team in this comp.""" 81 resp = self._http.request("GET", f"/competitions/{self._cid}/invites") 82 _raise_for_status(resp) 83 return _extract_items(resp.json(), TeamInviteInfo) 84 85 def revoke(self, invite_id: str) -> None: 86 """Invalidate a previously-minted invite.""" 87 resp = self._http.request("DELETE", f"/competitions/{self._cid}/invites/{invite_id}") 88 _raise_for_status(resp) 89 90 def send(self, email: str) -> TeamInviteInfo: 91 """Mint an invite addressed to one exact email. The lookup is 92 case-insensitive but otherwise unforgiving (no fuzzy match, no 93 enumeration vectors). The recipient accepts via 94 :meth:`Competition.accept_invite`.""" 95 resp = self._http.request( 96 "POST", 97 f"/competitions/{self._cid}/invites/direct", 98 json={"email": email}, 99 ) 100 _raise_for_status(resp) 101 return TeamInviteInfo.model_validate(resp.json())
Captain-only: the team's outgoing invites for this competition.
67 def create(self, *, max_uses: int = 1, ttl_seconds: int = 24 * 3600) -> TeamInviteInfo: 68 """Mint an invite code scoped to this comp. ``max_uses=0`` is 69 unlimited; ``ttl_seconds=0`` never expires. Others redeem it via 70 ``client.competition(id).register(mode="join", code=…)``.""" 71 resp = self._http.request( 72 "POST", 73 f"/competitions/{self._cid}/invites", 74 json={"max_uses": max_uses, "ttl_seconds": ttl_seconds}, 75 ) 76 _raise_for_status(resp) 77 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=…).
79 def list(self) -> PagedList[TeamInviteInfo]: 80 """All outstanding invites for the caller's team in this comp.""" 81 resp = self._http.request("GET", f"/competitions/{self._cid}/invites") 82 _raise_for_status(resp) 83 return _extract_items(resp.json(), TeamInviteInfo)
All outstanding invites for the caller's team in this comp.
85 def revoke(self, invite_id: str) -> None: 86 """Invalidate a previously-minted invite.""" 87 resp = self._http.request("DELETE", f"/competitions/{self._cid}/invites/{invite_id}") 88 _raise_for_status(resp)
Invalidate a previously-minted invite.
90 def send(self, email: str) -> TeamInviteInfo: 91 """Mint an invite addressed to one exact email. The lookup is 92 case-insensitive but otherwise unforgiving (no fuzzy match, no 93 enumeration vectors). The recipient accepts via 94 :meth:`Competition.accept_invite`.""" 95 resp = self._http.request( 96 "POST", 97 f"/competitions/{self._cid}/invites/direct", 98 json={"email": email}, 99 ) 100 _raise_for_status(resp) 101 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().
104class CompetitionRequests: 105 """Captain-only: incoming join requests for this competition.""" 106 107 def __init__(self, http: BaseHttpClient, competition_id: str) -> None: 108 self._http = http 109 self._cid = competition_id 110 111 def approve(self, invite_id: str) -> TeamDetail: 112 """Approve a pending join request → the requester joins the team.""" 113 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/approve") 114 _raise_for_status(resp) 115 return TeamDetail.model_validate(resp.json()) 116 117 def reject(self, invite_id: str) -> None: 118 """Reject a pending join request → invite revoked. The requester 119 can re-open a fresh request afterwards.""" 120 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/reject") 121 _raise_for_status(resp)
Captain-only: incoming join requests for this competition.
111 def approve(self, invite_id: str) -> TeamDetail: 112 """Approve a pending join request → the requester joins the team.""" 113 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/approve") 114 _raise_for_status(resp) 115 return TeamDetail.model_validate(resp.json())
Approve a pending join request → the requester joins the team.
117 def reject(self, invite_id: str) -> None: 118 """Reject a pending join request → invite revoked. The requester 119 can re-open a fresh request afterwards.""" 120 resp = self._http.request("POST", f"/competitions/{self._cid}/invites/{invite_id}/reject") 121 _raise_for_status(resp)
Reject a pending join request → invite revoked. The requester can re-open a fresh request afterwards.