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())
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.
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.
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.
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.
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.
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.