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