lucidicai 2.1.2__tar.gz → 2.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {lucidicai-2.1.2 → lucidicai-2.1.3}/PKG-INFO +1 -1
  2. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/__init__.py +1 -1
  3. lucidicai-2.1.3/lucidicai/api/client.py +357 -0
  4. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/sdk/features/dataset.py +23 -237
  5. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/sdk/init.py +69 -0
  6. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/utils/images.py +3 -3
  7. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/utils/queue.py +2 -2
  8. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai.egg-info/PKG-INFO +1 -1
  9. {lucidicai-2.1.2 → lucidicai-2.1.3}/setup.py +1 -1
  10. lucidicai-2.1.2/lucidicai/api/client.py +0 -218
  11. {lucidicai-2.1.2 → lucidicai-2.1.3}/README.md +0 -0
  12. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/api/__init__.py +0 -0
  13. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/api/resources/__init__.py +0 -0
  14. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/api/resources/dataset.py +0 -0
  15. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/api/resources/event.py +0 -0
  16. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/api/resources/session.py +0 -0
  17. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/core/__init__.py +0 -0
  18. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/core/config.py +0 -0
  19. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/core/errors.py +0 -0
  20. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/core/types.py +0 -0
  21. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/sdk/__init__.py +0 -0
  22. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/sdk/context.py +0 -0
  23. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/sdk/decorators.py +0 -0
  24. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/sdk/error_boundary.py +0 -0
  25. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/sdk/event.py +0 -0
  26. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/sdk/event_builder.py +0 -0
  27. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/sdk/features/__init__.py +0 -0
  28. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/sdk/features/feature_flag.py +0 -0
  29. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/sdk/shutdown_manager.py +0 -0
  30. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/telemetry/__init__.py +0 -0
  31. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/telemetry/context_bridge.py +0 -0
  32. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/telemetry/context_capture_processor.py +0 -0
  33. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/telemetry/extract.py +0 -0
  34. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/telemetry/litellm_bridge.py +0 -0
  35. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/telemetry/lucidic_exporter.py +0 -0
  36. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/telemetry/openai_agents_instrumentor.py +0 -0
  37. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/telemetry/openai_patch.py +0 -0
  38. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/telemetry/openai_uninstrument.py +0 -0
  39. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/telemetry/telemetry_init.py +0 -0
  40. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/telemetry/utils/__init__.py +0 -0
  41. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/telemetry/utils/model_pricing.py +0 -0
  42. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/utils/__init__.py +0 -0
  43. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai/utils/logger.py +0 -0
  44. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai.egg-info/SOURCES.txt +0 -0
  45. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai.egg-info/dependency_links.txt +0 -0
  46. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai.egg-info/requires.txt +0 -0
  47. {lucidicai-2.1.2 → lucidicai-2.1.3}/lucidicai.egg-info/top_level.txt +0 -0
  48. {lucidicai-2.1.2 → lucidicai-2.1.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lucidicai
3
- Version: 2.1.2
3
+ Version: 2.1.3
4
4
  Summary: Lucidic AI Python SDK
5
5
  Author: Andy Liang
6
6
  Author-email: andy@lucidic.ai
@@ -297,7 +297,7 @@ get_error_history = error_boundary.get_error_history
297
297
  clear_error_history = error_boundary.clear_error_history
298
298
 
299
299
  # Version
300
- __version__ = "2.1.2"
300
+ __version__ = "2.1.3"
301
301
 
302
302
  # Apply error boundary wrapping to all SDK functions
303
303
  from .sdk.error_boundary import wrap_sdk_function
@@ -0,0 +1,357 @@
1
+ """Pure HTTP client for Lucidic API communication.
2
+
3
+ This module contains only the HTTP client logic using httpx,
4
+ supporting both synchronous and asynchronous operations.
5
+ """
6
+ import asyncio
7
+ from datetime import datetime, timezone
8
+ from typing import Any, Dict, Optional
9
+
10
+ import httpx
11
+
12
+ from ..core.config import SDKConfig, get_config
13
+ from ..core.errors import APIKeyVerificationError
14
+ from ..utils.logger import debug, info, warning, error, mask_sensitive, truncate_data
15
+
16
+
17
+ class HttpClient:
18
+ """HTTP client for API communication with sync and async support."""
19
+
20
+ def __init__(self, config: Optional[SDKConfig] = None):
21
+ """Initialize the HTTP client.
22
+
23
+ Args:
24
+ config: SDK configuration (uses global if not provided)
25
+ """
26
+ self.config = config or get_config()
27
+ self.base_url = self.config.network.base_url
28
+
29
+ # Build default headers
30
+ self._headers = self._build_headers()
31
+
32
+ # Transport configuration for connection pooling and retries
33
+ self._transport_kwargs = {
34
+ "retries": self.config.network.max_retries,
35
+ }
36
+
37
+ # Connection limits for pooling
38
+ self._limits = httpx.Limits(
39
+ max_connections=self.config.network.connection_pool_maxsize,
40
+ max_keepalive_connections=self.config.network.connection_pool_size,
41
+ )
42
+
43
+ # Lazy-initialized clients
44
+ self._sync_client: Optional[httpx.Client] = None
45
+ self._async_client: Optional[httpx.AsyncClient] = None
46
+
47
+ def _build_headers(self) -> Dict[str, str]:
48
+ """Build default headers for requests."""
49
+ headers = {
50
+ "User-Agent": "lucidic-sdk/2.0",
51
+ "Content-Type": "application/json"
52
+ }
53
+
54
+ if self.config.api_key:
55
+ headers["Authorization"] = f"Api-Key {self.config.api_key}"
56
+
57
+ if self.config.agent_id:
58
+ headers["x-agent-id"] = self.config.agent_id
59
+
60
+ return headers
61
+
62
+ @property
63
+ def sync_client(self) -> httpx.Client:
64
+ """Get or create the synchronous HTTP client."""
65
+ if self._sync_client is None or self._sync_client.is_closed:
66
+ transport = httpx.HTTPTransport(**self._transport_kwargs)
67
+ self._sync_client = httpx.Client(
68
+ base_url=self.base_url,
69
+ headers=self._headers,
70
+ timeout=httpx.Timeout(self.config.network.timeout),
71
+ limits=self._limits,
72
+ transport=transport,
73
+ )
74
+ return self._sync_client
75
+
76
+ @property
77
+ def async_client(self) -> httpx.AsyncClient:
78
+ """Get or create the asynchronous HTTP client."""
79
+ if self._async_client is None or self._async_client.is_closed:
80
+ transport = httpx.AsyncHTTPTransport(**self._transport_kwargs)
81
+ self._async_client = httpx.AsyncClient(
82
+ base_url=self.base_url,
83
+ headers=self._headers,
84
+ timeout=httpx.Timeout(self.config.network.timeout),
85
+ limits=self._limits,
86
+ transport=transport,
87
+ )
88
+ return self._async_client
89
+
90
+ def _add_timestamp(self, data: Optional[Dict[str, Any]]) -> Dict[str, Any]:
91
+ """Add current_time to request data."""
92
+ if data is None:
93
+ data = {}
94
+ data["current_time"] = datetime.now(timezone.utc).isoformat()
95
+ return data
96
+
97
+ def _handle_response(self, response: httpx.Response) -> Dict[str, Any]:
98
+ """Handle HTTP response and parse JSON.
99
+
100
+ Args:
101
+ response: httpx Response object
102
+
103
+ Returns:
104
+ Response data as dictionary
105
+
106
+ Raises:
107
+ APIKeyVerificationError: On 401 Unauthorized responses
108
+ httpx.HTTPStatusError: On other HTTP errors
109
+ """
110
+ # Log and raise for HTTP errors
111
+ if not response.is_success:
112
+ try:
113
+ error_data = response.json()
114
+ error_msg = error_data.get('detail', response.text)
115
+ except Exception:
116
+ error_msg = response.text
117
+
118
+ error(f"[HTTP] Error {response.status_code}: {error_msg}")
119
+
120
+ # Raise specific error for authentication/authorization failures
121
+ if response.status_code in (401, 403):
122
+ raise APIKeyVerificationError(f"Authentication failed: {error_msg}")
123
+
124
+ response.raise_for_status()
125
+
126
+ # Parse JSON response
127
+ try:
128
+ data = response.json()
129
+ except ValueError:
130
+ # For empty responses (like verifyapikey), return success
131
+ if response.status_code == 200 and not response.text:
132
+ data = {"success": True}
133
+ else:
134
+ # Return text if not JSON
135
+ data = {"response": response.text}
136
+
137
+ debug(f"[HTTP] Response ({response.status_code}): {truncate_data(data)}")
138
+
139
+ return data
140
+
141
+ # ==================== Synchronous Methods ====================
142
+
143
+ def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
144
+ """Make a synchronous GET request.
145
+
146
+ Args:
147
+ endpoint: API endpoint (without base URL)
148
+ params: Query parameters
149
+
150
+ Returns:
151
+ Response data as dictionary
152
+ """
153
+ return self.request("GET", endpoint, params=params)
154
+
155
+ def post(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
156
+ """Make a synchronous POST request.
157
+
158
+ Args:
159
+ endpoint: API endpoint (without base URL)
160
+ data: Request body data
161
+
162
+ Returns:
163
+ Response data as dictionary
164
+ """
165
+ data = self._add_timestamp(data)
166
+ return self.request("POST", endpoint, json=data)
167
+
168
+ def put(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
169
+ """Make a synchronous PUT request.
170
+
171
+ Args:
172
+ endpoint: API endpoint (without base URL)
173
+ data: Request body data
174
+
175
+ Returns:
176
+ Response data as dictionary
177
+ """
178
+ data = self._add_timestamp(data)
179
+ return self.request("PUT", endpoint, json=data)
180
+
181
+ def delete(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
182
+ """Make a synchronous DELETE request.
183
+
184
+ Args:
185
+ endpoint: API endpoint (without base URL)
186
+ params: Query parameters
187
+
188
+ Returns:
189
+ Response data as dictionary
190
+ """
191
+ return self.request("DELETE", endpoint, params=params)
192
+
193
+ def request(
194
+ self,
195
+ method: str,
196
+ endpoint: str,
197
+ params: Optional[Dict[str, Any]] = None,
198
+ json: Optional[Dict[str, Any]] = None,
199
+ **kwargs
200
+ ) -> Dict[str, Any]:
201
+ """Make a synchronous HTTP request.
202
+
203
+ Args:
204
+ method: HTTP method
205
+ endpoint: API endpoint (without base URL)
206
+ params: Query parameters
207
+ json: Request body (for POST/PUT)
208
+ **kwargs: Additional arguments for httpx
209
+
210
+ Returns:
211
+ Response data as dictionary
212
+
213
+ Raises:
214
+ httpx.HTTPError: On HTTP errors
215
+ """
216
+ url = f"/{endpoint}"
217
+
218
+ # Log request details
219
+ debug(f"[HTTP] {method} {self.base_url}{url}")
220
+ if params:
221
+ debug(f"[HTTP] Query params: {mask_sensitive(params)}")
222
+ if json:
223
+ debug(f"[HTTP] Request body: {truncate_data(mask_sensitive(json))}")
224
+
225
+ response = self.sync_client.request(
226
+ method=method,
227
+ url=url,
228
+ params=params,
229
+ json=json,
230
+ **kwargs
231
+ )
232
+
233
+ return self._handle_response(response)
234
+
235
+ # ==================== Asynchronous Methods ====================
236
+
237
+ async def aget(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
238
+ """Make an asynchronous GET request.
239
+
240
+ Args:
241
+ endpoint: API endpoint (without base URL)
242
+ params: Query parameters
243
+
244
+ Returns:
245
+ Response data as dictionary
246
+ """
247
+ return await self.arequest("GET", endpoint, params=params)
248
+
249
+ async def apost(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
250
+ """Make an asynchronous POST request.
251
+
252
+ Args:
253
+ endpoint: API endpoint (without base URL)
254
+ data: Request body data
255
+
256
+ Returns:
257
+ Response data as dictionary
258
+ """
259
+ data = self._add_timestamp(data)
260
+ return await self.arequest("POST", endpoint, json=data)
261
+
262
+ async def aput(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
263
+ """Make an asynchronous PUT request.
264
+
265
+ Args:
266
+ endpoint: API endpoint (without base URL)
267
+ data: Request body data
268
+
269
+ Returns:
270
+ Response data as dictionary
271
+ """
272
+ data = self._add_timestamp(data)
273
+ return await self.arequest("PUT", endpoint, json=data)
274
+
275
+ async def adelete(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
276
+ """Make an asynchronous DELETE request.
277
+
278
+ Args:
279
+ endpoint: API endpoint (without base URL)
280
+ params: Query parameters
281
+
282
+ Returns:
283
+ Response data as dictionary
284
+ """
285
+ return await self.arequest("DELETE", endpoint, params=params)
286
+
287
+ async def arequest(
288
+ self,
289
+ method: str,
290
+ endpoint: str,
291
+ params: Optional[Dict[str, Any]] = None,
292
+ json: Optional[Dict[str, Any]] = None,
293
+ **kwargs
294
+ ) -> Dict[str, Any]:
295
+ """Make an asynchronous HTTP request.
296
+
297
+ Args:
298
+ method: HTTP method
299
+ endpoint: API endpoint (without base URL)
300
+ params: Query parameters
301
+ json: Request body (for POST/PUT)
302
+ **kwargs: Additional arguments for httpx
303
+
304
+ Returns:
305
+ Response data as dictionary
306
+
307
+ Raises:
308
+ httpx.HTTPError: On HTTP errors
309
+ """
310
+ url = f"/{endpoint}"
311
+
312
+ # Log request details
313
+ debug(f"[HTTP] {method} {self.base_url}{url}")
314
+ if params:
315
+ debug(f"[HTTP] Query params: {mask_sensitive(params)}")
316
+ if json:
317
+ debug(f"[HTTP] Request body: {truncate_data(mask_sensitive(json))}")
318
+
319
+ response = await self.async_client.request(
320
+ method=method,
321
+ url=url,
322
+ params=params,
323
+ json=json,
324
+ **kwargs
325
+ )
326
+
327
+ return self._handle_response(response)
328
+
329
+ # ==================== Lifecycle Methods ====================
330
+
331
+ def close(self) -> None:
332
+ """Close the synchronous HTTP client."""
333
+ if self._sync_client is not None and not self._sync_client.is_closed:
334
+ self._sync_client.close()
335
+ self._sync_client = None
336
+
337
+ async def aclose(self) -> None:
338
+ """Close the asynchronous HTTP client."""
339
+ if self._async_client is not None and not self._async_client.is_closed:
340
+ await self._async_client.aclose()
341
+ self._async_client = None
342
+
343
+ def __enter__(self) -> "HttpClient":
344
+ """Context manager entry for sync client."""
345
+ return self
346
+
347
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
348
+ """Context manager exit for sync client."""
349
+ self.close()
350
+
351
+ async def __aenter__(self) -> "HttpClient":
352
+ """Async context manager entry."""
353
+ return self
354
+
355
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
356
+ """Async context manager exit."""
357
+ await self.aclose()
@@ -1,9 +1,5 @@
1
- import os
2
1
  import logging
3
2
  from typing import Optional, Dict, List, Any
4
- from dotenv import load_dotenv
5
-
6
- from ...core.errors import APIKeyVerificationError
7
3
 
8
4
  logger = logging.getLogger("Lucidic")
9
5
 
@@ -36,37 +32,15 @@ def get_dataset(
36
32
  APIKeyVerificationError: If API key or agent ID is missing or invalid.
37
33
  ValueError: If dataset_id is not provided.
38
34
  """
39
- load_dotenv()
40
-
41
35
  # Validation
42
36
  if not dataset_id:
43
37
  raise ValueError("Dataset ID is required")
44
38
 
45
- # Get credentials
46
- if api_key is None:
47
- api_key = os.getenv("LUCIDIC_API_KEY", None)
48
- if api_key is None:
49
- raise APIKeyVerificationError(
50
- "Make sure to either pass your API key into get_dataset() or set the LUCIDIC_API_KEY environment variable."
51
- )
52
-
53
- if agent_id is None:
54
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
55
- if agent_id is None:
56
- raise APIKeyVerificationError(
57
- "Lucidic agent ID not specified. Make sure to either pass your agent ID into get_dataset() or set the LUCIDIC_AGENT_ID environment variable."
58
- )
59
-
60
- # Get HTTP client
61
- from ..init import get_http
62
- from ...core.config import SDKConfig
63
- from ...api.client import HttpClient
39
+ from ..init import ensure_http_and_resources, get_http
64
40
 
41
+ # Ensure HTTP client is initialized and stored in SDK state
42
+ ensure_http_and_resources(api_key=api_key, agent_id=agent_id)
65
43
  http = get_http()
66
- if not http:
67
- # Create a new HTTP client if needed
68
- config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
69
- http = HttpClient(config)
70
44
 
71
45
  # Make request to get dataset
72
46
  response = http.get('getdataset', {'dataset_id': dataset_id})
@@ -126,31 +100,9 @@ def list_datasets(
126
100
  Raises:
127
101
  APIKeyVerificationError: If API key or agent ID is missing or invalid.
128
102
  """
129
- from ..init import get_resources, get_http
130
- from ...core.config import SDKConfig
131
- from ...api.client import HttpClient
132
-
133
- # Get or create resources
134
- resources = get_resources()
135
- if not resources or 'datasets' not in resources:
136
- load_dotenv()
137
-
138
- # Get credentials
139
- if api_key is None:
140
- api_key = os.getenv("LUCIDIC_API_KEY", None)
141
- if api_key is None:
142
- raise APIKeyVerificationError(
143
- "Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
144
- )
145
-
146
- if agent_id is None:
147
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
148
-
149
- config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
150
- http = HttpClient(config)
151
- from ...api.resources.dataset import DatasetResource
152
- resources = {'datasets': DatasetResource(http)}
103
+ from ..init import ensure_http_and_resources
153
104
 
105
+ resources = ensure_http_and_resources(api_key=api_key, agent_id=agent_id)
154
106
  return resources['datasets'].list_datasets(agent_id)
155
107
 
156
108
 
@@ -179,30 +131,9 @@ def create_dataset(
179
131
  Raises:
180
132
  APIKeyVerificationError: If API key or agent ID is missing or invalid.
181
133
  """
182
- from ..init import get_resources, get_http
183
- from ...core.config import SDKConfig
184
- from ...api.client import HttpClient
185
-
186
- # Get or create resources
187
- resources = get_resources()
188
- if not resources or 'datasets' not in resources:
189
- load_dotenv()
190
-
191
- if api_key is None:
192
- api_key = os.getenv("LUCIDIC_API_KEY", None)
193
- if api_key is None:
194
- raise APIKeyVerificationError(
195
- "Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
196
- )
197
-
198
- if agent_id is None:
199
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
200
-
201
- config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
202
- http = HttpClient(config)
203
- from ...api.resources.dataset import DatasetResource
204
- resources = {'datasets': DatasetResource(http)}
134
+ from ..init import ensure_http_and_resources
205
135
 
136
+ resources = ensure_http_and_resources(api_key=api_key, agent_id=agent_id)
206
137
  return resources['datasets'].create_dataset(name, description, tags, suggested_flag_config, agent_id)
207
138
 
208
139
 
@@ -233,29 +164,9 @@ def update_dataset(
233
164
  Raises:
234
165
  APIKeyVerificationError: If API key or agent ID is missing or invalid.
235
166
  """
236
- from ..init import get_resources, get_http
237
- from ...core.config import SDKConfig
238
- from ...api.client import HttpClient
239
-
240
- # Get or create resources
241
- resources = get_resources()
242
- if not resources or 'datasets' not in resources:
243
- load_dotenv()
244
-
245
- if api_key is None:
246
- api_key = os.getenv("LUCIDIC_API_KEY", None)
247
- if api_key is None:
248
- raise APIKeyVerificationError(
249
- "Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
250
- )
251
-
252
- if agent_id is None:
253
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
254
-
255
- config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
256
- http = HttpClient(config)
257
- from ...api.resources.dataset import DatasetResource
258
- resources = {'datasets': DatasetResource(http)}
167
+ from ..init import ensure_http_and_resources
168
+
169
+ resources = ensure_http_and_resources(api_key=api_key, agent_id=agent_id)
259
170
 
260
171
  kwargs = {}
261
172
  if name is not None:
@@ -289,30 +200,9 @@ def delete_dataset(
289
200
  Raises:
290
201
  APIKeyVerificationError: If API key or agent ID is missing or invalid.
291
202
  """
292
- from ..init import get_resources, get_http
293
- from ...core.config import SDKConfig
294
- from ...api.client import HttpClient
295
-
296
- # Get or create resources
297
- resources = get_resources()
298
- if not resources or 'datasets' not in resources:
299
- load_dotenv()
300
-
301
- if api_key is None:
302
- api_key = os.getenv("LUCIDIC_API_KEY", None)
303
- if api_key is None:
304
- raise APIKeyVerificationError(
305
- "Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
306
- )
307
-
308
- if agent_id is None:
309
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
310
-
311
- config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
312
- http = HttpClient(config)
313
- from ...api.resources.dataset import DatasetResource
314
- resources = {'datasets': DatasetResource(http)}
203
+ from ..init import ensure_http_and_resources
315
204
 
205
+ resources = ensure_http_and_resources(api_key=api_key, agent_id=agent_id)
316
206
  return resources['datasets'].delete_dataset(dataset_id)
317
207
 
318
208
 
@@ -349,30 +239,9 @@ def create_dataset_item(
349
239
  Raises:
350
240
  APIKeyVerificationError: If API key or agent ID is missing or invalid.
351
241
  """
352
- from ..init import get_resources, get_http
353
- from ...core.config import SDKConfig
354
- from ...api.client import HttpClient
355
-
356
- # Get or create resources
357
- resources = get_resources()
358
- if not resources or 'datasets' not in resources:
359
- load_dotenv()
360
-
361
- if api_key is None:
362
- api_key = os.getenv("LUCIDIC_API_KEY", None)
363
- if api_key is None:
364
- raise APIKeyVerificationError(
365
- "Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
366
- )
367
-
368
- if agent_id is None:
369
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
370
-
371
- config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
372
- http = HttpClient(config)
373
- from ...api.resources.dataset import DatasetResource
374
- resources = {'datasets': DatasetResource(http)}
242
+ from ..init import ensure_http_and_resources
375
243
 
244
+ resources = ensure_http_and_resources(api_key=api_key, agent_id=agent_id)
376
245
  return resources['datasets'].create_item(
377
246
  dataset_id, name, input_data,
378
247
  expected_output=expected_output,
@@ -404,30 +273,9 @@ def get_dataset_item(
404
273
  Raises:
405
274
  APIKeyVerificationError: If API key or agent ID is missing or invalid.
406
275
  """
407
- from ..init import get_resources, get_http
408
- from ...core.config import SDKConfig
409
- from ...api.client import HttpClient
410
-
411
- # Get or create resources
412
- resources = get_resources()
413
- if not resources or 'datasets' not in resources:
414
- load_dotenv()
415
-
416
- if api_key is None:
417
- api_key = os.getenv("LUCIDIC_API_KEY", None)
418
- if api_key is None:
419
- raise APIKeyVerificationError(
420
- "Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
421
- )
422
-
423
- if agent_id is None:
424
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
425
-
426
- config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
427
- http = HttpClient(config)
428
- from ...api.resources.dataset import DatasetResource
429
- resources = {'datasets': DatasetResource(http)}
276
+ from ..init import ensure_http_and_resources
430
277
 
278
+ resources = ensure_http_and_resources(api_key=api_key, agent_id=agent_id)
431
279
  return resources['datasets'].get_item(dataset_id, item_id)
432
280
 
433
281
 
@@ -466,29 +314,9 @@ def update_dataset_item(
466
314
  Raises:
467
315
  APIKeyVerificationError: If API key or agent ID is missing or invalid.
468
316
  """
469
- from ..init import get_resources, get_http
470
- from ...core.config import SDKConfig
471
- from ...api.client import HttpClient
472
-
473
- # Get or create resources
474
- resources = get_resources()
475
- if not resources or 'datasets' not in resources:
476
- load_dotenv()
477
-
478
- if api_key is None:
479
- api_key = os.getenv("LUCIDIC_API_KEY", None)
480
- if api_key is None:
481
- raise APIKeyVerificationError(
482
- "Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
483
- )
484
-
485
- if agent_id is None:
486
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
487
-
488
- config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
489
- http = HttpClient(config)
490
- from ...api.resources.dataset import DatasetResource
491
- resources = {'datasets': DatasetResource(http)}
317
+ from ..init import ensure_http_and_resources
318
+
319
+ resources = ensure_http_and_resources(api_key=api_key, agent_id=agent_id)
492
320
 
493
321
  kwargs = {}
494
322
  if name is not None:
@@ -530,30 +358,9 @@ def delete_dataset_item(
530
358
  Raises:
531
359
  APIKeyVerificationError: If API key or agent ID is missing or invalid.
532
360
  """
533
- from ..init import get_resources, get_http
534
- from ...core.config import SDKConfig
535
- from ...api.client import HttpClient
536
-
537
- # Get or create resources
538
- resources = get_resources()
539
- if not resources or 'datasets' not in resources:
540
- load_dotenv()
541
-
542
- if api_key is None:
543
- api_key = os.getenv("LUCIDIC_API_KEY", None)
544
- if api_key is None:
545
- raise APIKeyVerificationError(
546
- "Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
547
- )
548
-
549
- if agent_id is None:
550
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
551
-
552
- config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
553
- http = HttpClient(config)
554
- from ...api.resources.dataset import DatasetResource
555
- resources = {'datasets': DatasetResource(http)}
361
+ from ..init import ensure_http_and_resources
556
362
 
363
+ resources = ensure_http_and_resources(api_key=api_key, agent_id=agent_id)
557
364
  return resources['datasets'].delete_item(dataset_id, item_id)
558
365
 
559
366
 
@@ -578,28 +385,7 @@ def list_dataset_item_sessions(
578
385
  Raises:
579
386
  APIKeyVerificationError: If API key or agent ID is missing or invalid.
580
387
  """
581
- from ..init import get_resources, get_http
582
- from ...core.config import SDKConfig
583
- from ...api.client import HttpClient
584
-
585
- # Get or create resources
586
- resources = get_resources()
587
- if not resources or 'datasets' not in resources:
588
- load_dotenv()
589
-
590
- if api_key is None:
591
- api_key = os.getenv("LUCIDIC_API_KEY", None)
592
- if api_key is None:
593
- raise APIKeyVerificationError(
594
- "Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
595
- )
596
-
597
- if agent_id is None:
598
- agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
599
-
600
- config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
601
- http = HttpClient(config)
602
- from ...api.resources.dataset import DatasetResource
603
- resources = {'datasets': DatasetResource(http)}
388
+ from ..init import ensure_http_and_resources
604
389
 
390
+ resources = ensure_http_and_resources(api_key=api_key, agent_id=agent_id)
605
391
  return resources['datasets'].list_item_sessions(dataset_id, item_id)
@@ -353,6 +353,75 @@ def get_resources() -> dict:
353
353
  return _sdk_state.resources
354
354
 
355
355
 
356
+ def set_http(http: HttpClient) -> None:
357
+ """Set the HTTP client instance in SDK state."""
358
+ global _sdk_state
359
+ _sdk_state.http = http
360
+
361
+
362
+ def set_resources(resources: dict) -> None:
363
+ """Set API resource instances in SDK state."""
364
+ global _sdk_state
365
+ _sdk_state.resources = resources
366
+
367
+
368
+ def ensure_http_and_resources(api_key: Optional[str] = None, agent_id: Optional[str] = None) -> dict:
369
+ """Ensure HTTP client and resources are initialized, creating them if needed.
370
+
371
+ This function checks if the HTTP client and resources already exist in SDK state.
372
+ If not, it creates them and stores them in SDK state for reuse.
373
+
374
+ Args:
375
+ api_key: API key (uses env if not provided)
376
+ agent_id: Agent ID (uses env if not provided)
377
+
378
+ Returns:
379
+ Dictionary of API resources with 'datasets' key
380
+
381
+ Raises:
382
+ APIKeyVerificationError: If API key is not available
383
+ """
384
+ global _sdk_state
385
+
386
+ # If we already have resources with datasets, return them
387
+ if _sdk_state.resources and 'datasets' in _sdk_state.resources:
388
+ return _sdk_state.resources
389
+
390
+ # Need to create HTTP client and resources
391
+ from dotenv import load_dotenv
392
+ import os
393
+ from ..core.errors import APIKeyVerificationError
394
+
395
+ load_dotenv()
396
+
397
+ # Get credentials
398
+ if api_key is None:
399
+ api_key = os.getenv("LUCIDIC_API_KEY", None)
400
+ if api_key is None:
401
+ raise APIKeyVerificationError(
402
+ "Make sure to either pass your API key or set the LUCIDIC_API_KEY environment variable."
403
+ )
404
+
405
+ if agent_id is None:
406
+ agent_id = os.getenv("LUCIDIC_AGENT_ID", None)
407
+
408
+ # Create or reuse HTTP client
409
+ if not _sdk_state.http:
410
+ debug("[SDK] Creating HTTP client for standalone use")
411
+ config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
412
+ _sdk_state.http = HttpClient(config)
413
+
414
+ # Create resources if not already present
415
+ if not _sdk_state.resources:
416
+ _sdk_state.resources = {}
417
+
418
+ if 'datasets' not in _sdk_state.resources:
419
+ debug("[SDK] Creating DatasetResource for standalone use")
420
+ _sdk_state.resources['datasets'] = DatasetResource(_sdk_state.http)
421
+
422
+ return _sdk_state.resources
423
+
424
+
356
425
  def get_tracer_provider() -> Optional[TracerProvider]:
357
426
  """Get the tracer provider instance."""
358
427
  return _sdk_state.tracer_provider
@@ -11,7 +11,7 @@ import logging
11
11
  import threading
12
12
  from typing import List, Dict, Any, Optional, Tuple, Union
13
13
  from PIL import Image
14
- import requests
14
+ import httpx
15
15
 
16
16
  logger = logging.getLogger("Lucidic")
17
17
 
@@ -299,9 +299,9 @@ class ImageUploader:
299
299
  image_obj, content_type = ImageHandler.prepare_for_upload(image_data, format)
300
300
 
301
301
  # Upload to S3
302
- upload_response = requests.put(
302
+ upload_response = httpx.put(
303
303
  url,
304
- data=image_obj.getvalue() if hasattr(image_obj, 'getvalue') else image_obj,
304
+ content=image_obj.getvalue() if hasattr(image_obj, 'getvalue') else image_obj,
305
305
  headers={"Content-Type": content_type}
306
306
  )
307
307
  upload_response.raise_for_status()
@@ -9,7 +9,7 @@ import json
9
9
  import queue
10
10
  import threading
11
11
  import time
12
- import requests
12
+ import httpx
13
13
  from concurrent.futures import ThreadPoolExecutor, as_completed
14
14
  from datetime import datetime, timezone
15
15
  from typing import Any, Dict, List, Optional, Set, Tuple
@@ -362,7 +362,7 @@ class EventQueue:
362
362
  def _upload_blob(self, blob_url: str, data: bytes) -> None:
363
363
  """Upload compressed blob to presigned URL."""
364
364
  headers = {"Content-Type": "application/json", "Content-Encoding": "gzip"}
365
- resp = requests.put(blob_url, data=data, headers=headers)
365
+ resp = httpx.put(blob_url, content=data, headers=headers)
366
366
  resp.raise_for_status()
367
367
 
368
368
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lucidicai
3
- Version: 2.1.2
3
+ Version: 2.1.3
4
4
  Summary: Lucidic AI Python SDK
5
5
  Author: Andy Liang
6
6
  Author-email: andy@lucidic.ai
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="lucidicai",
5
- version="2.1.2",
5
+ version="2.1.3",
6
6
  packages=find_packages(),
7
7
  install_requires=[
8
8
  "requests>=2.25.1",
@@ -1,218 +0,0 @@
1
- """Pure HTTP client for Lucidic API communication.
2
-
3
- This module contains only the HTTP client logic, separated from
4
- session management and other concerns.
5
- """
6
- import json
7
- from typing import Any, Dict, Optional
8
- from urllib.parse import urlencode
9
-
10
- import requests
11
- from requests.adapters import HTTPAdapter
12
- from urllib3.util import Retry
13
-
14
- from ..core.config import SDKConfig, get_config
15
- from ..core.errors import APIKeyVerificationError
16
- from ..utils.logger import debug, info, warning, error, mask_sensitive, truncate_data
17
-
18
-
19
- class HttpClient:
20
- """HTTP client for API communication."""
21
-
22
- def __init__(self, config: Optional[SDKConfig] = None):
23
- """Initialize the HTTP client.
24
-
25
- Args:
26
- config: SDK configuration (uses global if not provided)
27
- """
28
- self.config = config or get_config()
29
- self.base_url = self.config.network.base_url
30
-
31
- # Create session with connection pooling
32
- self.session = requests.Session()
33
-
34
- # Configure retries
35
- retry_cfg = Retry(
36
- total=self.config.network.max_retries,
37
- backoff_factor=self.config.network.backoff_factor,
38
- status_forcelist=[502, 503, 504],
39
- allowed_methods=["GET", "POST", "PUT", "DELETE"],
40
- )
41
-
42
- # Configure adapter with connection pooling
43
- adapter = HTTPAdapter(
44
- max_retries=retry_cfg,
45
- pool_connections=self.config.network.connection_pool_size,
46
- pool_maxsize=self.config.network.connection_pool_maxsize
47
- )
48
-
49
- self.session.mount("https://", adapter)
50
- self.session.mount("http://", adapter)
51
-
52
- # Set headers
53
- self._update_headers()
54
-
55
- # Verify API key if configured
56
- if self.config.api_key:
57
- self._verify_api_key()
58
-
59
- def _update_headers(self) -> None:
60
- """Update session headers with authentication."""
61
- headers = {
62
- "User-Agent": "lucidic-sdk/2.0",
63
- "Content-Type": "application/json"
64
- }
65
-
66
- if self.config.api_key:
67
- headers["Authorization"] = f"Api-Key {self.config.api_key}"
68
-
69
- if self.config.agent_id:
70
- headers["x-agent-id"] = self.config.agent_id
71
-
72
- self.session.headers.update(headers)
73
-
74
- def _verify_api_key(self) -> None:
75
- """Verify the API key with the backend."""
76
- debug("[HTTP] Verifying API key")
77
- try:
78
- response = self.get("verifyapikey")
79
- # Backend returns 200 OK for valid key, check if we got a response
80
- if response is None:
81
- raise APIKeyVerificationError("No response from API")
82
- info("[HTTP] API key verified successfully")
83
- except APIKeyVerificationError:
84
- raise
85
- except requests.RequestException as e:
86
- raise APIKeyVerificationError(f"Could not verify API key: {e}")
87
-
88
- def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
89
- """Make a GET request.
90
-
91
- Args:
92
- endpoint: API endpoint (without base URL)
93
- params: Query parameters
94
-
95
- Returns:
96
- Response data as dictionary
97
- """
98
- return self.request("GET", endpoint, params=params)
99
-
100
- def post(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
101
- """Make a POST request.
102
-
103
- Args:
104
- endpoint: API endpoint (without base URL)
105
- data: Request body data
106
-
107
- Returns:
108
- Response data as dictionary
109
- """
110
- # Add current_time to all POST requests like TypeScript SDK does
111
- from datetime import datetime, timezone
112
- if data is None:
113
- data = {}
114
- data["current_time"] = datetime.now(timezone.utc).isoformat()
115
- return self.request("POST", endpoint, json=data)
116
-
117
- def put(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
118
- """Make a PUT request.
119
-
120
- Args:
121
- endpoint: API endpoint (without base URL)
122
- data: Request body data
123
-
124
- Returns:
125
- Response data as dictionary
126
- """
127
- # Add current_time to all PUT requests like TypeScript SDK does
128
- from datetime import datetime, timezone
129
- if data is None:
130
- data = {}
131
- data["current_time"] = datetime.now(timezone.utc).isoformat()
132
- return self.request("PUT", endpoint, json=data)
133
-
134
- def delete(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
135
- """Make a DELETE request.
136
-
137
- Args:
138
- endpoint: API endpoint (without base URL)
139
- params: Query parameters
140
-
141
- Returns:
142
- Response data as dictionary
143
- """
144
- return self.request("DELETE", endpoint, params=params)
145
-
146
- def request(
147
- self,
148
- method: str,
149
- endpoint: str,
150
- params: Optional[Dict[str, Any]] = None,
151
- json: Optional[Dict[str, Any]] = None,
152
- **kwargs
153
- ) -> Dict[str, Any]:
154
- """Make an HTTP request.
155
-
156
- Args:
157
- method: HTTP method
158
- endpoint: API endpoint (without base URL)
159
- params: Query parameters
160
- json: Request body (for POST/PUT)
161
- **kwargs: Additional arguments for requests
162
-
163
- Returns:
164
- Response data as dictionary
165
-
166
- Raises:
167
- requests.RequestException: On HTTP errors
168
- """
169
- url = f"{self.base_url}/{endpoint}"
170
-
171
- # Log request details
172
- debug(f"[HTTP] {method} {url}")
173
- if params:
174
- debug(f"[HTTP] Query params: {mask_sensitive(params)}")
175
- if json:
176
- debug(f"[HTTP] Request body: {truncate_data(mask_sensitive(json))}")
177
-
178
- response = self.session.request(
179
- method=method,
180
- url=url,
181
- params=params,
182
- json=json,
183
- timeout=self.config.network.timeout,
184
- **kwargs
185
- )
186
-
187
- # Raise for HTTP errors with more detail
188
- if not response.ok:
189
- # Try to get error details from response
190
- try:
191
- error_data = response.json()
192
- error_msg = error_data.get('detail', response.text)
193
- except:
194
- error_msg = response.text
195
-
196
- error(f"[HTTP] Error {response.status_code}: {error_msg}")
197
-
198
- response.raise_for_status()
199
-
200
- # Parse JSON response
201
- try:
202
- data = response.json()
203
- except ValueError:
204
- # For empty responses (like verifyapikey), return success
205
- if response.status_code == 200 and not response.text:
206
- data = {"success": True}
207
- else:
208
- # Return text if not JSON
209
- data = {"response": response.text}
210
-
211
- debug(f"[HTTP] Response ({response.status_code}): {truncate_data(data)}")
212
-
213
- return data
214
-
215
- def close(self) -> None:
216
- """Close the HTTP session."""
217
- if self.session:
218
- self.session.close()
File without changes
File without changes