ctfy.sdk.resources.registration
client.registration — the entrant's own competition registration.
1"""``client.registration`` — the entrant's own competition registration.""" 2 3from __future__ import annotations 4 5from typing import Any 6 7from ctfy.sdk._helpers import _raise_for_status 8from ctfy.sdk.base import BaseHttpClient 9from ctfy.server.models import ( 10 MyRegistrationResponse, 11 MyReviewTimeline, 12 MyTeamTodosResponse, 13 TeamLogoUploadResponse, 14) 15 16 17class RegistrationResource: 18 """Read and submit the calling user's registration for one competition. 19 20 The GET carries the competition's form spec alongside the values, so 21 a client can render the right fields without hard-coding any event's 22 rules — which fields exist and which are mandatory is the organiser's 23 decision, made per competition. 24 """ 25 26 def __init__(self, http: BaseHttpClient) -> None: 27 self._http = http 28 29 def get(self, competition_id: str) -> MyRegistrationResponse: 30 """The caller's dossier plus the form spec that governs it.""" 31 resp = self._http.request("GET", f"/competitions/{competition_id}/registration") 32 _raise_for_status(resp) 33 return MyRegistrationResponse.model_validate(resp.json()) 34 35 def submit(self, competition_id: str, **fields: Any) -> MyRegistrationResponse: 36 """Create or update the caller's dossier. 37 38 An idempotent upsert — resubmitting to correct one field is the 39 normal case, not an error. Fields the competition did not ask for 40 are dropped server-side rather than rejected. 41 42 The response's ``missing_required`` names anything still 43 outstanding, so a caller never has to reimplement the server's 44 completeness rule. 45 """ 46 resp = self._http.request( 47 "PUT", f"/competitions/{competition_id}/registration", json=fields 48 ) 49 _raise_for_status(resp) 50 return MyRegistrationResponse.model_validate(resp.json()) 51 52 def review_timeline(self, competition_id: str) -> MyReviewTimeline: 53 """The caller's own team's review history (#2524): submitted, verdicts 54 with their reasons, reopens and reject to-dos. Standing only -- no 55 reviewer, no field, no value.""" 56 resp = self._http.request("GET", f"/competitions/{competition_id}/my-team/review-timeline") 57 _raise_for_status(resp) 58 return MyReviewTimeline.model_validate(resp.json()) 59 60 def todos(self, competition_id: str) -> MyTeamTodosResponse: 61 """What the team's standing rejection asked the caller to fix (#2527), 62 with the caller's own current value, plus the team's progress as 63 names and items only.""" 64 resp = self._http.request("GET", f"/competitions/{competition_id}/my-team/todos") 65 _raise_for_status(resp) 66 return MyTeamTodosResponse.model_validate(resp.json()) 67 68 def resubmit(self, competition_id: str) -> MyTeamTodosResponse: 69 """Captain only: send a rejected team back to the review queue before 70 every to-do is done (#2527). ``409 review_not_rejected`` when the team 71 is not rejected (a verdict that landed first stands).""" 72 resp = self._http.request("POST", f"/competitions/{competition_id}/my-team/resubmit") 73 _raise_for_status(resp) 74 return MyTeamTodosResponse.model_validate(resp.json()) 75 76 def upload_team_logo( 77 self, 78 competition_id: str, 79 *, 80 content: bytes, 81 filename: str = "logo.png", 82 content_type: str = "image/png", 83 ) -> TeamLogoUploadResponse: 84 """Set the team's logo. Captain only. 85 86 The server sniffs the actual bytes, so ``filename`` and 87 ``content_type`` are conveniences for the multipart envelope, not 88 a way to smuggle a non-image through. 89 """ 90 resp = self._http.request( 91 "POST", 92 f"/competitions/{competition_id}/team/logo", 93 files={"file": (filename, content, content_type)}, 94 ) 95 _raise_for_status(resp) 96 return TeamLogoUploadResponse.model_validate(resp.json()) 97 98 def team_logo(self, competition_id: str, team_id: str) -> bytes: 99 """Fetch a team's logo bytes. 100 101 Streamed through the platform rather than from a bucket URL, so 102 the object store needs no public-read policy. 103 """ 104 resp = self._http.request("GET", f"/competitions/{competition_id}/teams/{team_id}/logo") 105 _raise_for_status(resp) 106 return resp.content
18class RegistrationResource: 19 """Read and submit the calling user's registration for one competition. 20 21 The GET carries the competition's form spec alongside the values, so 22 a client can render the right fields without hard-coding any event's 23 rules — which fields exist and which are mandatory is the organiser's 24 decision, made per competition. 25 """ 26 27 def __init__(self, http: BaseHttpClient) -> None: 28 self._http = http 29 30 def get(self, competition_id: str) -> MyRegistrationResponse: 31 """The caller's dossier plus the form spec that governs it.""" 32 resp = self._http.request("GET", f"/competitions/{competition_id}/registration") 33 _raise_for_status(resp) 34 return MyRegistrationResponse.model_validate(resp.json()) 35 36 def submit(self, competition_id: str, **fields: Any) -> MyRegistrationResponse: 37 """Create or update the caller's dossier. 38 39 An idempotent upsert — resubmitting to correct one field is the 40 normal case, not an error. Fields the competition did not ask for 41 are dropped server-side rather than rejected. 42 43 The response's ``missing_required`` names anything still 44 outstanding, so a caller never has to reimplement the server's 45 completeness rule. 46 """ 47 resp = self._http.request( 48 "PUT", f"/competitions/{competition_id}/registration", json=fields 49 ) 50 _raise_for_status(resp) 51 return MyRegistrationResponse.model_validate(resp.json()) 52 53 def review_timeline(self, competition_id: str) -> MyReviewTimeline: 54 """The caller's own team's review history (#2524): submitted, verdicts 55 with their reasons, reopens and reject to-dos. Standing only -- no 56 reviewer, no field, no value.""" 57 resp = self._http.request("GET", f"/competitions/{competition_id}/my-team/review-timeline") 58 _raise_for_status(resp) 59 return MyReviewTimeline.model_validate(resp.json()) 60 61 def todos(self, competition_id: str) -> MyTeamTodosResponse: 62 """What the team's standing rejection asked the caller to fix (#2527), 63 with the caller's own current value, plus the team's progress as 64 names and items only.""" 65 resp = self._http.request("GET", f"/competitions/{competition_id}/my-team/todos") 66 _raise_for_status(resp) 67 return MyTeamTodosResponse.model_validate(resp.json()) 68 69 def resubmit(self, competition_id: str) -> MyTeamTodosResponse: 70 """Captain only: send a rejected team back to the review queue before 71 every to-do is done (#2527). ``409 review_not_rejected`` when the team 72 is not rejected (a verdict that landed first stands).""" 73 resp = self._http.request("POST", f"/competitions/{competition_id}/my-team/resubmit") 74 _raise_for_status(resp) 75 return MyTeamTodosResponse.model_validate(resp.json()) 76 77 def upload_team_logo( 78 self, 79 competition_id: str, 80 *, 81 content: bytes, 82 filename: str = "logo.png", 83 content_type: str = "image/png", 84 ) -> TeamLogoUploadResponse: 85 """Set the team's logo. Captain only. 86 87 The server sniffs the actual bytes, so ``filename`` and 88 ``content_type`` are conveniences for the multipart envelope, not 89 a way to smuggle a non-image through. 90 """ 91 resp = self._http.request( 92 "POST", 93 f"/competitions/{competition_id}/team/logo", 94 files={"file": (filename, content, content_type)}, 95 ) 96 _raise_for_status(resp) 97 return TeamLogoUploadResponse.model_validate(resp.json()) 98 99 def team_logo(self, competition_id: str, team_id: str) -> bytes: 100 """Fetch a team's logo bytes. 101 102 Streamed through the platform rather than from a bucket URL, so 103 the object store needs no public-read policy. 104 """ 105 resp = self._http.request("GET", f"/competitions/{competition_id}/teams/{team_id}/logo") 106 _raise_for_status(resp) 107 return resp.content
Read and submit the calling user's registration for one competition.
The GET carries the competition's form spec alongside the values, so a client can render the right fields without hard-coding any event's rules — which fields exist and which are mandatory is the organiser's decision, made per competition.
30 def get(self, competition_id: str) -> MyRegistrationResponse: 31 """The caller's dossier plus the form spec that governs it.""" 32 resp = self._http.request("GET", f"/competitions/{competition_id}/registration") 33 _raise_for_status(resp) 34 return MyRegistrationResponse.model_validate(resp.json())
The caller's dossier plus the form spec that governs it.
36 def submit(self, competition_id: str, **fields: Any) -> MyRegistrationResponse: 37 """Create or update the caller's dossier. 38 39 An idempotent upsert — resubmitting to correct one field is the 40 normal case, not an error. Fields the competition did not ask for 41 are dropped server-side rather than rejected. 42 43 The response's ``missing_required`` names anything still 44 outstanding, so a caller never has to reimplement the server's 45 completeness rule. 46 """ 47 resp = self._http.request( 48 "PUT", f"/competitions/{competition_id}/registration", json=fields 49 ) 50 _raise_for_status(resp) 51 return MyRegistrationResponse.model_validate(resp.json())
Create or update the caller's dossier.
An idempotent upsert — resubmitting to correct one field is the normal case, not an error. Fields the competition did not ask for are dropped server-side rather than rejected.
The response's missing_required names anything still
outstanding, so a caller never has to reimplement the server's
completeness rule.
53 def review_timeline(self, competition_id: str) -> MyReviewTimeline: 54 """The caller's own team's review history (#2524): submitted, verdicts 55 with their reasons, reopens and reject to-dos. Standing only -- no 56 reviewer, no field, no value.""" 57 resp = self._http.request("GET", f"/competitions/{competition_id}/my-team/review-timeline") 58 _raise_for_status(resp) 59 return MyReviewTimeline.model_validate(resp.json())
The caller's own team's review history (#2524): submitted, verdicts with their reasons, reopens and reject to-dos. Standing only -- no reviewer, no field, no value.
61 def todos(self, competition_id: str) -> MyTeamTodosResponse: 62 """What the team's standing rejection asked the caller to fix (#2527), 63 with the caller's own current value, plus the team's progress as 64 names and items only.""" 65 resp = self._http.request("GET", f"/competitions/{competition_id}/my-team/todos") 66 _raise_for_status(resp) 67 return MyTeamTodosResponse.model_validate(resp.json())
What the team's standing rejection asked the caller to fix (#2527), with the caller's own current value, plus the team's progress as names and items only.
69 def resubmit(self, competition_id: str) -> MyTeamTodosResponse: 70 """Captain only: send a rejected team back to the review queue before 71 every to-do is done (#2527). ``409 review_not_rejected`` when the team 72 is not rejected (a verdict that landed first stands).""" 73 resp = self._http.request("POST", f"/competitions/{competition_id}/my-team/resubmit") 74 _raise_for_status(resp) 75 return MyTeamTodosResponse.model_validate(resp.json())
Captain only: send a rejected team back to the review queue before
every to-do is done (#2527). 409 review_not_rejected when the team
is not rejected (a verdict that landed first stands).
77 def upload_team_logo( 78 self, 79 competition_id: str, 80 *, 81 content: bytes, 82 filename: str = "logo.png", 83 content_type: str = "image/png", 84 ) -> TeamLogoUploadResponse: 85 """Set the team's logo. Captain only. 86 87 The server sniffs the actual bytes, so ``filename`` and 88 ``content_type`` are conveniences for the multipart envelope, not 89 a way to smuggle a non-image through. 90 """ 91 resp = self._http.request( 92 "POST", 93 f"/competitions/{competition_id}/team/logo", 94 files={"file": (filename, content, content_type)}, 95 ) 96 _raise_for_status(resp) 97 return TeamLogoUploadResponse.model_validate(resp.json())
Set the team's logo. Captain only.
The server sniffs the actual bytes, so filename and
content_type are conveniences for the multipart envelope, not
a way to smuggle a non-image through.
99 def team_logo(self, competition_id: str, team_id: str) -> bytes: 100 """Fetch a team's logo bytes. 101 102 Streamed through the platform rather than from a bucket URL, so 103 the object store needs no public-read policy. 104 """ 105 resp = self._http.request("GET", f"/competitions/{competition_id}/teams/{team_id}/logo") 106 _raise_for_status(resp) 107 return resp.content
Fetch a team's logo bytes.
Streamed through the platform rather than from a bucket URL, so the object store needs no public-read policy.