Initial commit: add Gitea PyPI publish workflow
This commit is contained in:
226
docs/usage.md
Normal file
226
docs/usage.md
Normal 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())
|
||||
```
|
||||
Reference in New Issue
Block a user