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