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

RegistrationResource(http: ctfy.sdk.base.BaseHttpClient)
27    def __init__(self, http: BaseHttpClient) -> None:
28        self._http = http
def get( self, competition_id: str) -> ctfy.server.models.MyRegistrationResponse:
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.

def submit( self, competition_id: str, **fields: Any) -> ctfy.server.models.MyRegistrationResponse:
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.

def review_timeline( self, competition_id: str) -> ctfy.server.models.MyReviewTimeline:
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.

def todos( self, competition_id: str) -> ctfy.server.models.MyTeamTodosResponse:
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.

def resubmit( self, competition_id: str) -> ctfy.server.models.MyTeamTodosResponse:
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).