ispconfig-py/src/ispconfig/types.py
Kayos 9438b4e751 feat: initial ispconfig-py SDK (sites / dns / mail / databases / clients)
Python 3.10+ SDK wrapping the ISPConfig remote SOAP API so we stop writing
throwaway PHP snippets every time we need to touch a site, zone, or mailbox.

Why no zeep: ISPConfig's /remote/index.php exposes PHP SoapServer in non-WSDL
mode and refuses WSDL generation (?wsdl returns a fault). zeep requires WSDL,
so the stated dependency wouldn't actually work. Instead we hand-roll SOAP
envelopes with the stdlib (urllib + xml.etree). Zero runtime deps.

Structure:
- src/ispconfig/_soap.py        — envelope encode/decode, fault surfacing
- src/ispconfig/client.py       — ISPConfigClient context manager, retry
- src/ispconfig/exceptions.py   — ISPConfigError / Auth / Permission / NotFound / Fault
- src/ispconfig/sites.py        — web_domain get/add/update/delete + enable_php / enable_letsencrypt helpers
- src/ispconfig/dns.py          — zones + A/CNAME/MX/TXT records, dns_a_add type-column workaround
- src/ispconfig/mail.py         — mail domains, users, forwards, create_mailbox helper
- src/ispconfig/databases.py    — convenience facade over sites_database_*
- src/ispconfig/clients.py      — client + sys_groupid lookups
- src/ispconfig/types.py        — TypedDicts for response shapes (no pydantic)

Institutional knowledge baked into docstrings + README footgun list:
- sites_web_domain_update 2nd arg is client_id (not primary_id); admin = 0
- sys_groupid=1 -> pass client_id=0 on update, else ownership churns
- fastcgi_php_version vs server_php_id depending on panel version
- dns_a_add type-column bug (<= 3.2.11) — wrapper issues follow-up update
- dns_zone_get_id wants origin WITHOUT trailing dot on 3.2.11+ (contrary to
  what the older snippets say). Verified live against Rackham 2026-04-22.
- mail_user_get returns a bare map on exactly-one-match filter dicts —
  wrapper normalizes to list
- session timeouts mid-op: client detects + re-auths once (max_retries knob)

Tests:
- tests/test_unit.py   — 12 unit tests against a fake transport
- tests/test_smoke.py  — live read-only smoke test, gated on env vars:
    ISPCONFIG_TEST_URL, ISPCONFIG_TEST_USER, ISPCONFIG_TEST_PASS
  Covers login, web_domain_get(156), dns_zone_get_id, mail_user_get filter.

Tooling:
- mypy strict-ish (disallow untyped defs, warn-return-any, no implicit optional)
- ruff with E/F/W/I/B/UP/N/SLF/RUF lint sets
- pip install -e .[dev] for pytest / mypy / ruff
2026-04-22 13:24:58 -07:00

156 lines
3 KiB
Python

"""Shared TypedDicts for ISPConfig response shapes.
We use ``TypedDict`` with ``total=False`` because SOAP returns dicts and the
schema varies by ISPConfig version. This gives type hints without forcing
runtime validation (no pydantic).
Field values from the API are mostly strings (PHP stringifies DB output);
callers that want integers should cast explicitly.
"""
from __future__ import annotations
from typing import TypedDict
class WebDomain(TypedDict, total=False):
"""Row from ``web_domain`` — returned by ``sites_web_domain_get``."""
domain_id: str
sys_userid: str
sys_groupid: str
server_id: str
ip_address: str
ipv6_address: str
domain: str
type: str
parent_domain_id: str
vhost_type: str
document_root: str
system_user: str
system_group: str
hd_quota: str
traffic_quota: str
cgi: str
ssi: str
suexec: str
php: str
fastcgi_php_version: str
server_php_id: str
pm: str
pm_max_children: str
pm_start_servers: str
pm_min_spare_servers: str
pm_max_spare_servers: str
ssl: str
ssl_letsencrypt: str
rewrite_to_https: str
active: str
class DnsZone(TypedDict, total=False):
id: str
sys_userid: str
sys_groupid: str
server_id: str
origin: str
ns: str
mbox: str
serial: str
refresh: str
retry: str
expire: str
minimum: str
ttl: str
active: str
dnssec_wanted: str
dnssec_initialized: str
class DnsRr(TypedDict, total=False):
id: str
zone: str
name: str
type: str
data: str
aux: str
ttl: str
active: str
class MailDomain(TypedDict, total=False):
domain_id: str
sys_groupid: str
server_id: str
domain: str
dkim: str
dkim_private: str
dkim_public: str
dkim_selector: str
active: str
class MailUser(TypedDict, total=False):
mailuser_id: str
sys_groupid: str
server_id: str
email: str
login: str
password: str
name: str
maildir: str
quota: str
cc: str
homedir: str
autoresponder: str
postfix: str
access: str
disableimap: str
disablepop3: str
disablesmtp: str
class MailForward(TypedDict, total=False):
forwarding_id: str
sys_groupid: str
server_id: str
source: str
destination: str
type: str
active: str
class Database(TypedDict, total=False):
database_id: str
sys_groupid: str
server_id: str
type: str
database_name: str
database_user_id: str
database_ro_user_id: str
database_charset: str
remote_access: str
remote_ips: str
backup_copies: str
active: str
class DatabaseUser(TypedDict, total=False):
database_user_id: str
sys_groupid: str
database_user: str
database_password: str
class Client(TypedDict, total=False):
client_id: str
sys_userid: str
sys_groupid: str
username: str
contact_name: str
company_name: str
email: str
customer_no: str
limit_web_domain: str
limit_mail_domain: str
limit_database: str