ctfy.sdk.admin_resources.changelog

client.admin.changelog — authoring, publishing, announcing, syncing.

  1"""``client.admin.changelog`` — authoring, publishing, announcing, syncing."""
  2
  3from __future__ import annotations
  4
  5import builtins
  6from typing import Any
  7
  8from ctfy.sdk._helpers import _raise_for_status
  9from ctfy.sdk.base import BaseHttpClient
 10from ctfy.server.models import (
 11    AdminChangelogEntryInfo,
 12    ChangelogAnnounceResponse,
 13    ChangelogImportEntry,
 14    ChangelogImportResponse,
 15)
 16
 17
 18class AdminChangelogResource:
 19    """The operator's half of the "What's New" surface.
 20
 21    Three verbs kept apart on purpose — see
 22    :mod:`ctfy.server.routes.admin_changelog`: :meth:`sync` refreshes copy
 23    from the repository and never publishes or announces anything,
 24    ``published=True`` puts an entry on the public page, and
 25    :meth:`announce` tells every player, once.
 26    """
 27
 28    def __init__(self, http: BaseHttpClient) -> None:
 29        self._http = http
 30
 31    def list(self) -> builtins.list[AdminChangelogEntryInfo]:
 32        """Every entry, drafts included, newest first."""
 33        resp = self._http.request("GET", "/admin/changelog")
 34        _raise_for_status(resp)
 35        return [AdminChangelogEntryInfo.model_validate(e) for e in resp.json()]
 36
 37    def create(
 38        self,
 39        *,
 40        slug: str,
 41        title: dict[str, str],
 42        body: dict[str, str],
 43        summary: dict[str, str] | None = None,
 44        category: str = "feature",
 45        version: str = "",
 46        emoji: str = "",
 47        published: bool = False,
 48    ) -> AdminChangelogEntryInfo:
 49        """Author one entry. ``title`` / ``summary`` / ``body`` are
 50        ``{locale: text}`` maps and must carry ``en`` — it is the
 51        fallback every other locale reads."""
 52        resp = self._http.request(
 53            "POST",
 54            "/admin/changelog",
 55            json={
 56                "slug": slug,
 57                "category": category,
 58                "version": version,
 59                "title": title,
 60                "summary": summary or {},
 61                "body": body,
 62                "emoji": emoji,
 63                "published": published,
 64            },
 65        )
 66        _raise_for_status(resp)
 67        return AdminChangelogEntryInfo.model_validate(resp.json())
 68
 69    def update(self, entry_id: str, **fields: Any) -> AdminChangelogEntryInfo:
 70        """PATCH any subset. Omitted fields are left alone — pass only
 71        what changed, since sending a field you did not mean to touch is
 72        how the other five get blanked."""
 73        resp = self._http.request("PATCH", f"/admin/changelog/{entry_id}", json=fields)
 74        _raise_for_status(resp)
 75        return AdminChangelogEntryInfo.model_validate(resp.json())
 76
 77    def delete(self, entry_id: str) -> None:
 78        """Remove one entry. Any announcement it created is left alone."""
 79        resp = self._http.request("DELETE", f"/admin/changelog/{entry_id}")
 80        _raise_for_status(resp)
 81
 82    def announce(self, entry_id: str, *, severity: str = "info") -> ChangelogAnnounceResponse:
 83        """Publish (if still a draft) and tell every player, once.
 84
 85        Idempotent: a second call reports the entry unchanged with an
 86        empty ``announcement_id``. ``severity`` decides whether the
 87        notice is also mailed — the platform mails ``warning`` and
 88        ``critical`` announcements only.
 89        """
 90        resp = self._http.request(
 91            "POST", f"/admin/changelog/{entry_id}/announce", json={"severity": severity}
 92        )
 93        _raise_for_status(resp)
 94        return ChangelogAnnounceResponse.model_validate(resp.json())
 95
 96    def sync(self, entries: builtins.list[ChangelogImportEntry]) -> ChangelogImportResponse:
 97        """Upsert the repository's authored entries. Idempotent on slug.
 98
 99        Safe to run on every deploy: a ``date`` fills ``published_at``
100        but never overwrites one, and nothing here ever announces.
101        """
102        resp = self._http.request(
103            "POST",
104            "/admin/changelog/import",
105            json={"entries": [e.model_dump(mode="json") for e in entries]},
106        )
107        _raise_for_status(resp)
108        return ChangelogImportResponse.model_validate(resp.json())
class AdminChangelogResource:
 19class AdminChangelogResource:
 20    """The operator's half of the "What's New" surface.
 21
 22    Three verbs kept apart on purpose — see
 23    :mod:`ctfy.server.routes.admin_changelog`: :meth:`sync` refreshes copy
 24    from the repository and never publishes or announces anything,
 25    ``published=True`` puts an entry on the public page, and
 26    :meth:`announce` tells every player, once.
 27    """
 28
 29    def __init__(self, http: BaseHttpClient) -> None:
 30        self._http = http
 31
 32    def list(self) -> builtins.list[AdminChangelogEntryInfo]:
 33        """Every entry, drafts included, newest first."""
 34        resp = self._http.request("GET", "/admin/changelog")
 35        _raise_for_status(resp)
 36        return [AdminChangelogEntryInfo.model_validate(e) for e in resp.json()]
 37
 38    def create(
 39        self,
 40        *,
 41        slug: str,
 42        title: dict[str, str],
 43        body: dict[str, str],
 44        summary: dict[str, str] | None = None,
 45        category: str = "feature",
 46        version: str = "",
 47        emoji: str = "",
 48        published: bool = False,
 49    ) -> AdminChangelogEntryInfo:
 50        """Author one entry. ``title`` / ``summary`` / ``body`` are
 51        ``{locale: text}`` maps and must carry ``en`` — it is the
 52        fallback every other locale reads."""
 53        resp = self._http.request(
 54            "POST",
 55            "/admin/changelog",
 56            json={
 57                "slug": slug,
 58                "category": category,
 59                "version": version,
 60                "title": title,
 61                "summary": summary or {},
 62                "body": body,
 63                "emoji": emoji,
 64                "published": published,
 65            },
 66        )
 67        _raise_for_status(resp)
 68        return AdminChangelogEntryInfo.model_validate(resp.json())
 69
 70    def update(self, entry_id: str, **fields: Any) -> AdminChangelogEntryInfo:
 71        """PATCH any subset. Omitted fields are left alone — pass only
 72        what changed, since sending a field you did not mean to touch is
 73        how the other five get blanked."""
 74        resp = self._http.request("PATCH", f"/admin/changelog/{entry_id}", json=fields)
 75        _raise_for_status(resp)
 76        return AdminChangelogEntryInfo.model_validate(resp.json())
 77
 78    def delete(self, entry_id: str) -> None:
 79        """Remove one entry. Any announcement it created is left alone."""
 80        resp = self._http.request("DELETE", f"/admin/changelog/{entry_id}")
 81        _raise_for_status(resp)
 82
 83    def announce(self, entry_id: str, *, severity: str = "info") -> ChangelogAnnounceResponse:
 84        """Publish (if still a draft) and tell every player, once.
 85
 86        Idempotent: a second call reports the entry unchanged with an
 87        empty ``announcement_id``. ``severity`` decides whether the
 88        notice is also mailed — the platform mails ``warning`` and
 89        ``critical`` announcements only.
 90        """
 91        resp = self._http.request(
 92            "POST", f"/admin/changelog/{entry_id}/announce", json={"severity": severity}
 93        )
 94        _raise_for_status(resp)
 95        return ChangelogAnnounceResponse.model_validate(resp.json())
 96
 97    def sync(self, entries: builtins.list[ChangelogImportEntry]) -> ChangelogImportResponse:
 98        """Upsert the repository's authored entries. Idempotent on slug.
 99
100        Safe to run on every deploy: a ``date`` fills ``published_at``
101        but never overwrites one, and nothing here ever announces.
102        """
103        resp = self._http.request(
104            "POST",
105            "/admin/changelog/import",
106            json={"entries": [e.model_dump(mode="json") for e in entries]},
107        )
108        _raise_for_status(resp)
109        return ChangelogImportResponse.model_validate(resp.json())

The operator's half of the "What's New" surface.

Three verbs kept apart on purpose — see ctfy.server.routes.admin_changelog: sync() refreshes copy from the repository and never publishes or announces anything, published=True puts an entry on the public page, and announce() tells every player, once.

AdminChangelogResource(http: ctfy.sdk.base.BaseHttpClient)
29    def __init__(self, http: BaseHttpClient) -> None:
30        self._http = http
def list(self) -> list[ctfy.server.models.AdminChangelogEntryInfo]:
32    def list(self) -> builtins.list[AdminChangelogEntryInfo]:
33        """Every entry, drafts included, newest first."""
34        resp = self._http.request("GET", "/admin/changelog")
35        _raise_for_status(resp)
36        return [AdminChangelogEntryInfo.model_validate(e) for e in resp.json()]

Every entry, drafts included, newest first.

def create( self, *, slug: str, title: dict[str, str], body: dict[str, str], summary: dict[str, str] | None = None, category: str = 'feature', version: str = '', emoji: str = '', published: bool = False) -> ctfy.server.models.AdminChangelogEntryInfo:
38    def create(
39        self,
40        *,
41        slug: str,
42        title: dict[str, str],
43        body: dict[str, str],
44        summary: dict[str, str] | None = None,
45        category: str = "feature",
46        version: str = "",
47        emoji: str = "",
48        published: bool = False,
49    ) -> AdminChangelogEntryInfo:
50        """Author one entry. ``title`` / ``summary`` / ``body`` are
51        ``{locale: text}`` maps and must carry ``en`` — it is the
52        fallback every other locale reads."""
53        resp = self._http.request(
54            "POST",
55            "/admin/changelog",
56            json={
57                "slug": slug,
58                "category": category,
59                "version": version,
60                "title": title,
61                "summary": summary or {},
62                "body": body,
63                "emoji": emoji,
64                "published": published,
65            },
66        )
67        _raise_for_status(resp)
68        return AdminChangelogEntryInfo.model_validate(resp.json())

Author one entry. title / summary / body are {locale: text} maps and must carry en — it is the fallback every other locale reads.

def update( self, entry_id: str, **fields: Any) -> ctfy.server.models.AdminChangelogEntryInfo:
70    def update(self, entry_id: str, **fields: Any) -> AdminChangelogEntryInfo:
71        """PATCH any subset. Omitted fields are left alone — pass only
72        what changed, since sending a field you did not mean to touch is
73        how the other five get blanked."""
74        resp = self._http.request("PATCH", f"/admin/changelog/{entry_id}", json=fields)
75        _raise_for_status(resp)
76        return AdminChangelogEntryInfo.model_validate(resp.json())

PATCH any subset. Omitted fields are left alone — pass only what changed, since sending a field you did not mean to touch is how the other five get blanked.

def delete(self, entry_id: str) -> None:
78    def delete(self, entry_id: str) -> None:
79        """Remove one entry. Any announcement it created is left alone."""
80        resp = self._http.request("DELETE", f"/admin/changelog/{entry_id}")
81        _raise_for_status(resp)

Remove one entry. Any announcement it created is left alone.

def announce( self, entry_id: str, *, severity: str = 'info') -> ctfy.server.models.ChangelogAnnounceResponse:
83    def announce(self, entry_id: str, *, severity: str = "info") -> ChangelogAnnounceResponse:
84        """Publish (if still a draft) and tell every player, once.
85
86        Idempotent: a second call reports the entry unchanged with an
87        empty ``announcement_id``. ``severity`` decides whether the
88        notice is also mailed — the platform mails ``warning`` and
89        ``critical`` announcements only.
90        """
91        resp = self._http.request(
92            "POST", f"/admin/changelog/{entry_id}/announce", json={"severity": severity}
93        )
94        _raise_for_status(resp)
95        return ChangelogAnnounceResponse.model_validate(resp.json())

Publish (if still a draft) and tell every player, once.

Idempotent: a second call reports the entry unchanged with an empty announcement_id. severity decides whether the notice is also mailed — the platform mails warning and critical announcements only.

 97    def sync(self, entries: builtins.list[ChangelogImportEntry]) -> ChangelogImportResponse:
 98        """Upsert the repository's authored entries. Idempotent on slug.
 99
100        Safe to run on every deploy: a ``date`` fills ``published_at``
101        but never overwrites one, and nothing here ever announces.
102        """
103        resp = self._http.request(
104            "POST",
105            "/admin/changelog/import",
106            json={"entries": [e.model_dump(mode="json") for e in entries]},
107        )
108        _raise_for_status(resp)
109        return ChangelogImportResponse.model_validate(resp.json())

Upsert the repository's authored entries. Idempotent on slug.

Safe to run on every deploy: a date fills published_at but never overwrites one, and nothing here ever announces.