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
95 lines
2.5 KiB
TOML
95 lines
2.5 KiB
TOML
[build-system]
|
|
requires = ["setuptools>=68", "wheel"]
|
|
build-backend = "setuptools.build_meta"
|
|
|
|
[project]
|
|
name = "ispconfig"
|
|
version = "0.1.0"
|
|
description = "Python SDK for the ISPConfig remote SOAP API — Sulkta Coop internal tooling."
|
|
readme = "README.md"
|
|
license = { text = "MIT" }
|
|
requires-python = ">=3.10"
|
|
authors = [
|
|
{ name = "Sulkta Coop" },
|
|
]
|
|
# Zero runtime deps on purpose. ISPConfig's SOAP endpoint disables WSDL
|
|
# generation (?wsdl returns a fault), so zeep can't help us anyway — we
|
|
# hand-roll envelopes with the stdlib.
|
|
dependencies = []
|
|
|
|
[project.optional-dependencies]
|
|
dev = [
|
|
"pytest>=7",
|
|
"mypy>=1.8",
|
|
"ruff>=0.3",
|
|
]
|
|
|
|
[project.urls]
|
|
Homepage = "http://192.168.0.5:3001/Sulkta-Coop/ispconfig-py"
|
|
|
|
[tool.setuptools.packages.find]
|
|
where = ["src"]
|
|
|
|
[tool.setuptools.package-data]
|
|
ispconfig = ["py.typed"]
|
|
|
|
# ---- mypy -----------------------------------------------------------
|
|
|
|
[tool.mypy]
|
|
python_version = "3.10"
|
|
packages = ["ispconfig"]
|
|
mypy_path = "src"
|
|
strict_optional = true
|
|
warn_unused_ignores = true
|
|
warn_redundant_casts = true
|
|
warn_return_any = true
|
|
disallow_untyped_defs = true
|
|
disallow_incomplete_defs = true
|
|
check_untyped_defs = true
|
|
no_implicit_optional = true
|
|
# We intentionally don't enable `disallow_any_expr` — SOAP responses are
|
|
# dicts of Any and forcing typing everywhere would create fake certainty.
|
|
|
|
[[tool.mypy.overrides]]
|
|
module = "ispconfig.types"
|
|
disallow_untyped_defs = false
|
|
|
|
# ---- ruff -----------------------------------------------------------
|
|
|
|
[tool.ruff]
|
|
line-length = 110
|
|
target-version = "py310"
|
|
src = ["src"]
|
|
|
|
[tool.ruff.lint]
|
|
select = [
|
|
"E", # pycodestyle errors
|
|
"F", # pyflakes
|
|
"W", # pycodestyle warnings
|
|
"I", # isort
|
|
"B", # bugbear
|
|
"UP", # pyupgrade
|
|
"N", # pep8-naming
|
|
"SLF", # flake8-self (private access)
|
|
"RUF",
|
|
]
|
|
ignore = [
|
|
"E501", # line length — handled by formatter when it cares
|
|
"N818", # exception suffix — our hierarchy predates this rule
|
|
"B008", # function calls in argument defaults — not our style anyway
|
|
]
|
|
|
|
[tool.ruff.lint.per-file-ignores]
|
|
"tests/*" = ["SLF001", "N802"]
|
|
# Submodules call back into the client's dispatcher (`_call`) by design —
|
|
# it's the single chokepoint for session management and retry logic.
|
|
"src/ispconfig/sites.py" = ["SLF001"]
|
|
"src/ispconfig/dns.py" = ["SLF001"]
|
|
"src/ispconfig/mail.py" = ["SLF001"]
|
|
"src/ispconfig/clients.py" = ["SLF001"]
|
|
|
|
# ---- pytest ---------------------------------------------------------
|
|
|
|
[tool.pytest.ini_options]
|
|
testpaths = ["tests"]
|
|
addopts = "-ra"
|