cogspace 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.
@@ -0,0 +1,65 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ venv/
23
+ ENV/
24
+ env/
25
+ .venv
26
+
27
+ # Node
28
+ node_modules/
29
+ npm-debug.log
30
+ yarn-error.log
31
+ .next/
32
+ out/
33
+ .turbo/
34
+
35
+ # IDE
36
+ .vscode/
37
+ .idea/
38
+ *.swp
39
+ *.swo
40
+ *~
41
+ .DS_Store
42
+
43
+ # Environment
44
+ .env
45
+ .env.local
46
+ .env.*.local
47
+
48
+ # Data
49
+ *.db
50
+ *.sqlite
51
+ *.sqlite3
52
+ data/
53
+ .cogspace/
54
+ examples/*/memory.db
55
+ examples/*/bm25_index/
56
+ examples/*/lancedb/
57
+
58
+ # Coverage
59
+ .coverage
60
+ htmlcov/
61
+ .pytest_cache/
62
+
63
+ # Logs
64
+ *.log
65
+ logs/
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: cogspace
3
+ Version: 0.1.0
4
+ Summary: Official Cogspace SDK — add a knowledge layer to any AI agent
5
+ Project-URL: Homepage, https://cogspace.ai
6
+ Project-URL: Documentation, https://docs.cogspace.ai
7
+ Project-URL: Repository, https://github.com/Jack-Pision/cogspace-ai
8
+ Project-URL: Issues, https://github.com/Jack-Pision/cogspace-ai/issues
9
+ Author-email: Cogspace <sdk@cogspace.ai>
10
+ License: MIT
11
+ Keywords: agents,ai,knowledge,memory,rag,sdk
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: pydantic>=2.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
26
+ Requires-Dist: pytest>=8; extra == 'dev'
27
+ Requires-Dist: respx>=0.21; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # cogspace
31
+
32
+ Official Python SDK for [Cogspace](https://cogspace.ai) — add a persistent knowledge layer to any AI agent.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install cogspace
38
+ ```
39
+
40
+ ## Quickstart
41
+
42
+ ```python
43
+ from cogspace import Cogspace
44
+
45
+ cog = Cogspace(api_key="cs-...")
46
+
47
+ # Get a space-scoped client
48
+ space = cog.space("my-agent")
49
+
50
+ # Search
51
+ results = space.search("retry logic with backoff")
52
+ for r in results.results:
53
+ print(r.file_path, r.score)
54
+
55
+ # Write knowledge
56
+ space.write("expertise/retry.md", """
57
+ # Retry patterns
58
+ Always use exponential backoff with jitter.
59
+ """)
60
+
61
+ # Read the hot layer (always-injected context)
62
+ context = space.hot_layer()
63
+
64
+ # Memory
65
+ space.write_memory("Currently working on the payment service refactor.")
66
+ memory = space.read_memory()
67
+ ```
68
+
69
+ ## Async
70
+
71
+ ```python
72
+ from cogspace import AsyncCogspace
73
+
74
+ async def main():
75
+ cog = AsyncCogspace(api_key="cs-...")
76
+ space = await cog.space("my-agent")
77
+ results = await space.search("retry logic")
78
+ await cog.aclose()
79
+ ```
80
+
81
+ Or use as a context manager:
82
+
83
+ ```python
84
+ async with AsyncCogspace(api_key="cs-...") as cog:
85
+ space = await cog.space("my-agent")
86
+ await space.write("expertise/notes.md", "...")
87
+ ```
88
+
89
+ ## API Reference
90
+
91
+ ### `Cogspace(api_key, base_url, timeout, max_retries)`
92
+
93
+ | Method | Description |
94
+ |---|---|
95
+ | `cog.space(name_or_id)` | Get a space client |
96
+ | `cog.list_spaces()` | List all your spaces |
97
+ | `cog.create_space(name)` | Create a new space |
98
+
99
+ ### `SpaceClient`
100
+
101
+ | Method | Description |
102
+ |---|---|
103
+ | `space.search(query, mode, top_k, layer)` | Hybrid / vector / keyword search |
104
+ | `space.read(path)` | Read a file by path |
105
+ | `space.write(path, content, metadata)` | Write or update a file |
106
+ | `space.list(folder)` | List files in a folder |
107
+ | `space.read_memory()` | Read `memory.md` |
108
+ | `space.write_memory(content, confidence)` | Update `memory.md` |
109
+ | `space.read_context()` | Read `memory/user/` context files |
110
+ | `space.hot_layer()` | Get full hot layer injection string |
111
+ | `space.graph_traverse(path, depth, rel_type)` | Traverse knowledge graph |
112
+ | `space.health()` | Space health and storage stats |
113
+
114
+ ## Errors
115
+
116
+ All exceptions inherit from `CogspaceError`:
117
+
118
+ ```python
119
+ from cogspace.exceptions import AuthError, NotFoundError, RateLimitError
120
+
121
+ try:
122
+ space.search("query")
123
+ except AuthError:
124
+ print("Invalid API key")
125
+ except NotFoundError:
126
+ print("Space not found")
127
+ ```
128
+
129
+ ## Get an API key
130
+
131
+ Sign in at [platform.cogspace.ai](https://platform.cogspace.ai), go to **Settings → API keys**, and create a key.
@@ -0,0 +1,102 @@
1
+ # cogspace
2
+
3
+ Official Python SDK for [Cogspace](https://cogspace.ai) — add a persistent knowledge layer to any AI agent.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install cogspace
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```python
14
+ from cogspace import Cogspace
15
+
16
+ cog = Cogspace(api_key="cs-...")
17
+
18
+ # Get a space-scoped client
19
+ space = cog.space("my-agent")
20
+
21
+ # Search
22
+ results = space.search("retry logic with backoff")
23
+ for r in results.results:
24
+ print(r.file_path, r.score)
25
+
26
+ # Write knowledge
27
+ space.write("expertise/retry.md", """
28
+ # Retry patterns
29
+ Always use exponential backoff with jitter.
30
+ """)
31
+
32
+ # Read the hot layer (always-injected context)
33
+ context = space.hot_layer()
34
+
35
+ # Memory
36
+ space.write_memory("Currently working on the payment service refactor.")
37
+ memory = space.read_memory()
38
+ ```
39
+
40
+ ## Async
41
+
42
+ ```python
43
+ from cogspace import AsyncCogspace
44
+
45
+ async def main():
46
+ cog = AsyncCogspace(api_key="cs-...")
47
+ space = await cog.space("my-agent")
48
+ results = await space.search("retry logic")
49
+ await cog.aclose()
50
+ ```
51
+
52
+ Or use as a context manager:
53
+
54
+ ```python
55
+ async with AsyncCogspace(api_key="cs-...") as cog:
56
+ space = await cog.space("my-agent")
57
+ await space.write("expertise/notes.md", "...")
58
+ ```
59
+
60
+ ## API Reference
61
+
62
+ ### `Cogspace(api_key, base_url, timeout, max_retries)`
63
+
64
+ | Method | Description |
65
+ |---|---|
66
+ | `cog.space(name_or_id)` | Get a space client |
67
+ | `cog.list_spaces()` | List all your spaces |
68
+ | `cog.create_space(name)` | Create a new space |
69
+
70
+ ### `SpaceClient`
71
+
72
+ | Method | Description |
73
+ |---|---|
74
+ | `space.search(query, mode, top_k, layer)` | Hybrid / vector / keyword search |
75
+ | `space.read(path)` | Read a file by path |
76
+ | `space.write(path, content, metadata)` | Write or update a file |
77
+ | `space.list(folder)` | List files in a folder |
78
+ | `space.read_memory()` | Read `memory.md` |
79
+ | `space.write_memory(content, confidence)` | Update `memory.md` |
80
+ | `space.read_context()` | Read `memory/user/` context files |
81
+ | `space.hot_layer()` | Get full hot layer injection string |
82
+ | `space.graph_traverse(path, depth, rel_type)` | Traverse knowledge graph |
83
+ | `space.health()` | Space health and storage stats |
84
+
85
+ ## Errors
86
+
87
+ All exceptions inherit from `CogspaceError`:
88
+
89
+ ```python
90
+ from cogspace.exceptions import AuthError, NotFoundError, RateLimitError
91
+
92
+ try:
93
+ space.search("query")
94
+ except AuthError:
95
+ print("Invalid API key")
96
+ except NotFoundError:
97
+ print("Space not found")
98
+ ```
99
+
100
+ ## Get an API key
101
+
102
+ Sign in at [platform.cogspace.ai](https://platform.cogspace.ai), go to **Settings → API keys**, and create a key.
@@ -0,0 +1,203 @@
1
+ """
2
+ Cogspace SDK — add a knowledge layer to any AI agent.
3
+
4
+ Quickstart (sync):
5
+ from cogspace import Cogspace
6
+
7
+ cog = Cogspace(api_key="cs-...")
8
+ space = cog.space("my-agent")
9
+ results = space.search("retry logic with backoff")
10
+ space.write("expertise/retry.md", "# Retry patterns\\n...")
11
+ context = space.hot_layer()
12
+
13
+ Quickstart (async):
14
+ from cogspace import AsyncCogspace
15
+
16
+ cog = AsyncCogspace(api_key="cs-...")
17
+ space = cog.space("my-agent")
18
+ results = await space.search("retry logic with backoff")
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Optional
24
+
25
+ from cogspace._async_client import AsyncClient
26
+ from cogspace._async_space import AsyncSpaceClient
27
+ from cogspace._client import SyncClient
28
+ from cogspace._space import SpaceClient
29
+ from cogspace.exceptions import (
30
+ AuthError,
31
+ CogspaceError,
32
+ NotFoundError,
33
+ RateLimitError,
34
+ ServerError,
35
+ TimeoutError,
36
+ )
37
+ from cogspace.types import (
38
+ ContextFile,
39
+ GraphEdge,
40
+ GraphNode,
41
+ GraphTraverseResponse,
42
+ HealthResponse,
43
+ ListResponse,
44
+ ReadContextResponse,
45
+ ReadResponse,
46
+ SearchItem,
47
+ SearchResponse,
48
+ Space,
49
+ WriteResponse,
50
+ )
51
+
52
+ DEFAULT_BASE_URL = "https://platform.cogspace.ai"
53
+
54
+
55
+ class Cogspace:
56
+ """
57
+ Sync Cogspace client.
58
+
59
+ Args:
60
+ api_key: Your Cogspace API key (starts with 'cs-').
61
+ base_url: Platform URL. Defaults to https://platform.cogspace.ai.
62
+ timeout: Request timeout in seconds. Default 30.
63
+ max_retries: Number of retries on 429/5xx. Default 3.
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ api_key: str,
69
+ base_url: str = DEFAULT_BASE_URL,
70
+ timeout: float = 30.0,
71
+ max_retries: int = 3,
72
+ ) -> None:
73
+ self._client = SyncClient(
74
+ api_key=api_key,
75
+ base_url=base_url,
76
+ timeout=timeout,
77
+ max_retries=max_retries,
78
+ )
79
+ self._space_cache: dict[str, str] = {} # name -> id
80
+
81
+ def _resolve_space_id(self, space_name_or_id: str) -> str:
82
+ """Resolve a space name to its ID, caching the result."""
83
+ if space_name_or_id in self._space_cache:
84
+ return self._space_cache[space_name_or_id]
85
+ spaces = self._client.get("/spaces")
86
+ for s in spaces:
87
+ if s["name"] == space_name_or_id or s["id"] == space_name_or_id:
88
+ self._space_cache[s["name"]] = s["id"]
89
+ self._space_cache[s["id"]] = s["id"]
90
+ return s["id"]
91
+ raise NotFoundError(f"Space not found: {space_name_or_id!r}")
92
+
93
+ def space(self, name_or_id: str) -> SpaceClient:
94
+ """Get a space-scoped client by space name or ID."""
95
+ space_id = self._resolve_space_id(name_or_id)
96
+ return SpaceClient(self._client, space_id)
97
+
98
+ def list_spaces(self) -> list[Space]:
99
+ """List all your spaces."""
100
+ data = self._client.get("/spaces")
101
+ return [Space.model_validate(s) for s in data]
102
+
103
+ def create_space(self, name: str) -> Space:
104
+ """Create a new space."""
105
+ data = self._client.post("/spaces", json={"name": name})
106
+ return Space.model_validate(data)
107
+
108
+ def close(self) -> None:
109
+ self._client.close()
110
+
111
+ def __enter__(self) -> "Cogspace":
112
+ return self
113
+
114
+ def __exit__(self, *_) -> None:
115
+ self.close()
116
+
117
+
118
+ class AsyncCogspace:
119
+ """
120
+ Async Cogspace client.
121
+
122
+ Args:
123
+ api_key: Your Cogspace API key (starts with 'cs-').
124
+ base_url: Platform URL. Defaults to https://platform.cogspace.ai.
125
+ timeout: Request timeout in seconds. Default 30.
126
+ max_retries: Number of retries on 429/5xx. Default 3.
127
+ """
128
+
129
+ def __init__(
130
+ self,
131
+ api_key: str,
132
+ base_url: str = DEFAULT_BASE_URL,
133
+ timeout: float = 30.0,
134
+ max_retries: int = 3,
135
+ ) -> None:
136
+ self._client = AsyncClient(
137
+ api_key=api_key,
138
+ base_url=base_url,
139
+ timeout=timeout,
140
+ max_retries=max_retries,
141
+ )
142
+ self._space_cache: dict[str, str] = {}
143
+
144
+ async def _resolve_space_id(self, space_name_or_id: str) -> str:
145
+ if space_name_or_id in self._space_cache:
146
+ return self._space_cache[space_name_or_id]
147
+ spaces = await self._client.get("/spaces")
148
+ for s in spaces:
149
+ if s["name"] == space_name_or_id or s["id"] == space_name_or_id:
150
+ self._space_cache[s["name"]] = s["id"]
151
+ self._space_cache[s["id"]] = s["id"]
152
+ return s["id"]
153
+ raise NotFoundError(f"Space not found: {space_name_or_id!r}")
154
+
155
+ async def space(self, name_or_id: str) -> AsyncSpaceClient:
156
+ """Get a space-scoped async client by space name or ID."""
157
+ space_id = await self._resolve_space_id(name_or_id)
158
+ return AsyncSpaceClient(self._client, space_id)
159
+
160
+ async def list_spaces(self) -> list[Space]:
161
+ data = await self._client.get("/spaces")
162
+ return [Space.model_validate(s) for s in data]
163
+
164
+ async def create_space(self, name: str) -> Space:
165
+ data = await self._client.post("/spaces", json={"name": name})
166
+ return Space.model_validate(data)
167
+
168
+ async def aclose(self) -> None:
169
+ await self._client.aclose()
170
+
171
+ async def __aenter__(self) -> "AsyncCogspace":
172
+ return self
173
+
174
+ async def __aexit__(self, *_) -> None:
175
+ await self.aclose()
176
+
177
+
178
+ __all__ = [
179
+ "Cogspace",
180
+ "AsyncCogspace",
181
+ "SpaceClient",
182
+ "AsyncSpaceClient",
183
+ "CogspaceError",
184
+ "AuthError",
185
+ "NotFoundError",
186
+ "RateLimitError",
187
+ "ServerError",
188
+ "TimeoutError",
189
+ "SearchResponse",
190
+ "SearchItem",
191
+ "ReadResponse",
192
+ "WriteResponse",
193
+ "ListResponse",
194
+ "ReadContextResponse",
195
+ "ContextFile",
196
+ "GraphTraverseResponse",
197
+ "GraphNode",
198
+ "GraphEdge",
199
+ "HealthResponse",
200
+ "Space",
201
+ ]
202
+
203
+ __version__ = "0.1.0"
@@ -0,0 +1,93 @@
1
+ """Async HTTP client for the Cogspace SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from cogspace.exceptions import RateLimitError, TimeoutError, _raise_for_status
11
+
12
+ DEFAULT_BASE_URL = "https://platform.cogspace.ai"
13
+ DEFAULT_TIMEOUT = 30.0
14
+ DEFAULT_MAX_RETRIES = 3
15
+ _RETRY_STATUSES = {429, 500, 502, 503, 504}
16
+
17
+
18
+ class AsyncClient:
19
+ def __init__(
20
+ self,
21
+ api_key: str,
22
+ base_url: str = DEFAULT_BASE_URL,
23
+ timeout: float = DEFAULT_TIMEOUT,
24
+ max_retries: int = DEFAULT_MAX_RETRIES,
25
+ ) -> None:
26
+ self._api_key = api_key
27
+ self._base_url = base_url.rstrip("/")
28
+ self._timeout = timeout
29
+ self._max_retries = max_retries
30
+ self._http = httpx.AsyncClient(
31
+ base_url=self._base_url,
32
+ headers={"Authorization": f"Bearer {api_key}", "User-Agent": "cogspace-python/0.1.0"},
33
+ timeout=timeout,
34
+ )
35
+
36
+ async def aclose(self) -> None:
37
+ await self._http.aclose()
38
+
39
+ async def __aenter__(self) -> "AsyncClient":
40
+ return self
41
+
42
+ async def __aexit__(self, *_: Any) -> None:
43
+ await self.aclose()
44
+
45
+ async def _request(self, method: str, path: str, **kwargs: Any) -> dict:
46
+ last_exc: Exception | None = None
47
+ for attempt in range(self._max_retries):
48
+ try:
49
+ response = await self._http.request(method, path, **kwargs)
50
+ except httpx.TimeoutException as exc:
51
+ raise TimeoutError(f"Request timed out: {path}") from exc
52
+
53
+ if response.status_code not in _RETRY_STATUSES:
54
+ if response.status_code >= 400:
55
+ try:
56
+ body = response.json()
57
+ except Exception:
58
+ body = {}
59
+ _raise_for_status(response.status_code, body)
60
+ return response.json() if response.content else {}
61
+
62
+ if attempt < self._max_retries - 1:
63
+ wait = 2 ** attempt
64
+ if response.status_code == 429:
65
+ retry_after = response.headers.get("Retry-After")
66
+ if retry_after:
67
+ wait = float(retry_after)
68
+ await asyncio.sleep(wait)
69
+ try:
70
+ body = response.json()
71
+ except Exception:
72
+ body = {}
73
+ last_exc = RateLimitError(
74
+ body.get("detail", f"HTTP {response.status_code}"),
75
+ status=response.status_code,
76
+ )
77
+ else:
78
+ try:
79
+ body = response.json()
80
+ except Exception:
81
+ body = {}
82
+ _raise_for_status(response.status_code, body)
83
+
84
+ raise last_exc or RateLimitError("Max retries exceeded")
85
+
86
+ async def get(self, path: str, params: dict | None = None) -> dict:
87
+ return await self._request("GET", path, params=params)
88
+
89
+ async def post(self, path: str, json: dict | None = None) -> dict:
90
+ return await self._request("POST", path, json=json)
91
+
92
+ async def delete(self, path: str) -> dict:
93
+ return await self._request("DELETE", path)
@@ -0,0 +1,105 @@
1
+ """AsyncSpaceClient — async space-scoped operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal, Optional
6
+
7
+ from cogspace._async_client import AsyncClient
8
+ from cogspace.types import (
9
+ GraphTraverseResponse,
10
+ HealthResponse,
11
+ ListResponse,
12
+ ReadContextResponse,
13
+ ReadResponse,
14
+ SearchResponse,
15
+ WriteResponse,
16
+ )
17
+
18
+
19
+ class AsyncSpaceClient:
20
+ """Async client scoped to a single Cogspace space."""
21
+
22
+ def __init__(self, client: AsyncClient, space_id: str) -> None:
23
+ self._c = client
24
+ self._space_id = space_id
25
+
26
+ def _path(self, endpoint: str) -> str:
27
+ return f"/spaces/{self._space_id}/{endpoint}"
28
+
29
+ async def search(
30
+ self,
31
+ query: str,
32
+ mode: Literal["hybrid", "vector", "bm25"] = "hybrid",
33
+ top_k: int = 5,
34
+ layer: Optional[str] = None,
35
+ min_score: float = 0.0,
36
+ ) -> SearchResponse:
37
+ endpoint = {
38
+ "hybrid": "search/semantic",
39
+ "vector": "search/semantic",
40
+ "bm25": "search/keyword",
41
+ }[mode]
42
+ data = await self._c.post(self._path(endpoint), json={
43
+ "query": query,
44
+ "top_k": top_k,
45
+ "layer": layer,
46
+ "min_score": min_score,
47
+ "mode": mode,
48
+ })
49
+ return SearchResponse.model_validate(data)
50
+
51
+ async def read(self, path: str) -> ReadResponse:
52
+ data = await self._c.get(self._path("read"), params={"path": path})
53
+ return ReadResponse.model_validate(data)
54
+
55
+ async def write(
56
+ self,
57
+ path: str,
58
+ content: str,
59
+ metadata: Optional[dict] = None,
60
+ agent_id: str = "default",
61
+ ) -> WriteResponse:
62
+ data = await self._c.post(self._path("write"), json={
63
+ "path": path,
64
+ "content": content,
65
+ "metadata": metadata,
66
+ "agent_id": agent_id,
67
+ })
68
+ return WriteResponse.model_validate(data)
69
+
70
+ async def list(self, folder: str = "") -> ListResponse:
71
+ data = await self._c.get(self._path("list"), params={"folder": folder})
72
+ return ListResponse.model_validate(data)
73
+
74
+ async def read_memory(self) -> ReadResponse:
75
+ data = await self._c.get(self._path("memory"))
76
+ return ReadResponse.model_validate(data)
77
+
78
+ async def write_memory(self, content: str, confidence: float = 0.9) -> WriteResponse:
79
+ data = await self._c.post(self._path("memory"), json={"content": content, "confidence": confidence})
80
+ return WriteResponse.model_validate(data)
81
+
82
+ async def read_context(self) -> ReadContextResponse:
83
+ data = await self._c.get(self._path("memory/context"))
84
+ return ReadContextResponse.model_validate(data)
85
+
86
+ async def graph_traverse(
87
+ self,
88
+ path: str,
89
+ depth: int = 2,
90
+ rel_type: Optional[str] = None,
91
+ ) -> GraphTraverseResponse:
92
+ data = await self._c.post(self._path("graph/traverse"), json={
93
+ "path": path,
94
+ "depth": depth,
95
+ "rel_type": rel_type,
96
+ })
97
+ return GraphTraverseResponse.model_validate(data)
98
+
99
+ async def hot_layer(self) -> str:
100
+ data = await self._c.get(self._path("hot-layer"))
101
+ return data["hot_layer"]
102
+
103
+ async def health(self) -> HealthResponse:
104
+ data = await self._c.get(self._path("health"))
105
+ return HealthResponse.model_validate(data)
@@ -0,0 +1,96 @@
1
+ """Sync HTTP client for the Cogspace SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any, TypeVar
7
+
8
+ import httpx
9
+
10
+ from cogspace.exceptions import RateLimitError, TimeoutError, _raise_for_status
11
+
12
+ T = TypeVar("T")
13
+
14
+ DEFAULT_BASE_URL = "https://platform.cogspace.ai"
15
+ DEFAULT_TIMEOUT = 30.0
16
+ DEFAULT_MAX_RETRIES = 3
17
+ _RETRY_STATUSES = {429, 500, 502, 503, 504}
18
+
19
+
20
+ class SyncClient:
21
+ def __init__(
22
+ self,
23
+ api_key: str,
24
+ base_url: str = DEFAULT_BASE_URL,
25
+ timeout: float = DEFAULT_TIMEOUT,
26
+ max_retries: int = DEFAULT_MAX_RETRIES,
27
+ ) -> None:
28
+ self._api_key = api_key
29
+ self._base_url = base_url.rstrip("/")
30
+ self._timeout = timeout
31
+ self._max_retries = max_retries
32
+ self._http = httpx.Client(
33
+ base_url=self._base_url,
34
+ headers={"Authorization": f"Bearer {api_key}", "User-Agent": "cogspace-python/0.1.0"},
35
+ timeout=timeout,
36
+ )
37
+
38
+ def close(self) -> None:
39
+ self._http.close()
40
+
41
+ def __enter__(self) -> "SyncClient":
42
+ return self
43
+
44
+ def __exit__(self, *_: Any) -> None:
45
+ self.close()
46
+
47
+ def _request(self, method: str, path: str, **kwargs: Any) -> dict:
48
+ last_exc: Exception | None = None
49
+ for attempt in range(self._max_retries):
50
+ try:
51
+ response = self._http.request(method, path, **kwargs)
52
+ except httpx.TimeoutException as exc:
53
+ raise TimeoutError(f"Request timed out: {path}") from exc
54
+
55
+ if response.status_code not in _RETRY_STATUSES:
56
+ if response.status_code >= 400:
57
+ try:
58
+ body = response.json()
59
+ except Exception:
60
+ body = {}
61
+ _raise_for_status(response.status_code, body)
62
+ return response.json() if response.content else {}
63
+
64
+ # Retryable status
65
+ if attempt < self._max_retries - 1:
66
+ wait = 2 ** attempt
67
+ if response.status_code == 429:
68
+ retry_after = response.headers.get("Retry-After")
69
+ if retry_after:
70
+ wait = float(retry_after)
71
+ time.sleep(wait)
72
+ try:
73
+ body = response.json()
74
+ except Exception:
75
+ body = {}
76
+ last_exc = RateLimitError(
77
+ body.get("detail", f"HTTP {response.status_code}"),
78
+ status=response.status_code,
79
+ )
80
+ else:
81
+ try:
82
+ body = response.json()
83
+ except Exception:
84
+ body = {}
85
+ _raise_for_status(response.status_code, body)
86
+
87
+ raise last_exc or RateLimitError("Max retries exceeded")
88
+
89
+ def get(self, path: str, params: dict | None = None) -> dict:
90
+ return self._request("GET", path, params=params)
91
+
92
+ def post(self, path: str, json: dict | None = None) -> dict:
93
+ return self._request("POST", path, json=json)
94
+
95
+ def delete(self, path: str) -> dict:
96
+ return self._request("DELETE", path)
@@ -0,0 +1,127 @@
1
+ """SpaceClient — sync space-scoped operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal, Optional
6
+
7
+ from cogspace._client import SyncClient
8
+ from cogspace.types import (
9
+ GraphTraverseResponse,
10
+ HealthResponse,
11
+ ListResponse,
12
+ ReadContextResponse,
13
+ ReadResponse,
14
+ SearchResponse,
15
+ WriteResponse,
16
+ )
17
+
18
+
19
+ class SpaceClient:
20
+ """Sync client scoped to a single Cogspace space."""
21
+
22
+ def __init__(self, client: SyncClient, space_id: str) -> None:
23
+ self._c = client
24
+ self._space_id = space_id
25
+
26
+ def _path(self, endpoint: str) -> str:
27
+ return f"/spaces/{self._space_id}/{endpoint}"
28
+
29
+ # ── Search ────────────────────────────────────────────────────────────────
30
+
31
+ def search(
32
+ self,
33
+ query: str,
34
+ mode: Literal["hybrid", "vector", "bm25"] = "hybrid",
35
+ top_k: int = 5,
36
+ layer: Optional[str] = None,
37
+ min_score: float = 0.0,
38
+ ) -> SearchResponse:
39
+ """Search the knowledge space."""
40
+ endpoint = {
41
+ "hybrid": "search/semantic",
42
+ "vector": "search/semantic",
43
+ "bm25": "search/keyword",
44
+ }[mode]
45
+ data = self._c.post(self._path(endpoint), json={
46
+ "query": query,
47
+ "top_k": top_k,
48
+ "layer": layer,
49
+ "min_score": min_score,
50
+ "mode": mode,
51
+ })
52
+ return SearchResponse.model_validate(data)
53
+
54
+ # ── Core ──────────────────────────────────────────────────────────────────
55
+
56
+ def read(self, path: str) -> ReadResponse:
57
+ """Read a file by path."""
58
+ data = self._c.get(self._path("read"), params={"path": path})
59
+ return ReadResponse.model_validate(data)
60
+
61
+ def write(
62
+ self,
63
+ path: str,
64
+ content: str,
65
+ metadata: Optional[dict] = None,
66
+ agent_id: str = "default",
67
+ ) -> WriteResponse:
68
+ """Write (create or update) a file."""
69
+ data = self._c.post(self._path("write"), json={
70
+ "path": path,
71
+ "content": content,
72
+ "metadata": metadata,
73
+ "agent_id": agent_id,
74
+ })
75
+ return WriteResponse.model_validate(data)
76
+
77
+ def list(self, folder: str = "") -> ListResponse:
78
+ """List files in a folder."""
79
+ data = self._c.get(self._path("list"), params={"folder": folder})
80
+ return ListResponse.model_validate(data)
81
+
82
+ # ── Memory ────────────────────────────────────────────────────────────────
83
+
84
+ def read_memory(self) -> ReadResponse:
85
+ """Read the agent's memory.md."""
86
+ data = self._c.get(self._path("memory"))
87
+ return ReadResponse.model_validate(data)
88
+
89
+ def write_memory(self, content: str, confidence: float = 0.9) -> WriteResponse:
90
+ """Update the agent's memory."""
91
+ data = self._c.post(self._path("memory"), json={"content": content, "confidence": confidence})
92
+ return WriteResponse.model_validate(data)
93
+
94
+ def read_context(self) -> ReadContextResponse:
95
+ """Read user context files (memory/user/)."""
96
+ data = self._c.get(self._path("memory/context"))
97
+ return ReadContextResponse.model_validate(data)
98
+
99
+ # ── Graph ─────────────────────────────────────────────────────────────────
100
+
101
+ def graph_traverse(
102
+ self,
103
+ path: str,
104
+ depth: int = 2,
105
+ rel_type: Optional[str] = None,
106
+ ) -> GraphTraverseResponse:
107
+ """Traverse the knowledge graph from a file."""
108
+ data = self._c.post(self._path("graph/traverse"), json={
109
+ "path": path,
110
+ "depth": depth,
111
+ "rel_type": rel_type,
112
+ })
113
+ return GraphTraverseResponse.model_validate(data)
114
+
115
+ # ── Hot layer ─────────────────────────────────────────────────────────────
116
+
117
+ def hot_layer(self) -> str:
118
+ """Get the full hot layer injection string (always-injected context)."""
119
+ data = self._c.get(self._path("hot-layer"))
120
+ return data["hot_layer"]
121
+
122
+ # ── Health ────────────────────────────────────────────────────────────────
123
+
124
+ def health(self) -> HealthResponse:
125
+ """Get space health and storage layer stats."""
126
+ data = self._c.get(self._path("health"))
127
+ return HealthResponse.model_validate(data)
@@ -0,0 +1,46 @@
1
+ """Cogspace SDK exception hierarchy."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class CogspaceError(Exception):
7
+ """Base exception for all Cogspace SDK errors."""
8
+
9
+ def __init__(self, message: str, status: int | None = None, code: str | None = None) -> None:
10
+ super().__init__(message)
11
+ self.status = status
12
+ self.code = code
13
+
14
+
15
+ class AuthError(CogspaceError):
16
+ """Invalid or revoked API key."""
17
+
18
+
19
+ class NotFoundError(CogspaceError):
20
+ """Requested resource does not exist."""
21
+
22
+
23
+ class RateLimitError(CogspaceError):
24
+ """Rate limit exceeded. The SDK will retry automatically."""
25
+
26
+
27
+ class ServerError(CogspaceError):
28
+ """Unexpected server-side error (5xx)."""
29
+
30
+
31
+ class TimeoutError(CogspaceError):
32
+ """Request timed out."""
33
+
34
+
35
+ def _raise_for_status(status: int, body: dict) -> None:
36
+ message = body.get("detail") or body.get("message") or f"HTTP {status}"
37
+ code = body.get("error")
38
+ if status == 401:
39
+ raise AuthError(message, status=status, code=code)
40
+ if status == 404:
41
+ raise NotFoundError(message, status=status, code=code)
42
+ if status == 429:
43
+ raise RateLimitError(message, status=status, code=code)
44
+ if status >= 500:
45
+ raise ServerError(message, status=status, code=code)
46
+ raise CogspaceError(message, status=status, code=code)
@@ -0,0 +1,113 @@
1
+ """Response types for the Cogspace SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal, Optional
6
+
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class SearchItem(BaseModel):
11
+ file_path: str
12
+ content: str
13
+ score: float
14
+ layer: str
15
+ file_type: str
16
+ topic: str
17
+ confidence: float
18
+
19
+
20
+ class SearchResponse(BaseModel):
21
+ results: list[SearchItem]
22
+ total: int
23
+ query: str
24
+ mode: str
25
+
26
+
27
+ class ReadResponse(BaseModel):
28
+ path: str
29
+ frontmatter: dict[str, Any]
30
+ content: str
31
+ layer: str
32
+ size_bytes: int
33
+
34
+
35
+ class WriteResponse(BaseModel):
36
+ path: str
37
+ status: Literal["created", "updated"]
38
+ reindex_queued: bool
39
+
40
+
41
+ class FileTreeNode(BaseModel):
42
+ model_config = {"extra": "allow"}
43
+
44
+
45
+ class ListResponse(BaseModel):
46
+ tree: dict[str, Any]
47
+ file_count: int
48
+ folder: str
49
+
50
+
51
+ class ContextFile(BaseModel):
52
+ path: str
53
+ content: str
54
+ frontmatter: dict[str, Any]
55
+ confidence: float
56
+
57
+
58
+ class ReadContextResponse(BaseModel):
59
+ files: list[ContextFile]
60
+ count: int
61
+ folder: str
62
+
63
+
64
+ class GraphNode(BaseModel):
65
+ path: str
66
+ file_type: str
67
+ topic: str
68
+ summary: Optional[str] = None
69
+
70
+
71
+ class GraphEdge(BaseModel):
72
+ from_: str
73
+ to: str
74
+ rel_type: str
75
+
76
+ class Config:
77
+ populate_by_name = True
78
+ fields = {"from_": "from"}
79
+
80
+
81
+ class GraphTraverseResponse(BaseModel):
82
+ source: str
83
+ depth: int
84
+ rel_type: Optional[str] = None
85
+ nodes: list[GraphNode]
86
+ edges: list[GraphEdge]
87
+ graph_available: bool
88
+
89
+
90
+ class HealthResponse(BaseModel):
91
+ status: str
92
+ space_id: str
93
+ vector_count: int
94
+ bm25_doc_count: int
95
+ graph_available: bool
96
+ graph_node_count: int = 0
97
+ graph_edge_count: int = 0
98
+ hot_layer_files: dict[str, bool] = {}
99
+
100
+
101
+ class Space(BaseModel):
102
+ id: str
103
+ user_id: str
104
+ name: str
105
+ created_at: str
106
+
107
+
108
+ class ApiKey(BaseModel):
109
+ id: str
110
+ name: str
111
+ key_prefix: str
112
+ created_at: str
113
+ last_used: Optional[str] = None
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cogspace"
7
+ version = "0.1.0"
8
+ description = "Official Cogspace SDK — add a knowledge layer to any AI agent"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Cogspace", email = "sdk@cogspace.ai" }]
12
+ requires-python = ">=3.10"
13
+ keywords = ["ai", "agents", "knowledge", "memory", "rag", "sdk"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "httpx>=0.27",
27
+ "pydantic>=2.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest>=8",
33
+ "pytest-asyncio>=0.23",
34
+ "respx>=0.21",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://cogspace.ai"
39
+ Documentation = "https://docs.cogspace.ai"
40
+ Repository = "https://github.com/Jack-Pision/cogspace-ai"
41
+ Issues = "https://github.com/Jack-Pision/cogspace-ai/issues"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["cogspace"]
45
+
46
+ [tool.pytest.ini_options]
47
+ asyncio_mode = "auto"
48
+ testpaths = ["tests"]
49
+
50
+ [tool.ruff]
51
+ target-version = "py310"
52
+ line-length = 100
File without changes
@@ -0,0 +1,54 @@
1
+ """Shared pytest fixtures for Cogspace SDK tests."""
2
+
3
+ import pytest
4
+ import respx
5
+ import httpx
6
+
7
+ BASE_URL = "http://test.cogspace.local"
8
+ API_KEY = "cs-testkey1234567890abcdef1234567890abcdef12"
9
+
10
+ MOCK_SPACES = [
11
+ {"id": "space-001", "user_id": "user-1", "name": "code-patterns", "created_at": "2026-01-01T00:00:00"}
12
+ ]
13
+
14
+ MOCK_SEARCH = {
15
+ "results": [
16
+ {
17
+ "file_path": "expertise/retry.md",
18
+ "content": "Retry with exponential backoff...",
19
+ "score": 0.91,
20
+ "layer": "expertise",
21
+ "file_type": "knowledge",
22
+ "topic": "retry",
23
+ "confidence": 0.9,
24
+ }
25
+ ],
26
+ "total": 1,
27
+ "query": "retry logic",
28
+ "mode": "hybrid",
29
+ }
30
+
31
+ MOCK_READ = {
32
+ "path": "expertise/retry.md",
33
+ "frontmatter": {"type": "knowledge", "topic": "retry"},
34
+ "content": "# Retry patterns\n...",
35
+ "layer": "expertise",
36
+ "size_bytes": 42,
37
+ }
38
+
39
+ MOCK_WRITE = {
40
+ "path": "expertise/retry.md",
41
+ "status": "created",
42
+ "reindex_queued": True,
43
+ }
44
+
45
+ MOCK_HEALTH = {
46
+ "status": "ok",
47
+ "space_id": "space-001",
48
+ "vector_count": 10,
49
+ "bm25_doc_count": 5,
50
+ "graph_available": False,
51
+ "graph_node_count": 0,
52
+ "graph_edge_count": 0,
53
+ "hot_layer_files": {"cogspace_md": True, "memory_md": False},
54
+ }
@@ -0,0 +1,65 @@
1
+ """Tests for the async Cogspace SDK client."""
2
+
3
+ import pytest
4
+ import respx
5
+ import httpx
6
+
7
+ from tests.conftest import BASE_URL, API_KEY, MOCK_SPACES, MOCK_SEARCH, MOCK_WRITE, MOCK_HEALTH
8
+ from cogspace import AsyncCogspace
9
+ from cogspace.exceptions import AuthError, NotFoundError
10
+
11
+
12
+ @pytest.mark.asyncio
13
+ @respx.mock
14
+ async def test_async_search():
15
+ respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
16
+ respx.post(f"{BASE_URL}/spaces/space-001/search/semantic").mock(
17
+ return_value=httpx.Response(200, json=MOCK_SEARCH)
18
+ )
19
+ cog = AsyncCogspace(api_key=API_KEY, base_url=BASE_URL)
20
+ space = await cog.space("code-patterns")
21
+ results = await space.search("retry logic")
22
+ assert results.total == 1
23
+ await cog.aclose()
24
+
25
+
26
+ @pytest.mark.asyncio
27
+ @respx.mock
28
+ async def test_async_write():
29
+ respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
30
+ respx.post(f"{BASE_URL}/spaces/space-001/write").mock(return_value=httpx.Response(200, json=MOCK_WRITE))
31
+ async with AsyncCogspace(api_key=API_KEY, base_url=BASE_URL) as cog:
32
+ space = await cog.space("code-patterns")
33
+ result = await space.write("expertise/retry.md", "# Retry\n...")
34
+ assert result.status == "created"
35
+
36
+
37
+ @pytest.mark.asyncio
38
+ @respx.mock
39
+ async def test_async_space_not_found():
40
+ respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
41
+ cog = AsyncCogspace(api_key=API_KEY, base_url=BASE_URL)
42
+ with pytest.raises(NotFoundError):
43
+ await cog.space("nonexistent")
44
+ await cog.aclose()
45
+
46
+
47
+ @pytest.mark.asyncio
48
+ @respx.mock
49
+ async def test_async_auth_error():
50
+ respx.get(f"{BASE_URL}/spaces").mock(
51
+ return_value=httpx.Response(401, json={"detail": "Invalid or revoked API key"})
52
+ )
53
+ cog = AsyncCogspace(api_key="cs-badkey", base_url=BASE_URL)
54
+ with pytest.raises(AuthError):
55
+ await cog.list_spaces()
56
+ await cog.aclose()
57
+
58
+
59
+ @pytest.mark.asyncio
60
+ @respx.mock
61
+ async def test_async_context_manager():
62
+ respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
63
+ async with AsyncCogspace(api_key=API_KEY, base_url=BASE_URL) as cog:
64
+ spaces = await cog.list_spaces()
65
+ assert len(spaces) == 1
@@ -0,0 +1,111 @@
1
+ """Tests for the sync Cogspace SDK client."""
2
+
3
+ import pytest
4
+ import respx
5
+ import httpx
6
+
7
+ from tests.conftest import BASE_URL, API_KEY, MOCK_SPACES, MOCK_SEARCH, MOCK_READ, MOCK_WRITE, MOCK_HEALTH
8
+ from cogspace import Cogspace
9
+ from cogspace.exceptions import AuthError, NotFoundError
10
+
11
+
12
+ @respx.mock
13
+ def test_space_by_name_resolves():
14
+ respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
15
+ respx.post(f"{BASE_URL}/spaces/space-001/search/semantic").mock(
16
+ return_value=httpx.Response(200, json=MOCK_SEARCH)
17
+ )
18
+ cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
19
+ space = cog.space("code-patterns")
20
+ results = space.search("retry logic")
21
+ assert results.total == 1
22
+ assert results.results[0].file_path == "expertise/retry.md"
23
+
24
+
25
+ @respx.mock
26
+ def test_space_by_id_resolves():
27
+ respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
28
+ cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
29
+ space = cog.space("space-001")
30
+ assert space._space_id == "space-001"
31
+
32
+
33
+ @respx.mock
34
+ def test_space_name_cached():
35
+ route = respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
36
+ cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
37
+ cog.space("code-patterns")
38
+ cog.space("code-patterns")
39
+ assert route.call_count == 1 # only resolved once
40
+
41
+
42
+ @respx.mock
43
+ def test_space_not_found():
44
+ respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
45
+ cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
46
+ with pytest.raises(NotFoundError):
47
+ cog.space("nonexistent-space")
48
+
49
+
50
+ @respx.mock
51
+ def test_search():
52
+ respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
53
+ respx.post(f"{BASE_URL}/spaces/space-001/search/semantic").mock(
54
+ return_value=httpx.Response(200, json=MOCK_SEARCH)
55
+ )
56
+ cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
57
+ space = cog.space("code-patterns")
58
+ results = space.search("retry logic")
59
+ assert len(results.results) == 1
60
+ assert results.results[0].score == 0.91
61
+
62
+
63
+ @respx.mock
64
+ def test_read():
65
+ respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
66
+ respx.get(f"{BASE_URL}/spaces/space-001/read").mock(return_value=httpx.Response(200, json=MOCK_READ))
67
+ cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
68
+ space = cog.space("code-patterns")
69
+ result = space.read("expertise/retry.md")
70
+ assert result.path == "expertise/retry.md"
71
+ assert result.layer == "expertise"
72
+
73
+
74
+ @respx.mock
75
+ def test_write():
76
+ respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
77
+ respx.post(f"{BASE_URL}/spaces/space-001/write").mock(return_value=httpx.Response(200, json=MOCK_WRITE))
78
+ cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
79
+ space = cog.space("code-patterns")
80
+ result = space.write("expertise/retry.md", "# Retry patterns\n...")
81
+ assert result.status == "created"
82
+ assert result.reindex_queued is True
83
+
84
+
85
+ @respx.mock
86
+ def test_health():
87
+ respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
88
+ respx.get(f"{BASE_URL}/spaces/space-001/health").mock(return_value=httpx.Response(200, json=MOCK_HEALTH))
89
+ cog = Cogspace(api_key=API_KEY, base_url=BASE_URL)
90
+ space = cog.space("code-patterns")
91
+ h = space.health()
92
+ assert h.status == "ok"
93
+ assert h.vector_count == 10
94
+
95
+
96
+ @respx.mock
97
+ def test_auth_error():
98
+ respx.get(f"{BASE_URL}/spaces").mock(
99
+ return_value=httpx.Response(401, json={"detail": "Invalid or revoked API key"})
100
+ )
101
+ cog = Cogspace(api_key="cs-badkey", base_url=BASE_URL)
102
+ with pytest.raises(AuthError):
103
+ cog.list_spaces()
104
+
105
+
106
+ @respx.mock
107
+ def test_context_manager():
108
+ respx.get(f"{BASE_URL}/spaces").mock(return_value=httpx.Response(200, json=MOCK_SPACES))
109
+ with Cogspace(api_key=API_KEY, base_url=BASE_URL) as cog:
110
+ spaces = cog.list_spaces()
111
+ assert len(spaces) == 1