ispconfig-py/pyproject.toml
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

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"