ctfy.sdk.admin_resources.recap

client.admin.recap — a competition's recap for its organisers (#3023): the figures, what the organisers stated for it, publishing it, and the report email to the organising committee.

  1"""``client.admin.recap`` — a competition's recap for its organisers (#3023):
  2the figures, what the organisers stated for it, publishing it, and the
  3report email to the organising committee."""
  4
  5from __future__ import annotations
  6
  7from collections.abc import Sequence
  8from typing import Any
  9
 10from ctfy.core.state.models import RecapMailLanguage
 11from ctfy.sdk._helpers import _raise_for_status
 12from ctfy.sdk.base import BaseHttpClient
 13from ctfy.server.models import (
 14    AwardBandInfo,
 15    CompetitionRecapMail,
 16    CompetitionRecapMailUpdate,
 17    CompetitionRecapSettings,
 18    CompetitionRecapSettingsUpdate,
 19    Recap,
 20    RecapMailPreview,
 21    RecapSendInfo,
 22)
 23
 24
 25class AdminRecapResource:
 26    """Read and run one competition's recap. For its organisers (global admins
 27    and the event's own competition admins)."""
 28
 29    def __init__(self, http: BaseHttpClient) -> None:
 30        self._http = http
 31
 32    def get(self, competition_id: str) -> Recap:
 33        """The recap now: reach, contest, integrity, platform and what needs
 34        attention. The figures are the public board's (frozen, anonymised)."""
 35        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/recap")
 36        _raise_for_status(resp)
 37        return Recap.model_validate(resp.json())
 38
 39    def settings(self, competition_id: str) -> CompetitionRecapSettings:
 40        """The registration target, previous edition, prize tiers and whether
 41        the recap is published."""
 42        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/recap-settings")
 43        _raise_for_status(resp)
 44        return CompetitionRecapSettings.model_validate(resp.json())
 45
 46    def update_settings(
 47        self,
 48        competition_id: str,
 49        *,
 50        registration_target: int | None = None,
 51        previous_edition_id: str | None = None,
 52        award_bands: Sequence[AwardBandInfo] | None = None,
 53        published: bool | None = None,
 54    ) -> CompetitionRecapSettings:
 55        """Set any of them; ``None`` leaves a field as stored.
 56
 57        ``previous_edition_id=""`` clears the link. ``published=True`` is
 58        refused with ``recap_not_ended`` (409) until the event has ended;
 59        ``False`` withdraws it in any phase.
 60        """
 61        body = CompetitionRecapSettingsUpdate(
 62            registration_target=registration_target,
 63            previous_edition_id=previous_edition_id,
 64            award_bands=None if award_bands is None else list(award_bands),
 65            recap_published=published,
 66        )
 67        resp = self._http.request(
 68            "PUT",
 69            f"/admin/competitions/{competition_id}/recap-settings",
 70            json=body.model_dump(mode="json", exclude_none=True),
 71        )
 72        _raise_for_status(resp)
 73        return CompetitionRecapSettings.model_validate(resp.json())
 74
 75    def mail(self, competition_id: str) -> CompetitionRecapMail:
 76        """The report email's switch, language, recipients (masked), the
 77        report due now and the last sends."""
 78        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/recap-mail")
 79        _raise_for_status(resp)
 80        return CompetitionRecapMail.model_validate(resp.json())
 81
 82    def update_mail(
 83        self,
 84        competition_id: str,
 85        *,
 86        enabled: bool | None = None,
 87        language: RecapMailLanguage | None = None,
 88        add_recipients: Sequence[str] = (),
 89        remove_recipients: Sequence[str] = (),
 90    ) -> CompetitionRecapMail:
 91        """Switch the email on or off, choose its language, add addresses
 92        whole and remove them by the ``id`` :meth:`mail` lists."""
 93        body = CompetitionRecapMailUpdate(
 94            enabled=enabled,
 95            language=language,
 96            add_recipients=list(add_recipients) or None,
 97            remove_recipients=list(remove_recipients) or None,
 98        )
 99        payload: dict[str, Any] = body.model_dump(mode="json", exclude_none=True)
100        resp = self._http.request(
101            "PUT", f"/admin/competitions/{competition_id}/recap-mail", json=payload
102        )
103        _raise_for_status(resp)
104        return CompetitionRecapMail.model_validate(resp.json())
105
106    def preview_mail(self, competition_id: str) -> RecapMailPreview:
107        """The mail :meth:`send_mail` would send, rendered now. Its link is a
108        placeholder: a preview mints no credential."""
109        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/recap-mail/preview")
110        _raise_for_status(resp)
111        return RecapMailPreview.model_validate(resp.json())
112
113    def send_mail(self, competition_id: str) -> RecapSendInfo:
114        """Mail the recap to every recipient now. 409 ``recap_mail_no_recipients``
115        or ``recap_mail_too_soon`` (``detail.retry_after_s``)."""
116        resp = self._http.request("POST", f"/admin/competitions/{competition_id}/recap-mail/send")
117        _raise_for_status(resp)
118        return RecapSendInfo.model_validate(resp.json())
119
120    def revoke_links(self, competition_id: str) -> CompetitionRecapMail:
121        """Stop every read-only link this competition's emails have carried."""
122        resp = self._http.request(
123            "POST", f"/admin/competitions/{competition_id}/recap-mail/revoke-links"
124        )
125        _raise_for_status(resp)
126        return CompetitionRecapMail.model_validate(resp.json())
class AdminRecapResource:
 26class AdminRecapResource:
 27    """Read and run one competition's recap. For its organisers (global admins
 28    and the event's own competition admins)."""
 29
 30    def __init__(self, http: BaseHttpClient) -> None:
 31        self._http = http
 32
 33    def get(self, competition_id: str) -> Recap:
 34        """The recap now: reach, contest, integrity, platform and what needs
 35        attention. The figures are the public board's (frozen, anonymised)."""
 36        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/recap")
 37        _raise_for_status(resp)
 38        return Recap.model_validate(resp.json())
 39
 40    def settings(self, competition_id: str) -> CompetitionRecapSettings:
 41        """The registration target, previous edition, prize tiers and whether
 42        the recap is published."""
 43        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/recap-settings")
 44        _raise_for_status(resp)
 45        return CompetitionRecapSettings.model_validate(resp.json())
 46
 47    def update_settings(
 48        self,
 49        competition_id: str,
 50        *,
 51        registration_target: int | None = None,
 52        previous_edition_id: str | None = None,
 53        award_bands: Sequence[AwardBandInfo] | None = None,
 54        published: bool | None = None,
 55    ) -> CompetitionRecapSettings:
 56        """Set any of them; ``None`` leaves a field as stored.
 57
 58        ``previous_edition_id=""`` clears the link. ``published=True`` is
 59        refused with ``recap_not_ended`` (409) until the event has ended;
 60        ``False`` withdraws it in any phase.
 61        """
 62        body = CompetitionRecapSettingsUpdate(
 63            registration_target=registration_target,
 64            previous_edition_id=previous_edition_id,
 65            award_bands=None if award_bands is None else list(award_bands),
 66            recap_published=published,
 67        )
 68        resp = self._http.request(
 69            "PUT",
 70            f"/admin/competitions/{competition_id}/recap-settings",
 71            json=body.model_dump(mode="json", exclude_none=True),
 72        )
 73        _raise_for_status(resp)
 74        return CompetitionRecapSettings.model_validate(resp.json())
 75
 76    def mail(self, competition_id: str) -> CompetitionRecapMail:
 77        """The report email's switch, language, recipients (masked), the
 78        report due now and the last sends."""
 79        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/recap-mail")
 80        _raise_for_status(resp)
 81        return CompetitionRecapMail.model_validate(resp.json())
 82
 83    def update_mail(
 84        self,
 85        competition_id: str,
 86        *,
 87        enabled: bool | None = None,
 88        language: RecapMailLanguage | None = None,
 89        add_recipients: Sequence[str] = (),
 90        remove_recipients: Sequence[str] = (),
 91    ) -> CompetitionRecapMail:
 92        """Switch the email on or off, choose its language, add addresses
 93        whole and remove them by the ``id`` :meth:`mail` lists."""
 94        body = CompetitionRecapMailUpdate(
 95            enabled=enabled,
 96            language=language,
 97            add_recipients=list(add_recipients) or None,
 98            remove_recipients=list(remove_recipients) or None,
 99        )
100        payload: dict[str, Any] = body.model_dump(mode="json", exclude_none=True)
101        resp = self._http.request(
102            "PUT", f"/admin/competitions/{competition_id}/recap-mail", json=payload
103        )
104        _raise_for_status(resp)
105        return CompetitionRecapMail.model_validate(resp.json())
106
107    def preview_mail(self, competition_id: str) -> RecapMailPreview:
108        """The mail :meth:`send_mail` would send, rendered now. Its link is a
109        placeholder: a preview mints no credential."""
110        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/recap-mail/preview")
111        _raise_for_status(resp)
112        return RecapMailPreview.model_validate(resp.json())
113
114    def send_mail(self, competition_id: str) -> RecapSendInfo:
115        """Mail the recap to every recipient now. 409 ``recap_mail_no_recipients``
116        or ``recap_mail_too_soon`` (``detail.retry_after_s``)."""
117        resp = self._http.request("POST", f"/admin/competitions/{competition_id}/recap-mail/send")
118        _raise_for_status(resp)
119        return RecapSendInfo.model_validate(resp.json())
120
121    def revoke_links(self, competition_id: str) -> CompetitionRecapMail:
122        """Stop every read-only link this competition's emails have carried."""
123        resp = self._http.request(
124            "POST", f"/admin/competitions/{competition_id}/recap-mail/revoke-links"
125        )
126        _raise_for_status(resp)
127        return CompetitionRecapMail.model_validate(resp.json())

Read and run one competition's recap. For its organisers (global admins and the event's own competition admins).

AdminRecapResource(http: ctfy.sdk.base.BaseHttpClient)
30    def __init__(self, http: BaseHttpClient) -> None:
31        self._http = http
def get(self, competition_id: str) -> ctfy.core.recap.Recap:
33    def get(self, competition_id: str) -> Recap:
34        """The recap now: reach, contest, integrity, platform and what needs
35        attention. The figures are the public board's (frozen, anonymised)."""
36        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/recap")
37        _raise_for_status(resp)
38        return Recap.model_validate(resp.json())

The recap now: reach, contest, integrity, platform and what needs attention. The figures are the public board's (frozen, anonymised).

def settings( self, competition_id: str) -> ctfy.server.models.CompetitionRecapSettings:
40    def settings(self, competition_id: str) -> CompetitionRecapSettings:
41        """The registration target, previous edition, prize tiers and whether
42        the recap is published."""
43        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/recap-settings")
44        _raise_for_status(resp)
45        return CompetitionRecapSettings.model_validate(resp.json())

The registration target, previous edition, prize tiers and whether the recap is published.

def update_settings( self, competition_id: str, *, registration_target: int | None = None, previous_edition_id: str | None = None, award_bands: Sequence[ctfy.server.models.AwardBandInfo] | None = None, published: bool | None = None) -> ctfy.server.models.CompetitionRecapSettings:
47    def update_settings(
48        self,
49        competition_id: str,
50        *,
51        registration_target: int | None = None,
52        previous_edition_id: str | None = None,
53        award_bands: Sequence[AwardBandInfo] | None = None,
54        published: bool | None = None,
55    ) -> CompetitionRecapSettings:
56        """Set any of them; ``None`` leaves a field as stored.
57
58        ``previous_edition_id=""`` clears the link. ``published=True`` is
59        refused with ``recap_not_ended`` (409) until the event has ended;
60        ``False`` withdraws it in any phase.
61        """
62        body = CompetitionRecapSettingsUpdate(
63            registration_target=registration_target,
64            previous_edition_id=previous_edition_id,
65            award_bands=None if award_bands is None else list(award_bands),
66            recap_published=published,
67        )
68        resp = self._http.request(
69            "PUT",
70            f"/admin/competitions/{competition_id}/recap-settings",
71            json=body.model_dump(mode="json", exclude_none=True),
72        )
73        _raise_for_status(resp)
74        return CompetitionRecapSettings.model_validate(resp.json())

Set any of them; None leaves a field as stored.

previous_edition_id="" clears the link. published=True is refused with recap_not_ended (409) until the event has ended; False withdraws it in any phase.

def mail( self, competition_id: str) -> ctfy.server.models.CompetitionRecapMail:
76    def mail(self, competition_id: str) -> CompetitionRecapMail:
77        """The report email's switch, language, recipients (masked), the
78        report due now and the last sends."""
79        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/recap-mail")
80        _raise_for_status(resp)
81        return CompetitionRecapMail.model_validate(resp.json())

The report email's switch, language, recipients (masked), the report due now and the last sends.

def update_mail( self, competition_id: str, *, enabled: bool | None = None, language: Optional[Literal['zh-CN', 'en', 'both']] = None, add_recipients: Sequence[str] = (), remove_recipients: Sequence[str] = ()) -> ctfy.server.models.CompetitionRecapMail:
 83    def update_mail(
 84        self,
 85        competition_id: str,
 86        *,
 87        enabled: bool | None = None,
 88        language: RecapMailLanguage | None = None,
 89        add_recipients: Sequence[str] = (),
 90        remove_recipients: Sequence[str] = (),
 91    ) -> CompetitionRecapMail:
 92        """Switch the email on or off, choose its language, add addresses
 93        whole and remove them by the ``id`` :meth:`mail` lists."""
 94        body = CompetitionRecapMailUpdate(
 95            enabled=enabled,
 96            language=language,
 97            add_recipients=list(add_recipients) or None,
 98            remove_recipients=list(remove_recipients) or None,
 99        )
100        payload: dict[str, Any] = body.model_dump(mode="json", exclude_none=True)
101        resp = self._http.request(
102            "PUT", f"/admin/competitions/{competition_id}/recap-mail", json=payload
103        )
104        _raise_for_status(resp)
105        return CompetitionRecapMail.model_validate(resp.json())

Switch the email on or off, choose its language, add addresses whole and remove them by the id mail() lists.

def preview_mail(self, competition_id: str) -> ctfy.server.models.RecapMailPreview:
107    def preview_mail(self, competition_id: str) -> RecapMailPreview:
108        """The mail :meth:`send_mail` would send, rendered now. Its link is a
109        placeholder: a preview mints no credential."""
110        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/recap-mail/preview")
111        _raise_for_status(resp)
112        return RecapMailPreview.model_validate(resp.json())

The mail send_mail() would send, rendered now. Its link is a placeholder: a preview mints no credential.

def send_mail(self, competition_id: str) -> ctfy.server.models.RecapSendInfo:
114    def send_mail(self, competition_id: str) -> RecapSendInfo:
115        """Mail the recap to every recipient now. 409 ``recap_mail_no_recipients``
116        or ``recap_mail_too_soon`` (``detail.retry_after_s``)."""
117        resp = self._http.request("POST", f"/admin/competitions/{competition_id}/recap-mail/send")
118        _raise_for_status(resp)
119        return RecapSendInfo.model_validate(resp.json())

Mail the recap to every recipient now. 409 recap_mail_no_recipients or recap_mail_too_soon (detail.retry_after_s).