switchy-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .venv/
8
+ .claude-flow/
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: switchy-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Switchy AI memory and chat API
5
+ Project-URL: Homepage, https://switchy.build
6
+ Project-URL: Documentation, https://switchy.build/api-dashboard
7
+ Project-URL: Repository, https://github.com/Switchy-AI/switchy
8
+ Project-URL: Bug Tracker, https://github.com/Switchy-AI/switchy/issues
9
+ Author-email: Switchy AI <contact@switchy.build>
10
+ License: MIT
11
+ Keywords: ai,knowledge-graph,memory,openrouter,sdk,switchy
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.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: httpx>=0.25.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
25
+ Requires-Dist: pytest>=7.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # switchy-sdk
30
+
31
+ Official Python SDK for the [Switchy AI](https://switchy.build) memory, knowledge-graph, and multi-model chat API.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install switchy-sdk
37
+ ```
38
+
39
+ ## Quick start
40
+
41
+ ```python
42
+ from switchy import Switchy
43
+
44
+ client = Switchy(api_key="switchy_...")
45
+
46
+ response = client.chat.complete(
47
+ model="anthropic/claude-sonnet-4",
48
+ message="Summarise my recent project notes",
49
+ memory={"enabled": True, "extractMemories": True},
50
+ )
51
+
52
+ print(response["message"]["content"])
53
+ ```
54
+
55
+ ## Streaming
56
+
57
+ ```python
58
+ for chunk in client.chat.stream(
59
+ model="openai/gpt-5",
60
+ message="Write a haiku about memory",
61
+ ):
62
+ if chunk.get("type") == "token":
63
+ print(chunk.get("content", ""), end="", flush=True)
64
+ ```
65
+
66
+ ## Async
67
+
68
+ ```python
69
+ import asyncio
70
+ from switchy import AsyncSwitchy
71
+
72
+ async def main():
73
+ async with AsyncSwitchy(api_key="switchy_...") as client:
74
+ res = await client.chat.complete(
75
+ model="anthropic/claude-sonnet-4",
76
+ message="Hello",
77
+ )
78
+ print(res["message"]["content"])
79
+
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ ## Memory
84
+
85
+ ```python
86
+ # Create a namespace
87
+ client.namespaces.create(name="my-project")
88
+
89
+ # Store a memory frame
90
+ client.memory.create_frame(
91
+ "my-project",
92
+ content="User prefers dark mode and TypeScript",
93
+ metadata={"source": "onboarding"},
94
+ )
95
+
96
+ # Contextual retrieval
97
+ relevant = client.memory.context("my-project", query="user preferences", limit=5)
98
+ ```
99
+
100
+ ## Knowledge graph
101
+
102
+ ```python
103
+ client.knowledge_graph.create_entity(
104
+ "my-project", name="AuthService", type="service"
105
+ )
106
+
107
+ client.knowledge_graph.create_relation(
108
+ "my-project", source="AuthService", target="User", type="authenticates"
109
+ )
110
+ ```
111
+
112
+ ## Error handling
113
+
114
+ ```python
115
+ from switchy import Switchy, SwitchyError, RateLimitError
116
+
117
+ try:
118
+ client.chat.complete(model="openai/gpt-5", message="hi")
119
+ except RateLimitError as e:
120
+ print(f"Rate limited, retry in {e.retry_after}s (limit={e.limit})")
121
+ except SwitchyError as e:
122
+ print(f"API error: {e.code} — {e}")
123
+ ```
124
+
125
+ ## API reference
126
+
127
+ - `chat.complete(...)` — single-turn completion
128
+ - `chat.stream(...)` — SSE streaming iterator
129
+ - `models.list(featured=True)` — list available models (350+)
130
+ - `namespaces.{create,list,get,update,delete}` — memory namespace management
131
+ - `memory.{create_frame,list_frames,context,semantic,search,bridge,consolidate}` — memory operations
132
+ - `knowledge_graph.{create_entity,create_relation,query}` — graph operations
133
+ - `sessions.{create,list,get}` — session management
134
+
135
+ Full OpenAPI spec: https://switchy.build/api/v1/openapi.json
136
+
137
+ ## License
138
+
139
+ MIT
@@ -0,0 +1,111 @@
1
+ # switchy-sdk
2
+
3
+ Official Python SDK for the [Switchy AI](https://switchy.build) memory, knowledge-graph, and multi-model chat API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install switchy-sdk
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from switchy import Switchy
15
+
16
+ client = Switchy(api_key="switchy_...")
17
+
18
+ response = client.chat.complete(
19
+ model="anthropic/claude-sonnet-4",
20
+ message="Summarise my recent project notes",
21
+ memory={"enabled": True, "extractMemories": True},
22
+ )
23
+
24
+ print(response["message"]["content"])
25
+ ```
26
+
27
+ ## Streaming
28
+
29
+ ```python
30
+ for chunk in client.chat.stream(
31
+ model="openai/gpt-5",
32
+ message="Write a haiku about memory",
33
+ ):
34
+ if chunk.get("type") == "token":
35
+ print(chunk.get("content", ""), end="", flush=True)
36
+ ```
37
+
38
+ ## Async
39
+
40
+ ```python
41
+ import asyncio
42
+ from switchy import AsyncSwitchy
43
+
44
+ async def main():
45
+ async with AsyncSwitchy(api_key="switchy_...") as client:
46
+ res = await client.chat.complete(
47
+ model="anthropic/claude-sonnet-4",
48
+ message="Hello",
49
+ )
50
+ print(res["message"]["content"])
51
+
52
+ asyncio.run(main())
53
+ ```
54
+
55
+ ## Memory
56
+
57
+ ```python
58
+ # Create a namespace
59
+ client.namespaces.create(name="my-project")
60
+
61
+ # Store a memory frame
62
+ client.memory.create_frame(
63
+ "my-project",
64
+ content="User prefers dark mode and TypeScript",
65
+ metadata={"source": "onboarding"},
66
+ )
67
+
68
+ # Contextual retrieval
69
+ relevant = client.memory.context("my-project", query="user preferences", limit=5)
70
+ ```
71
+
72
+ ## Knowledge graph
73
+
74
+ ```python
75
+ client.knowledge_graph.create_entity(
76
+ "my-project", name="AuthService", type="service"
77
+ )
78
+
79
+ client.knowledge_graph.create_relation(
80
+ "my-project", source="AuthService", target="User", type="authenticates"
81
+ )
82
+ ```
83
+
84
+ ## Error handling
85
+
86
+ ```python
87
+ from switchy import Switchy, SwitchyError, RateLimitError
88
+
89
+ try:
90
+ client.chat.complete(model="openai/gpt-5", message="hi")
91
+ except RateLimitError as e:
92
+ print(f"Rate limited, retry in {e.retry_after}s (limit={e.limit})")
93
+ except SwitchyError as e:
94
+ print(f"API error: {e.code} — {e}")
95
+ ```
96
+
97
+ ## API reference
98
+
99
+ - `chat.complete(...)` — single-turn completion
100
+ - `chat.stream(...)` — SSE streaming iterator
101
+ - `models.list(featured=True)` — list available models (350+)
102
+ - `namespaces.{create,list,get,update,delete}` — memory namespace management
103
+ - `memory.{create_frame,list_frames,context,semantic,search,bridge,consolidate}` — memory operations
104
+ - `knowledge_graph.{create_entity,create_relation,query}` — graph operations
105
+ - `sessions.{create,list,get}` — session management
106
+
107
+ Full OpenAPI spec: https://switchy.build/api/v1/openapi.json
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "switchy-sdk"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the Switchy AI memory and chat API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Switchy AI", email = "contact@switchy.build" }]
13
+ keywords = ["switchy", "ai", "memory", "knowledge-graph", "openrouter", "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.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ ]
25
+ dependencies = [
26
+ "httpx>=0.25.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = ["pytest>=7.0", "pytest-asyncio>=0.21", "ruff>=0.1.0"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://switchy.build"
34
+ Documentation = "https://switchy.build/api-dashboard"
35
+ Repository = "https://github.com/Switchy-AI/switchy"
36
+ "Bug Tracker" = "https://github.com/Switchy-AI/switchy/issues"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["switchy"]
@@ -0,0 +1,17 @@
1
+ """Switchy AI Python SDK.
2
+
3
+ Install:
4
+ pip install switchy-sdk
5
+
6
+ Usage:
7
+ from switchy import Switchy
8
+ client = Switchy(api_key="switchy_...")
9
+ res = client.chat.complete(model="anthropic/claude-sonnet-4", message="Hello")
10
+ print(res["message"]["content"])
11
+ """
12
+
13
+ from .client import Switchy, AsyncSwitchy
14
+ from .errors import SwitchyError, RateLimitError
15
+
16
+ __version__ = "0.1.0"
17
+ __all__ = ["Switchy", "AsyncSwitchy", "SwitchyError", "RateLimitError"]
@@ -0,0 +1,372 @@
1
+ """Synchronous and asynchronous Switchy API clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, AsyncIterator, Dict, Iterator, List, Optional
7
+
8
+ import httpx
9
+
10
+ from .errors import RateLimitError, SwitchyError
11
+
12
+
13
+ DEFAULT_BASE_URL = "https://switchy.build/api/v1"
14
+ DEFAULT_TIMEOUT = 60.0
15
+ USER_AGENT = "switchy-sdk-python/0.1.0"
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Shared helpers
20
+ # ---------------------------------------------------------------------------
21
+
22
+ def _unwrap(resp: httpx.Response) -> Any:
23
+ """Parse a Switchy API envelope and return `data`, raising on error."""
24
+ if resp.status_code == 429:
25
+ try:
26
+ body = resp.json()
27
+ except Exception:
28
+ body = {}
29
+ err = body.get("error") or {}
30
+ raise RateLimitError(err.get("message") or "Rate limit exceeded", err.get("details"))
31
+ if resp.status_code >= 400:
32
+ try:
33
+ body = resp.json()
34
+ except Exception:
35
+ raise SwitchyError(f"HTTP {resp.status_code}", "HTTP_ERROR", resp.status_code)
36
+ err = body.get("error") or {}
37
+ raise SwitchyError(
38
+ err.get("message") or f"HTTP {resp.status_code}",
39
+ err.get("code") or "HTTP_ERROR",
40
+ resp.status_code,
41
+ err.get("details"),
42
+ )
43
+ body = resp.json()
44
+ if not body.get("success"):
45
+ err = body.get("error") or {}
46
+ raise SwitchyError(
47
+ err.get("message") or "Unknown error",
48
+ err.get("code") or "UNKNOWN",
49
+ resp.status_code,
50
+ err.get("details"),
51
+ )
52
+ return body.get("data")
53
+
54
+
55
+ def _clean_params(params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
56
+ if not params:
57
+ return {}
58
+ return {k: v for k, v in params.items() if v is not None}
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Sync client
63
+ # ---------------------------------------------------------------------------
64
+
65
+ class Switchy:
66
+ """Synchronous Switchy API client."""
67
+
68
+ def __init__(
69
+ self,
70
+ api_key: str,
71
+ base_url: str = DEFAULT_BASE_URL,
72
+ timeout: float = DEFAULT_TIMEOUT,
73
+ client: Optional[httpx.Client] = None,
74
+ ) -> None:
75
+ if not api_key:
76
+ raise ValueError("api_key is required")
77
+ self.api_key = api_key
78
+ self.base_url = base_url.rstrip("/")
79
+ self._client = client or httpx.Client(
80
+ timeout=timeout,
81
+ headers={
82
+ "Authorization": f"Bearer {api_key}",
83
+ "Content-Type": "application/json",
84
+ "User-Agent": USER_AGENT,
85
+ },
86
+ )
87
+ self.chat = _Chat(self)
88
+ self.models = _Models(self)
89
+ self.memory = _Memory(self)
90
+ self.namespaces = _Namespaces(self)
91
+ self.sessions = _Sessions(self)
92
+ self.knowledge_graph = _KnowledgeGraph(self)
93
+
94
+ # ── internal ───────────────────────────────────────────────────────
95
+
96
+ def _request(
97
+ self,
98
+ method: str,
99
+ path: str,
100
+ json_body: Optional[Any] = None,
101
+ params: Optional[Dict[str, Any]] = None,
102
+ ) -> Any:
103
+ resp = self._client.request(
104
+ method,
105
+ self.base_url + path,
106
+ json=json_body,
107
+ params=_clean_params(params),
108
+ )
109
+ return _unwrap(resp)
110
+
111
+ def _stream(self, path: str, json_body: Any) -> Iterator[Dict[str, Any]]:
112
+ with self._client.stream(
113
+ "POST",
114
+ self.base_url + path,
115
+ json=json_body,
116
+ headers={"Accept": "text/event-stream"},
117
+ ) as resp:
118
+ if resp.status_code != 200:
119
+ resp.read()
120
+ _unwrap(resp)
121
+ for line in resp.iter_lines():
122
+ if not line or not line.startswith("data:"):
123
+ continue
124
+ data = line[5:].strip()
125
+ if not data or data == "[DONE]":
126
+ continue
127
+ try:
128
+ yield json.loads(data)
129
+ except json.JSONDecodeError:
130
+ continue
131
+
132
+ def close(self) -> None:
133
+ self._client.close()
134
+
135
+ def __enter__(self) -> "Switchy":
136
+ return self
137
+
138
+ def __exit__(self, *a: Any) -> None:
139
+ self.close()
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Async client
144
+ # ---------------------------------------------------------------------------
145
+
146
+ class AsyncSwitchy:
147
+ """Asynchronous Switchy API client (asyncio)."""
148
+
149
+ def __init__(
150
+ self,
151
+ api_key: str,
152
+ base_url: str = DEFAULT_BASE_URL,
153
+ timeout: float = DEFAULT_TIMEOUT,
154
+ client: Optional[httpx.AsyncClient] = None,
155
+ ) -> None:
156
+ if not api_key:
157
+ raise ValueError("api_key is required")
158
+ self.api_key = api_key
159
+ self.base_url = base_url.rstrip("/")
160
+ self._client = client or httpx.AsyncClient(
161
+ timeout=timeout,
162
+ headers={
163
+ "Authorization": f"Bearer {api_key}",
164
+ "Content-Type": "application/json",
165
+ "User-Agent": USER_AGENT,
166
+ },
167
+ )
168
+ self.chat = _AsyncChat(self)
169
+ self.models = _AsyncModels(self)
170
+ self.memory = _AsyncMemory(self)
171
+ self.namespaces = _AsyncNamespaces(self)
172
+ self.sessions = _AsyncSessions(self)
173
+ self.knowledge_graph = _AsyncKnowledgeGraph(self)
174
+
175
+ async def _request(
176
+ self,
177
+ method: str,
178
+ path: str,
179
+ json_body: Optional[Any] = None,
180
+ params: Optional[Dict[str, Any]] = None,
181
+ ) -> Any:
182
+ resp = await self._client.request(
183
+ method,
184
+ self.base_url + path,
185
+ json=json_body,
186
+ params=_clean_params(params),
187
+ )
188
+ return _unwrap(resp)
189
+
190
+ async def _stream(self, path: str, json_body: Any) -> AsyncIterator[Dict[str, Any]]:
191
+ async with self._client.stream(
192
+ "POST",
193
+ self.base_url + path,
194
+ json=json_body,
195
+ headers={"Accept": "text/event-stream"},
196
+ ) as resp:
197
+ if resp.status_code != 200:
198
+ await resp.aread()
199
+ _unwrap(resp)
200
+ async for line in resp.aiter_lines():
201
+ if not line or not line.startswith("data:"):
202
+ continue
203
+ data = line[5:].strip()
204
+ if not data or data == "[DONE]":
205
+ continue
206
+ try:
207
+ yield json.loads(data)
208
+ except json.JSONDecodeError:
209
+ continue
210
+
211
+ async def close(self) -> None:
212
+ await self._client.aclose()
213
+
214
+ async def __aenter__(self) -> "AsyncSwitchy":
215
+ return self
216
+
217
+ async def __aexit__(self, *a: Any) -> None:
218
+ await self.close()
219
+
220
+
221
+ # ---------------------------------------------------------------------------
222
+ # Resources (sync)
223
+ # ---------------------------------------------------------------------------
224
+
225
+ class _Chat:
226
+ def __init__(self, c: Switchy) -> None: self._c = c
227
+ def complete(self, *, model: str, message: str, **kwargs: Any) -> Dict[str, Any]:
228
+ body = {"model": model, "message": message, "stream": False, **kwargs}
229
+ return self._c._request("POST", "/chat", body)
230
+ def stream(self, *, model: str, message: str, **kwargs: Any) -> Iterator[Dict[str, Any]]:
231
+ body = {"model": model, "message": message, "stream": True, **kwargs}
232
+ return self._c._stream("/chat", body)
233
+
234
+ class _Models:
235
+ def __init__(self, c: Switchy) -> None: self._c = c
236
+ def list(self, *, category: Optional[str] = None, q: Optional[str] = None, featured: Optional[bool] = None) -> Dict[str, Any]:
237
+ return self._c._request("GET", "/models", params={"category": category, "q": q, "featured": featured})
238
+
239
+ class _Namespaces:
240
+ def __init__(self, c: Switchy) -> None: self._c = c
241
+ def create(self, *, name: str, description: Optional[str] = None) -> Dict[str, Any]:
242
+ return self._c._request("POST", "/namespaces", {"name": name, "description": description})
243
+ def list(self) -> List[Dict[str, Any]]:
244
+ return self._c._request("GET", "/namespaces")
245
+ def get(self, id: str) -> Dict[str, Any]:
246
+ return self._c._request("GET", f"/namespaces/{id}")
247
+ def update(self, id: str, **data: Any) -> Dict[str, Any]:
248
+ return self._c._request("PATCH", f"/namespaces/{id}", data)
249
+ def delete(self, id: str) -> None:
250
+ self._c._request("DELETE", f"/namespaces/{id}")
251
+
252
+ class _Memory:
253
+ def __init__(self, c: Switchy) -> None: self._c = c
254
+ def create_frame(self, namespace: str, *, content: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
255
+ return self._c._request("POST", f"/memory/{namespace}/frames", {"content": content, "metadata": metadata})
256
+ def list_frames(self, namespace: str, *, limit: Optional[int] = None, offset: Optional[int] = None, status: Optional[str] = None) -> List[Dict[str, Any]]:
257
+ return self._c._request("GET", f"/memory/{namespace}/frames", params={"limit": limit, "offset": offset, "status": status})
258
+ def get_frame(self, namespace: str, id: str) -> Dict[str, Any]:
259
+ return self._c._request("GET", f"/memory/{namespace}/frames/{id}")
260
+ def update_frame(self, namespace: str, id: str, **data: Any) -> Dict[str, Any]:
261
+ return self._c._request("PATCH", f"/memory/{namespace}/frames/{id}", data)
262
+ def close_frame(self, namespace: str, id: str) -> Dict[str, Any]:
263
+ return self._c._request("POST", f"/memory/{namespace}/frames/{id}/close")
264
+ def context(self, namespace: str, *, query: str, limit: Optional[int] = None) -> Dict[str, Any]:
265
+ return self._c._request("POST", f"/memory/{namespace}/context", {"query": query, "limit": limit})
266
+ def semantic(self, namespace: str, *, query: str, limit: Optional[int] = None, threshold: Optional[float] = None) -> Dict[str, Any]:
267
+ return self._c._request("POST", f"/memory/{namespace}/semantic", {"query": query, "limit": limit, "threshold": threshold})
268
+ def search(self, *, query: str, namespaces: Optional[List[str]] = None, limit: Optional[int] = None) -> Dict[str, Any]:
269
+ return self._c._request("POST", "/memory/search", {"query": query, "namespaces": namespaces, "limit": limit})
270
+ def bridge(self, namespace: str, *, source_namespace: str, frame_ids: Optional[List[str]] = None, mode: str = "link") -> Dict[str, Any]:
271
+ return self._c._request("POST", f"/memory/{namespace}/bridge", {"sourceNamespace": source_namespace, "frameIds": frame_ids, "mode": mode})
272
+ def consolidate(self, namespace: str) -> Dict[str, Any]:
273
+ return self._c._request("POST", f"/memory/{namespace}/consolidate")
274
+
275
+ class _KnowledgeGraph:
276
+ def __init__(self, c: Switchy) -> None: self._c = c
277
+ def list_entities(self, namespace: str) -> List[Dict[str, Any]]:
278
+ return self._c._request("GET", f"/memory/{namespace}/graph/entities")
279
+ def create_entity(self, namespace: str, *, name: str, type: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
280
+ return self._c._request("POST", f"/memory/{namespace}/graph/entities", {"name": name, "type": type, "metadata": metadata})
281
+ def list_relations(self, namespace: str) -> List[Dict[str, Any]]:
282
+ return self._c._request("GET", f"/memory/{namespace}/graph/relations")
283
+ def create_relation(self, namespace: str, *, source: str, target: str, type: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
284
+ return self._c._request("POST", f"/memory/{namespace}/graph/relations", {"source": source, "target": target, "type": type, "metadata": metadata})
285
+ def query(self, *, query: str, depth: Optional[int] = None, namespace: Optional[str] = None) -> Dict[str, Any]:
286
+ return self._c._request("POST", "/knowledge-graph", {"query": query, "depth": depth, "namespace": namespace})
287
+
288
+ class _Sessions:
289
+ def __init__(self, c: Switchy) -> None: self._c = c
290
+ def create(self, **data: Any) -> Dict[str, Any]:
291
+ return self._c._request("POST", "/sessions", data)
292
+ def list(self) -> List[Dict[str, Any]]:
293
+ return self._c._request("GET", "/sessions")
294
+ def get(self, id: str) -> Dict[str, Any]:
295
+ return self._c._request("GET", f"/sessions/{id}")
296
+
297
+
298
+ # ---------------------------------------------------------------------------
299
+ # Resources (async) — mirrors the sync ones
300
+ # ---------------------------------------------------------------------------
301
+
302
+ class _AsyncChat:
303
+ def __init__(self, c: AsyncSwitchy) -> None: self._c = c
304
+ async def complete(self, *, model: str, message: str, **kwargs: Any) -> Dict[str, Any]:
305
+ body = {"model": model, "message": message, "stream": False, **kwargs}
306
+ return await self._c._request("POST", "/chat", body)
307
+ def stream(self, *, model: str, message: str, **kwargs: Any) -> AsyncIterator[Dict[str, Any]]:
308
+ body = {"model": model, "message": message, "stream": True, **kwargs}
309
+ return self._c._stream("/chat", body)
310
+
311
+ class _AsyncModels:
312
+ def __init__(self, c: AsyncSwitchy) -> None: self._c = c
313
+ async def list(self, *, category: Optional[str] = None, q: Optional[str] = None, featured: Optional[bool] = None) -> Dict[str, Any]:
314
+ return await self._c._request("GET", "/models", params={"category": category, "q": q, "featured": featured})
315
+
316
+ class _AsyncNamespaces:
317
+ def __init__(self, c: AsyncSwitchy) -> None: self._c = c
318
+ async def create(self, *, name: str, description: Optional[str] = None) -> Dict[str, Any]:
319
+ return await self._c._request("POST", "/namespaces", {"name": name, "description": description})
320
+ async def list(self) -> List[Dict[str, Any]]:
321
+ return await self._c._request("GET", "/namespaces")
322
+ async def get(self, id: str) -> Dict[str, Any]:
323
+ return await self._c._request("GET", f"/namespaces/{id}")
324
+ async def update(self, id: str, **data: Any) -> Dict[str, Any]:
325
+ return await self._c._request("PATCH", f"/namespaces/{id}", data)
326
+ async def delete(self, id: str) -> None:
327
+ await self._c._request("DELETE", f"/namespaces/{id}")
328
+
329
+ class _AsyncMemory:
330
+ def __init__(self, c: AsyncSwitchy) -> None: self._c = c
331
+ async def create_frame(self, namespace: str, *, content: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
332
+ return await self._c._request("POST", f"/memory/{namespace}/frames", {"content": content, "metadata": metadata})
333
+ async def list_frames(self, namespace: str, *, limit: Optional[int] = None, offset: Optional[int] = None, status: Optional[str] = None) -> List[Dict[str, Any]]:
334
+ return await self._c._request("GET", f"/memory/{namespace}/frames", params={"limit": limit, "offset": offset, "status": status})
335
+ async def get_frame(self, namespace: str, id: str) -> Dict[str, Any]:
336
+ return await self._c._request("GET", f"/memory/{namespace}/frames/{id}")
337
+ async def update_frame(self, namespace: str, id: str, **data: Any) -> Dict[str, Any]:
338
+ return await self._c._request("PATCH", f"/memory/{namespace}/frames/{id}", data)
339
+ async def close_frame(self, namespace: str, id: str) -> Dict[str, Any]:
340
+ return await self._c._request("POST", f"/memory/{namespace}/frames/{id}/close")
341
+ async def context(self, namespace: str, *, query: str, limit: Optional[int] = None) -> Dict[str, Any]:
342
+ return await self._c._request("POST", f"/memory/{namespace}/context", {"query": query, "limit": limit})
343
+ async def semantic(self, namespace: str, *, query: str, limit: Optional[int] = None, threshold: Optional[float] = None) -> Dict[str, Any]:
344
+ return await self._c._request("POST", f"/memory/{namespace}/semantic", {"query": query, "limit": limit, "threshold": threshold})
345
+ async def search(self, *, query: str, namespaces: Optional[List[str]] = None, limit: Optional[int] = None) -> Dict[str, Any]:
346
+ return await self._c._request("POST", "/memory/search", {"query": query, "namespaces": namespaces, "limit": limit})
347
+ async def bridge(self, namespace: str, *, source_namespace: str, frame_ids: Optional[List[str]] = None, mode: str = "link") -> Dict[str, Any]:
348
+ return await self._c._request("POST", f"/memory/{namespace}/bridge", {"sourceNamespace": source_namespace, "frameIds": frame_ids, "mode": mode})
349
+ async def consolidate(self, namespace: str) -> Dict[str, Any]:
350
+ return await self._c._request("POST", f"/memory/{namespace}/consolidate")
351
+
352
+ class _AsyncKnowledgeGraph:
353
+ def __init__(self, c: AsyncSwitchy) -> None: self._c = c
354
+ async def list_entities(self, namespace: str) -> List[Dict[str, Any]]:
355
+ return await self._c._request("GET", f"/memory/{namespace}/graph/entities")
356
+ async def create_entity(self, namespace: str, *, name: str, type: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
357
+ return await self._c._request("POST", f"/memory/{namespace}/graph/entities", {"name": name, "type": type, "metadata": metadata})
358
+ async def list_relations(self, namespace: str) -> List[Dict[str, Any]]:
359
+ return await self._c._request("GET", f"/memory/{namespace}/graph/relations")
360
+ async def create_relation(self, namespace: str, *, source: str, target: str, type: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
361
+ return await self._c._request("POST", f"/memory/{namespace}/graph/relations", {"source": source, "target": target, "type": type, "metadata": metadata})
362
+ async def query(self, *, query: str, depth: Optional[int] = None, namespace: Optional[str] = None) -> Dict[str, Any]:
363
+ return await self._c._request("POST", "/knowledge-graph", {"query": query, "depth": depth, "namespace": namespace})
364
+
365
+ class _AsyncSessions:
366
+ def __init__(self, c: AsyncSwitchy) -> None: self._c = c
367
+ async def create(self, **data: Any) -> Dict[str, Any]:
368
+ return await self._c._request("POST", "/sessions", data)
369
+ async def list(self) -> List[Dict[str, Any]]:
370
+ return await self._c._request("GET", "/sessions")
371
+ async def get(self, id: str) -> Dict[str, Any]:
372
+ return await self._c._request("GET", f"/sessions/{id}")
@@ -0,0 +1,39 @@
1
+ """Exception types raised by the Switchy SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import Any, Optional
7
+
8
+
9
+ class SwitchyError(Exception):
10
+ """Base error for all SDK failures."""
11
+
12
+ def __init__(
13
+ self,
14
+ message: str,
15
+ code: str = "UNKNOWN",
16
+ status: int = 0,
17
+ details: Optional[Any] = None,
18
+ ) -> None:
19
+ super().__init__(message)
20
+ self.code = code
21
+ self.status = status
22
+ self.details = details
23
+
24
+ def __repr__(self) -> str:
25
+ return f"SwitchyError(code={self.code!r}, status={self.status}, message={self.args[0]!r})"
26
+
27
+
28
+ class RateLimitError(SwitchyError):
29
+ """Raised when the API returns 429 Too Many Requests."""
30
+
31
+ def __init__(self, message: str, details: Optional[dict] = None) -> None:
32
+ super().__init__(message, "RATE_LIMIT_EXCEEDED", 429, details)
33
+ details = details or {}
34
+ self.retry_after: int = int(details.get("retryAfter") or 60)
35
+ self.limit: Optional[int] = details.get("limit")
36
+ reset_at = details.get("resetAt")
37
+ self.reset_at: Optional[datetime] = (
38
+ datetime.fromisoformat(reset_at.replace("Z", "+00:00")) if reset_at else None
39
+ )