ctfy.sdk.admin_resources.competition_invites
client.admin.competition_invites — private-competition
participation allowlist (admin).
1"""``client.admin.competition_invites`` — private-competition 2participation allowlist (admin).""" 3 4from __future__ import annotations 5 6from ctfy.sdk._helpers import PagedList, _extract_items, _raise_for_status 7from ctfy.sdk.base import BaseHttpClient 8from ctfy.server.models import BulkInviteResponse, CompetitionInviteInfo 9 10 11class AdminCompetitionInvitesResource: 12 """Invite / revoke users on a private competition's participation 13 allowlist. Managed by any competition admin (global or per-comp).""" 14 15 def __init__(self, http: BaseHttpClient) -> None: 16 self._http = http 17 18 def list( 19 self, competition_id: str, offset: int = 0, limit: int = 50 20 ) -> PagedList[CompetitionInviteInfo]: 21 """Everyone invited to participate in one competition, and how 22 far each got. 23 24 ``status`` is the funnel — ``pending_signup`` (an address nobody 25 has claimed) / ``signed_up`` (an account, but no team for this 26 competition) / ``registered`` (in, with ``team_id`` and 27 ``registered_at`` set). The middle one is what "who did I invite 28 that never turned up" actually means; ``pending_signup`` the 29 boolean is derived from it and kept for older callers. 30 31 A claimed email invitation appears once, on its allowlist row, 32 carrying the *original* invitation's ``invited_at`` / 33 ``invited_by`` and the moment it was claimed as ``signed_up_at``. 34 """ 35 resp = self._http.request( 36 "GET", 37 f"/admin/competitions/{competition_id}/invites", 38 params={"offset": offset, "limit": limit}, 39 ) 40 _raise_for_status(resp) 41 return _extract_items(resp.json(), CompetitionInviteInfo) 42 43 def invite( 44 self, competition_id: str, *, user_id: str = "", email: str = "" 45 ) -> CompetitionInviteInfo: 46 """Invite someone to participate. 47 48 Identify the target by ``user_id`` or ``email``. An ``email`` 49 with no account yet is recorded against the address and claimed 50 when its owner first signs in with a verified address — the 51 returned row carries ``pending_signup=True`` in that case. A 52 ``user_id`` that matches nothing is still a 404: an id names an 53 account or nothing. 54 """ 55 resp = self._http.request( 56 "POST", 57 f"/admin/competitions/{competition_id}/invites", 58 json={"user_id": user_id, "email": email}, 59 ) 60 _raise_for_status(resp) 61 return CompetitionInviteInfo.model_validate(resp.json()) 62 63 def invite_bulk(self, competition_id: str, emails: str) -> BulkInviteResponse: 64 """Invite a pasted list of addresses in one call. 65 66 ``emails`` is the raw blob — newline / comma / semicolon 67 separated. Splitting happens server-side so every client agrees 68 on the rules. The response classifies each address as 69 ``invited`` / ``pending`` / ``already`` / ``invalid``. 70 """ 71 resp = self._http.request( 72 "POST", 73 f"/admin/competitions/{competition_id}/invites/bulk", 74 json={"emails": emails}, 75 ) 76 _raise_for_status(resp) 77 return BulkInviteResponse.model_validate(resp.json()) 78 79 def revoke(self, competition_id: str, user_id: str) -> None: 80 """Remove a user from the participation allowlist.""" 81 resp = self._http.request( 82 "DELETE", f"/admin/competitions/{competition_id}/invites/{user_id}" 83 ) 84 _raise_for_status(resp) 85 86 def revoke_email(self, competition_id: str, email: str) -> None: 87 """Withdraw an invitation still waiting for its person.""" 88 resp = self._http.request( 89 "DELETE", f"/admin/competitions/{competition_id}/invites/by-email/{email}" 90 ) 91 _raise_for_status(resp)
12class AdminCompetitionInvitesResource: 13 """Invite / revoke users on a private competition's participation 14 allowlist. Managed by any competition admin (global or per-comp).""" 15 16 def __init__(self, http: BaseHttpClient) -> None: 17 self._http = http 18 19 def list( 20 self, competition_id: str, offset: int = 0, limit: int = 50 21 ) -> PagedList[CompetitionInviteInfo]: 22 """Everyone invited to participate in one competition, and how 23 far each got. 24 25 ``status`` is the funnel — ``pending_signup`` (an address nobody 26 has claimed) / ``signed_up`` (an account, but no team for this 27 competition) / ``registered`` (in, with ``team_id`` and 28 ``registered_at`` set). The middle one is what "who did I invite 29 that never turned up" actually means; ``pending_signup`` the 30 boolean is derived from it and kept for older callers. 31 32 A claimed email invitation appears once, on its allowlist row, 33 carrying the *original* invitation's ``invited_at`` / 34 ``invited_by`` and the moment it was claimed as ``signed_up_at``. 35 """ 36 resp = self._http.request( 37 "GET", 38 f"/admin/competitions/{competition_id}/invites", 39 params={"offset": offset, "limit": limit}, 40 ) 41 _raise_for_status(resp) 42 return _extract_items(resp.json(), CompetitionInviteInfo) 43 44 def invite( 45 self, competition_id: str, *, user_id: str = "", email: str = "" 46 ) -> CompetitionInviteInfo: 47 """Invite someone to participate. 48 49 Identify the target by ``user_id`` or ``email``. An ``email`` 50 with no account yet is recorded against the address and claimed 51 when its owner first signs in with a verified address — the 52 returned row carries ``pending_signup=True`` in that case. A 53 ``user_id`` that matches nothing is still a 404: an id names an 54 account or nothing. 55 """ 56 resp = self._http.request( 57 "POST", 58 f"/admin/competitions/{competition_id}/invites", 59 json={"user_id": user_id, "email": email}, 60 ) 61 _raise_for_status(resp) 62 return CompetitionInviteInfo.model_validate(resp.json()) 63 64 def invite_bulk(self, competition_id: str, emails: str) -> BulkInviteResponse: 65 """Invite a pasted list of addresses in one call. 66 67 ``emails`` is the raw blob — newline / comma / semicolon 68 separated. Splitting happens server-side so every client agrees 69 on the rules. The response classifies each address as 70 ``invited`` / ``pending`` / ``already`` / ``invalid``. 71 """ 72 resp = self._http.request( 73 "POST", 74 f"/admin/competitions/{competition_id}/invites/bulk", 75 json={"emails": emails}, 76 ) 77 _raise_for_status(resp) 78 return BulkInviteResponse.model_validate(resp.json()) 79 80 def revoke(self, competition_id: str, user_id: str) -> None: 81 """Remove a user from the participation allowlist.""" 82 resp = self._http.request( 83 "DELETE", f"/admin/competitions/{competition_id}/invites/{user_id}" 84 ) 85 _raise_for_status(resp) 86 87 def revoke_email(self, competition_id: str, email: str) -> None: 88 """Withdraw an invitation still waiting for its person.""" 89 resp = self._http.request( 90 "DELETE", f"/admin/competitions/{competition_id}/invites/by-email/{email}" 91 ) 92 _raise_for_status(resp)
Invite / revoke users on a private competition's participation allowlist. Managed by any competition admin (global or per-comp).
19 def list( 20 self, competition_id: str, offset: int = 0, limit: int = 50 21 ) -> PagedList[CompetitionInviteInfo]: 22 """Everyone invited to participate in one competition, and how 23 far each got. 24 25 ``status`` is the funnel — ``pending_signup`` (an address nobody 26 has claimed) / ``signed_up`` (an account, but no team for this 27 competition) / ``registered`` (in, with ``team_id`` and 28 ``registered_at`` set). The middle one is what "who did I invite 29 that never turned up" actually means; ``pending_signup`` the 30 boolean is derived from it and kept for older callers. 31 32 A claimed email invitation appears once, on its allowlist row, 33 carrying the *original* invitation's ``invited_at`` / 34 ``invited_by`` and the moment it was claimed as ``signed_up_at``. 35 """ 36 resp = self._http.request( 37 "GET", 38 f"/admin/competitions/{competition_id}/invites", 39 params={"offset": offset, "limit": limit}, 40 ) 41 _raise_for_status(resp) 42 return _extract_items(resp.json(), CompetitionInviteInfo)
Everyone invited to participate in one competition, and how far each got.
status is the funnel — pending_signup (an address nobody
has claimed) / signed_up (an account, but no team for this
competition) / registered (in, with team_id and
registered_at set). The middle one is what "who did I invite
that never turned up" actually means; pending_signup the
boolean is derived from it and kept for older callers.
A claimed email invitation appears once, on its allowlist row,
carrying the original invitation's invited_at /
invited_by and the moment it was claimed as signed_up_at.
44 def invite( 45 self, competition_id: str, *, user_id: str = "", email: str = "" 46 ) -> CompetitionInviteInfo: 47 """Invite someone to participate. 48 49 Identify the target by ``user_id`` or ``email``. An ``email`` 50 with no account yet is recorded against the address and claimed 51 when its owner first signs in with a verified address — the 52 returned row carries ``pending_signup=True`` in that case. A 53 ``user_id`` that matches nothing is still a 404: an id names an 54 account or nothing. 55 """ 56 resp = self._http.request( 57 "POST", 58 f"/admin/competitions/{competition_id}/invites", 59 json={"user_id": user_id, "email": email}, 60 ) 61 _raise_for_status(resp) 62 return CompetitionInviteInfo.model_validate(resp.json())
Invite someone to participate.
Identify the target by user_id or email. An email
with no account yet is recorded against the address and claimed
when its owner first signs in with a verified address — the
returned row carries pending_signup=True in that case. A
user_id that matches nothing is still a 404: an id names an
account or nothing.
64 def invite_bulk(self, competition_id: str, emails: str) -> BulkInviteResponse: 65 """Invite a pasted list of addresses in one call. 66 67 ``emails`` is the raw blob — newline / comma / semicolon 68 separated. Splitting happens server-side so every client agrees 69 on the rules. The response classifies each address as 70 ``invited`` / ``pending`` / ``already`` / ``invalid``. 71 """ 72 resp = self._http.request( 73 "POST", 74 f"/admin/competitions/{competition_id}/invites/bulk", 75 json={"emails": emails}, 76 ) 77 _raise_for_status(resp) 78 return BulkInviteResponse.model_validate(resp.json())
Invite a pasted list of addresses in one call.
emails is the raw blob — newline / comma / semicolon
separated. Splitting happens server-side so every client agrees
on the rules. The response classifies each address as
invited / pending / already / invalid.
80 def revoke(self, competition_id: str, user_id: str) -> None: 81 """Remove a user from the participation allowlist.""" 82 resp = self._http.request( 83 "DELETE", f"/admin/competitions/{competition_id}/invites/{user_id}" 84 ) 85 _raise_for_status(resp)
Remove a user from the participation allowlist.
87 def revoke_email(self, competition_id: str, email: str) -> None: 88 """Withdraw an invitation still waiting for its person.""" 89 resp = self._http.request( 90 "DELETE", f"/admin/competitions/{competition_id}/invites/by-email/{email}" 91 ) 92 _raise_for_status(resp)
Withdraw an invitation still waiting for its person.