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.
manyagi_sdk/__init__.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
manyagi_sdk
|