tracia 0.0.1__py3-none-any.whl → 0.1.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.
tracia/_constants.py ADDED
@@ -0,0 +1,39 @@
1
+ """Constants and configuration for the Tracia SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # SDK Version (defined here to avoid circular imports)
6
+ SDK_VERSION = "0.1.0"
7
+
8
+ # API Configuration
9
+ BASE_URL = "https://app.tracia.io"
10
+
11
+ # Timeout Configuration (in milliseconds)
12
+ DEFAULT_TIMEOUT_MS = 120_000 # 2 minutes
13
+
14
+ # Span Management
15
+ MAX_PENDING_SPANS = 1000
16
+ SPAN_RETRY_ATTEMPTS = 2
17
+ SPAN_RETRY_DELAY_MS = 500
18
+
19
+ # Span Status
20
+ SPAN_STATUS_SUCCESS = "SUCCESS"
21
+ SPAN_STATUS_ERROR = "ERROR"
22
+
23
+ # ID Prefixes
24
+ SPAN_ID_PREFIX = "sp_"
25
+ TRACE_ID_PREFIX = "tr_"
26
+
27
+ # Environment Variable Names for Provider API Keys
28
+ ENV_VAR_MAP = {
29
+ "openai": "OPENAI_API_KEY",
30
+ "anthropic": "ANTHROPIC_API_KEY",
31
+ "google": "GOOGLE_API_KEY",
32
+ }
33
+
34
+ # Evaluation Constants
35
+ class Eval:
36
+ """Evaluation value constants."""
37
+
38
+ POSITIVE = 1
39
+ NEGATIVE = 0
tracia/_errors.py ADDED
@@ -0,0 +1,87 @@
1
+ """Error handling for the Tracia SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from enum import Enum
7
+
8
+
9
+ class TraciaErrorCode(str, Enum):
10
+ """Error codes for Tracia SDK errors."""
11
+
12
+ UNAUTHORIZED = "UNAUTHORIZED"
13
+ NOT_FOUND = "NOT_FOUND"
14
+ CONFLICT = "CONFLICT"
15
+ PROVIDER_ERROR = "PROVIDER_ERROR"
16
+ MISSING_VARIABLES = "MISSING_VARIABLES"
17
+ INVALID_REQUEST = "INVALID_REQUEST"
18
+ NETWORK_ERROR = "NETWORK_ERROR"
19
+ TIMEOUT = "TIMEOUT"
20
+ ABORTED = "ABORTED"
21
+ UNKNOWN = "UNKNOWN"
22
+ MISSING_PROVIDER_SDK = "MISSING_PROVIDER_SDK"
23
+ MISSING_PROVIDER_API_KEY = "MISSING_PROVIDER_API_KEY"
24
+ UNSUPPORTED_MODEL = "UNSUPPORTED_MODEL"
25
+
26
+
27
+ class TraciaError(Exception):
28
+ """Exception raised for Tracia SDK errors."""
29
+
30
+ def __init__(
31
+ self,
32
+ code: TraciaErrorCode,
33
+ message: str,
34
+ status_code: int | None = None,
35
+ ) -> None:
36
+ self.code = code
37
+ self.message = message
38
+ self.status_code = status_code
39
+ super().__init__(message)
40
+
41
+ def __str__(self) -> str:
42
+ if self.status_code:
43
+ return f"[{self.code.value}] {self.message} (status: {self.status_code})"
44
+ return f"[{self.code.value}] {self.message}"
45
+
46
+ def __repr__(self) -> str:
47
+ return f"TraciaError(code={self.code!r}, message={self.message!r}, status_code={self.status_code!r})"
48
+
49
+
50
+ # Patterns for sanitizing sensitive data from error messages
51
+ _SENSITIVE_PATTERNS = [
52
+ re.compile(r"sk-[a-zA-Z0-9]{20,}"), # OpenAI API keys
53
+ re.compile(r"sk-ant-[a-zA-Z0-9-]{20,}"), # Anthropic API keys
54
+ re.compile(r"Bearer\s+[a-zA-Z0-9._-]+"), # Bearer tokens
55
+ re.compile(r"Basic\s+[a-zA-Z0-9+/=]+"), # Basic auth
56
+ re.compile(r"Authorization:\s*[^\s]+", re.IGNORECASE), # Auth headers
57
+ ]
58
+
59
+
60
+ def sanitize_error_message(message: str) -> str:
61
+ """Remove sensitive data from error messages.
62
+
63
+ Args:
64
+ message: The error message to sanitize.
65
+
66
+ Returns:
67
+ The sanitized error message.
68
+ """
69
+ result = message
70
+ for pattern in _SENSITIVE_PATTERNS:
71
+ result = pattern.sub("[REDACTED]", result)
72
+ return result
73
+
74
+
75
+ def map_api_error_code(code: str) -> TraciaErrorCode:
76
+ """Map an API error code string to a TraciaErrorCode.
77
+
78
+ Args:
79
+ code: The error code from the API response.
80
+
81
+ Returns:
82
+ The corresponding TraciaErrorCode.
83
+ """
84
+ try:
85
+ return TraciaErrorCode(code)
86
+ except ValueError:
87
+ return TraciaErrorCode.UNKNOWN
tracia/_http.py ADDED
@@ -0,0 +1,362 @@
1
+ """HTTP client for the Tracia API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, TypeVar
6
+
7
+ import httpx
8
+
9
+ from ._constants import BASE_URL, DEFAULT_TIMEOUT_MS, SDK_VERSION
10
+ from ._errors import TraciaError, TraciaErrorCode, map_api_error_code
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ class HttpClient:
16
+ """Synchronous HTTP client for the Tracia API."""
17
+
18
+ def __init__(
19
+ self,
20
+ api_key: str,
21
+ base_url: str = BASE_URL,
22
+ timeout_ms: int = DEFAULT_TIMEOUT_MS,
23
+ ) -> None:
24
+ """Initialize the HTTP client.
25
+
26
+ Args:
27
+ api_key: The Tracia API key.
28
+ base_url: The base URL for the API.
29
+ timeout_ms: Request timeout in milliseconds.
30
+ """
31
+ self._api_key = api_key
32
+ self._base_url = base_url.rstrip("/")
33
+ self._timeout = timeout_ms / 1000.0 # Convert to seconds
34
+
35
+ self._client = httpx.Client(
36
+ base_url=self._base_url,
37
+ headers=self._get_headers(),
38
+ timeout=httpx.Timeout(self._timeout),
39
+ )
40
+
41
+ def _get_headers(self) -> dict[str, str]:
42
+ """Get the default headers for requests."""
43
+ return {
44
+ "Content-Type": "application/json",
45
+ "Authorization": f"Bearer {self._api_key}",
46
+ "User-Agent": f"tracia-sdk-python/{SDK_VERSION}",
47
+ }
48
+
49
+ def _handle_response(self, response: httpx.Response) -> Any:
50
+ """Handle the API response and raise errors if needed."""
51
+ if response.status_code >= 400:
52
+ try:
53
+ data = response.json()
54
+ error_data = data.get("error", {})
55
+ code = map_api_error_code(error_data.get("code", "UNKNOWN"))
56
+ message = error_data.get("message", "Unknown error")
57
+ except Exception:
58
+ code = TraciaErrorCode.UNKNOWN
59
+ message = response.text or f"HTTP {response.status_code}"
60
+
61
+ raise TraciaError(
62
+ code=code,
63
+ message=message,
64
+ status_code=response.status_code,
65
+ )
66
+
67
+ if response.status_code == 204:
68
+ return None
69
+
70
+ return response.json()
71
+
72
+ def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
73
+ """Make a GET request.
74
+
75
+ Args:
76
+ path: The API path.
77
+ params: Optional query parameters.
78
+
79
+ Returns:
80
+ The JSON response.
81
+
82
+ Raises:
83
+ TraciaError: If the request fails.
84
+ """
85
+ try:
86
+ response = self._client.get(path, params=params)
87
+ return self._handle_response(response)
88
+ except httpx.TimeoutException as e:
89
+ raise TraciaError(
90
+ code=TraciaErrorCode.TIMEOUT,
91
+ message=f"Request timed out after {int(self._timeout * 1000)}ms",
92
+ ) from e
93
+ except httpx.RequestError as e:
94
+ raise TraciaError(
95
+ code=TraciaErrorCode.NETWORK_ERROR,
96
+ message=f"Network error: {e}",
97
+ ) from e
98
+
99
+ def post(self, path: str, body: Any = None) -> Any:
100
+ """Make a POST request.
101
+
102
+ Args:
103
+ path: The API path.
104
+ body: The request body.
105
+
106
+ Returns:
107
+ The JSON response.
108
+
109
+ Raises:
110
+ TraciaError: If the request fails.
111
+ """
112
+ try:
113
+ response = self._client.post(path, json=body)
114
+ return self._handle_response(response)
115
+ except httpx.TimeoutException as e:
116
+ raise TraciaError(
117
+ code=TraciaErrorCode.TIMEOUT,
118
+ message=f"Request timed out after {int(self._timeout * 1000)}ms",
119
+ ) from e
120
+ except httpx.RequestError as e:
121
+ raise TraciaError(
122
+ code=TraciaErrorCode.NETWORK_ERROR,
123
+ message=f"Network error: {e}",
124
+ ) from e
125
+
126
+ def put(self, path: str, body: Any = None) -> Any:
127
+ """Make a PUT request.
128
+
129
+ Args:
130
+ path: The API path.
131
+ body: The request body.
132
+
133
+ Returns:
134
+ The JSON response.
135
+
136
+ Raises:
137
+ TraciaError: If the request fails.
138
+ """
139
+ try:
140
+ response = self._client.put(path, json=body)
141
+ return self._handle_response(response)
142
+ except httpx.TimeoutException as e:
143
+ raise TraciaError(
144
+ code=TraciaErrorCode.TIMEOUT,
145
+ message=f"Request timed out after {int(self._timeout * 1000)}ms",
146
+ ) from e
147
+ except httpx.RequestError as e:
148
+ raise TraciaError(
149
+ code=TraciaErrorCode.NETWORK_ERROR,
150
+ message=f"Network error: {e}",
151
+ ) from e
152
+
153
+ def delete(self, path: str) -> Any:
154
+ """Make a DELETE request.
155
+
156
+ Args:
157
+ path: The API path.
158
+
159
+ Returns:
160
+ The JSON response (or None for 204).
161
+
162
+ Raises:
163
+ TraciaError: If the request fails.
164
+ """
165
+ try:
166
+ response = self._client.delete(path)
167
+ return self._handle_response(response)
168
+ except httpx.TimeoutException as e:
169
+ raise TraciaError(
170
+ code=TraciaErrorCode.TIMEOUT,
171
+ message=f"Request timed out after {int(self._timeout * 1000)}ms",
172
+ ) from e
173
+ except httpx.RequestError as e:
174
+ raise TraciaError(
175
+ code=TraciaErrorCode.NETWORK_ERROR,
176
+ message=f"Network error: {e}",
177
+ ) from e
178
+
179
+ def close(self) -> None:
180
+ """Close the HTTP client."""
181
+ self._client.close()
182
+
183
+ def __enter__(self) -> "HttpClient":
184
+ return self
185
+
186
+ def __exit__(self, *args: Any) -> None:
187
+ self.close()
188
+
189
+
190
+ class AsyncHttpClient:
191
+ """Asynchronous HTTP client for the Tracia API."""
192
+
193
+ def __init__(
194
+ self,
195
+ api_key: str,
196
+ base_url: str = BASE_URL,
197
+ timeout_ms: int = DEFAULT_TIMEOUT_MS,
198
+ ) -> None:
199
+ """Initialize the async HTTP client.
200
+
201
+ Args:
202
+ api_key: The Tracia API key.
203
+ base_url: The base URL for the API.
204
+ timeout_ms: Request timeout in milliseconds.
205
+ """
206
+ self._api_key = api_key
207
+ self._base_url = base_url.rstrip("/")
208
+ self._timeout = timeout_ms / 1000.0 # Convert to seconds
209
+
210
+ self._client = httpx.AsyncClient(
211
+ base_url=self._base_url,
212
+ headers=self._get_headers(),
213
+ timeout=httpx.Timeout(self._timeout),
214
+ )
215
+
216
+ def _get_headers(self) -> dict[str, str]:
217
+ """Get the default headers for requests."""
218
+ return {
219
+ "Content-Type": "application/json",
220
+ "Authorization": f"Bearer {self._api_key}",
221
+ "User-Agent": f"tracia-sdk-python/{SDK_VERSION}",
222
+ }
223
+
224
+ def _handle_response(self, response: httpx.Response) -> Any:
225
+ """Handle the API response and raise errors if needed."""
226
+ if response.status_code >= 400:
227
+ try:
228
+ data = response.json()
229
+ error_data = data.get("error", {})
230
+ code = map_api_error_code(error_data.get("code", "UNKNOWN"))
231
+ message = error_data.get("message", "Unknown error")
232
+ except Exception:
233
+ code = TraciaErrorCode.UNKNOWN
234
+ message = response.text or f"HTTP {response.status_code}"
235
+
236
+ raise TraciaError(
237
+ code=code,
238
+ message=message,
239
+ status_code=response.status_code,
240
+ )
241
+
242
+ if response.status_code == 204:
243
+ return None
244
+
245
+ return response.json()
246
+
247
+ async def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
248
+ """Make a GET request.
249
+
250
+ Args:
251
+ path: The API path.
252
+ params: Optional query parameters.
253
+
254
+ Returns:
255
+ The JSON response.
256
+
257
+ Raises:
258
+ TraciaError: If the request fails.
259
+ """
260
+ try:
261
+ response = await self._client.get(path, params=params)
262
+ return self._handle_response(response)
263
+ except httpx.TimeoutException as e:
264
+ raise TraciaError(
265
+ code=TraciaErrorCode.TIMEOUT,
266
+ message=f"Request timed out after {int(self._timeout * 1000)}ms",
267
+ ) from e
268
+ except httpx.RequestError as e:
269
+ raise TraciaError(
270
+ code=TraciaErrorCode.NETWORK_ERROR,
271
+ message=f"Network error: {e}",
272
+ ) from e
273
+
274
+ async def post(self, path: str, body: Any = None) -> Any:
275
+ """Make a POST request.
276
+
277
+ Args:
278
+ path: The API path.
279
+ body: The request body.
280
+
281
+ Returns:
282
+ The JSON response.
283
+
284
+ Raises:
285
+ TraciaError: If the request fails.
286
+ """
287
+ try:
288
+ response = await self._client.post(path, json=body)
289
+ return self._handle_response(response)
290
+ except httpx.TimeoutException as e:
291
+ raise TraciaError(
292
+ code=TraciaErrorCode.TIMEOUT,
293
+ message=f"Request timed out after {int(self._timeout * 1000)}ms",
294
+ ) from e
295
+ except httpx.RequestError as e:
296
+ raise TraciaError(
297
+ code=TraciaErrorCode.NETWORK_ERROR,
298
+ message=f"Network error: {e}",
299
+ ) from e
300
+
301
+ async def put(self, path: str, body: Any = None) -> Any:
302
+ """Make a PUT request.
303
+
304
+ Args:
305
+ path: The API path.
306
+ body: The request body.
307
+
308
+ Returns:
309
+ The JSON response.
310
+
311
+ Raises:
312
+ TraciaError: If the request fails.
313
+ """
314
+ try:
315
+ response = await self._client.put(path, json=body)
316
+ return self._handle_response(response)
317
+ except httpx.TimeoutException as e:
318
+ raise TraciaError(
319
+ code=TraciaErrorCode.TIMEOUT,
320
+ message=f"Request timed out after {int(self._timeout * 1000)}ms",
321
+ ) from e
322
+ except httpx.RequestError as e:
323
+ raise TraciaError(
324
+ code=TraciaErrorCode.NETWORK_ERROR,
325
+ message=f"Network error: {e}",
326
+ ) from e
327
+
328
+ async def delete(self, path: str) -> Any:
329
+ """Make a DELETE request.
330
+
331
+ Args:
332
+ path: The API path.
333
+
334
+ Returns:
335
+ The JSON response (or None for 204).
336
+
337
+ Raises:
338
+ TraciaError: If the request fails.
339
+ """
340
+ try:
341
+ response = await self._client.delete(path)
342
+ return self._handle_response(response)
343
+ except httpx.TimeoutException as e:
344
+ raise TraciaError(
345
+ code=TraciaErrorCode.TIMEOUT,
346
+ message=f"Request timed out after {int(self._timeout * 1000)}ms",
347
+ ) from e
348
+ except httpx.RequestError as e:
349
+ raise TraciaError(
350
+ code=TraciaErrorCode.NETWORK_ERROR,
351
+ message=f"Network error: {e}",
352
+ ) from e
353
+
354
+ async def aclose(self) -> None:
355
+ """Close the async HTTP client."""
356
+ await self._client.aclose()
357
+
358
+ async def __aenter__(self) -> "AsyncHttpClient":
359
+ return self
360
+
361
+ async def __aexit__(self, *args: Any) -> None:
362
+ await self.aclose()