Initial commit: add Gitea PyPI publish workflow

This commit is contained in:
2026-07-06 11:02:19 -05:00
commit b0d2873cbc
40 changed files with 5831 additions and 0 deletions

226
docs/usage.md Normal file
View File

@@ -0,0 +1,226 @@
# 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_<prefix>_<secret>` 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())
```