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
156 lines
3 KiB
Python
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
|