ctfy.sdk.admin_resources.institutions

client.admin.institutions — the domain → institution catalogue.

  1"""``client.admin.institutions`` — the domain → institution catalogue."""
  2
  3from __future__ import annotations
  4
  5import builtins
  6
  7from ctfy.core.enums import InstitutionKind
  8from ctfy.sdk._helpers import _raise_for_status
  9from ctfy.sdk.base import BaseHttpClient
 10from ctfy.server.models import (
 11    InstitutionInfo,
 12    InstitutionPage,
 13    InstitutionSuggestionPage,
 14)
 15
 16
 17class AdminInstitutionsResource:
 18    """Which email domains this deployment recognises, and as what.
 19
 20    A catalogue an operator curates, not a fact about anybody: naming
 21    ``tsinghua.edu.cn`` lets a page say *holds a verified address at
 22    Tsinghua*, and says nothing about enrolment — alumni addresses
 23    outlast graduation and a contractor holds a ``@bigcorp.com`` one.
 24
 25    :meth:`suggest` is where a catalogue starts: it measures the domains
 26    this deployment's own people hold **proved** addresses at, so the
 27    list is evidence rather than a guess at somebody else's DNS.
 28
 29    ⚠️ **:meth:`list` is admin; everything else is super-admin.** Naming
 30    a domain changes what every page says about every account on the
 31    deployment, for every event — while reading which domains are named
 32    is a different act with a second caller, ``/admin/users``'
 33    institution filter.
 34    """
 35
 36    def __init__(self, http: BaseHttpClient) -> None:
 37        self._http = http
 38
 39    def list(self) -> InstitutionPage:
 40        """Every named domain.
 41
 42        Unwindowed — the population is what one operator typed, and
 43        ``total`` is the count rather than an answer to "was this cut".
 44        """
 45        resp = self._http.request("GET", "/admin/institutions")
 46        _raise_for_status(resp)
 47        return InstitutionPage.model_validate(resp.json())
 48
 49    def suggest(self, *, limit: int = 20) -> InstitutionSuggestionPage:
 50        """Domains worth naming next, busiest first.
 51
 52        Already-named domains drop out as you work — including their
 53        subdomains, since naming ``tsinghua.edu.cn`` is what makes
 54        ``mails.tsinghua.edu.cn`` resolve. ``total`` is the whole
 55        remainder, so it says how much is left rather than how much came
 56        back.
 57        """
 58        resp = self._http.request("GET", "/admin/institutions/suggest", params={"limit": limit})
 59        _raise_for_status(resp)
 60        return InstitutionSuggestionPage.model_validate(resp.json())
 61
 62    def put(
 63        self,
 64        domain: str,
 65        *,
 66        names: dict[str, str] | None = None,
 67        short_name: str = "",
 68        kind: InstitutionKind = "other",
 69        url: str = "",
 70    ) -> InstitutionInfo:
 71        """Name a domain, or replace what one is named.
 72
 73        An upsert: the domain is the identity, so renaming one is a
 74        :meth:`delete` and a :meth:`put`. ``names`` is a
 75        ``{locale: text}`` map, because 清华大学 and *Tsinghua
 76        University* are one institution and a single column publishes one
 77        language to everybody.
 78        """
 79        body: dict[str, object] = {
 80            "domain": domain,
 81            "names": names or {},
 82            "short_name": short_name,
 83            "kind": kind,
 84        }
 85        # ⚠️ Omitted rather than sent blank: the field is ``WebUrl |
 86        # None`` and ``""`` is not a URL, so a caller who names no link
 87        # would get a 422 for saying nothing.
 88        if url:
 89            body["url"] = url
 90        resp = self._http.request("POST", "/admin/institutions", json=body)
 91        _raise_for_status(resp)
 92        return InstitutionInfo.model_validate(resp.json())
 93
 94    def delete(self, domain: str) -> InstitutionInfo:
 95        """Stop naming a domain, and answer with the row that went.
 96
 97        No account holds a foreign key here, so this takes a label off
 98        some pages and takes nothing away from anybody.
 99        """
100        resp = self._http.request("DELETE", f"/admin/institutions/{domain}")
101        _raise_for_status(resp)
102        return InstitutionInfo.model_validate(resp.json())
103
104
105__all__: builtins.list[str] = ["AdminInstitutionsResource"]
class AdminInstitutionsResource:
 18class AdminInstitutionsResource:
 19    """Which email domains this deployment recognises, and as what.
 20
 21    A catalogue an operator curates, not a fact about anybody: naming
 22    ``tsinghua.edu.cn`` lets a page say *holds a verified address at
 23    Tsinghua*, and says nothing about enrolment — alumni addresses
 24    outlast graduation and a contractor holds a ``@bigcorp.com`` one.
 25
 26    :meth:`suggest` is where a catalogue starts: it measures the domains
 27    this deployment's own people hold **proved** addresses at, so the
 28    list is evidence rather than a guess at somebody else's DNS.
 29
 30    ⚠️ **:meth:`list` is admin; everything else is super-admin.** Naming
 31    a domain changes what every page says about every account on the
 32    deployment, for every event — while reading which domains are named
 33    is a different act with a second caller, ``/admin/users``'
 34    institution filter.
 35    """
 36
 37    def __init__(self, http: BaseHttpClient) -> None:
 38        self._http = http
 39
 40    def list(self) -> InstitutionPage:
 41        """Every named domain.
 42
 43        Unwindowed — the population is what one operator typed, and
 44        ``total`` is the count rather than an answer to "was this cut".
 45        """
 46        resp = self._http.request("GET", "/admin/institutions")
 47        _raise_for_status(resp)
 48        return InstitutionPage.model_validate(resp.json())
 49
 50    def suggest(self, *, limit: int = 20) -> InstitutionSuggestionPage:
 51        """Domains worth naming next, busiest first.
 52
 53        Already-named domains drop out as you work — including their
 54        subdomains, since naming ``tsinghua.edu.cn`` is what makes
 55        ``mails.tsinghua.edu.cn`` resolve. ``total`` is the whole
 56        remainder, so it says how much is left rather than how much came
 57        back.
 58        """
 59        resp = self._http.request("GET", "/admin/institutions/suggest", params={"limit": limit})
 60        _raise_for_status(resp)
 61        return InstitutionSuggestionPage.model_validate(resp.json())
 62
 63    def put(
 64        self,
 65        domain: str,
 66        *,
 67        names: dict[str, str] | None = None,
 68        short_name: str = "",
 69        kind: InstitutionKind = "other",
 70        url: str = "",
 71    ) -> InstitutionInfo:
 72        """Name a domain, or replace what one is named.
 73
 74        An upsert: the domain is the identity, so renaming one is a
 75        :meth:`delete` and a :meth:`put`. ``names`` is a
 76        ``{locale: text}`` map, because 清华大学 and *Tsinghua
 77        University* are one institution and a single column publishes one
 78        language to everybody.
 79        """
 80        body: dict[str, object] = {
 81            "domain": domain,
 82            "names": names or {},
 83            "short_name": short_name,
 84            "kind": kind,
 85        }
 86        # ⚠️ Omitted rather than sent blank: the field is ``WebUrl |
 87        # None`` and ``""`` is not a URL, so a caller who names no link
 88        # would get a 422 for saying nothing.
 89        if url:
 90            body["url"] = url
 91        resp = self._http.request("POST", "/admin/institutions", json=body)
 92        _raise_for_status(resp)
 93        return InstitutionInfo.model_validate(resp.json())
 94
 95    def delete(self, domain: str) -> InstitutionInfo:
 96        """Stop naming a domain, and answer with the row that went.
 97
 98        No account holds a foreign key here, so this takes a label off
 99        some pages and takes nothing away from anybody.
100        """
101        resp = self._http.request("DELETE", f"/admin/institutions/{domain}")
102        _raise_for_status(resp)
103        return InstitutionInfo.model_validate(resp.json())

Which email domains this deployment recognises, and as what.

A catalogue an operator curates, not a fact about anybody: naming tsinghua.edu.cn lets a page say holds a verified address at Tsinghua, and says nothing about enrolment — alumni addresses outlast graduation and a contractor holds a @bigcorp.com one.

suggest() is where a catalogue starts: it measures the domains this deployment's own people hold proved addresses at, so the list is evidence rather than a guess at somebody else's DNS.

⚠️ list() is admin; everything else is super-admin. Naming a domain changes what every page says about every account on the deployment, for every event — while reading which domains are named is a different act with a second caller, /admin/users' institution filter.

AdminInstitutionsResource(http: ctfy.sdk.base.BaseHttpClient)
37    def __init__(self, http: BaseHttpClient) -> None:
38        self._http = http
def list(self) -> ctfy.server.models.InstitutionPage:
40    def list(self) -> InstitutionPage:
41        """Every named domain.
42
43        Unwindowed — the population is what one operator typed, and
44        ``total`` is the count rather than an answer to "was this cut".
45        """
46        resp = self._http.request("GET", "/admin/institutions")
47        _raise_for_status(resp)
48        return InstitutionPage.model_validate(resp.json())

Every named domain.

Unwindowed — the population is what one operator typed, and total is the count rather than an answer to "was this cut".

def suggest( self, *, limit: int = 20) -> ctfy.server.models.InstitutionSuggestionPage:
50    def suggest(self, *, limit: int = 20) -> InstitutionSuggestionPage:
51        """Domains worth naming next, busiest first.
52
53        Already-named domains drop out as you work — including their
54        subdomains, since naming ``tsinghua.edu.cn`` is what makes
55        ``mails.tsinghua.edu.cn`` resolve. ``total`` is the whole
56        remainder, so it says how much is left rather than how much came
57        back.
58        """
59        resp = self._http.request("GET", "/admin/institutions/suggest", params={"limit": limit})
60        _raise_for_status(resp)
61        return InstitutionSuggestionPage.model_validate(resp.json())

Domains worth naming next, busiest first.

Already-named domains drop out as you work — including their subdomains, since naming tsinghua.edu.cn is what makes mails.tsinghua.edu.cn resolve. total is the whole remainder, so it says how much is left rather than how much came back.

def put( self, domain: str, *, names: dict[str, str] | None = None, short_name: str = '', kind: Literal['university', 'company', 'other'] = 'other', url: str = '') -> ctfy.server.models.InstitutionInfo:
63    def put(
64        self,
65        domain: str,
66        *,
67        names: dict[str, str] | None = None,
68        short_name: str = "",
69        kind: InstitutionKind = "other",
70        url: str = "",
71    ) -> InstitutionInfo:
72        """Name a domain, or replace what one is named.
73
74        An upsert: the domain is the identity, so renaming one is a
75        :meth:`delete` and a :meth:`put`. ``names`` is a
76        ``{locale: text}`` map, because 清华大学 and *Tsinghua
77        University* are one institution and a single column publishes one
78        language to everybody.
79        """
80        body: dict[str, object] = {
81            "domain": domain,
82            "names": names or {},
83            "short_name": short_name,
84            "kind": kind,
85        }
86        # ⚠️ Omitted rather than sent blank: the field is ``WebUrl |
87        # None`` and ``""`` is not a URL, so a caller who names no link
88        # would get a 422 for saying nothing.
89        if url:
90            body["url"] = url
91        resp = self._http.request("POST", "/admin/institutions", json=body)
92        _raise_for_status(resp)
93        return InstitutionInfo.model_validate(resp.json())

Name a domain, or replace what one is named.

An upsert: the domain is the identity, so renaming one is a delete() and a put(). names is a {locale: text} map, because 清华大学 and Tsinghua University are one institution and a single column publishes one language to everybody.

def delete(self, domain: str) -> ctfy.server.models.InstitutionInfo:
 95    def delete(self, domain: str) -> InstitutionInfo:
 96        """Stop naming a domain, and answer with the row that went.
 97
 98        No account holds a foreign key here, so this takes a label off
 99        some pages and takes nothing away from anybody.
100        """
101        resp = self._http.request("DELETE", f"/admin/institutions/{domain}")
102        _raise_for_status(resp)
103        return InstitutionInfo.model_validate(resp.json())

Stop naming a domain, and answer with the row that went.

No account holds a foreign key here, so this takes a label off some pages and takes nothing away from anybody.