From aa0f2973c970b248e4f111b1cc705b5d0d3ceca6 Mon Sep 17 00:00:00 2001 From: George-iam Date: Sun, 1 Mar 2026 07:16:40 +0000 Subject: [PATCH] feat: add media upload helpers to Python SDK Expose media create/get/finalize helpers with tests and quickstart examples so Track C parity covers the media contract family set in GA clients. Made-with: Cursor --- README.md | 17 ++++++++ axme_sdk/client.py | 35 ++++++++++++++++ tests/test_client.py | 97 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) diff --git a/README.md b/README.md index dc7231d..4b6d65a 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,23 @@ with AxmeClient(config) as client: idempotency_key="invite-accept-001", ) print(accepted["public_address"]) + media_upload = client.create_media_upload( + { + "owner_agent": "agent://example/receiver", + "filename": "contract.pdf", + "mime_type": "application/pdf", + "size_bytes": 12345, + }, + idempotency_key="media-create-001", + ) + print(media_upload["upload_id"]) + media_state = client.get_media_upload(media_upload["upload_id"]) + print(media_state["upload"]["status"]) + finalized = client.finalize_media_upload( + {"upload_id": media_upload["upload_id"], "size_bytes": 12345}, + idempotency_key="media-finalize-001", + ) + print(finalized["status"]) subscription = client.upsert_webhook_subscription( { "callback_url": "https://integrator.example/webhooks/axme", diff --git a/axme_sdk/client.py b/axme_sdk/client.py index 1bc6867..dfe8af0 100644 --- a/axme_sdk/client.py +++ b/axme_sdk/client.py @@ -198,6 +198,41 @@ def accept_invite( retryable=idempotency_key is not None, ) + def create_media_upload( + self, + payload: dict[str, Any], + *, + idempotency_key: str | None = None, + trace_id: str | None = None, + ) -> dict[str, Any]: + return self._request_json( + "POST", + "/v1/media/create-upload", + json_body=payload, + idempotency_key=idempotency_key, + trace_id=trace_id, + retryable=idempotency_key is not None, + ) + + def get_media_upload(self, upload_id: str, *, trace_id: str | None = None) -> dict[str, Any]: + return self._request_json("GET", f"/v1/media/{upload_id}", trace_id=trace_id, retryable=True) + + def finalize_media_upload( + self, + payload: dict[str, Any], + *, + idempotency_key: str | None = None, + trace_id: str | None = None, + ) -> dict[str, Any]: + return self._request_json( + "POST", + "/v1/media/finalize-upload", + json_body=payload, + idempotency_key=idempotency_key, + trace_id=trace_id, + retryable=idempotency_key is not None, + ) + def upsert_webhook_subscription( self, payload: dict[str, Any], diff --git a/tests/test_client.py b/tests/test_client.py index 9cf9908..ed9a886 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -364,6 +364,103 @@ def handler(request: httpx.Request) -> httpx.Response: assert client.accept_invite(token, payload, idempotency_key="invite-accept-1")["status"] == "accepted" +def test_create_media_upload_success() -> None: + upload_id = "77777777-7777-4777-8777-777777777777" + payload = { + "owner_agent": "agent://owner", + "filename": "contract.pdf", + "mime_type": "application/pdf", + "size_bytes": 12345, + } + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == "/v1/media/create-upload" + assert request.headers["idempotency-key"] == "media-create-1" + body = json.loads(request.read().decode("utf-8")) + assert body == payload + return httpx.Response( + 200, + json={ + "ok": True, + "upload_id": upload_id, + "owner_agent": "agent://owner", + "bucket": "axme-media", + "object_path": "agent-owner/contract.pdf", + "upload_url": "https://upload.example/media/1", + "status": "pending", + "expires_at": "2026-03-01T00:00:00Z", + "max_size_bytes": 10485760, + }, + ) + + client = _client(handler) + assert client.create_media_upload(payload, idempotency_key="media-create-1")["upload_id"] == upload_id + + +def test_get_media_upload_success() -> None: + upload_id = "77777777-7777-4777-8777-777777777777" + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "GET" + assert request.url.path == f"/v1/media/{upload_id}" + return httpx.Response( + 200, + json={ + "ok": True, + "upload": { + "upload_id": upload_id, + "owner_agent": "agent://owner", + "bucket": "axme-media", + "object_path": "agent-owner/contract.pdf", + "mime_type": "application/pdf", + "filename": "contract.pdf", + "size_bytes": 12345, + "sha256": None, + "status": "pending", + "created_at": "2026-02-28T00:00:00Z", + "expires_at": "2026-03-01T00:00:00Z", + "finalized_at": None, + "download_url": None, + "preview_url": None, + }, + }, + ) + + client = _client(handler) + assert client.get_media_upload(upload_id)["upload"]["status"] == "pending" + + +def test_finalize_media_upload_success() -> None: + upload_id = "77777777-7777-4777-8777-777777777777" + payload = {"upload_id": upload_id, "size_bytes": 12345} + + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert request.url.path == "/v1/media/finalize-upload" + assert request.headers["idempotency-key"] == "media-finalize-1" + body = json.loads(request.read().decode("utf-8")) + assert body == payload + return httpx.Response( + 200, + json={ + "ok": True, + "upload_id": upload_id, + "owner_agent": "agent://owner", + "bucket": "axme-media", + "object_path": "agent-owner/contract.pdf", + "mime_type": "application/pdf", + "size_bytes": 12345, + "sha256": None, + "status": "ready", + "finalized_at": "2026-02-28T00:00:10Z", + }, + ) + + client = _client(handler) + assert client.finalize_media_upload(payload, idempotency_key="media-finalize-1")["status"] == "ready" + + @pytest.mark.parametrize( ("status_code", "expected_exception"), [