acontext 0.0.1.dev2__tar.gz → 0.0.1.dev3__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.
Files changed (26) hide show
  1. {acontext-0.0.1.dev2 → acontext-0.0.1.dev3}/PKG-INFO +3 -2
  2. {acontext-0.0.1.dev2 → acontext-0.0.1.dev3}/README.md +1 -1
  3. {acontext-0.0.1.dev2 → acontext-0.0.1.dev3}/pyproject.toml +3 -2
  4. {acontext-0.0.1.dev2 → acontext-0.0.1.dev3}/src/acontext/_constants.py +0 -1
  5. acontext-0.0.1.dev3/src/acontext/_utils.py +42 -0
  6. {acontext-0.0.1.dev2 → acontext-0.0.1.dev3}/src/acontext/client.py +56 -24
  7. acontext-0.0.1.dev3/src/acontext/resources/blocks.py +162 -0
  8. acontext-0.0.1.dev3/src/acontext/resources/disks.py +194 -0
  9. acontext-0.0.1.dev3/src/acontext/resources/sessions.py +255 -0
  10. acontext-0.0.1.dev3/src/acontext/resources/spaces.py +89 -0
  11. acontext-0.0.1.dev3/src/acontext/types/__init__.py +54 -0
  12. acontext-0.0.1.dev3/src/acontext/types/block.py +26 -0
  13. acontext-0.0.1.dev3/src/acontext/types/disk.py +65 -0
  14. acontext-0.0.1.dev3/src/acontext/types/session.py +123 -0
  15. acontext-0.0.1.dev3/src/acontext/types/space.py +24 -0
  16. acontext-0.0.1.dev2/src/acontext/resources/blocks.py +0 -94
  17. acontext-0.0.1.dev2/src/acontext/resources/disks.py +0 -109
  18. acontext-0.0.1.dev2/src/acontext/resources/sessions.py +0 -148
  19. acontext-0.0.1.dev2/src/acontext/resources/spaces.py +0 -58
  20. {acontext-0.0.1.dev2 → acontext-0.0.1.dev3}/src/acontext/__init__.py +0 -0
  21. {acontext-0.0.1.dev2 → acontext-0.0.1.dev3}/src/acontext/client_types.py +0 -0
  22. {acontext-0.0.1.dev2 → acontext-0.0.1.dev3}/src/acontext/errors.py +0 -0
  23. {acontext-0.0.1.dev2 → acontext-0.0.1.dev3}/src/acontext/messages.py +0 -0
  24. {acontext-0.0.1.dev2 → acontext-0.0.1.dev3}/src/acontext/py.typed +0 -0
  25. {acontext-0.0.1.dev2 → acontext-0.0.1.dev3}/src/acontext/resources/__init__.py +0 -0
  26. {acontext-0.0.1.dev2 → acontext-0.0.1.dev3}/src/acontext/uploads.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: acontext
3
- Version: 0.0.1.dev2
3
+ Version: 0.0.1.dev3
4
4
  Summary: Python SDK for the Acontext API
5
5
  Keywords: acontext,sdk,client,api
6
6
  Requires-Dist: httpx>=0.28.1
7
7
  Requires-Dist: openai>=2.6.1
8
8
  Requires-Dist: anthropic>=0.72.0
9
+ Requires-Dist: pydantic>=2.12.3
9
10
  Requires-Python: >=3.10
10
11
  Project-URL: Homepage, https://github.com/memodb-io/Acontext
11
12
  Project-URL: Issues, https://github.com/memodb-io/Acontext/issues
@@ -63,7 +64,7 @@ try:
63
64
  content=b"# Retro Notes\nWe shipped file uploads successfully!\n",
64
65
  content_type="text/markdown",
65
66
  ),
66
- file_path="notes/retro.md",
67
+ file_path="/notes/",
67
68
  meta={"source": "readme-demo"},
68
69
  )
69
70
  finally:
@@ -49,7 +49,7 @@ try:
49
49
  content=b"# Retro Notes\nWe shipped file uploads successfully!\n",
50
50
  content_type="text/markdown",
51
51
  ),
52
- file_path="notes/retro.md",
52
+ file_path="/notes/",
53
53
  meta={"source": "readme-demo"},
54
54
  )
55
55
  finally:
@@ -1,13 +1,14 @@
1
1
  [project]
2
2
  name = "acontext"
3
- version = "0.0.1.dev2"
3
+ version = "0.0.1.dev3"
4
4
  description = "Python SDK for the Acontext API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
8
  "httpx>=0.28.1",
9
9
  "openai>=2.6.1",
10
- "anthropic>=0.72.0"
10
+ "anthropic>=0.72.0",
11
+ "pydantic>=2.12.3",
11
12
  ]
12
13
  keywords = ["acontext", "sdk", "client", "api"]
13
14
 
@@ -5,7 +5,6 @@ Internal constants shared across the Python SDK.
5
5
  from importlib import metadata as _metadata
6
6
 
7
7
  DEFAULT_BASE_URL = "https://api.acontext.io/api/v1"
8
- SUPPORTED_ROLES = {"user", "assistant", "system", "tool", "function"}
9
8
 
10
9
  try:
11
10
  _VERSION = _metadata.version("acontext-py")
@@ -0,0 +1,42 @@
1
+ """Utility functions for the acontext Python client."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def bool_to_str(value: bool) -> str:
7
+ """Convert a boolean value to string representation used by the API.
8
+
9
+ Args:
10
+ value: The boolean value to convert.
11
+
12
+ Returns:
13
+ "true" if value is True, "false" otherwise.
14
+ """
15
+ return "true" if value else "false"
16
+
17
+
18
+ def build_params(**kwargs: Any) -> dict[str, Any]:
19
+ """Build query parameters dictionary, filtering None values and converting booleans.
20
+
21
+ This function filters out None values and converts boolean values to their
22
+ string representations ("true" or "false") as expected by the API.
23
+
24
+ Args:
25
+ **kwargs: Keyword arguments to build parameters from.
26
+
27
+ Returns:
28
+ Dictionary with non-None parameters, with booleans converted to strings.
29
+
30
+ Example:
31
+ >>> build_params(limit=10, cursor=None, time_desc=True)
32
+ {'limit': 10, 'time_desc': 'true'}
33
+ """
34
+ params: dict[str, Any] = {}
35
+ for key, value in kwargs.items():
36
+ if value is not None:
37
+ if isinstance(value, bool):
38
+ params[key] = bool_to_str(value)
39
+ else:
40
+ params[key] = value
41
+ return params
42
+
@@ -2,6 +2,7 @@
2
2
  High-level synchronous client for the Acontext API.
3
3
  """
4
4
 
5
+ import os
5
6
  from collections.abc import Mapping, MutableMapping
6
7
  from typing import Any, BinaryIO
7
8
 
@@ -16,41 +17,68 @@ from .resources.blocks import BlocksAPI as BlocksAPI
16
17
  from .resources.sessions import SessionsAPI as SessionsAPI
17
18
  from .resources.spaces import SpacesAPI as SpacesAPI
18
19
 
19
- class AcontextClient:
20
- """
21
- Synchronous HTTP client for the Acontext REST API.
22
-
23
- Example::
24
-
25
- from acontext import AcontextClient, MessagePart
26
20
 
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
- """
21
+ class AcontextClient:
36
22
 
37
23
  def __init__(
38
24
  self,
39
25
  *,
40
- api_key: str,
41
- base_url: str = DEFAULT_BASE_URL,
26
+ api_key: str | None = None,
27
+ base_url: str | None = None,
42
28
  timeout: float | httpx.Timeout | None = 10.0,
43
29
  user_agent: str | None = None,
44
30
  client: httpx.Client | None = None,
45
31
  ) -> None:
46
- if not api_key:
47
- raise ValueError("api_key is required")
32
+ """
33
+ Initialize the Acontext client.
34
+
35
+ Args:
36
+ api_key: API key for authentication. Can also be set via ACONTEXT_API_KEY env var.
37
+ base_url: Base URL for the API. Defaults to DEFAULT_BASE_URL. Can also be set via ACONTEXT_BASE_URL env var.
38
+ timeout: Request timeout in seconds. Defaults to 10.0. Can also be set via ACONTEXT_TIMEOUT env var.
39
+ Can also be an httpx.Timeout object.
40
+ user_agent: Custom user agent string. Can also be set via ACONTEXT_USER_AGENT env var.
41
+ client: Optional httpx.Client instance to reuse. If provided, headers and base_url
42
+ will be merged with the client configuration.
43
+ """
44
+ # Priority: explicit parameters > environment variables > defaults
45
+ # Load api_key from parameter or environment variable
46
+ api_key = api_key or os.getenv("ACONTEXT_API_KEY")
47
+ if not api_key or not api_key.strip():
48
+ raise ValueError(
49
+ "api_key is required. Provide it either as a parameter (api_key='...') "
50
+ "or set the ACONTEXT_API_KEY environment variable."
51
+ )
48
52
 
53
+ # Load other parameters from environment variables if not provided
54
+ if base_url is None:
55
+ base_url = os.getenv("ACONTEXT_BASE_URL", DEFAULT_BASE_URL)
49
56
  base_url = base_url.rstrip("/")
57
+
58
+ if user_agent is None:
59
+ user_agent = os.getenv("ACONTEXT_USER_AGENT", DEFAULT_USER_AGENT)
60
+
61
+ # Handle timeout: support both float and httpx.Timeout
62
+ if timeout is None:
63
+ timeout_str = os.getenv("ACONTEXT_TIMEOUT")
64
+ if timeout_str:
65
+ try:
66
+ timeout = float(timeout_str)
67
+ except ValueError:
68
+ timeout = 10.0
69
+ else:
70
+ timeout = 10.0
71
+
72
+ # Determine actual timeout value
73
+ if isinstance(timeout, httpx.Timeout):
74
+ actual_timeout = timeout
75
+ else:
76
+ actual_timeout = float(timeout)
77
+
50
78
  headers = {
51
79
  "Authorization": f"Bearer {api_key}",
52
80
  "Accept": "application/json",
53
- "User-Agent": user_agent or DEFAULT_USER_AGENT,
81
+ "User-Agent": user_agent,
54
82
  }
55
83
 
56
84
  if client is not None:
@@ -63,11 +91,15 @@ class AcontextClient:
63
91
  client.headers[name] = value
64
92
  self._base_url = str(client.base_url) or base_url
65
93
  else:
66
- self._client = httpx.Client(base_url=base_url, headers=headers, timeout=timeout)
94
+ self._client = httpx.Client(
95
+ base_url=base_url,
96
+ headers=headers,
97
+ timeout=actual_timeout,
98
+ )
67
99
  self._owns_client = True
68
100
  self._base_url = base_url
69
101
 
70
- self._timeout = timeout
102
+ self._timeout = actual_timeout
71
103
 
72
104
  self.spaces = SpacesAPI(self)
73
105
  self.sessions = SessionsAPI(self)
@@ -123,7 +155,7 @@ class AcontextClient:
123
155
  content_type = response.headers.get("content-type", "")
124
156
 
125
157
  parsed: Mapping[str, Any] | MutableMapping[str, Any] | None
126
- if "application/json" in content_type or content_type.startswith("application/problem+json"):
158
+ if "application/json" in content_type:
127
159
  try:
128
160
  parsed = response.json()
129
161
  except ValueError:
@@ -0,0 +1,162 @@
1
+ """
2
+ Block endpoints.
3
+ """
4
+
5
+ from collections.abc import Mapping, MutableMapping
6
+ from typing import Any
7
+
8
+ from ..client_types import RequesterProtocol
9
+ from ..types.block import Block
10
+
11
+
12
+ class BlocksAPI:
13
+ def __init__(self, requester: RequesterProtocol) -> None:
14
+ self._requester = requester
15
+
16
+ def list(
17
+ self,
18
+ space_id: str,
19
+ *,
20
+ parent_id: str | None = None,
21
+ block_type: str | None = None,
22
+ ) -> list[Block]:
23
+ """List blocks in a space.
24
+
25
+ Args:
26
+ space_id: The UUID of the space.
27
+ parent_id: Filter blocks by parent ID. Defaults to None.
28
+ block_type: Filter blocks by type (e.g., "page", "folder", "text", "sop"). Defaults to None.
29
+
30
+ Returns:
31
+ List of Block objects.
32
+ """
33
+ params: dict[str, Any] = {}
34
+ if parent_id is not None:
35
+ params["parent_id"] = parent_id
36
+ if block_type is not None:
37
+ params["type"] = block_type
38
+ data = self._requester.request("GET", f"/space/{space_id}/block", params=params or None)
39
+ return [Block.model_validate(item) for item in data]
40
+
41
+ def create(
42
+ self,
43
+ space_id: str,
44
+ *,
45
+ block_type: str,
46
+ parent_id: str | None = None,
47
+ title: str | None = None,
48
+ props: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
49
+ ) -> Block:
50
+ """Create a new block in a space.
51
+
52
+ Args:
53
+ space_id: The UUID of the space.
54
+ block_type: The type of block (e.g., "page", "folder", "text", "sop").
55
+ parent_id: Optional parent block ID. Defaults to None.
56
+ title: Optional block title. Defaults to None.
57
+ props: Optional block properties dictionary. Defaults to None.
58
+
59
+ Returns:
60
+ The created Block object.
61
+ """
62
+ payload: dict[str, Any] = {"type": block_type}
63
+ if parent_id is not None:
64
+ payload["parent_id"] = parent_id
65
+ if title is not None:
66
+ payload["title"] = title
67
+ if props is not None:
68
+ payload["props"] = props
69
+ data = self._requester.request("POST", f"/space/{space_id}/block", json_data=payload)
70
+ return Block.model_validate(data)
71
+
72
+ def delete(self, space_id: str, block_id: str) -> None:
73
+ """Delete a block by its ID.
74
+
75
+ Args:
76
+ space_id: The UUID of the space.
77
+ block_id: The UUID of the block to delete.
78
+ """
79
+ self._requester.request("DELETE", f"/space/{space_id}/block/{block_id}")
80
+
81
+ def get_properties(self, space_id: str, block_id: str) -> Block:
82
+ """Get block properties.
83
+
84
+ Args:
85
+ space_id: The UUID of the space.
86
+ block_id: The UUID of the block.
87
+
88
+ Returns:
89
+ Block object containing the properties.
90
+ """
91
+ data = self._requester.request("GET", f"/space/{space_id}/block/{block_id}/properties")
92
+ return Block.model_validate(data)
93
+
94
+ def update_properties(
95
+ self,
96
+ space_id: str,
97
+ block_id: str,
98
+ *,
99
+ title: str | None = None,
100
+ props: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
101
+ ) -> None:
102
+ """Update block properties.
103
+
104
+ Args:
105
+ space_id: The UUID of the space.
106
+ block_id: The UUID of the block.
107
+ title: Optional block title. Defaults to None.
108
+ props: Optional block properties dictionary. Defaults to None.
109
+
110
+ Raises:
111
+ ValueError: If both title and props are None.
112
+ """
113
+ payload: dict[str, Any] = {}
114
+ if title is not None:
115
+ payload["title"] = title
116
+ if props is not None:
117
+ payload["props"] = props
118
+ if not payload:
119
+ raise ValueError("title or props must be provided")
120
+ self._requester.request("PUT", f"/space/{space_id}/block/{block_id}/properties", json_data=payload)
121
+
122
+ def move(
123
+ self,
124
+ space_id: str,
125
+ block_id: str,
126
+ *,
127
+ parent_id: str | None = None,
128
+ sort: int | None = None,
129
+ ) -> None:
130
+ """Move a block by updating its parent or sort order.
131
+
132
+ Args:
133
+ space_id: The UUID of the space.
134
+ block_id: The UUID of the block to move.
135
+ parent_id: Optional new parent block ID. Defaults to None.
136
+ sort: Optional new sort order. Defaults to None.
137
+
138
+ Raises:
139
+ ValueError: If both parent_id and sort are None.
140
+ """
141
+ payload: dict[str, Any] = {}
142
+ if parent_id is not None:
143
+ payload["parent_id"] = parent_id
144
+ if sort is not None:
145
+ payload["sort"] = sort
146
+ if not payload:
147
+ raise ValueError("parent_id or sort must be provided")
148
+ self._requester.request("PUT", f"/space/{space_id}/block/{block_id}/move", json_data=payload)
149
+
150
+ def update_sort(self, space_id: str, block_id: str, *, sort: int) -> None:
151
+ """Update block sort order.
152
+
153
+ Args:
154
+ space_id: The UUID of the space.
155
+ block_id: The UUID of the block.
156
+ sort: The new sort order.
157
+ """
158
+ self._requester.request(
159
+ "PUT",
160
+ f"/space/{space_id}/block/{block_id}/sort",
161
+ json_data={"sort": sort},
162
+ )
@@ -0,0 +1,194 @@
1
+ """
2
+ Disk and artifact endpoints.
3
+ """
4
+
5
+ import json
6
+ from collections.abc import Mapping, MutableMapping
7
+ from typing import Any, BinaryIO, cast
8
+
9
+ from .._utils import build_params
10
+ from ..client_types import RequesterProtocol
11
+ from ..types.disk import (
12
+ Artifact,
13
+ Disk,
14
+ GetArtifactResp,
15
+ ListArtifactsResp,
16
+ ListDisksOutput,
17
+ UpdateArtifactResp,
18
+ )
19
+ from ..uploads import FileUpload, normalize_file_upload
20
+
21
+
22
+ class DisksAPI:
23
+ def __init__(self, requester: RequesterProtocol) -> None:
24
+ self._requester = requester
25
+ self.artifacts = DiskArtifactsAPI(requester)
26
+
27
+ def list(
28
+ self,
29
+ *,
30
+ limit: int | None = None,
31
+ cursor: str | None = None,
32
+ time_desc: bool | None = None,
33
+ ) -> ListDisksOutput:
34
+ """List all disks in the project.
35
+
36
+ Args:
37
+ limit: Maximum number of disks to return. Defaults to None.
38
+ cursor: Cursor for pagination. Defaults to None.
39
+ time_desc: Order by created_at descending if True, ascending if False. Defaults to None.
40
+
41
+ Returns:
42
+ ListDisksOutput containing the list of disks and pagination information.
43
+ """
44
+ params = build_params(limit=limit, cursor=cursor, time_desc=time_desc)
45
+ data = self._requester.request("GET", "/disk", params=params or None)
46
+ return ListDisksOutput.model_validate(data)
47
+
48
+ def create(self) -> Disk:
49
+ """Create a new disk.
50
+
51
+ Returns:
52
+ The created Disk object.
53
+ """
54
+ data = self._requester.request("POST", "/disk")
55
+ return Disk.model_validate(data)
56
+
57
+ def delete(self, disk_id: str) -> None:
58
+ """Delete a disk by its ID.
59
+
60
+ Args:
61
+ disk_id: The UUID of the disk to delete.
62
+ """
63
+ self._requester.request("DELETE", f"/disk/{disk_id}")
64
+
65
+
66
+ class DiskArtifactsAPI:
67
+ def __init__(self, requester: RequesterProtocol) -> None:
68
+ self._requester = requester
69
+
70
+ def upsert(
71
+ self,
72
+ disk_id: str,
73
+ *,
74
+ file: FileUpload
75
+ | tuple[str, BinaryIO | bytes]
76
+ | tuple[str, BinaryIO | bytes, str | None],
77
+ file_path: str | None = None,
78
+ meta: Mapping[str, Any] | MutableMapping[str, Any] | None = None,
79
+ ) -> Artifact:
80
+ """Upload a file to create or update an artifact.
81
+
82
+ Args:
83
+ disk_id: The UUID of the disk.
84
+ file: The file to upload (FileUpload object or tuple format).
85
+ file_path: Directory path (not including filename), defaults to "/".
86
+ meta: Custom metadata as JSON-serializable dict, defaults to None.
87
+
88
+ Returns:
89
+ Artifact containing the created/updated artifact information.
90
+ """
91
+ upload = normalize_file_upload(file)
92
+ files = {"file": upload.as_httpx()}
93
+ form: dict[str, Any] = {}
94
+ if file_path:
95
+ form["file_path"] = file_path
96
+ if meta is not None:
97
+ form["meta"] = json.dumps(cast(Mapping[str, Any], meta))
98
+ data = self._requester.request(
99
+ "POST",
100
+ f"/disk/{disk_id}/artifact",
101
+ data=form or None,
102
+ files=files,
103
+ )
104
+ return Artifact.model_validate(data)
105
+
106
+ def get(
107
+ self,
108
+ disk_id: str,
109
+ *,
110
+ file_path: str,
111
+ filename: str,
112
+ with_public_url: bool | None = None,
113
+ with_content: bool | None = None,
114
+ expire: int | None = None,
115
+ ) -> GetArtifactResp:
116
+ """Get an artifact by disk ID, file path, and filename.
117
+
118
+ Args:
119
+ disk_id: The UUID of the disk.
120
+ file_path: Directory path (not including filename).
121
+ filename: The filename of the artifact.
122
+ with_public_url: Whether to include a presigned public URL. Defaults to None.
123
+ with_content: Whether to include file content. Defaults to None.
124
+ expire: URL expiration time in seconds. Defaults to None.
125
+
126
+ Returns:
127
+ GetArtifactResp containing the artifact and optionally public URL and content.
128
+ """
129
+ full_path = f"{file_path.rstrip('/')}/{filename}"
130
+ params = build_params(
131
+ file_path=full_path,
132
+ with_public_url=with_public_url,
133
+ with_content=with_content,
134
+ expire=expire,
135
+ )
136
+ data = self._requester.request("GET", f"/disk/{disk_id}/artifact", params=params)
137
+ return GetArtifactResp.model_validate(data)
138
+
139
+ def update(
140
+ self,
141
+ disk_id: str,
142
+ *,
143
+ file_path: str,
144
+ filename: str,
145
+ meta: Mapping[str, Any] | MutableMapping[str, Any],
146
+ ) -> UpdateArtifactResp:
147
+ """Update an artifact's metadata.
148
+
149
+ Args:
150
+ disk_id: The UUID of the disk.
151
+ file_path: Directory path (not including filename).
152
+ filename: The filename of the artifact.
153
+ meta: Custom metadata as JSON-serializable dict.
154
+
155
+ Returns:
156
+ UpdateArtifactResp containing the updated artifact information.
157
+ """
158
+ full_path = f"{file_path.rstrip('/')}/{filename}"
159
+ payload = {
160
+ "file_path": full_path,
161
+ "meta": json.dumps(cast(Mapping[str, Any], meta)),
162
+ }
163
+ data = self._requester.request("PUT", f"/disk/{disk_id}/artifact", json_data=payload)
164
+ return UpdateArtifactResp.model_validate(data)
165
+
166
+ def delete(
167
+ self,
168
+ disk_id: str,
169
+ *,
170
+ file_path: str,
171
+ filename: str,
172
+ ) -> None:
173
+ """Delete an artifact by disk ID, file path, and filename.
174
+
175
+ Args:
176
+ disk_id: The UUID of the disk.
177
+ file_path: Directory path (not including filename).
178
+ filename: The filename of the artifact.
179
+ """
180
+ full_path = f"{file_path.rstrip('/')}/{filename}"
181
+ params = {"file_path": full_path}
182
+ self._requester.request("DELETE", f"/disk/{disk_id}/artifact", params=params)
183
+
184
+ def list(
185
+ self,
186
+ disk_id: str,
187
+ *,
188
+ path: str | None = None,
189
+ ) -> ListArtifactsResp:
190
+ params: dict[str, Any] = {}
191
+ if path is not None:
192
+ params["path"] = path
193
+ data = self._requester.request("GET", f"/disk/{disk_id}/artifact/ls", params=params or None)
194
+ return ListArtifactsResp.model_validate(data)