feat: full ISPConfig remote API coverage + re-runnable generator (v0.2)

v0.1 shipped ~15 hand-audited methods across sites/dns/mail/databases/clients.
That's enough for daily ops but every new Tort Host / cWHO feature has been
hitting the wall at the edge of that coverage. This extends the SDK to wrap
every method the panel exposes — 312 of them as of Rackham 2026-04-22,
verified against the live list_functions() introspection call with only one
name-mismatch (``__construct``, a PHP lifecycle artifact, not a real API
method).

The hand-audited helpers stay where they are. Every module now has two
clearly-delimited sections: an auto-generated block at the top (emitted by
tools/gen_methods.py from tools/method_inventory.json), and a HAND-EDIT ONLY
BELOW block at the bottom that survives regeneration. Name collisions
between auto and hand always resolve in favor of the hand version — the
generator emits a ``# skipped foo: hand-audited helper takes precedence``
comment in the auto block for traceability.

Pipeline:
- tools/extract_inventory.py reads remote.*.inc.php + remoting.inc.php,
  pulls docblocks + param defaults, dumps one JSON record per method.
  Regex is balanced-paren aware so ``$params = array()`` defaults don't
  truncate the signature at the wrong close-paren (that footgun hid three
  methods from the first run — sites_aps_available_packages_list,
  sites_aps_instance_delete, openvz_vm_add_from_template).
- tools/method_inventory.json is the committed inventory — future ISPConfig
  upgrades diff against this file to see scope at a glance.
- tools/gen_methods.py groups by method-name prefix onto the module classes
  listed in the README table, emits a 1:1 Python wrapper per method with
  the original PHP filename + line number in the docstring, and ensures
  ``from typing import Any`` is present in preexisting modules before
  emitting ``Any`` type annotations.

New submodules (all auto-generated, wired into ISPConfigClient.__init__):
admin, aps, backups, cron, domains, ftp, misc, monitor, openvz, server,
shell, webdav. Existing modules (sites, dns, mail, databases, clients) got
their auto block filled in and their hand-audited helpers preserved.

Escape hatches on the top-level client:
- raw_call(method, *args) routes an arbitrary method name through the same
  session-management + retry + fault-mapping pipeline the typed wrappers
  use. Fix for "panel shipped a new method, SDK hasn't caught up" —
  callers don't have to reach back into _soap.
- list_functions() wraps get_function_list() for panel introspection.

Fault mapping widened: ``no_client_found`` and "no user account" messages
now map to NotFoundError instead of FaultError, matching the existing
``no_domain_found`` convention. Older code that caught raw FaultError
there will still work (NotFoundError extends ISPConfigError) but callers
can now catch the specific type.

Testing:
- tests/test_unit.py — 12 existing pure-unit tests pass unchanged.
- tests/test_smoke.py — extended from 4 read-only calls to 21. One probe
  per new auto-generated module plus raw_call and list_functions smoke
  tests. Methods gated behind admin permission skip gracefully with a
  documented reason (kayos is a reseller, not admin): monitor_jobqueue_count,
  sites_cron_get, sites_ftp_user_get, openvz_get_free_ip,
  quota_get_by_user. Results against Rackham 2026-04-22:
  28 passed, 5 skipped (all documented admin-only), 0 failed.
- ISPCONFIG_TEST_VERIFY_SSL=0 env-var knob added to conftest for panels
  with self-signed or mismatched certs.

Version bump 0.1.0 -> 0.2.0. README restructured into Hand-audited /
Auto-generated / Escape hatch / Footguns sections with a regeneration
recipe for future ISPConfig upgrades. Ruff per-file SLF001 ignore extended
to every submodule (submodules are all authorized callers of the client's
private ``_call`` dispatcher by design). mypy strict passes; ruff check
passes; ruff format applied across src / tools / tests.
This commit is contained in:
Sulkta 2026-04-22 13:58:38 -07:00
parent fb6e235f2d
commit 04b10427f5
28 changed files with 14173 additions and 184 deletions

View file

@ -8,11 +8,23 @@ from typing import Any
from . import exceptions as _exc
from ._soap import SoapFault, SoapTransport
from .admin import AdminModule
from .aps import ApsModule
from .backups import BackupsModule
from .clients import ClientsModule
from .cron import CronModule
from .databases import DatabasesModule
from .dns import DnsModule
from .domains import DomainsModule
from .ftp import FtpModule
from .mail import MailModule
from .misc import MiscModule
from .monitor import MonitorModule
from .openvz import OpenvzModule
from .server import ServerModule
from .shell import ShellModule
from .sites import SitesModule
from .webdav import WebdavModule
log = logging.getLogger("ispconfig")
@ -48,11 +60,26 @@ class ISPConfigClient:
self._transport = SoapTransport(url, verify_ssl=verify_ssl, timeout=timeout)
self._session_id: str | None = None
# Hand-audited modules (stable API).
self.sites = SitesModule(self)
self.dns = DnsModule(self)
self.mail = MailModule(self)
self.databases = DatabasesModule(self)
self.clients = ClientsModule(self)
# Auto-generated modules (full surface, param shapes not yet
# verified in prod use — see per-method docstrings).
self.admin = AdminModule(self)
self.aps = ApsModule(self)
self.backups = BackupsModule(self)
self.cron = CronModule(self)
self.domains = DomainsModule(self)
self.ftp = FtpModule(self)
self.misc = MiscModule(self)
self.monitor = MonitorModule(self)
self.openvz = OpenvzModule(self)
self.server = ServerModule(self)
self.shell = ShellModule(self)
self.webdav = WebdavModule(self)
# ---- context manager ---------------------------------------------
@ -105,6 +132,38 @@ class ISPConfigClient:
"""Read-only accessor — exposed for debugging, not for API calls."""
return self._session_id
# ---- escape hatches ----------------------------------------------
def raw_call(self, method: str, *args: Any) -> Any:
"""Invoke an arbitrary ISPConfig remote method by name.
Use this when the SDK doesn't yet wrap the method you need —
newer ISPConfig versions may expose calls our inventory hasn't
caught up with. Args are passed positionally; names are cosmetic
on the wire, so we auto-number them as ``arg1``, ``arg2``, ...
If the call fails, capture ``FaultError.faultcode`` /
``FaultError.faultstring`` and file an issue against
``Sulkta-OSS/ispconfig-py`` so we can add the method properly.
"""
named_args = tuple((f"arg{i + 1}", v) for i, v in enumerate(args))
return self._call(method, *named_args)
def list_functions(self) -> list[str]:
"""Introspect the panel: return the list of remote methods it exposes.
Wrapper for ISPConfig's own ``get_function_list``. Handy when
checking whether your panel version supports a given call before
attempting it via :meth:`raw_call`.
"""
result = self._call("get_function_list")
if isinstance(result, list):
return [str(x) for x in result]
if isinstance(result, dict):
# Some versions return a map keyed by integer index.
return [str(v) for v in result.values()]
return []
# ---- the hot path ------------------------------------------------
def _call(self, method: str, *args: tuple[str, Any]) -> Any: