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

22
tests/conftest.py Normal file
View File

@@ -0,0 +1,22 @@
"""Shared pytest fixtures."""
from __future__ import annotations
import pytest
from conduit_client import AsyncConduitClient, ConduitClient
@pytest.fixture
def api_key() -> str:
return "mega_sk_live_test_prefix_testsecret"
@pytest.fixture
def client(api_key: str) -> ConduitClient:
return ConduitClient(api_key, base_url="https://api.example.com")
@pytest.fixture
def async_client(api_key: str) -> AsyncConduitClient:
return AsyncConduitClient(api_key, base_url="https://api.example.com")

119
tests/test_base_client.py Normal file
View File

@@ -0,0 +1,119 @@
"""Tests for the shared HTTP transport layer."""
from __future__ import annotations
import pytest
import respx
from httpx import Response
from conduit_client import (
AsyncConduitClient,
AuthenticationError,
ConduitClient,
NotFoundError,
RateLimitError,
ValidationError,
)
from conduit_client._base_client import _BaseClientMixin
@respx.mock
def test_sync_auth_header(api_key: str) -> None:
route = respx.get("https://api.example.com/auth/me").mock(return_value=Response(200, json={}))
with ConduitClient(api_key, base_url="https://api.example.com") as client:
client._transport.request("GET", "/auth/me")
assert route.calls[0].request.headers["Authorization"] == f"Bearer {api_key}"
@pytest.mark.asyncio
@respx.mock
async def test_async_auth_header(api_key: str) -> None:
route = respx.get("https://api.example.com/auth/me").mock(return_value=Response(200, json={}))
async with AsyncConduitClient(api_key, base_url="https://api.example.com") as client:
await client._transport.request("GET", "/auth/me")
assert route.calls[0].request.headers["Authorization"] == f"Bearer {api_key}"
@respx.mock
def test_sync_json_response(api_key: str) -> None:
respx.get("https://api.example.com/auth/me").mock(return_value=Response(200, json={"id": 1}))
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client._transport.request("GET", "/auth/me")
assert result == {"id": 1}
@respx.mock
def test_sync_text_response(api_key: str) -> None:
respx.get("https://api.example.com/health").mock(return_value=Response(200, text="ok"))
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client._transport.request("GET", "/health")
assert result == "ok"
@respx.mock
def test_sync_204_returns_none(api_key: str) -> None:
respx.delete("https://api.example.com/keys/abc").mock(return_value=Response(204))
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client._transport.request("DELETE", "/keys/abc")
assert result is None
@respx.mock
def test_authentication_error(api_key: str) -> None:
respx.get("https://api.example.com/auth/me").mock(
return_value=Response(401, json={"error": "unauthorized"})
)
with pytest.raises(AuthenticationError) as exc, ConduitClient(
api_key, base_url="https://api.example.com"
) as client:
client._transport.request("GET", "/auth/me")
assert exc.value.status_code == 401
@respx.mock
def test_not_found_error(api_key: str) -> None:
respx.get("https://api.example.com/wiki/pages/missing").mock(
return_value=Response(404, text="not found")
)
with pytest.raises(NotFoundError) as exc, ConduitClient(
api_key, base_url="https://api.example.com"
) as client:
client._transport.request("GET", "/wiki/pages/missing")
assert exc.value.status_code == 404
@respx.mock
def test_validation_error(api_key: str) -> None:
respx.post("https://api.example.com/keys").mock(
return_value=Response(422, json={"error": "unknown scopes"})
)
with pytest.raises(ValidationError) as exc, ConduitClient(
api_key, base_url="https://api.example.com"
) as client:
client._transport.request("POST", "/keys", json={"scopes": ["bad"]})
assert exc.value.status_code == 422
@respx.mock
def test_rate_limit_retry_then_success(api_key: str) -> None:
route = respx.get("https://api.example.com/health").mock(
side_effect=[Response(429, text="slow down"), Response(200, text="ok")]
)
with ConduitClient(api_key, base_url="https://api.example.com", max_retries=2) as client:
result = client._transport.request("GET", "/health")
assert result == "ok"
assert route.call_count == 2
@respx.mock
def test_rate_limit_exhausted(api_key: str) -> None:
respx.get("https://api.example.com/health").mock(return_value=Response(429, text="slow down"))
with pytest.raises(RateLimitError), ConduitClient(
api_key, base_url="https://api.example.com", max_retries=1
) as client:
client._transport.request("GET", "/health")
def test_encode_path_param() -> None:
assert _BaseClientMixin._encode_path_param("foo/bar") == "foo%2Fbar"
assert _BaseClientMixin._encode_path_param("example.com") == "example.com"

242
tests/test_compute.py Normal file
View File

@@ -0,0 +1,242 @@
"""Tests for the Proxmox compute client."""
from __future__ import annotations
import json
import pytest
import respx
from httpx import Response
from conduit_client import AsyncConduitClient, ConduitClient
from conduit_client.models.compute import (
CloneVmRequest,
ComputeTicketRequest,
CreateContainerRequest,
CreateSnapshotRequest,
CreateVmRequest,
MigrateVmRequest,
NodeTasksQuery,
StorageQuery,
UploadStorageRequest,
)
@respx.mock
def test_auth_ticket(api_key: str) -> None:
route = respx.post("https://api.example.com/compute/auth/ticket").mock(
return_value=Response(
200,
json={
"ticket": "tck",
"CSRFPreventionToken": "csrf",
"username": "root",
},
)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.compute.auth_ticket(
ComputeTicketRequest(username="root", password="pw")
)
assert result.ticket == "tck"
assert result.csrfpreventiontoken == "csrf"
assert json.loads(route.calls[0].request.content) == {
"username": "root",
"password": "pw",
}
@respx.mock
def test_list_resources_and_nodes(api_key: str) -> None:
respx.get("https://api.example.com/compute/resources").mock(
return_value=Response(200, json=[{"id": "qemu/100"}])
)
respx.get("https://api.example.com/compute/resources").mock(
return_value=Response(200, json=[{"id": "qemu/100"}])
)
respx.get("https://api.example.com/compute/cluster/status").mock(
return_value=Response(200, json={"quorate": True})
)
respx.get("https://api.example.com/compute/nodes").mock(
return_value=Response(200, json=[{"node": "pve"}])
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
assert client.compute.list_resources() == [{"id": "qemu/100"}]
assert client.compute.list_resources(type_="vm") == [{"id": "qemu/100"}]
assert client.compute.cluster_status() == {"quorate": True}
assert client.compute.list_nodes() == [{"node": "pve"}]
@respx.mock
def test_node_status_and_tasks(api_key: str) -> None:
respx.get("https://api.example.com/compute/nodes/pve/status").mock(
return_value=Response(200, json={"cpu": 0.1})
)
route = respx.get("https://api.example.com/compute/nodes/pve/tasks").mock(
return_value=Response(200, json=[])
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
assert client.compute.get_node_status("pve") == {"cpu": 0.1}
assert client.compute.list_node_tasks("pve", NodeTasksQuery(limit=10)) == []
assert route.calls[0].request.url.params["limit"] == "10"
@respx.mock
def test_vm_lifecycle(api_key: str) -> None:
respx.get("https://api.example.com/compute/vms").mock(
return_value=Response(200, json=[{"vmid": 100}])
)
create_route = respx.post("https://api.example.com/compute/vms").mock(
return_value=Response(200, json={"task": "UPID:abc"})
)
respx.get("https://api.example.com/compute/vms/100").mock(
return_value=Response(200, json={"vmid": 100, "name": "vm1"})
)
patch_route = respx.patch("https://api.example.com/compute/vms/100").mock(
return_value=Response(200, json={"task": "UPID:patch"})
)
respx.delete("https://api.example.com/compute/vms/100").mock(
return_value=Response(204)
)
respx.post("https://api.example.com/compute/vms/100/start").mock(
return_value=Response(200, json={"task": "UPID:start"})
)
respx.post("https://api.example.com/compute/vms/100/stop").mock(
return_value=Response(200, json={"task": "UPID:stop"})
)
respx.post("https://api.example.com/compute/vms/100/shutdown").mock(
return_value=Response(200, json={"task": "UPID:shutdown"})
)
respx.post("https://api.example.com/compute/vms/100/reboot").mock(
return_value=Response(200, json={"task": "UPID:reboot"})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
assert client.compute.list_vms() == [{"vmid": 100}]
result = client.compute.create_vm(
CreateVmRequest(node="pve", vmid=100, cores=2, memory=1024)
)
assert result.task == "UPID:abc"
assert client.compute.get_vm(100)["name"] == "vm1"
assert client.compute.patch_vm(100, {"memory": 2048}).task == "UPID:patch"
client.compute.delete_vm(100, confirm=100)
assert client.compute.start_vm(100).task == "UPID:start"
assert client.compute.stop_vm(100).task == "UPID:stop"
assert client.compute.shutdown_vm(100).task == "UPID:shutdown"
assert client.compute.reboot_vm(100).task == "UPID:reboot"
body = json.loads(create_route.calls[0].request.content)
assert body["node"] == "pve"
assert body["cores"] == 2
assert json.loads(patch_route.calls[0].request.content) == {"memory": 2048}
@respx.mock
def test_vm_clone_migrate_snapshots_agent(api_key: str) -> None:
respx.post("https://api.example.com/compute/vms/100/clone").mock(
return_value=Response(200, json={"task": "UPID:clone"})
)
respx.post("https://api.example.com/compute/vms/100/migrate").mock(
return_value=Response(200, json={"task": "UPID:migrate"})
)
respx.get("https://api.example.com/compute/vms/100/snapshots").mock(
return_value=Response(200, json=[{"name": "snap1"}])
)
respx.post("https://api.example.com/compute/vms/100/snapshots").mock(
return_value=Response(200, json={"task": "UPID:snap"})
)
respx.post("https://api.example.com/compute/vms/100/agent/exec").mock(
return_value=Response(200, json={"out-data": "ok"})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
assert client.compute.clone_vm(100, CloneVmRequest(newid=200)).task == "UPID:clone"
assert client.compute.migrate_vm(
100, MigrateVmRequest(target="pve2", online=True)
).task == "UPID:migrate"
assert client.compute.list_snapshots(100) == [{"name": "snap1"}]
assert client.compute.create_snapshot(
100, CreateSnapshotRequest(snapname="snap2")
).task == "UPID:snap"
assert client.compute.agent_action(100, "exec", {"command": "ls"}) == {"out-data": "ok"}
@respx.mock
def test_containers(api_key: str) -> None:
create_route = respx.post("https://api.example.com/compute/containers").mock(
return_value=Response(200, json={"task": "UPID:ct"})
)
respx.post("https://api.example.com/compute/containers/200/start").mock(
return_value=Response(200, json={"task": "UPID:start"})
)
respx.post("https://api.example.com/compute/containers/200/stop").mock(
return_value=Response(200, json={"task": "UPID:stop"})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.compute.create_container(
CreateContainerRequest(node="pve", ostemplate="local:vztmpl/debian.tar.gz", vmid=200)
)
assert result.task == "UPID:ct"
assert client.compute.start_container(200).task == "UPID:start"
assert client.compute.stop_container(200).task == "UPID:stop"
body = json.loads(create_route.calls[0].request.content)
assert body["node"] == "pve"
assert body["ostemplate"] == "local:vztmpl/debian.tar.gz"
@respx.mock
def test_storage(api_key: str) -> None:
respx.get("https://api.example.com/compute/storage").mock(
return_value=Response(200, json=[{"storage": "local"}])
)
respx.get("https://api.example.com/compute/storage/local/content").mock(
return_value=Response(200, json=[{"volid": "local:iso/img.iso"}])
)
upload_route = respx.post("https://api.example.com/compute/storage/local/upload").mock(
return_value=Response(200, json={"task": "UPID:upload"})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
assert client.compute.list_storage(StorageQuery(node="pve")) == [{"storage": "local"}]
assert client.compute.list_storage_content("local", StorageQuery(node="pve")) == [
{"volid": "local:iso/img.iso"}
]
result = client.compute.upload_to_storage(
"local",
UploadStorageRequest(content="abc", filename="img.iso"),
node="pve",
)
assert result.task == "UPID:upload"
assert upload_route.calls[0].request.url.params["node"] == "pve"
@respx.mock
def test_backup_jobs_and_misc(api_key: str) -> None:
respx.get("https://api.example.com/compute/backups/jobs").mock(
return_value=Response(200, json=[])
)
respx.post("https://api.example.com/compute/backups/jobs").mock(
return_value=Response(200, json={"task": "UPID:backup"})
)
respx.get("https://api.example.com/compute/ha/resources").mock(
return_value=Response(200, json=[])
)
respx.get("https://api.example.com/compute/access/users").mock(
return_value=Response(200, json=[])
)
respx.get("https://api.example.com/compute/access/acl").mock(
return_value=Response(200, json=[])
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
assert client.compute.list_backup_jobs() == []
assert client.compute.create_backup_job({"id": "job1"}).task == "UPID:backup"
assert client.compute.list_ha_resources() == []
assert client.compute.list_access_users() == []
assert client.compute.list_access_acl() == []
@pytest.mark.asyncio
@respx.mock
async def test_async_start_vm(api_key: str) -> None:
respx.post("https://api.example.com/compute/vms/100/start").mock(
return_value=Response(200, json={"task": "UPID:start"})
)
async with AsyncConduitClient(api_key, base_url="https://api.example.com") as client:
result = await client.compute.start_vm(100)
assert result.task == "UPID:start"

136
tests/test_dns.py Normal file
View File

@@ -0,0 +1,136 @@
"""Tests for the BIND9 DNS client."""
from __future__ import annotations
import json
import pytest
import respx
from httpx import Response
from conduit_client import AsyncConduitClient, ConduitClient
from conduit_client.models.dns import (
AddZoneRequest,
BatchRecordUpdate,
BatchUpdateRecordsRequest,
CacheFlushRequest,
ServerReloadRequest,
)
@respx.mock
def test_server_status(api_key: str) -> None:
respx.get("https://api.example.com/dns/servers/default/status").mock(
return_value=Response(200, text="named is running")
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.dns.server_status("default")
assert result == "named is running"
@respx.mock
def test_server_reload_and_stats(api_key: str) -> None:
reload_route = respx.post("https://api.example.com/dns/servers/default/reload").mock(
return_value=Response(204)
)
respx.get("https://api.example.com/dns/servers/default/stats").mock(
return_value=Response(200, json={"zones": 5})
)
respx.get("https://api.example.com/dns/servers/default/config").mock(
return_value=Response(200, json={"options": {}})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
client.dns.reload_server("default", ServerReloadRequest(mode="reconfig"))
assert client.dns.server_stats("default") == {"zones": 5}
assert client.dns.server_config("default") == {"options": {}}
assert json.loads(reload_route.calls[0].request.content) == {"mode": "reconfig"}
@respx.mock
def test_cache_flush(api_key: str) -> None:
route = respx.post("https://api.example.com/dns/servers/default/cache/flush").mock(
return_value=Response(204)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
client.dns.flush_cache(
"default", CacheFlushRequest(mode="flushname", name="example.com")
)
assert json.loads(route.calls[0].request.content) == {
"mode": "flushname",
"name": "example.com",
}
@respx.mock
def test_zone_crud(api_key: str) -> None:
create_route = respx.post("https://api.example.com/dns/zones").mock(
return_value=Response(200, json={"zone": "example.com"})
)
respx.get("https://api.example.com/dns/zones/example.com").mock(
return_value=Response(200, json={"zone": "example.com", "type": "master"})
)
patch_route = respx.patch("https://api.example.com/dns/zones/example.com").mock(
return_value=Response(204)
)
respx.delete("https://api.example.com/dns/zones/example.com").mock(
return_value=Response(204)
)
respx.post("https://api.example.com/dns/zones/example.com/freeze").mock(
return_value=Response(204)
)
respx.post("https://api.example.com/dns/zones/example.com/thaw").mock(
return_value=Response(204)
)
respx.post("https://api.example.com/dns/zones/example.com/sync").mock(
return_value=Response(204)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.dns.create_zone(
AddZoneRequest(zone="example.com", klass="IN", extra="value")
)
assert result["zone"] == "example.com"
assert client.dns.get_zone("example.com")["type"] == "master"
client.dns.patch_zone("example.com", {"type": "slave"})
client.dns.delete_zone("example.com", confirm="example.com")
client.dns.freeze_zone("example.com")
client.dns.thaw_zone("example.com")
client.dns.sync_zone("example.com")
body = json.loads(create_route.calls[0].request.content)
assert body["zone"] == "example.com"
assert body["klass"] == "IN"
assert body["extra"] == "value"
assert json.loads(patch_route.calls[0].request.content) == {"type": "slave"}
@respx.mock
def test_batch_update_records(api_key: str) -> None:
route = respx.post("https://api.example.com/dns/zones/example.com/records:batchUpdate").mock(
return_value=Response(204)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
client.dns.batch_update_records(
"example.com",
BatchUpdateRecordsRequest(
updates=[
BatchRecordUpdate(
op="add", name="www", ttl=300, rtype="A", rdata="1.2.3.4"
)
]
),
)
assert json.loads(route.calls[0].request.content) == {
"updates": [
{"op": "add", "name": "www", "ttl": 300, "rtype": "A", "rdata": "1.2.3.4"}
]
}
@pytest.mark.asyncio
@respx.mock
async def test_async_server_status(api_key: str) -> None:
respx.get("https://api.example.com/dns/servers/default/status").mock(
return_value=Response(200, text="named is running")
)
async with AsyncConduitClient(api_key, base_url="https://api.example.com") as client:
result = await client.dns.server_status("default")
assert result == "named is running"

145
tests/test_gateway.py Normal file
View File

@@ -0,0 +1,145 @@
"""Tests for the gateway client."""
from __future__ import annotations
import json
from datetime import datetime, timezone
import pytest
import respx
from httpx import Response
from conduit_client import AsyncConduitClient, ConduitClient
from conduit_client.models.common import CreateKeyRequest
from conduit_client.models.gateway import AuditLogQuery
@respx.mock
def test_health(api_key: str) -> None:
respx.get("https://api.example.com/health").mock(return_value=Response(200, text="ok"))
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.gateway.health()
assert result == "ok"
@respx.mock
def test_me(api_key: str) -> None:
respx.get("https://api.example.com/auth/me").mock(
return_value=Response(
200,
json={
"subject": "user:123",
"actor_type": "user",
"auth_method": "api_key",
"scopes": ["gateway:read"],
"grantable_scopes": ["gateway:read", "sms:send"],
},
)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.gateway.me()
assert result.subject == "user:123"
assert result.scopes == ["gateway:read"]
@respx.mock
def test_services(api_key: str) -> None:
respx.get("https://api.example.com/services").mock(
return_value=Response(
200,
json=[
{"id": "sms", "name": "SMS", "prefixes": ["/sms"], "enabled": True},
],
)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.gateway.services()
assert len(result) == 1
assert result[0].id == "sms"
@respx.mock
def test_list_keys(api_key: str) -> None:
created = datetime.now(timezone.utc).isoformat()
respx.get("https://api.example.com/keys").mock(
return_value=Response(
200,
json=[
{
"key_prefix": "abc123",
"display_name": "test",
"scopes": ["gateway:read"],
"created_at": created,
},
],
)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.gateway.list_keys()
assert result[0].key_prefix == "abc123"
@respx.mock
def test_create_key(api_key: str) -> None:
created = datetime.now(timezone.utc).isoformat()
route = respx.post("https://api.example.com/keys").mock(
return_value=Response(
200,
json={
"key": "mega_sk_live_abc_secret",
"key_prefix": "abc",
"display_name": "new key",
"scopes": ["sms:read"],
"created_at": created,
},
)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.gateway.create_key(CreateKeyRequest(display_name="new key", scopes=["sms:read"]))
assert result.key == "mega_sk_live_abc_secret"
assert json.loads(route.calls[0].request.content) == {
"display_name": "new key",
"scopes": ["sms:read"],
}
@respx.mock
def test_revoke_key(api_key: str) -> None:
respx.delete("https://api.example.com/keys/abc").mock(return_value=Response(204))
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.gateway.revoke_key("abc")
assert result is None
@respx.mock
def test_admin_audit(api_key: str) -> None:
route = respx.get("https://api.example.com/admin/audit").mock(
return_value=Response(200, json=[])
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.gateway.admin_audit(
AuditLogQuery(actor_subject="user:123", limit=50)
)
assert result == []
assert route.calls[0].request.url.params["actor_subject"] == "user:123"
assert route.calls[0].request.url.params["limit"] == "50"
@pytest.mark.asyncio
@respx.mock
async def test_async_me(api_key: str) -> None:
respx.get("https://api.example.com/auth/me").mock(
return_value=Response(
200,
json={
"subject": "user:123",
"actor_type": "user",
"auth_method": "api_key",
"scopes": ["gateway:read"],
"grantable_scopes": ["gateway:read", "sms:send"],
},
)
)
async with AsyncConduitClient(api_key, base_url="https://api.example.com") as client:
result = await client.gateway.me()
assert result.subject == "user:123"

129
tests/test_media_ingest.py Normal file
View File

@@ -0,0 +1,129 @@
"""Tests for the MeTube media ingestion client."""
from __future__ import annotations
import json
import pytest
import respx
from httpx import Response
from conduit_client import AsyncConduitClient, ConduitClient
from conduit_client.models.media_ingest import (
CreateIngestJobRequest,
CreateIngestSubscriptionRequest,
PatchIngestSubscriptionRequest,
)
@respx.mock
def test_list_jobs(api_key: str) -> None:
respx.get("https://api.example.com/media/ingest/jobs").mock(
return_value=Response(
200,
json={"done": [{"id": "job1", "status": "done"}], "downloading": []},
)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.media_ingest.list_jobs()
assert len(result.done or []) == 1
@respx.mock
def test_create_job(api_key: str) -> None:
route = respx.post("https://api.example.com/media/ingest/jobs").mock(
return_value=Response(200, json={"id": "job2", "status": "pending"})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.media_ingest.create_job(
CreateIngestJobRequest(url="https://youtube.com/watch?v=abc", quality="best")
)
assert result.id == "job2"
assert json.loads(route.calls[0].request.content) == {
"url": "https://youtube.com/watch?v=abc",
"quality": "best",
}
@respx.mock
def test_start_cancel_delete_job(api_key: str) -> None:
respx.post("https://api.example.com/media/ingest/jobs/job3/start").mock(
return_value=Response(204)
)
respx.post("https://api.example.com/media/ingest/jobs/job3/cancel").mock(
return_value=Response(204)
)
respx.delete("https://api.example.com/media/ingest/jobs/job3").mock(
return_value=Response(204)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
client.media_ingest.start_job("job3")
client.media_ingest.cancel_job("job3")
client.media_ingest.delete_job("job3", delete_from_file=True)
@respx.mock
def test_list_presets(api_key: str) -> None:
respx.get("https://api.example.com/media/ingest/presets").mock(
return_value=Response(200, json=[{"name": "best", "quality": "best", "format": "mp4"}])
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.media_ingest.list_presets()
assert result[0].name == "best"
@respx.mock
def test_subscriptions(api_key: str) -> None:
respx.get("https://api.example.com/media/ingest/subscriptions").mock(
return_value=Response(200, json=[{"id": "sub1", "url": "https://feed.example.com"}])
)
create_route = respx.post("https://api.example.com/media/ingest/subscriptions").mock(
return_value=Response(200, json={"id": "sub2", "url": "https://feed2.example.com"})
)
respx.patch("https://api.example.com/media/ingest/subscriptions/sub1").mock(
return_value=Response(200, json={"id": "sub1", "url": "https://feed.example.com", "name": "new"})
)
respx.delete("https://api.example.com/media/ingest/subscriptions/sub1").mock(
return_value=Response(204)
)
respx.post("https://api.example.com/media/ingest/subscriptions/sub1/check").mock(
return_value=Response(204)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
subs = client.media_ingest.list_subscriptions()
assert len(subs) == 1
new = client.media_ingest.create_subscription(
CreateIngestSubscriptionRequest(url="https://feed2.example.com")
)
assert new.id == "sub2"
client.media_ingest.patch_subscription(
"sub1", PatchIngestSubscriptionRequest(name="new")
)
client.media_ingest.delete_subscription("sub1")
client.media_ingest.check_subscription("sub1")
assert json.loads(create_route.calls[0].request.content) == {
"url": "https://feed2.example.com"
}
@respx.mock
def test_version(api_key: str) -> None:
respx.get("https://api.example.com/media/ingest/version").mock(
return_value=Response(200, json={"metube_version": "1.0", "ytdlp_version": "2024.01.01"})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.media_ingest.version()
assert result.metube_version == "1.0"
@pytest.mark.asyncio
@respx.mock
async def test_async_create_job(api_key: str) -> None:
respx.post("https://api.example.com/media/ingest/jobs").mock(
return_value=Response(200, json={"id": "job2", "status": "pending"})
)
async with AsyncConduitClient(api_key, base_url="https://api.example.com") as client:
result = await client.media_ingest.create_job(
CreateIngestJobRequest(url="https://youtube.com/watch?v=abc")
)
assert result.id == "job2"

View File

@@ -0,0 +1,93 @@
"""Tests for the Jellyfin media library client."""
from __future__ import annotations
import json
import pytest
import respx
from httpx import Response
from conduit_client import AsyncConduitClient, ConduitClient
from conduit_client.models.media_library import LibraryLoginRequest, LibrarySearchQuery
@respx.mock
def test_login(api_key: str) -> None:
route = respx.post("https://api.example.com/media/library/auth/login").mock(
return_value=Response(200, json={"access_token": "tok", "user_id": "u1"})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.media_library.login(LibraryLoginRequest(username="user", password="pw"))
assert result.access_token == "tok"
assert json.loads(route.calls[0].request.content) == {
"username": "user",
"password": "pw",
}
@respx.mock
def test_search(api_key: str) -> None:
route = respx.get("https://api.example.com/media/library/search").mock(
return_value=Response(200, json={"Items": []})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.media_library.search(LibrarySearchQuery(term="cat"))
assert result == {"Items": []}
assert route.calls[0].request.url.params["term"] == "cat"
@respx.mock
def test_get_item_and_playback(api_key: str) -> None:
respx.get("https://api.example.com/media/library/items/i1").mock(
return_value=Response(200, json={"Id": "i1", "Name": "Movie"})
)
respx.get("https://api.example.com/media/library/items/i1/playback").mock(
return_value=Response(200, json={"MediaSources": []})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
item = client.media_library.get_item("i1")
playback = client.media_library.get_playback_info("i1")
assert item["Name"] == "Movie"
assert playback == {"MediaSources": []}
@respx.mock
def test_refresh(api_key: str) -> None:
respx.post("https://api.example.com/media/library/refresh").mock(return_value=Response(204))
with ConduitClient(api_key, base_url="https://api.example.com") as client:
client.media_library.refresh()
@respx.mock
def test_list_sessions(api_key: str) -> None:
respx.get("https://api.example.com/media/library/sessions").mock(
return_value=Response(200, json=[{"id": "s1", "device_name": "TV"}])
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.media_library.list_sessions()
assert result[0].device_name == "TV"
@respx.mock
def test_system_info(api_key: str) -> None:
route = respx.get("https://api.example.com/media/library/system/info").mock(
return_value=Response(200, json={"version": "10.0", "id": "abc"})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.media_library.system_info(public=True)
assert result.version == "10.0"
assert route.calls[0].request.url.params["public"] == "true"
@pytest.mark.asyncio
@respx.mock
async def test_async_login(api_key: str) -> None:
respx.post("https://api.example.com/media/library/auth/login").mock(
return_value=Response(200, json={"access_token": "tok", "user_id": "u1"})
)
async with AsyncConduitClient(api_key, base_url="https://api.example.com") as client:
result = await client.media_library.login(
LibraryLoginRequest(username="user", password="pw")
)
assert result.access_token == "tok"

112
tests/test_sms.py Normal file
View File

@@ -0,0 +1,112 @@
"""Tests for the SMS/MMS client."""
from __future__ import annotations
import json
import pytest
import respx
from httpx import Response
from conduit_client import AsyncConduitClient, ConduitClient
from conduit_client.models.sms import (
SendMmsRequest,
SendSmsDefaultRequest,
SendSmsRequest,
)
@respx.mock
def test_list_sms(api_key: str) -> None:
respx.get("https://api.example.com/sms").mock(
return_value=Response(200, json=[{"id": 1, "did": "5550100", "dst": "5550200", "message": "hi"}])
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.sms.list_sms()
assert len(result) == 1
assert result[0].id == 1
@respx.mock
def test_send_sms(api_key: str) -> None:
route = respx.post("https://api.example.com/sms/send").mock(
return_value=Response(200, json={"id": 123456})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.sms.send_sms(SendSmsRequest(did="5550100", dst="5550200", message="hello"))
assert result.id == 123456
assert json.loads(route.calls[0].request.content) == {
"did": "5550100",
"dst": "5550200",
"message": "hello",
}
@respx.mock
def test_send_sms_default(api_key: str) -> None:
route = respx.post("https://api.example.com/sms/send/default").mock(
return_value=Response(200, json={"id": 123457})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.sms.send_sms_default(SendSmsDefaultRequest(dst="5550200", message="hello"))
assert result.id == 123457
assert json.loads(route.calls[0].request.content) == {
"dst": "5550200",
"message": "hello",
}
@respx.mock
def test_send_mms(api_key: str) -> None:
route = respx.post("https://api.example.com/mms/send").mock(
return_value=Response(200, json={"id": 123458})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.sms.send_mms(
SendMmsRequest(
did="5550100",
dst="5550200",
message="pic",
media1="https://example.com/img.jpg",
)
)
assert result.id == 123458
assert json.loads(route.calls[0].request.content)["media1"] == "https://example.com/img.jpg"
@respx.mock
def test_get_mms_media(api_key: str) -> None:
route = respx.get("https://api.example.com/mms/123/media").mock(
return_value=Response(
200,
json={"id": 123, "date": "2024-01-01", "media": [{"url": "https://x.com/a.jpg"}]},
)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.sms.get_mms_media(123, media_as_array=True)
assert result.media is not None
assert len(result.media) == 1
assert route.calls[0].request.url.params["media_as_array"] == "true"
@respx.mock
def test_delete_sms(api_key: str) -> None:
respx.delete("https://api.example.com/sms/123").mock(
return_value=Response(200, json={"status": "success"})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.sms.delete_sms(123)
assert result.status == "success"
@pytest.mark.asyncio
@respx.mock
async def test_async_send_sms(api_key: str) -> None:
respx.post("https://api.example.com/sms/send").mock(
return_value=Response(200, json={"id": 123456})
)
async with AsyncConduitClient(api_key, base_url="https://api.example.com") as client:
result = await client.sms.send_sms(
SendSmsRequest(did="5550100", dst="5550200", message="hello")
)
assert result.id == 123456

128
tests/test_torrents.py Normal file
View File

@@ -0,0 +1,128 @@
"""Tests for the Transmission torrents client."""
from __future__ import annotations
import json
import pytest
import respx
from httpx import Response
from conduit_client import AsyncConduitClient, ConduitClient
from conduit_client.models.torrents import (
AddTorrentRequest,
MoveQueueRequest,
MoveTorrentRequest,
PatchTorrentRequest,
RenamePathRequest,
)
@respx.mock
def test_add_torrent(api_key: str) -> None:
route = respx.post("https://api.example.com/torrents").mock(
return_value=Response(200, json={"id": 7, "name": "file.iso"})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.torrents.add(
AddTorrentRequest(url="magnet:?xt=urn:btih:abc", paused=True)
)
assert result["id"] == 7
assert json.loads(route.calls[0].request.content)["paused"] is True
@respx.mock
def test_list_torrents(api_key: str) -> None:
route = respx.get("https://api.example.com/torrents").mock(
return_value=Response(200, json={"torrents": []})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.torrents.list(fields=["id", "name"], ids=[1, 2])
assert result == {"torrents": []}
assert route.calls[0].request.url.params["fields"] == "id,name"
assert route.calls[0].request.url.params["ids"] == "1,2"
@respx.mock
def test_session_and_stats(api_key: str) -> None:
respx.get("https://api.example.com/torrents/session").mock(
return_value=Response(200, json={"download-dir": "/tmp"})
)
patch_route = respx.patch("https://api.example.com/torrents/session").mock(
return_value=Response(204)
)
respx.get("https://api.example.com/torrents/session/stats").mock(
return_value=Response(200, json={"cumulative-stats": {}})
)
respx.post("https://api.example.com/torrents/session/close").mock(
return_value=Response(204)
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
session = client.torrents.get_session()
assert session["download-dir"] == "/tmp"
client.torrents.patch_session({"download-dir": "/new"})
stats = client.torrents.get_session_stats()
assert stats == {"cumulative-stats": {}}
client.torrents.close_session()
assert json.loads(patch_route.calls[0].request.content) == {"download-dir": "/new"}
@respx.mock
def test_free_space(api_key: str) -> None:
route = respx.get("https://api.example.com/torrents/free-space").mock(
return_value=Response(200, json={"path": "/tmp", "size-bytes": 1000, "total_size": 2000})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.torrents.free_space("/tmp")
assert result.path == "/tmp"
assert result.size_bytes == 1000
assert route.calls[0].request.url.params["path"] == "/tmp"
@respx.mock
def test_torrent_actions(api_key: str) -> None:
respx.post("https://api.example.com/torrents/queue/move").mock(return_value=Response(204))
respx.get("https://api.example.com/torrents/groups").mock(return_value=Response(200, json={}))
respx.patch("https://api.example.com/torrents/groups/fast").mock(return_value=Response(204))
respx.get("https://api.example.com/torrents/7/download-link").mock(
return_value=Response(200, json={"torrent_link": "https://x.com/t", "file_links": []})
)
respx.get("https://api.example.com/torrents/7").mock(return_value=Response(200, json={"id": 7}))
respx.patch("https://api.example.com/torrents/7").mock(return_value=Response(204))
respx.post("https://api.example.com/torrents/7/start").mock(return_value=Response(204))
respx.post("https://api.example.com/torrents/7/start-now").mock(return_value=Response(204))
respx.post("https://api.example.com/torrents/7/stop").mock(return_value=Response(204))
respx.post("https://api.example.com/torrents/7/verify").mock(return_value=Response(204))
respx.post("https://api.example.com/torrents/7/reannounce").mock(return_value=Response(204))
respx.post("https://api.example.com/torrents/7/move").mock(return_value=Response(204))
respx.post("https://api.example.com/torrents/7/rename-path").mock(return_value=Response(204))
respx.delete("https://api.example.com/torrents/7").mock(return_value=Response(204))
with ConduitClient(api_key, base_url="https://api.example.com") as client:
client.torrents.move_queue(MoveQueueRequest(direction="top"))
client.torrents.list_groups()
client.torrents.patch_group("fast", {"speed_limit_down": 100})
link = client.torrents.download_link(7)
assert link.torrent_link == "https://x.com/t"
assert client.torrents.get(7)["id"] == 7
client.torrents.patch(7, PatchTorrentRequest(queue_position=1))
client.torrents.start(7)
client.torrents.start_now(7)
client.torrents.stop(7)
client.torrents.verify(7)
client.torrents.reannounce(7)
client.torrents.move(7, MoveTorrentRequest(location="/new", move=True))
client.torrents.rename_path(7, RenamePathRequest(path="old", name="new"))
client.torrents.remove(7, delete_local_data=True)
@pytest.mark.asyncio
@respx.mock
async def test_async_add_torrent(api_key: str) -> None:
respx.post("https://api.example.com/torrents").mock(
return_value=Response(200, json={"id": 7, "name": "file.iso"})
)
async with AsyncConduitClient(api_key, base_url="https://api.example.com") as client:
result = await client.torrents.add(
AddTorrentRequest(url="magnet:?xt=urn:btih:abc")
)
assert result["id"] == 7

55
tests/test_wiki.py Normal file
View File

@@ -0,0 +1,55 @@
"""Tests for the DokuWiki client."""
from __future__ import annotations
import json
import pytest
import respx
from httpx import Response
from conduit_client import AsyncConduitClient, ConduitClient
from conduit_client.models.wiki import WriteWikiPageRequest
@respx.mock
def test_get_raw(api_key: str) -> None:
route = respx.get("https://api.example.com/wiki/pages/foo%3Abar/raw").mock(
return_value=Response(200, json={"id": "foo:bar", "text": "hello"})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.wiki.get_raw("foo:bar")
assert result.text == "hello"
assert route.calls[0].request.url.path == "/wiki/pages/foo:bar/raw"
@respx.mock
def test_write(api_key: str) -> None:
route = respx.put("https://api.example.com/wiki/pages/foo%3Abar").mock(
return_value=Response(200, json={"id": "foo:bar", "url": "https://wiki.example.com/foo:bar", "verified": True})
)
with ConduitClient(api_key, base_url="https://api.example.com") as client:
result = client.wiki.write(
"foo:bar",
WriteWikiPageRequest(text="new content", summary="update", verify=False),
)
assert result.url == "https://wiki.example.com/foo:bar"
assert json.loads(route.calls[0].request.content) == {
"text": "new content",
"summary": "update",
"verify": False,
}
@pytest.mark.asyncio
@respx.mock
async def test_async_write(api_key: str) -> None:
respx.put("https://api.example.com/wiki/pages/foo%3Abar").mock(
return_value=Response(200, json={"id": "foo:bar", "url": "https://wiki.example.com/foo:bar", "verified": True})
)
async with AsyncConduitClient(api_key, base_url="https://api.example.com") as client:
result = await client.wiki.write(
"foo:bar",
WriteWikiPageRequest(text="new content"),
)
assert result.verified is True