acontext 0.0.1.dev0__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.
- acontext-0.0.1.dev0/PKG-INFO +66 -0
- acontext-0.0.1.dev0/README.md +54 -0
- acontext-0.0.1.dev0/pyproject.toml +20 -0
- acontext-0.0.1.dev0/src/acontext/__init__.py +33 -0
- acontext-0.0.1.dev0/src/acontext/_constants.py +15 -0
- acontext-0.0.1.dev0/src/acontext/client.py +176 -0
- acontext-0.0.1.dev0/src/acontext/client_types.py +20 -0
- acontext-0.0.1.dev0/src/acontext/errors.py +43 -0
- acontext-0.0.1.dev0/src/acontext/messages.py +94 -0
- acontext-0.0.1.dev0/src/acontext/py.typed +0 -0
- acontext-0.0.1.dev0/src/acontext/resources/__init__.py +19 -0
- acontext-0.0.1.dev0/src/acontext/resources/artifacts.py +98 -0
- acontext-0.0.1.dev0/src/acontext/resources/blocks.py +83 -0
- acontext-0.0.1.dev0/src/acontext/resources/pages.py +82 -0
- acontext-0.0.1.dev0/src/acontext/resources/sessions.py +107 -0
- acontext-0.0.1.dev0/src/acontext/resources/spaces.py +36 -0
- acontext-0.0.1.dev0/src/acontext/uploads.py +44 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: acontext
|
|
3
|
+
Version: 0.0.1.dev0
|
|
4
|
+
Summary: Python SDK for the Acontext API
|
|
5
|
+
Keywords: acontext,sdk,client,api
|
|
6
|
+
Requires-Dist: httpx>=0.28.1
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Project-URL: Homepage, https://github.com/memodb-io/Acontext
|
|
9
|
+
Project-URL: Issues, https://github.com/memodb-io/Acontext/issues
|
|
10
|
+
Project-URL: Repository, https://github.com/memodb-io/Acontext
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
## acontext client for python
|
|
14
|
+
|
|
15
|
+
Python SDK for interacting with the Acontext REST API.
|
|
16
|
+
|
|
17
|
+
### Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install acontext
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
> Requires Python 3.13 or newer.
|
|
24
|
+
|
|
25
|
+
### Quickstart
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from acontext import AcontextClient, MessagePart
|
|
29
|
+
|
|
30
|
+
with AcontextClient(api_key="sk_project_token") as client:
|
|
31
|
+
# List spaces for the authenticated project
|
|
32
|
+
spaces = client.spaces.list()
|
|
33
|
+
|
|
34
|
+
# Create a session bound to the first space
|
|
35
|
+
session = client.sessions.create(space_id=spaces[0]["id"])
|
|
36
|
+
|
|
37
|
+
# Send a text message to the session
|
|
38
|
+
client.sessions.send_message(
|
|
39
|
+
session["id"],
|
|
40
|
+
role="user",
|
|
41
|
+
parts=[MessagePart.text_part("Hello from Python!")],
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
See the inline docstrings for the full list of helpers covering sessions, spaces, artifacts and file uploads.
|
|
46
|
+
|
|
47
|
+
### Working with pages and blocks
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from acontext import AcontextClient
|
|
51
|
+
|
|
52
|
+
client = AcontextClient(api_key="sk_project_token")
|
|
53
|
+
|
|
54
|
+
space = client.spaces.create()
|
|
55
|
+
try:
|
|
56
|
+
page = client.pages.create(space["id"], title="Kick-off Notes")
|
|
57
|
+
client.blocks.create(
|
|
58
|
+
space["id"],
|
|
59
|
+
parent_id=page["id"],
|
|
60
|
+
block_type="text",
|
|
61
|
+
title="First block",
|
|
62
|
+
props={"text": "Plan the sprint goals"},
|
|
63
|
+
)
|
|
64
|
+
finally:
|
|
65
|
+
client.close()
|
|
66
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
## acontext client for python
|
|
2
|
+
|
|
3
|
+
Python SDK for interacting with the Acontext REST API.
|
|
4
|
+
|
|
5
|
+
### Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install acontext
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> Requires Python 3.13 or newer.
|
|
12
|
+
|
|
13
|
+
### Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from acontext import AcontextClient, MessagePart
|
|
17
|
+
|
|
18
|
+
with AcontextClient(api_key="sk_project_token") as client:
|
|
19
|
+
# List spaces for the authenticated project
|
|
20
|
+
spaces = client.spaces.list()
|
|
21
|
+
|
|
22
|
+
# Create a session bound to the first space
|
|
23
|
+
session = client.sessions.create(space_id=spaces[0]["id"])
|
|
24
|
+
|
|
25
|
+
# Send a text message to the session
|
|
26
|
+
client.sessions.send_message(
|
|
27
|
+
session["id"],
|
|
28
|
+
role="user",
|
|
29
|
+
parts=[MessagePart.text_part("Hello from Python!")],
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
See the inline docstrings for the full list of helpers covering sessions, spaces, artifacts and file uploads.
|
|
34
|
+
|
|
35
|
+
### Working with pages and blocks
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from acontext import AcontextClient
|
|
39
|
+
|
|
40
|
+
client = AcontextClient(api_key="sk_project_token")
|
|
41
|
+
|
|
42
|
+
space = client.spaces.create()
|
|
43
|
+
try:
|
|
44
|
+
page = client.pages.create(space["id"], title="Kick-off Notes")
|
|
45
|
+
client.blocks.create(
|
|
46
|
+
space["id"],
|
|
47
|
+
parent_id=page["id"],
|
|
48
|
+
block_type="text",
|
|
49
|
+
title="First block",
|
|
50
|
+
props={"text": "Plan the sprint goals"},
|
|
51
|
+
)
|
|
52
|
+
finally:
|
|
53
|
+
client.close()
|
|
54
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "acontext"
|
|
3
|
+
version = "0.0.1.dev0"
|
|
4
|
+
description = "Python SDK for the Acontext API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = ["httpx>=0.28.1"]
|
|
8
|
+
keywords = ["acontext", "sdk", "client", "api"]
|
|
9
|
+
|
|
10
|
+
[project.urls]
|
|
11
|
+
Homepage = "https://github.com/memodb-io/Acontext"
|
|
12
|
+
Repository = "https://github.com/memodb-io/Acontext"
|
|
13
|
+
Issues = "https://github.com/memodb-io/Acontext/issues"
|
|
14
|
+
|
|
15
|
+
[dependency-groups]
|
|
16
|
+
dev = ["pytest", "ruff"]
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.9.2,<0.10.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python SDK for the Acontext API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from importlib import metadata as _metadata
|
|
6
|
+
|
|
7
|
+
from .client import AcontextClient, FileUpload, MessagePart
|
|
8
|
+
from .resources import (
|
|
9
|
+
ArtifactFilesAPI,
|
|
10
|
+
ArtifactsAPI,
|
|
11
|
+
BlocksAPI,
|
|
12
|
+
PagesAPI,
|
|
13
|
+
SessionsAPI,
|
|
14
|
+
SpacesAPI,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AcontextClient",
|
|
19
|
+
"FileUpload",
|
|
20
|
+
"MessagePart",
|
|
21
|
+
"ArtifactsAPI",
|
|
22
|
+
"ArtifactFilesAPI",
|
|
23
|
+
"BlocksAPI",
|
|
24
|
+
"PagesAPI",
|
|
25
|
+
"SessionsAPI",
|
|
26
|
+
"SpacesAPI",
|
|
27
|
+
"__version__",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
__version__ = _metadata.version("acontext")
|
|
32
|
+
except _metadata.PackageNotFoundError: # pragma: no cover - local/checkout usage
|
|
33
|
+
__version__ = "0.0.0"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Internal constants shared across the Python SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from importlib import metadata as _metadata
|
|
6
|
+
|
|
7
|
+
DEFAULT_BASE_URL = "https://api.acontext.io/api/v1"
|
|
8
|
+
SUPPORTED_ROLES = {"user", "assistant", "system", "tool", "function"}
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
_VERSION = _metadata.version("acontext-py")
|
|
12
|
+
except _metadata.PackageNotFoundError: # pragma: no cover - local/checkout usage
|
|
13
|
+
_VERSION = "0.0.0"
|
|
14
|
+
|
|
15
|
+
DEFAULT_USER_AGENT = f"acontext-py/{_VERSION}"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level synchronous client for the Acontext API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, BinaryIO, Mapping, MutableMapping
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._constants import DEFAULT_BASE_URL, DEFAULT_USER_AGENT
|
|
10
|
+
from .errors import APIError, TransportError
|
|
11
|
+
from .messages import MessagePart as MessagePart
|
|
12
|
+
from .uploads import FileUpload as FileUpload
|
|
13
|
+
from .resources.artifacts import ArtifactsAPI as ArtifactsAPI
|
|
14
|
+
from .resources.blocks import BlocksAPI as BlocksAPI
|
|
15
|
+
from .resources.pages import PagesAPI as PagesAPI
|
|
16
|
+
from .resources.sessions import SessionsAPI as SessionsAPI
|
|
17
|
+
from .resources.spaces import SpacesAPI as SpacesAPI
|
|
18
|
+
|
|
19
|
+
class AcontextClient:
|
|
20
|
+
"""
|
|
21
|
+
Synchronous HTTP client for the Acontext REST API.
|
|
22
|
+
|
|
23
|
+
Example::
|
|
24
|
+
|
|
25
|
+
from acontext import AcontextClient, MessagePart
|
|
26
|
+
|
|
27
|
+
with AcontextClient(api_key="sk_...") as client:
|
|
28
|
+
spaces = client.spaces.list()
|
|
29
|
+
session = client.sessions.create(space_id=spaces[0]["id"])
|
|
30
|
+
client.sessions.send_message(
|
|
31
|
+
session["id"],
|
|
32
|
+
role="user",
|
|
33
|
+
parts=[MessagePart.text_part("Hello Acontext!")],
|
|
34
|
+
)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
api_key: str,
|
|
41
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
42
|
+
timeout: float | httpx.Timeout | None = 10.0,
|
|
43
|
+
user_agent: str | None = None,
|
|
44
|
+
client: httpx.Client | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
if not api_key:
|
|
47
|
+
raise ValueError("api_key is required")
|
|
48
|
+
|
|
49
|
+
base_url = base_url.rstrip("/")
|
|
50
|
+
headers = {
|
|
51
|
+
"Authorization": f"Bearer {api_key}",
|
|
52
|
+
"Accept": "application/json",
|
|
53
|
+
"User-Agent": user_agent or DEFAULT_USER_AGENT,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if client is not None:
|
|
57
|
+
self._client = client
|
|
58
|
+
self._owns_client = False
|
|
59
|
+
if client.base_url == httpx.URL():
|
|
60
|
+
client.base_url = httpx.URL(base_url)
|
|
61
|
+
for name, value in headers.items():
|
|
62
|
+
if name not in client.headers:
|
|
63
|
+
client.headers[name] = value
|
|
64
|
+
self._base_url = str(client.base_url) or base_url
|
|
65
|
+
else:
|
|
66
|
+
self._client = httpx.Client(base_url=base_url, headers=headers, timeout=timeout)
|
|
67
|
+
self._owns_client = True
|
|
68
|
+
self._base_url = base_url
|
|
69
|
+
|
|
70
|
+
self._timeout = timeout
|
|
71
|
+
|
|
72
|
+
self.spaces = SpacesAPI(self)
|
|
73
|
+
self.sessions = SessionsAPI(self)
|
|
74
|
+
self.artifacts = ArtifactsAPI(self)
|
|
75
|
+
self.pages = PagesAPI(self)
|
|
76
|
+
self.blocks = BlocksAPI(self)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def base_url(self) -> str:
|
|
80
|
+
return self._base_url
|
|
81
|
+
|
|
82
|
+
def close(self) -> None:
|
|
83
|
+
if self._owns_client:
|
|
84
|
+
self._client.close()
|
|
85
|
+
|
|
86
|
+
def __enter__(self) -> "AcontextClient":
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def __exit__(self, exc_type, exc, tb) -> None: # noqa: D401 - standard context manager protocol
|
|
90
|
+
self.close()
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
# HTTP plumbing shared by resource clients
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
def request(
|
|
96
|
+
self,
|
|
97
|
+
method: str,
|
|
98
|
+
path: str,
|
|
99
|
+
*,
|
|
100
|
+
params: Mapping[str, Any] | None = None,
|
|
101
|
+
json_data: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
|
|
102
|
+
data: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
|
|
103
|
+
files: Mapping[str, tuple[str, BinaryIO, str | None]] | None = None,
|
|
104
|
+
unwrap: bool = True,
|
|
105
|
+
) -> Any:
|
|
106
|
+
try:
|
|
107
|
+
response = self._client.request(
|
|
108
|
+
method=method,
|
|
109
|
+
url=path,
|
|
110
|
+
params=params,
|
|
111
|
+
json=json_data,
|
|
112
|
+
data=data,
|
|
113
|
+
files=files,
|
|
114
|
+
timeout=self._timeout,
|
|
115
|
+
)
|
|
116
|
+
except httpx.HTTPError as exc: # pragma: no cover - passthrough to caller
|
|
117
|
+
raise TransportError(str(exc)) from exc
|
|
118
|
+
|
|
119
|
+
return self._handle_response(response, unwrap=unwrap)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _handle_response(response: httpx.Response, *, unwrap: bool) -> Any:
|
|
123
|
+
content_type = response.headers.get("content-type", "")
|
|
124
|
+
|
|
125
|
+
parsed: Mapping[str, Any] | MutableMapping[str, Any] | None
|
|
126
|
+
if "application/json" in content_type or content_type.startswith("application/problem+json"):
|
|
127
|
+
try:
|
|
128
|
+
parsed = response.json()
|
|
129
|
+
except ValueError:
|
|
130
|
+
parsed = None
|
|
131
|
+
else:
|
|
132
|
+
parsed = None
|
|
133
|
+
|
|
134
|
+
if response.status_code >= 400:
|
|
135
|
+
message = response.reason_phrase
|
|
136
|
+
payload: Mapping[str, Any] | MutableMapping[str, Any] | None = parsed
|
|
137
|
+
code: int | None = None
|
|
138
|
+
error: str | None = None
|
|
139
|
+
if payload and isinstance(payload, Mapping):
|
|
140
|
+
message = str(payload.get("msg") or payload.get("message") or message)
|
|
141
|
+
error = payload.get("error")
|
|
142
|
+
try:
|
|
143
|
+
code_val = payload.get("code")
|
|
144
|
+
if isinstance(code_val, int):
|
|
145
|
+
code = code_val
|
|
146
|
+
except Exception: # pragma: no cover - defensive
|
|
147
|
+
code = None
|
|
148
|
+
raise APIError(
|
|
149
|
+
status_code=response.status_code,
|
|
150
|
+
code=code,
|
|
151
|
+
message=message,
|
|
152
|
+
error=error,
|
|
153
|
+
payload=payload,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if parsed is None:
|
|
157
|
+
if unwrap:
|
|
158
|
+
return response.text
|
|
159
|
+
return {"code": response.status_code, "data": response.text, "msg": response.reason_phrase}
|
|
160
|
+
|
|
161
|
+
if not isinstance(parsed, Mapping):
|
|
162
|
+
if unwrap:
|
|
163
|
+
return parsed
|
|
164
|
+
return parsed
|
|
165
|
+
|
|
166
|
+
app_code = parsed.get("code")
|
|
167
|
+
if isinstance(app_code, int) and app_code >= 400:
|
|
168
|
+
raise APIError(
|
|
169
|
+
status_code=response.status_code,
|
|
170
|
+
code=app_code,
|
|
171
|
+
message=str(parsed.get("msg") or response.reason_phrase),
|
|
172
|
+
error=parsed.get("error"),
|
|
173
|
+
payload=parsed,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return parsed.get("data") if unwrap else parsed
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common typing helpers used by resource modules to avoid circular imports.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, BinaryIO, Mapping, MutableMapping, Protocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RequesterProtocol(Protocol):
|
|
9
|
+
def request(
|
|
10
|
+
self,
|
|
11
|
+
method: str,
|
|
12
|
+
path: str,
|
|
13
|
+
*,
|
|
14
|
+
params: Mapping[str, Any] | None = None,
|
|
15
|
+
json_data: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
|
|
16
|
+
data: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
|
|
17
|
+
files: Mapping[str, tuple[str, BinaryIO, str | None]] | None = None,
|
|
18
|
+
unwrap: bool = True,
|
|
19
|
+
) -> Any:
|
|
20
|
+
...
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions raised by the acontext Python client.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, MutableMapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AcontextError(Exception):
|
|
9
|
+
"""Base exception for all errors raised by ``acontext``."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class APIError(AcontextError):
|
|
13
|
+
"""
|
|
14
|
+
Raised when the server returns an error response.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
status_code: HTTP status code returned by the server.
|
|
18
|
+
code: Optional application-level error code from the payload.
|
|
19
|
+
message: Human readable message if provided by the server.
|
|
20
|
+
error: Raw error field from the payload in non-release environments.
|
|
21
|
+
payload: The full parsed JSON payload.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
status_code: int,
|
|
28
|
+
code: int | None = None,
|
|
29
|
+
message: str | None = None,
|
|
30
|
+
error: str | None = None,
|
|
31
|
+
payload: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.status_code = status_code
|
|
34
|
+
self.code = code
|
|
35
|
+
self.message = message
|
|
36
|
+
self.error = error
|
|
37
|
+
self.payload = payload
|
|
38
|
+
details = message or error or "API request failed"
|
|
39
|
+
super().__init__(f"{status_code}: {details}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TransportError(AcontextError):
|
|
43
|
+
"""Raised when the underlying HTTP transport failed before receiving a response."""
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Support for constructing session messages.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, BinaryIO, Mapping, MutableMapping, Sequence, Tuple
|
|
7
|
+
|
|
8
|
+
from .uploads import FileUpload, normalize_file_upload
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class MessagePart:
|
|
13
|
+
"""
|
|
14
|
+
Represents a single message part for ``/session/{id}/messages``.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
type: One of ``text``, ``image``, ``audio``, ``video``, ``file``, ``tool-call``,
|
|
18
|
+
``tool-result`` or ``data``.
|
|
19
|
+
text: Optional textual payload for ``text`` parts.
|
|
20
|
+
meta: Optional metadata dictionary accepted by the API.
|
|
21
|
+
file: Optional file attachment; required for binary part types.
|
|
22
|
+
file_field: Optional field name to use in the multipart body. When omitted the
|
|
23
|
+
client will auto-generate deterministic field names.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
type: str
|
|
27
|
+
text: str | None = None
|
|
28
|
+
meta: Mapping[str, Any] | None = None
|
|
29
|
+
file: FileUpload | tuple[str, BinaryIO | bytes] | tuple[str, BinaryIO | bytes, str | None] | None = None
|
|
30
|
+
file_field: str | None = None
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def text_part(cls, text: str, *, meta: Mapping[str, Any] | None = None) -> "MessagePart":
|
|
34
|
+
return cls(type="text", text=text, meta=meta)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def file_part(
|
|
38
|
+
cls,
|
|
39
|
+
upload: FileUpload | tuple[str, BinaryIO | bytes] | tuple[str, BinaryIO | bytes, str | None],
|
|
40
|
+
*,
|
|
41
|
+
meta: Mapping[str, Any] | None = None,
|
|
42
|
+
type: str = "file",
|
|
43
|
+
) -> "MessagePart":
|
|
44
|
+
return cls(type=type, file=upload, meta=meta)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def normalize_message_part(part: MessagePart | str | Mapping[str, Any]) -> MessagePart:
|
|
48
|
+
if isinstance(part, MessagePart):
|
|
49
|
+
return part
|
|
50
|
+
if isinstance(part, str):
|
|
51
|
+
return MessagePart(type="text", text=part)
|
|
52
|
+
if isinstance(part, Mapping):
|
|
53
|
+
if "type" not in part:
|
|
54
|
+
raise ValueError("mapping message parts must include a 'type'")
|
|
55
|
+
file = part.get("file")
|
|
56
|
+
normalized_file: FileUpload | tuple[str, BinaryIO | bytes] | tuple[str, BinaryIO | bytes, str | None] | None
|
|
57
|
+
if file is None:
|
|
58
|
+
normalized_file = None
|
|
59
|
+
else:
|
|
60
|
+
normalized_file = file # type: ignore[assignment]
|
|
61
|
+
return MessagePart(
|
|
62
|
+
type=str(part["type"]),
|
|
63
|
+
text=part.get("text"),
|
|
64
|
+
meta=part.get("meta"),
|
|
65
|
+
file=normalized_file,
|
|
66
|
+
file_field=part.get("file_field"),
|
|
67
|
+
)
|
|
68
|
+
raise TypeError("unsupported message part type")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_message_payload(
|
|
72
|
+
parts: Sequence[MessagePart | str | Mapping[str, Any]],
|
|
73
|
+
) -> tuple[list[MutableMapping[str, Any]], dict[str, Tuple[str, BinaryIO, str | None]]]:
|
|
74
|
+
payload_parts: list[MutableMapping[str, Any]] = []
|
|
75
|
+
files: dict[str, Tuple[str, BinaryIO, str | None]] = {}
|
|
76
|
+
|
|
77
|
+
for idx, raw_part in enumerate(parts):
|
|
78
|
+
part = normalize_message_part(raw_part)
|
|
79
|
+
payload: MutableMapping[str, Any] = {"type": part.type}
|
|
80
|
+
|
|
81
|
+
if part.meta is not None:
|
|
82
|
+
payload["meta"] = dict(part.meta)
|
|
83
|
+
if part.text is not None:
|
|
84
|
+
payload["text"] = part.text
|
|
85
|
+
|
|
86
|
+
if part.file is not None:
|
|
87
|
+
upload = normalize_file_upload(part.file)
|
|
88
|
+
field_name = part.file_field or f"file_{idx}"
|
|
89
|
+
payload["file_field"] = field_name
|
|
90
|
+
files[field_name] = upload.as_httpx()
|
|
91
|
+
|
|
92
|
+
payload_parts.append(payload)
|
|
93
|
+
|
|
94
|
+
return payload_parts, files
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Resource-specific API helpers for the Acontext client."""
|
|
2
|
+
|
|
3
|
+
from .artifacts import (
|
|
4
|
+
ArtifactFilesAPI,
|
|
5
|
+
ArtifactsAPI,
|
|
6
|
+
)
|
|
7
|
+
from .blocks import BlocksAPI
|
|
8
|
+
from .pages import PagesAPI
|
|
9
|
+
from .sessions import SessionsAPI
|
|
10
|
+
from .spaces import SpacesAPI
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ArtifactsAPI",
|
|
14
|
+
"ArtifactFilesAPI",
|
|
15
|
+
"BlocksAPI",
|
|
16
|
+
"PagesAPI",
|
|
17
|
+
"SessionsAPI",
|
|
18
|
+
"SpacesAPI",
|
|
19
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Artifact and file endpoints.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, BinaryIO, Mapping, MutableMapping
|
|
7
|
+
|
|
8
|
+
from ..client_types import RequesterProtocol
|
|
9
|
+
from ..uploads import FileUpload, normalize_file_upload
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ArtifactsAPI:
|
|
13
|
+
def __init__(self, requester: RequesterProtocol) -> None:
|
|
14
|
+
self._requester = requester
|
|
15
|
+
self.files = ArtifactFilesAPI(requester)
|
|
16
|
+
|
|
17
|
+
def list(self) -> Any:
|
|
18
|
+
return self._requester.request("GET", "/artifact")
|
|
19
|
+
|
|
20
|
+
def create(self) -> Any:
|
|
21
|
+
return self._requester.request("POST", "/artifact")
|
|
22
|
+
|
|
23
|
+
def delete(self, artifact_id: str) -> None:
|
|
24
|
+
self._requester.request("DELETE", f"/artifact/{artifact_id}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ArtifactFilesAPI:
|
|
28
|
+
def __init__(self, requester: RequesterProtocol) -> None:
|
|
29
|
+
self._requester = requester
|
|
30
|
+
|
|
31
|
+
def upload(
|
|
32
|
+
self,
|
|
33
|
+
artifact_id: str,
|
|
34
|
+
*,
|
|
35
|
+
file: FileUpload | tuple[str, BinaryIO | bytes] | tuple[str, BinaryIO | bytes, str | None],
|
|
36
|
+
file_path: str | None = None,
|
|
37
|
+
meta: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
|
|
38
|
+
) -> Any:
|
|
39
|
+
upload = normalize_file_upload(file)
|
|
40
|
+
files = {"file": upload.as_httpx()}
|
|
41
|
+
form: dict[str, Any] = {}
|
|
42
|
+
if file_path:
|
|
43
|
+
form["file_path"] = file_path
|
|
44
|
+
if meta is not None:
|
|
45
|
+
form["meta"] = json.dumps(meta)
|
|
46
|
+
return self._requester.request(
|
|
47
|
+
"POST",
|
|
48
|
+
f"/artifact/{artifact_id}/file",
|
|
49
|
+
data=form or None,
|
|
50
|
+
files=files,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def update(
|
|
54
|
+
self,
|
|
55
|
+
artifact_id: str,
|
|
56
|
+
*,
|
|
57
|
+
file_path: str,
|
|
58
|
+
file: FileUpload | tuple[str, BinaryIO | bytes] | tuple[str, BinaryIO | bytes, str | None],
|
|
59
|
+
) -> Any:
|
|
60
|
+
upload = normalize_file_upload(file)
|
|
61
|
+
files = {"file": upload.as_httpx()}
|
|
62
|
+
form = {"file_path": file_path}
|
|
63
|
+
return self._requester.request(
|
|
64
|
+
"PUT",
|
|
65
|
+
f"/artifact/{artifact_id}/file",
|
|
66
|
+
data=form,
|
|
67
|
+
files=files,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def delete(self, artifact_id: str, *, file_path: str) -> None:
|
|
71
|
+
params = {"file_path": file_path}
|
|
72
|
+
self._requester.request("DELETE", f"/artifact/{artifact_id}/file", params=params)
|
|
73
|
+
|
|
74
|
+
def get(
|
|
75
|
+
self,
|
|
76
|
+
artifact_id: str,
|
|
77
|
+
*,
|
|
78
|
+
file_path: str,
|
|
79
|
+
with_public_url: bool | None = None,
|
|
80
|
+
expire: int | None = None,
|
|
81
|
+
) -> Any:
|
|
82
|
+
params: dict[str, Any] = {"file_path": file_path}
|
|
83
|
+
if with_public_url is not None:
|
|
84
|
+
params["with_public_url"] = "true" if with_public_url else "false"
|
|
85
|
+
if expire is not None:
|
|
86
|
+
params["expire"] = expire
|
|
87
|
+
return self._requester.request("GET", f"/artifact/{artifact_id}/file", params=params)
|
|
88
|
+
|
|
89
|
+
def list(
|
|
90
|
+
self,
|
|
91
|
+
artifact_id: str,
|
|
92
|
+
*,
|
|
93
|
+
path: str | None = None,
|
|
94
|
+
) -> Any:
|
|
95
|
+
params: dict[str, Any] = {}
|
|
96
|
+
if path is not None:
|
|
97
|
+
params["path"] = path
|
|
98
|
+
return self._requester.request("GET", f"/artifact/{artifact_id}/file/ls", params=params or None)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Block endpoints.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, MutableMapping
|
|
6
|
+
|
|
7
|
+
from ..client_types import RequesterProtocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BlocksAPI:
|
|
11
|
+
def __init__(self, requester: RequesterProtocol) -> None:
|
|
12
|
+
self._requester = requester
|
|
13
|
+
|
|
14
|
+
def list(self, space_id: str, *, parent_id: str) -> Any:
|
|
15
|
+
if not parent_id:
|
|
16
|
+
raise ValueError("parent_id is required")
|
|
17
|
+
params = {"parent_id": parent_id}
|
|
18
|
+
return self._requester.request("GET", f"/space/{space_id}/block", params=params)
|
|
19
|
+
|
|
20
|
+
def create(
|
|
21
|
+
self,
|
|
22
|
+
space_id: str,
|
|
23
|
+
*,
|
|
24
|
+
parent_id: str,
|
|
25
|
+
block_type: str,
|
|
26
|
+
title: str | None = None,
|
|
27
|
+
props: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
|
|
28
|
+
) -> Any:
|
|
29
|
+
if not parent_id:
|
|
30
|
+
raise ValueError("parent_id is required")
|
|
31
|
+
if not block_type:
|
|
32
|
+
raise ValueError("block_type is required")
|
|
33
|
+
payload: dict[str, Any] = {"parent_id": parent_id, "type": block_type}
|
|
34
|
+
if title is not None:
|
|
35
|
+
payload["title"] = title
|
|
36
|
+
if props is not None:
|
|
37
|
+
payload["props"] = props
|
|
38
|
+
return self._requester.request("POST", f"/space/{space_id}/block", json_data=payload)
|
|
39
|
+
|
|
40
|
+
def delete(self, space_id: str, block_id: str) -> None:
|
|
41
|
+
self._requester.request("DELETE", f"/space/{space_id}/block/{block_id}")
|
|
42
|
+
|
|
43
|
+
def get_properties(self, space_id: str, block_id: str) -> Any:
|
|
44
|
+
return self._requester.request("GET", f"/space/{space_id}/block/{block_id}/properties")
|
|
45
|
+
|
|
46
|
+
def update_properties(
|
|
47
|
+
self,
|
|
48
|
+
space_id: str,
|
|
49
|
+
block_id: str,
|
|
50
|
+
*,
|
|
51
|
+
title: str | None = None,
|
|
52
|
+
props: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
payload: dict[str, Any] = {}
|
|
55
|
+
if title is not None:
|
|
56
|
+
payload["title"] = title
|
|
57
|
+
if props is not None:
|
|
58
|
+
payload["props"] = props
|
|
59
|
+
if not payload:
|
|
60
|
+
raise ValueError("title or props must be provided")
|
|
61
|
+
self._requester.request("PUT", f"/space/{space_id}/block/{block_id}/properties", json_data=payload)
|
|
62
|
+
|
|
63
|
+
def move(
|
|
64
|
+
self,
|
|
65
|
+
space_id: str,
|
|
66
|
+
block_id: str,
|
|
67
|
+
*,
|
|
68
|
+
parent_id: str,
|
|
69
|
+
sort: int | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
if not parent_id:
|
|
72
|
+
raise ValueError("parent_id is required")
|
|
73
|
+
payload: dict[str, Any] = {"parent_id": parent_id}
|
|
74
|
+
if sort is not None:
|
|
75
|
+
payload["sort"] = sort
|
|
76
|
+
self._requester.request("PUT", f"/space/{space_id}/block/{block_id}/move", json_data=payload)
|
|
77
|
+
|
|
78
|
+
def update_sort(self, space_id: str, block_id: str, *, sort: int) -> None:
|
|
79
|
+
self._requester.request(
|
|
80
|
+
"PUT",
|
|
81
|
+
f"/space/{space_id}/block/{block_id}/sort",
|
|
82
|
+
json_data={"sort": sort},
|
|
83
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Page endpoints.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, MutableMapping
|
|
6
|
+
|
|
7
|
+
from ..client_types import RequesterProtocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PagesAPI:
|
|
11
|
+
def __init__(self, requester: RequesterProtocol) -> None:
|
|
12
|
+
self._requester = requester
|
|
13
|
+
|
|
14
|
+
def list(self, space_id: str, *, parent_id: str | None = None) -> Any:
|
|
15
|
+
params: dict[str, Any] = {}
|
|
16
|
+
if parent_id is not None:
|
|
17
|
+
params["parent_id"] = parent_id
|
|
18
|
+
return self._requester.request("GET", f"/space/{space_id}/page", params=params or None)
|
|
19
|
+
|
|
20
|
+
def create(
|
|
21
|
+
self,
|
|
22
|
+
space_id: str,
|
|
23
|
+
*,
|
|
24
|
+
parent_id: str | None = None,
|
|
25
|
+
title: str | None = None,
|
|
26
|
+
props: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
|
|
27
|
+
) -> Any:
|
|
28
|
+
payload: dict[str, Any] = {}
|
|
29
|
+
if parent_id is not None:
|
|
30
|
+
payload["parent_id"] = parent_id
|
|
31
|
+
if title is not None:
|
|
32
|
+
payload["title"] = title
|
|
33
|
+
if props is not None:
|
|
34
|
+
payload["props"] = props
|
|
35
|
+
return self._requester.request("POST", f"/space/{space_id}/page", json_data=payload)
|
|
36
|
+
|
|
37
|
+
def delete(self, space_id: str, page_id: str) -> None:
|
|
38
|
+
self._requester.request("DELETE", f"/space/{space_id}/page/{page_id}")
|
|
39
|
+
|
|
40
|
+
def get_properties(self, space_id: str, page_id: str) -> Any:
|
|
41
|
+
return self._requester.request("GET", f"/space/{space_id}/page/{page_id}/properties")
|
|
42
|
+
|
|
43
|
+
def update_properties(
|
|
44
|
+
self,
|
|
45
|
+
space_id: str,
|
|
46
|
+
page_id: str,
|
|
47
|
+
*,
|
|
48
|
+
title: str | None = None,
|
|
49
|
+
props: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
payload: dict[str, Any] = {}
|
|
52
|
+
if title is not None:
|
|
53
|
+
payload["title"] = title
|
|
54
|
+
if props is not None:
|
|
55
|
+
payload["props"] = props
|
|
56
|
+
if not payload:
|
|
57
|
+
raise ValueError("title or props must be provided")
|
|
58
|
+
self._requester.request("PUT", f"/space/{space_id}/page/{page_id}/properties", json_data=payload)
|
|
59
|
+
|
|
60
|
+
def move(
|
|
61
|
+
self,
|
|
62
|
+
space_id: str,
|
|
63
|
+
page_id: str,
|
|
64
|
+
*,
|
|
65
|
+
parent_id: str | None = None,
|
|
66
|
+
sort: int | None = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
payload: dict[str, Any] = {}
|
|
69
|
+
if parent_id is not None:
|
|
70
|
+
payload["parent_id"] = parent_id
|
|
71
|
+
if sort is not None:
|
|
72
|
+
payload["sort"] = sort
|
|
73
|
+
if not payload:
|
|
74
|
+
raise ValueError("parent_id or sort must be provided")
|
|
75
|
+
self._requester.request("PUT", f"/space/{space_id}/page/{page_id}/move", json_data=payload)
|
|
76
|
+
|
|
77
|
+
def update_sort(self, space_id: str, page_id: str, *, sort: int) -> None:
|
|
78
|
+
self._requester.request(
|
|
79
|
+
"PUT",
|
|
80
|
+
f"/space/{space_id}/page/{page_id}/sort",
|
|
81
|
+
json_data={"sort": sort},
|
|
82
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sessions endpoints.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Mapping, MutableMapping, Sequence
|
|
7
|
+
|
|
8
|
+
from .._constants import SUPPORTED_ROLES
|
|
9
|
+
from ..messages import MessagePart, build_message_payload
|
|
10
|
+
from ..client_types import RequesterProtocol
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SessionsAPI:
|
|
14
|
+
def __init__(self, requester: RequesterProtocol) -> None:
|
|
15
|
+
self._requester = requester
|
|
16
|
+
|
|
17
|
+
def list(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
space_id: str | None = None,
|
|
21
|
+
not_connected: bool | None = None,
|
|
22
|
+
) -> Any:
|
|
23
|
+
params: dict[str, Any] = {}
|
|
24
|
+
if space_id:
|
|
25
|
+
params["space_id"] = space_id
|
|
26
|
+
if not_connected is not None:
|
|
27
|
+
params["not_connected"] = "true" if not_connected else "false"
|
|
28
|
+
return self._requester.request("GET", "/session", params=params or None)
|
|
29
|
+
|
|
30
|
+
def create(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
space_id: str | None = None,
|
|
34
|
+
configs: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
|
|
35
|
+
) -> Any:
|
|
36
|
+
payload: dict[str, Any] = {}
|
|
37
|
+
if space_id:
|
|
38
|
+
payload["space_id"] = space_id
|
|
39
|
+
if configs is not None:
|
|
40
|
+
payload["configs"] = configs
|
|
41
|
+
return self._requester.request("POST", "/session", json_data=payload)
|
|
42
|
+
|
|
43
|
+
def delete(self, session_id: str) -> None:
|
|
44
|
+
self._requester.request("DELETE", f"/session/{session_id}")
|
|
45
|
+
|
|
46
|
+
def update_configs(
|
|
47
|
+
self,
|
|
48
|
+
session_id: str,
|
|
49
|
+
*,
|
|
50
|
+
configs: Mapping[str, Any] | MutableMapping[str, Any],
|
|
51
|
+
) -> None:
|
|
52
|
+
payload = {"configs": configs}
|
|
53
|
+
self._requester.request("PUT", f"/session/{session_id}/configs", json_data=payload)
|
|
54
|
+
|
|
55
|
+
def get_configs(self, session_id: str) -> Any:
|
|
56
|
+
return self._requester.request("GET", f"/session/{session_id}/configs")
|
|
57
|
+
|
|
58
|
+
def connect_to_space(self, session_id: str, *, space_id: str) -> None:
|
|
59
|
+
payload = {"space_id": space_id}
|
|
60
|
+
self._requester.request("POST", f"/session/{session_id}/connect_to_space", json_data=payload)
|
|
61
|
+
|
|
62
|
+
def send_message(
|
|
63
|
+
self,
|
|
64
|
+
session_id: str,
|
|
65
|
+
*,
|
|
66
|
+
role: str,
|
|
67
|
+
parts: Sequence[MessagePart | str | Mapping[str, Any]],
|
|
68
|
+
) -> Any:
|
|
69
|
+
if role not in SUPPORTED_ROLES:
|
|
70
|
+
raise ValueError(f"role must be one of {SUPPORTED_ROLES!r}")
|
|
71
|
+
if not parts:
|
|
72
|
+
raise ValueError("parts must contain at least one entry")
|
|
73
|
+
|
|
74
|
+
payload_parts, files = build_message_payload(parts)
|
|
75
|
+
payload = {"role": role, "parts": payload_parts}
|
|
76
|
+
|
|
77
|
+
if files:
|
|
78
|
+
form_data = {"payload": json.dumps(payload)}
|
|
79
|
+
return self._requester.request(
|
|
80
|
+
"POST",
|
|
81
|
+
f"/session/{session_id}/messages",
|
|
82
|
+
data=form_data,
|
|
83
|
+
files=files,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return self._requester.request(
|
|
87
|
+
"POST",
|
|
88
|
+
f"/session/{session_id}/messages",
|
|
89
|
+
json_data=payload,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def get_messages(
|
|
93
|
+
self,
|
|
94
|
+
session_id: str,
|
|
95
|
+
*,
|
|
96
|
+
limit: int | None = None,
|
|
97
|
+
cursor: str | None = None,
|
|
98
|
+
with_asset_public_url: bool | None = None,
|
|
99
|
+
) -> Any:
|
|
100
|
+
params: dict[str, Any] = {}
|
|
101
|
+
if limit is not None:
|
|
102
|
+
params["limit"] = limit
|
|
103
|
+
if cursor is not None:
|
|
104
|
+
params["cursor"] = cursor
|
|
105
|
+
if with_asset_public_url is not None:
|
|
106
|
+
params["with_asset_public_url"] = "true" if with_asset_public_url else "false"
|
|
107
|
+
return self._requester.request("GET", f"/session/{session_id}/messages", params=params or None)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Spaces endpoints.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, MutableMapping
|
|
6
|
+
|
|
7
|
+
from ..client_types import RequesterProtocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SpacesAPI:
|
|
11
|
+
def __init__(self, requester: RequesterProtocol) -> None:
|
|
12
|
+
self._requester = requester
|
|
13
|
+
|
|
14
|
+
def list(self) -> Any:
|
|
15
|
+
return self._requester.request("GET", "/space")
|
|
16
|
+
|
|
17
|
+
def create(self, *, configs: Mapping[str, Any] | MutableMapping[str, Any] | None = None) -> Any:
|
|
18
|
+
payload: dict[str, Any] = {}
|
|
19
|
+
if configs is not None:
|
|
20
|
+
payload["configs"] = configs
|
|
21
|
+
return self._requester.request("POST", "/space", json_data=payload)
|
|
22
|
+
|
|
23
|
+
def delete(self, space_id: str) -> None:
|
|
24
|
+
self._requester.request("DELETE", f"/space/{space_id}")
|
|
25
|
+
|
|
26
|
+
def update_configs(
|
|
27
|
+
self,
|
|
28
|
+
space_id: str,
|
|
29
|
+
*,
|
|
30
|
+
configs: Mapping[str, Any] | MutableMapping[str, Any],
|
|
31
|
+
) -> None:
|
|
32
|
+
payload = {"configs": configs}
|
|
33
|
+
self._requester.request("PUT", f"/space/{space_id}/configs", json_data=payload)
|
|
34
|
+
|
|
35
|
+
def get_configs(self, space_id: str) -> Any:
|
|
36
|
+
return self._requester.request("GET", f"/space/{space_id}/configs")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for working with file uploads.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import BinaryIO, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class FileUpload:
|
|
12
|
+
"""
|
|
13
|
+
Represents a file payload for multipart requests.
|
|
14
|
+
|
|
15
|
+
Accepts either a binary stream (any object exposing ``read``) or raw ``bytes``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
filename: str
|
|
19
|
+
content: BinaryIO | bytes
|
|
20
|
+
content_type: str | None = None
|
|
21
|
+
|
|
22
|
+
def as_httpx(self) -> Tuple[str, BinaryIO, str | None]:
|
|
23
|
+
"""
|
|
24
|
+
Convert to the tuple format expected by ``httpx``.
|
|
25
|
+
"""
|
|
26
|
+
if isinstance(self.content, (bytes, bytearray)):
|
|
27
|
+
buffer = io.BytesIO(self.content)
|
|
28
|
+
return self.filename, buffer, self.content_type or "application/octet-stream"
|
|
29
|
+
return self.filename, self.content, self.content_type or "application/octet-stream"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def normalize_file_upload(
|
|
33
|
+
upload: FileUpload | tuple[str, BinaryIO | bytes] | tuple[str, BinaryIO | bytes, str | None],
|
|
34
|
+
) -> FileUpload:
|
|
35
|
+
if isinstance(upload, FileUpload):
|
|
36
|
+
return upload
|
|
37
|
+
if isinstance(upload, tuple):
|
|
38
|
+
if len(upload) == 2:
|
|
39
|
+
filename, content = upload
|
|
40
|
+
return FileUpload(filename=filename, content=content)
|
|
41
|
+
if len(upload) == 3:
|
|
42
|
+
filename, content, content_type = upload
|
|
43
|
+
return FileUpload(filename=filename, content=content, content_type=content_type)
|
|
44
|
+
raise TypeError("Unsupported file upload payload")
|