Python client library + Typer CLI for the netcup SCP REST API. Package: netcup_api.
Console script: netcup. This is production tooling — keep it solid and spec-driven.
Never hard-code the API surface. Drive it from the OpenAPI spec.
The bundled netcup_api/openapi.json is the single source of truth. spec.py parses it
into an Endpoint registry; the CLI and the MCP server both consume that registry. When
netcup changes the API, the fix is netcup spec update — not editing endpoint lists.
Consequences for any change you make:
- New endpoints are already reachable via
netcup call <operationId>/netcup api …. Only add a curated command when it materially improves ergonomics for a common task. - Curated commands MUST route through
NetcupClient.request/.call— never open their ownhttpxclient, never rebuild URLs or auth headers. - Don't duplicate schema knowledge in code. If you need a column or field, read it from the
spec (
netcup describe <op>), don't guess.
netcup_api/
├── config.py Hosts, OIDC URLs, token TTLs, config-dir + spec-path resolution. All
│ tunable via NETCUP_* env vars. No hostname is hard-coded anywhere else.
├── auth.py TokenManager: device flow, token cache, refresh, revoke, userinfo.
│ Persists ONLY the offline refresh token (0600). Access token cached
│ separately with expiry; refreshed automatically (incl. 401 retry).
├── spec.py OpenAPI registry: Endpoint/Param dataclasses, deterministic operationId
│ generation (_make_operation_id), $ref resolution, lookups, load()/update().
├── client.py NetcupClient: generic request()/call() + thin high-level helpers. PATCH
│ defaults to application/merge-patch+json. Raises NetcupAPIError on >=400.
├── cli.py Typer app. Curated command groups + the generic layer
│ (endpoints/describe/call/api) + spec management.
└── openapi.json Bundled spec (shipped in the wheel via force-include).
Layering is strict: cli.py → client.py → {auth.py, spec.py} → config.py. Never invert it.
- Add a helper to
NetcupClientthat callsself.request(...)orself.call(...). Path params that are the user'suserIdare auto-filled — useself.user_id(). - Add the Typer command in
cli.py, wrapping the call inrun(lambda: ...)so errors become cleanerror: …+ exit 1 (never a traceback). - Output: use
emit()for objects,emit_table(rows, cols, title)for lists. Respect the global--json/-jflag (the emit helpers already do). Get column names from the schema, not from memory. - Add/extend an offline test in
tests/.
- Only the refresh token is persisted; the access token cache is disposable.
- Files in the config dir are mode 0600, the dir 0700.
- Never log, print, or commit tokens or
~/.config/netcup-api/contents. - The 401 path refreshes once then retries; never loop on refresh.
- The library never does interactive login on its own except via
TokenManager.login()(only thenetcup auth logincommand calls it).
netcup spec updatedownloads the live spec to~/.config/netcup-api/openapi.json(takes precedence over the bundled copy). To refresh the bundled copy for a release, download it and overwritenetcup_api/openapi.json, then run the tests.operationIds are derived from method+path and are stable as long as paths are stable. If netcup renames a path, dependent curated commands/tests may need updating — the tests named after operationIds will catch it.
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
ruff check . # lint (CI gate)
pytest -q # offline tests — no network, no auth (CI gate)
python -m build # wheel/sdist; CI asserts openapi.json is bundled- Tests must stay offline and credential-free. Mock or avoid network; never require a real login in CI.
- Python ≥ 3.10. Line length 100 (ruff). Keep imports clean (ruff F401 is enforced).
- Match the existing style: type hints,
from __future__ import annotations, small focused functions, docstrings on public classes/commands.
- Bump
versioninpyproject.tomlandnetcup_api.__version__together. netcup versionreports both the CLI version and the bundled spec version — keep them sane.netcup-mcpdepends on this package; don't break the public surface re-exported fromnetcup_api/__init__.py(NetcupClient,TokenManager,Spec,Endpoint,load_spec) without coordinating a bump there.
- Don't add a second HTTP path or auth scheme. One client, one auth flow.
- Don't hard-code server IDs, IPs, hostnames, or endpoint URLs.
- Don't print secrets or weaken file permissions.
- Don't make tests hit the network.