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
This commit is contained in:
commit
9438b4e751
19 changed files with 1797 additions and 0 deletions
95
pyproject.toml
Normal file
95
pyproject.toml
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
[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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue