sovant 1.0.0__py3-none-any.whl

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.

Potentially problematic release.


This version of sovant might be problematic. Click here for more details.

sovant/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ """
2
+ Sovant Python SDK
3
+
4
+ Official Python library for the Sovant Memory API.
5
+ """
6
+
7
+ __version__ = "1.0.0"
8
+
9
+ from .client import SovantClient, AsyncSovantClient
10
+ from .exceptions import (
11
+ SovantError,
12
+ AuthenticationError,
13
+ RateLimitError,
14
+ ValidationError,
15
+ NotFoundError,
16
+ NetworkError,
17
+ )
18
+ from .types import (
19
+ Memory,
20
+ MemoryType,
21
+ EmotionType,
22
+ EmotionalContext,
23
+ CreateMemoryInput,
24
+ UpdateMemoryInput,
25
+ SearchOptions,
26
+ SearchResult,
27
+ Thread,
28
+ ThreadStatus,
29
+ CreateThreadInput,
30
+ UpdateThreadInput,
31
+ ThreadStats,
32
+ BatchCreateResult,
33
+ PaginatedResponse,
34
+ )
35
+
36
+ __all__ = [
37
+ # Client classes
38
+ "SovantClient",
39
+ "AsyncSovantClient",
40
+ # Exceptions
41
+ "SovantError",
42
+ "AuthenticationError",
43
+ "RateLimitError",
44
+ "ValidationError",
45
+ "NotFoundError",
46
+ "NetworkError",
47
+ # Types
48
+ "Memory",
49
+ "MemoryType",
50
+ "EmotionType",
51
+ "EmotionalContext",
52
+ "CreateMemoryInput",
53
+ "UpdateMemoryInput",
54
+ "SearchOptions",
55
+ "SearchResult",
56
+ "Thread",
57
+ "ThreadStatus",
58
+ "CreateThreadInput",
59
+ "UpdateThreadInput",
60
+ "ThreadStats",
61
+ "BatchCreateResult",
62
+ "PaginatedResponse",
63
+ ]
sovant/base_client.py ADDED
@@ -0,0 +1,256 @@
1
+ """Base client for making API requests."""
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ from typing import Any, Dict, Optional, TypeVar, Union
7
+ from urllib.parse import urlencode
8
+
9
+ import httpx
10
+
11
+ from .exceptions import (
12
+ AuthenticationError,
13
+ NetworkError,
14
+ NotFoundError,
15
+ RateLimitError,
16
+ SovantError,
17
+ ValidationError,
18
+ )
19
+ from .types import Config
20
+
21
+ T = TypeVar("T")
22
+
23
+
24
+ class BaseClient:
25
+ """Base client for synchronous API requests."""
26
+
27
+ def __init__(self, config: Union[str, Config]):
28
+ if isinstance(config, str):
29
+ self.config = Config(api_key=config)
30
+ elif isinstance(config, Config):
31
+ self.config = config
32
+ else:
33
+ # Try to get from environment
34
+ api_key = os.environ.get("SOVANT_API_KEY")
35
+ if not api_key:
36
+ raise AuthenticationError("API key is required")
37
+ self.config = Config(api_key=api_key)
38
+
39
+ # Ensure HTTPS is used for security
40
+ if not self.config.base_url.startswith("https://"):
41
+ raise SovantError("API base URL must use HTTPS for security")
42
+
43
+ self.client = httpx.Client(
44
+ timeout=self.config.timeout,
45
+ headers=self._get_headers(),
46
+ )
47
+
48
+ def _get_headers(self) -> Dict[str, str]:
49
+ return {
50
+ "Authorization": f"Bearer {self.config.api_key}",
51
+ "Content-Type": "application/json",
52
+ "X-SDK-Version": "1.0.0",
53
+ "X-SDK-Language": "python",
54
+ }
55
+
56
+ def _build_url(self, path: str) -> str:
57
+ return f"{self.config.base_url}{path}"
58
+
59
+ def _build_query_string(self, params: Dict[str, Any]) -> str:
60
+ filtered_params = {
61
+ k: v for k, v in params.items()
62
+ if v is not None
63
+ }
64
+
65
+ # Handle list values
66
+ processed_params = {}
67
+ for k, v in filtered_params.items():
68
+ if isinstance(v, list):
69
+ # Convert list to comma-separated string
70
+ processed_params[k] = ",".join(str(item) for item in v)
71
+ elif isinstance(v, bool):
72
+ processed_params[k] = str(v).lower()
73
+ else:
74
+ processed_params[k] = str(v)
75
+
76
+ return f"?{urlencode(processed_params)}" if processed_params else ""
77
+
78
+ def _handle_response(self, response: httpx.Response) -> Any:
79
+ if response.status_code >= 200 and response.status_code < 300:
80
+ try:
81
+ return response.json()
82
+ except json.JSONDecodeError:
83
+ return None
84
+
85
+ # Handle error responses
86
+ try:
87
+ error_data = response.json()
88
+ message = error_data.get("message", f"HTTP {response.status_code}")
89
+ details = error_data.get("details", {})
90
+ except json.JSONDecodeError:
91
+ message = f"HTTP {response.status_code} {response.reason_phrase}"
92
+ details = {}
93
+
94
+ if response.status_code == 401:
95
+ raise AuthenticationError(message)
96
+ elif response.status_code == 404:
97
+ raise NotFoundError(message)
98
+ elif response.status_code == 429:
99
+ retry_after = response.headers.get("Retry-After")
100
+ raise RateLimitError(
101
+ message,
102
+ int(retry_after) if retry_after else None
103
+ )
104
+ elif response.status_code in (400, 422):
105
+ raise ValidationError(message, details.get("errors"))
106
+ else:
107
+ raise SovantError(message, response.status_code, details)
108
+
109
+ def request(
110
+ self,
111
+ method: str,
112
+ path: str,
113
+ data: Optional[Dict[str, Any]] = None,
114
+ retry_count: int = 0,
115
+ ) -> Any:
116
+ url = self._build_url(path)
117
+
118
+ try:
119
+ response = self.client.request(
120
+ method,
121
+ url,
122
+ json=data if data else None,
123
+ )
124
+ return self._handle_response(response)
125
+ except httpx.TimeoutException:
126
+ raise NetworkError("Request timeout")
127
+ except httpx.RequestError as e:
128
+ if retry_count < self.config.max_retries:
129
+ # Exponential backoff
130
+ delay = self.config.retry_delay * (2 ** retry_count)
131
+ import time
132
+ time.sleep(delay)
133
+ return self.request(method, path, data, retry_count + 1)
134
+ raise NetworkError(str(e))
135
+
136
+ def __del__(self):
137
+ if hasattr(self, "client"):
138
+ self.client.close()
139
+
140
+
141
+ class AsyncBaseClient:
142
+ """Base client for asynchronous API requests."""
143
+
144
+ def __init__(self, config: Union[str, Config]):
145
+ if isinstance(config, str):
146
+ self.config = Config(api_key=config)
147
+ elif isinstance(config, Config):
148
+ self.config = config
149
+ else:
150
+ # Try to get from environment
151
+ api_key = os.environ.get("SOVANT_API_KEY")
152
+ if not api_key:
153
+ raise AuthenticationError("API key is required")
154
+ self.config = Config(api_key=api_key)
155
+
156
+ # Ensure HTTPS is used for security
157
+ if not self.config.base_url.startswith("https://"):
158
+ raise SovantError("API base URL must use HTTPS for security")
159
+
160
+ self.client = httpx.AsyncClient(
161
+ timeout=self.config.timeout,
162
+ headers=self._get_headers(),
163
+ )
164
+
165
+ def _get_headers(self) -> Dict[str, str]:
166
+ return {
167
+ "Authorization": f"Bearer {self.config.api_key}",
168
+ "Content-Type": "application/json",
169
+ "X-SDK-Version": "1.0.0",
170
+ "X-SDK-Language": "python",
171
+ }
172
+
173
+ def _build_url(self, path: str) -> str:
174
+ return f"{self.config.base_url}{path}"
175
+
176
+ def _build_query_string(self, params: Dict[str, Any]) -> str:
177
+ filtered_params = {
178
+ k: v for k, v in params.items()
179
+ if v is not None
180
+ }
181
+
182
+ # Handle list values
183
+ processed_params = {}
184
+ for k, v in filtered_params.items():
185
+ if isinstance(v, list):
186
+ # Convert list to comma-separated string
187
+ processed_params[k] = ",".join(str(item) for item in v)
188
+ elif isinstance(v, bool):
189
+ processed_params[k] = str(v).lower()
190
+ else:
191
+ processed_params[k] = str(v)
192
+
193
+ return f"?{urlencode(processed_params)}" if processed_params else ""
194
+
195
+ async def _handle_response(self, response: httpx.Response) -> Any:
196
+ if response.status_code >= 200 and response.status_code < 300:
197
+ try:
198
+ return response.json()
199
+ except json.JSONDecodeError:
200
+ return None
201
+
202
+ # Handle error responses
203
+ try:
204
+ error_data = response.json()
205
+ message = error_data.get("message", f"HTTP {response.status_code}")
206
+ details = error_data.get("details", {})
207
+ except json.JSONDecodeError:
208
+ message = f"HTTP {response.status_code} {response.reason_phrase}"
209
+ details = {}
210
+
211
+ if response.status_code == 401:
212
+ raise AuthenticationError(message)
213
+ elif response.status_code == 404:
214
+ raise NotFoundError(message)
215
+ elif response.status_code == 429:
216
+ retry_after = response.headers.get("Retry-After")
217
+ raise RateLimitError(
218
+ message,
219
+ int(retry_after) if retry_after else None
220
+ )
221
+ elif response.status_code in (400, 422):
222
+ raise ValidationError(message, details.get("errors"))
223
+ else:
224
+ raise SovantError(message, response.status_code, details)
225
+
226
+ async def request(
227
+ self,
228
+ method: str,
229
+ path: str,
230
+ data: Optional[Dict[str, Any]] = None,
231
+ retry_count: int = 0,
232
+ ) -> Any:
233
+ url = self._build_url(path)
234
+
235
+ try:
236
+ response = await self.client.request(
237
+ method,
238
+ url,
239
+ json=data if data else None,
240
+ )
241
+ return await self._handle_response(response)
242
+ except httpx.TimeoutException:
243
+ raise NetworkError("Request timeout")
244
+ except httpx.RequestError as e:
245
+ if retry_count < self.config.max_retries:
246
+ # Exponential backoff
247
+ delay = self.config.retry_delay * (2 ** retry_count)
248
+ await asyncio.sleep(delay)
249
+ return await self.request(method, path, data, retry_count + 1)
250
+ raise NetworkError(str(e))
251
+
252
+ async def __aenter__(self):
253
+ return self
254
+
255
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
256
+ await self.client.aclose()
sovant/client.py ADDED
@@ -0,0 +1,87 @@
1
+ """Main client for the Sovant SDK."""
2
+
3
+ from typing import Any, Dict, Union
4
+
5
+ from .base_client import AsyncBaseClient, BaseClient
6
+ from .resources import AsyncMemories, AsyncThreads, Memories, Threads
7
+ from .types import Config
8
+
9
+
10
+ class SovantClient:
11
+ """Synchronous client for the Sovant API."""
12
+
13
+ def __init__(self, api_key: Union[str, Config, None] = None):
14
+ """
15
+ Initialize the Sovant client.
16
+
17
+ Args:
18
+ api_key: API key string, Config object, or None (uses env var)
19
+ """
20
+ if api_key is None:
21
+ import os
22
+ api_key = os.environ.get("SOVANT_API_KEY", "")
23
+
24
+ if isinstance(api_key, str):
25
+ config = Config(api_key=api_key)
26
+ else:
27
+ config = api_key
28
+
29
+ self.memories = Memories(config)
30
+ self.threads = Threads(config)
31
+ self._config = config
32
+
33
+ def ping(self) -> Dict[str, str]:
34
+ """Test API connection."""
35
+ client = BaseClient(self._config)
36
+ return client.request("GET", "/health")
37
+
38
+ def get_usage(self) -> Dict[str, Any]:
39
+ """Get current API usage and quota."""
40
+ client = BaseClient(self._config)
41
+ return client.request("GET", "/usage")
42
+
43
+
44
+ class AsyncSovantClient:
45
+ """Asynchronous client for the Sovant API."""
46
+
47
+ def __init__(self, api_key: Union[str, Config, None] = None):
48
+ """
49
+ Initialize the async Sovant client.
50
+
51
+ Args:
52
+ api_key: API key string, Config object, or None (uses env var)
53
+ """
54
+ if api_key is None:
55
+ import os
56
+ api_key = os.environ.get("SOVANT_API_KEY", "")
57
+
58
+ if isinstance(api_key, str):
59
+ config = Config(api_key=api_key)
60
+ else:
61
+ config = api_key
62
+
63
+ self.memories = AsyncMemories(config)
64
+ self.threads = AsyncThreads(config)
65
+ self._config = config
66
+
67
+ async def ping(self) -> Dict[str, str]:
68
+ """Test API connection."""
69
+ async with AsyncBaseClient(self._config) as client:
70
+ return await client.request("GET", "/health")
71
+
72
+ async def get_usage(self) -> Dict[str, Any]:
73
+ """Get current API usage and quota."""
74
+ async with AsyncBaseClient(self._config) as client:
75
+ return await client.request("GET", "/usage")
76
+
77
+ async def __aenter__(self):
78
+ """Context manager entry."""
79
+ return self
80
+
81
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
82
+ """Context manager exit."""
83
+ # Clean up any open connections
84
+ if hasattr(self.memories, "client"):
85
+ await self.memories.client.aclose()
86
+ if hasattr(self.threads, "client"):
87
+ await self.threads.client.aclose()
sovant/exceptions.py ADDED
@@ -0,0 +1,58 @@
1
+ """Exception classes for the Sovant SDK."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+
6
+ class SovantError(Exception):
7
+ """Base exception for all Sovant errors."""
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ status_code: Optional[int] = None,
13
+ details: Optional[Dict[str, Any]] = None,
14
+ ):
15
+ super().__init__(message)
16
+ self.status_code = status_code
17
+ self.details = details or {}
18
+
19
+
20
+ class AuthenticationError(SovantError):
21
+ """Raised when authentication fails."""
22
+
23
+ def __init__(self, message: str = "Authentication failed"):
24
+ super().__init__(message, 401)
25
+
26
+
27
+ class RateLimitError(SovantError):
28
+ """Raised when rate limit is exceeded."""
29
+
30
+ def __init__(self, message: str = "Rate limit exceeded", retry_after: Optional[int] = None):
31
+ super().__init__(message, 429)
32
+ self.retry_after = retry_after
33
+
34
+
35
+ class ValidationError(SovantError):
36
+ """Raised when validation fails."""
37
+
38
+ def __init__(
39
+ self,
40
+ message: str = "Validation failed",
41
+ errors: Optional[Dict[str, list[str]]] = None,
42
+ ):
43
+ super().__init__(message, 400)
44
+ self.errors = errors or {}
45
+
46
+
47
+ class NotFoundError(SovantError):
48
+ """Raised when a resource is not found."""
49
+
50
+ def __init__(self, message: str = "Resource not found"):
51
+ super().__init__(message, 404)
52
+
53
+
54
+ class NetworkError(SovantError):
55
+ """Raised when a network error occurs."""
56
+
57
+ def __init__(self, message: str = "Network request failed"):
58
+ super().__init__(message)
@@ -0,0 +1,6 @@
1
+ """Resources for the Sovant SDK."""
2
+
3
+ from .memories import AsyncMemories, Memories
4
+ from .threads import AsyncThreads, Threads
5
+
6
+ __all__ = ["Memories", "AsyncMemories", "Threads", "AsyncThreads"]