manyagi-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ from .client import (
2
+ EXTERNAL_MCP_RESOURCE,
3
+ EXTERNAL_SCOPES,
4
+ ManyagiClient,
5
+ ManyagiError,
6
+ assert_external_client_credential,
7
+ external_credential_kind,
8
+ )
9
+
10
+ __all__ = [
11
+ "EXTERNAL_MCP_RESOURCE",
12
+ "EXTERNAL_SCOPES",
13
+ "ManyagiClient",
14
+ "ManyagiError",
15
+ "assert_external_client_credential",
16
+ "external_credential_kind",
17
+ ]
18
+ __version__ = "0.1.0"
manyagi_sdk/client.py ADDED
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from typing import Any, Callable
6
+ from urllib.error import HTTPError
7
+ from urllib.parse import quote, urlencode
8
+ from urllib.request import Request, urlopen
9
+
10
+
11
+ class ManyagiError(Exception):
12
+ def __init__(self, status: int, code: str, message: str, request_id: str | None):
13
+ super().__init__(message)
14
+ self.status = status
15
+ self.code = code
16
+ self.request_id = request_id
17
+
18
+
19
+ @dataclass
20
+ class ManyagiClient:
21
+ """Product-session client. Passport tokens are not accepted. POST is not retried."""
22
+
23
+ base_url: str
24
+ access_token: str | None = None
25
+ timeout: float = 10.0
26
+ max_get_retries: int = 2
27
+ opener: Callable[[Request, float], Any] | None = None
28
+
29
+ def __post_init__(self) -> None:
30
+ self.base_url = self.base_url.rstrip("/")
31
+
32
+ def platform(self) -> dict[str, Any]:
33
+ return self._request("GET", "/api/v1/platform")
34
+
35
+ def open_api(self) -> dict[str, Any]:
36
+ return self._request("GET", "/api/v1/openapi.json")
37
+
38
+ def search_worlds(self, query: str = "", limit: int = 20, offset: int = 0) -> dict[str, Any]:
39
+ params = urlencode({k: v for k, v in {"q": query, "limit": limit, "offset": offset}.items() if v})
40
+ return self._request("GET", f"/api/v1/discovery/worlds?{params}" if params else "/api/v1/discovery/worlds")
41
+
42
+ def world(self, ref: str) -> dict[str, Any]:
43
+ return self._request("GET", f"/api/v1/discovery/worlds/{quote(ref)}")
44
+
45
+ def rights(self, world: str) -> dict[str, Any]:
46
+ return self._request("GET", f"/api/v1/discovery/rights?world={quote(world)}")
47
+
48
+ def list_my_worlds(self, limit: int = 20) -> dict[str, Any]:
49
+ return self._request("GET", f"/api/v1/me/worlds?limit={limit}")
50
+
51
+ def read_world(self, ref: str) -> dict[str, Any]:
52
+ return self._request("GET", f"/api/v1/me/worlds/{quote(ref)}")
53
+
54
+ def credits(self) -> dict[str, Any]:
55
+ return self._request("GET", "/api/v1/me/credits")
56
+
57
+ def prepare_brief(self, body: dict[str, Any], idempotency_key: str | None = None) -> dict[str, Any]:
58
+ return self._request("POST", "/api/v1/briefs", body, idempotency_key)
59
+
60
+ def prepare_song(self, body: dict[str, Any], idempotency_key: str | None = None) -> dict[str, Any]:
61
+ return self._request("POST", "/api/v1/briefs/song", body, idempotency_key)
62
+
63
+ def prepare_licensing_inquiry(self, body: dict[str, Any], idempotency_key: str | None = None) -> dict[str, Any]:
64
+ return self._request("POST", "/api/v1/licensing/inquiries/prepare", body, idempotency_key)
65
+
66
+ def assistant(self, body: dict[str, Any]) -> dict[str, Any]:
67
+ return self._request("POST", "/api/v1/assistant", body)
68
+
69
+ def _request(self, method: str, path: str, body: dict[str, Any] | None = None, idempotency_key: str | None = None) -> dict[str, Any]:
70
+ headers = {"Accept": "application/json"}
71
+ data = None
72
+ if self.access_token and external_credential_kind(self.access_token) == "external":
73
+ raise ManyagiError(0, "external_credential_rejected", "An external grant cannot be sent to the product API.", None)
74
+ if self.access_token:
75
+ headers["Authorization"] = f"Bearer {self.access_token}"
76
+ if body is not None:
77
+ headers["Content-Type"] = "application/json"
78
+ data = json.dumps(body).encode()
79
+ if idempotency_key:
80
+ headers["Idempotency-Key"] = idempotency_key
81
+ attempts = self.max_get_retries + 1 if method == "GET" else 1
82
+ last_error: HTTPError | None = None
83
+ for attempt in range(attempts):
84
+ request = Request(f"{self.base_url}{path}", data=data, headers=headers, method=method)
85
+ try:
86
+ if self.opener:
87
+ response = self.opener(request, self.timeout)
88
+ else:
89
+ response = urlopen(request, timeout=self.timeout)
90
+ except HTTPError as error:
91
+ last_error = error
92
+ if method == "GET" and error.code in (429, 503) and attempt + 1 < attempts:
93
+ continue
94
+ payload = json.loads(error.read().decode() or "{}")
95
+ detail = payload.get("error", {})
96
+ raise ManyagiError(error.code, detail.get("code", "request_failed"), detail.get("message", error.reason), error.headers.get("x-request-id")) from None
97
+ raw = response.read().decode()
98
+ parsed = json.loads(raw or "{}")
99
+ status = getattr(response, "status", 200)
100
+ if status >= 400:
101
+ detail = parsed.get("error", {})
102
+ raise ManyagiError(status, detail.get("code", "request_failed"), detail.get("message", "request failed"), None)
103
+ return parsed
104
+ if last_error is not None:
105
+ raise last_error
106
+ raise ManyagiError(0, "request_failed", "request failed", None)
107
+
108
+
109
+ EXTERNAL_MCP_RESOURCE = "https://manyagi.com/mcp"
110
+ EXTERNAL_SCOPES = (
111
+ "worlds:read_owned",
112
+ "credits:read",
113
+ "discovery:read_public",
114
+ "rights:read_public",
115
+ "briefs:prepare",
116
+ "licensing:prepare",
117
+ )
118
+
119
+
120
+ def external_credential_kind(token: str) -> str:
121
+ if token.startswith("mga_") and "." not in token:
122
+ return "external"
123
+ if token.count(".") == 2:
124
+ return "product_jwt"
125
+ return "rejected"
126
+
127
+
128
+ def assert_external_client_credential(token: str) -> None:
129
+ kind = external_credential_kind(token)
130
+ if kind == "external":
131
+ return
132
+ message = (
133
+ "A product session cannot be used as an external client credential."
134
+ if kind == "product_jwt"
135
+ else "The external credential was rejected."
136
+ )
137
+ raise ManyagiError(0, "external_credential_required", message, None)
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: manyagi-sdk
3
+ Version: 0.1.0
4
+ Summary: Manyagi platform client for the read and preparation API. Not yet published to PyPI.
5
+ Author: Manyagi
6
+ License-Expression: LicenseRef-Proprietary
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+
10
+ # manyagi-sdk
11
+
12
+ Python client for the Manyagi platform read and preparation API.
13
+
14
+ Version `0.1.0`. Publication uses GitHub Actions workflow `publish-pypi-sdk.yml` (environment `pypi-sdk`) with PyPI Trusted Publishing (OIDC). Until published, install from a checkout for tests:
15
+
16
+ ```bash
17
+ PYTHONPATH=packages/manyagi-sdk-python/src python -c "from manyagi_sdk import ManyagiClient; print(ManyagiClient('https://manyagi.com').platform())"
18
+ ```
19
+
20
+ Pass a Manyagi product-session access token. Do not pass a Passport token or a service-role key. `POST` requests are not retried.
@@ -0,0 +1,6 @@
1
+ manyagi_sdk/__init__.py,sha256=wfP4AT3r0uFrA59ygedy_1rIdkwZ8-BEa0WTBfJmZCk,381
2
+ manyagi_sdk/client.py,sha256=kkNT8XTlJpzre48HBhCcaSvVHx0dwUzXGAjwyyo4fs8,5764
3
+ manyagi_sdk-0.1.0.dist-info/METADATA,sha256=2tvEOJy-M5SfxMj_eVNYF_RX_ZIpzFXw9gW2zffzHRQ,842
4
+ manyagi_sdk-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
5
+ manyagi_sdk-0.1.0.dist-info/top_level.txt,sha256=PdJU09EHuFMxt94inBK9TIjiVvnQtNI2HxQzeL7BY9Q,12
6
+ manyagi_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ manyagi_sdk