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)
class CompetitionTeam:
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.

CompetitionTeam(http: ctfy.sdk.base.BaseHttpClient, competition_id: str)
21    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
22        self._http = http
23        self._cid = competition_id
def rename( self, *, name: str | None = None, description: str | None = None) -> ctfy.server.models.TeamDetail:
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.

def leave(self) -> None:
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.

def kick(self, user_id: str) -> None:
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).

invites: CompetitionInvites
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.

requests: CompetitionRequests
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.

class CompetitionInvites:
 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.

CompetitionInvites(http: ctfy.sdk.base.BaseHttpClient, competition_id: str)
63    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
64        self._http = http
65        self._cid = competition_id
def create( self, *, max_uses: int = 1, ttl_seconds: int = 86400) -> ctfy.server.models.TeamInviteInfo:
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=…).

def list( self) -> ctfy.sdk._helpers.PagedList[ctfy.server.models.TeamInviteInfo]:
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.

def revoke(self, invite_id: str) -> None:
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.

def send(self, email: str) -> ctfy.server.models.TeamInviteInfo:
 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().

class CompetitionRequests:
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.

CompetitionRequests(http: ctfy.sdk.base.BaseHttpClient, competition_id: str)
107    def __init__(self, http: BaseHttpClient, competition_id: str) -> None:
108        self._http = http
109        self._cid = competition_id
def approve(self, invite_id: str) -> ctfy.server.models.TeamDetail:
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.

def reject(self, invite_id: str) -> None:
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.