manyagi-sdk 0.1.0__tar.gz
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-0.1.0/PKG-INFO +20 -0
- manyagi_sdk-0.1.0/README.md +11 -0
- manyagi_sdk-0.1.0/pyproject.toml +15 -0
- manyagi_sdk-0.1.0/setup.cfg +4 -0
- manyagi_sdk-0.1.0/src/manyagi_sdk/__init__.py +18 -0
- manyagi_sdk-0.1.0/src/manyagi_sdk/client.py +137 -0
- manyagi_sdk-0.1.0/src/manyagi_sdk.egg-info/PKG-INFO +20 -0
- manyagi_sdk-0.1.0/src/manyagi_sdk.egg-info/SOURCES.txt +9 -0
- manyagi_sdk-0.1.0/src/manyagi_sdk.egg-info/dependency_links.txt +1 -0
- manyagi_sdk-0.1.0/src/manyagi_sdk.egg-info/top_level.txt +1 -0
- manyagi_sdk-0.1.0/tests/test_client.py +53 -0
|
@@ -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,11 @@
|
|
|
1
|
+
# manyagi-sdk
|
|
2
|
+
|
|
3
|
+
Python client for the Manyagi platform read and preparation API.
|
|
4
|
+
|
|
5
|
+
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:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
PYTHONPATH=packages/manyagi-sdk-python/src python -c "from manyagi_sdk import ManyagiClient; print(ManyagiClient('https://manyagi.com').platform())"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
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,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "manyagi-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Manyagi platform client for the read and preparation API. Not yet published to PyPI."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "LicenseRef-Proprietary"
|
|
8
|
+
authors = [{ name = "Manyagi" }]
|
|
9
|
+
|
|
10
|
+
[tool.setuptools.packages.find]
|
|
11
|
+
where = ["src"]
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["setuptools>=68"]
|
|
15
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -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"
|
|
@@ -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,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/manyagi_sdk/__init__.py
|
|
4
|
+
src/manyagi_sdk/client.py
|
|
5
|
+
src/manyagi_sdk.egg-info/PKG-INFO
|
|
6
|
+
src/manyagi_sdk.egg-info/SOURCES.txt
|
|
7
|
+
src/manyagi_sdk.egg-info/dependency_links.txt
|
|
8
|
+
src/manyagi_sdk.egg-info/top_level.txt
|
|
9
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
manyagi_sdk
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import unittest
|
|
3
|
+
from urllib.request import Request
|
|
4
|
+
|
|
5
|
+
from manyagi_sdk import ManyagiClient, ManyagiError, assert_external_client_credential
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FakeResponse:
|
|
9
|
+
def __init__(self, status, payload, headers=None):
|
|
10
|
+
self.status = status
|
|
11
|
+
self._payload = payload
|
|
12
|
+
self.headers = headers or {}
|
|
13
|
+
|
|
14
|
+
def read(self):
|
|
15
|
+
return json.dumps(self._payload).encode()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ClientTests(unittest.TestCase):
|
|
19
|
+
def test_get_sends_bearer_and_does_not_post_twice(self):
|
|
20
|
+
seen = []
|
|
21
|
+
|
|
22
|
+
def opener(request: Request, timeout: float):
|
|
23
|
+
seen.append((request.get_method(), request.full_url, request.get_header("Authorization"), timeout))
|
|
24
|
+
if request.get_method() == "POST":
|
|
25
|
+
return FakeResponse(401, {"error": {"code": "unauthorized", "message": "Sign in"}})
|
|
26
|
+
return FakeResponse(200, {"api_version": "2026-11-05", "writes_enabled": False})
|
|
27
|
+
|
|
28
|
+
client = ManyagiClient("https://manyagi.com/", access_token="product-token", opener=opener)
|
|
29
|
+
body = client.platform()
|
|
30
|
+
self.assertFalse(body["writes_enabled"])
|
|
31
|
+
self.assertEqual(seen[0][0], "GET")
|
|
32
|
+
self.assertEqual(seen[0][1], "https://manyagi.com/api/v1/platform")
|
|
33
|
+
self.assertEqual(seen[0][2], "Bearer product-token")
|
|
34
|
+
with self.assertRaises(ManyagiError) as raised:
|
|
35
|
+
client.prepare_brief({"idea": "x", "source_medium": "original_idea"})
|
|
36
|
+
self.assertEqual(raised.exception.code, "unauthorized")
|
|
37
|
+
self.assertEqual(len(seen), 2)
|
|
38
|
+
|
|
39
|
+
def test_product_client_refuses_external_grant(self):
|
|
40
|
+
client = ManyagiClient("https://manyagi.com/", access_token="mga_abc", opener=lambda request, timeout: None)
|
|
41
|
+
with self.assertRaises(ManyagiError) as raised:
|
|
42
|
+
client.platform()
|
|
43
|
+
self.assertEqual(raised.exception.code, "external_credential_rejected")
|
|
44
|
+
|
|
45
|
+
def test_external_credential_rejects_product_jwt(self):
|
|
46
|
+
assert_external_client_credential("mga_abc")
|
|
47
|
+
with self.assertRaises(ManyagiError) as raised:
|
|
48
|
+
assert_external_client_credential("aaa.bbb.ccc")
|
|
49
|
+
self.assertEqual(raised.exception.code, "external_credential_required")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
unittest.main()
|