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.
@@ -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")