# Usage guide ## Installation ```bash pip install conduit-client ``` For development: ```bash pip install conduit-client[dev] ``` ## Authentication The Conduit gateway uses API keys in the `Authorization: Bearer mega_sk_live__` header. Pass your key to `ConduitClient` or `AsyncConduitClient`: ```python from conduit_client import ConduitClient client = ConduitClient("mega_sk_live_abc123_xyz") ``` Async version: ```python import asyncio from conduit_client import AsyncConduitClient async def main() -> None: async with AsyncConduitClient("mega_sk_live_abc123_xyz") as client: me = await client.gateway.me() print(me.scopes) asyncio.run(main()) ``` You can create new keys programmatically using the gateway client: ```python from conduit_client.models.common import CreateKeyRequest new_key = client.gateway.create_key( CreateKeyRequest(display_name="production", scopes=["sms:send", "sms:read"]) ) print(new_key.key) # returned exactly once by the server ``` Async version: ```python from conduit_client import AsyncConduitClient from conduit_client.models.common import CreateKeyRequest async def main() -> None: async with AsyncConduitClient("mega_sk_live_...") as client: new_key = await client.gateway.create_key( CreateKeyRequest(display_name="production", scopes=["sms:send", "sms:read"]) ) print(new_key.key) asyncio.run(main()) ``` ## Sync vs async Both clients expose the same façade subclients: ```python # Sync with ConduitClient("mega_sk_live_...") as client: me = client.gateway.me() # Async async with AsyncConduitClient("mega_sk_live_...") as client: me = await client.gateway.me() ``` The asynchronous client uses `httpx.AsyncClient` under the hood and must be awaited. ## Timeouts and retries ```python client = ConduitClient( "mega_sk_live_...", timeout=60.0, max_retries=2, ) ``` Async version: ```python from conduit_client import AsyncConduitClient client = AsyncConduitClient( "mega_sk_live_...", timeout=60.0, max_retries=2, ) ``` Retries are applied only to idempotent methods (`GET`, `HEAD`, `OPTIONS`, `PUT`, `DELETE`) and only for status codes `429`, `502`, `503`, and `504`. Backoff is exponential (0.5s, 1s, 2s, ...). ## Custom httpx options Any extra keyword arguments are forwarded to the underlying `httpx.Client` or `httpx.AsyncClient`: ```python client = ConduitClient( "mega_sk_live_...", proxies="http://proxy.example.com:8080", ) ``` Async version: ```python from conduit_client import AsyncConduitClient client = AsyncConduitClient( "mega_sk_live_...", proxies="http://proxy.example.com:8080", ) ``` ## Handling free-form API bodies Some Conduit endpoints forward extra fields directly to the upstream provider (for example, Proxmox VM/container creation, Transmission session patches, DNS zone modifications). For these, the client accepts a `dict[str, Any]` or a Pydantic model that allows extra fields: ```python client.compute.patch_vm(100, {"memory": 4096, "cores": 4}) client.torrents.patch_session({"download-dir": "/mnt/media"}) client.dns.patch_zone("example.com", {"type": "slave"}) ``` Async version: ```python from conduit_client import AsyncConduitClient async def main() -> None: async with AsyncConduitClient("mega_sk_live_...") as client: await client.compute.patch_vm(100, {"memory": 4096, "cores": 4}) await client.torrents.patch_session({"download-dir": "/mnt/media"}) await client.dns.patch_zone("example.com", {"type": "slave"}) asyncio.run(main()) ``` ## Errors All client errors inherit from `ConduitError`. HTTP error responses are raised as typed exceptions: | Exception | Trigger | | --- | --- | | `AuthenticationError` | 401 / 403 | | `NotFoundError` | 404 | | `ValidationError` | 422 | | `ConflictError` | 409 | | `RateLimitError` | 429 | | `ConduitAPIError` | any other 4xx / 5xx | Each exception exposes `status_code` and `response_body`: ```python from conduit_client import NotFoundError try: client.wiki.get_raw("missing:page") except NotFoundError as exc: print(exc.status_code) print(exc.response_body) ``` Async version: ```python from conduit_client import AsyncConduitClient, NotFoundError async def main() -> None: async with AsyncConduitClient("mega_sk_live_...") as client: try: await client.wiki.get_raw("missing:page") except NotFoundError as exc: print(exc.status_code) print(exc.response_body) asyncio.run(main()) ``` ## Façade clients | Attribute | Scope | Description | | --- | --- | --- | | `client.gateway` | `gateway:read`, `keys:create`, etc. | Health, keys, audit log | | `client.sms` | `sms:read`, `sms:send` | SMS/MMS send/list/delete | | `client.media_ingest` | `video:read`, `video:admin` | MeTube jobs/subscriptions | | `client.media_library` | `video:read`, `video:admin` | Jellyfin search/sessions | | `client.torrents` | `video:read`, `video:admin` | Transmission management | | `client.wiki` | `wiki:read`, `wiki:write` | DokuWiki read/write | | `client.compute` | `vm:read`, `vm:start`, `vm:stop` | Proxmox VMs/containers/storage | | `client.dns` | `dns:read`, `dns:write`, `dns:delete` | BIND9 servers/zones/records | ## Path parameters Path parameters such as DokuWiki page IDs and DNS zone names are URL-encoded automatically. Slashes, colons, and other special characters are handled safely. ```python client.wiki.get_raw("namespace:sub:page") client.dns.get_zone("example.com") ``` Async version: ```python from conduit_client import AsyncConduitClient async def main() -> None: async with AsyncConduitClient("mega_sk_live_...") as client: await client.wiki.get_raw("namespace:sub:page") await client.dns.get_zone("example.com") asyncio.run(main()) ```