lucidicai 2.1.2__py3-none-any.whl → 2.1.3__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.
lucidicai/__init__.py CHANGED
@@ -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
lucidicai/api/client.py CHANGED
@@ -1,15 +1,13 @@
1
1
  """Pure HTTP client for Lucidic API communication.
2
2
 
3
- This module contains only the HTTP client logic, separated from
4
- session management and other concerns.
3
+ This module contains only the HTTP client logic using httpx,
4
+ supporting both synchronous and asynchronous operations.
5
5
  """
6
- import json
6
+ import asyncio
7
+ from datetime import datetime, timezone
7
8
  from typing import Any, Dict, Optional
8
- from urllib.parse import urlencode
9
9
 
10
- import requests
11
- from requests.adapters import HTTPAdapter
12
- from urllib3.util import Retry
10
+ import httpx
13
11
 
14
12
  from ..core.config import SDKConfig, get_config
15
13
  from ..core.errors import APIKeyVerificationError
@@ -17,7 +15,7 @@ from ..utils.logger import debug, info, warning, error, mask_sensitive, truncate
17
15
 
18
16
 
19
17
  class HttpClient:
20
- """HTTP client for API communication."""
18
+ """HTTP client for API communication with sync and async support."""
21
19
 
22
20
  def __init__(self, config: Optional[SDKConfig] = None):
23
21
  """Initialize the HTTP client.
@@ -28,36 +26,26 @@ class HttpClient:
28
26
  self.config = config or get_config()
29
27
  self.base_url = self.config.network.base_url
30
28
 
31
- # Create session with connection pooling
32
- self.session = requests.Session()
29
+ # Build default headers
30
+ self._headers = self._build_headers()
33
31
 
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
- )
32
+ # Transport configuration for connection pooling and retries
33
+ self._transport_kwargs = {
34
+ "retries": self.config.network.max_retries,
35
+ }
41
36
 
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
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,
47
41
  )
48
42
 
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()
43
+ # Lazy-initialized clients
44
+ self._sync_client: Optional[httpx.Client] = None
45
+ self._async_client: Optional[httpx.AsyncClient] = None
58
46
 
59
- def _update_headers(self) -> None:
60
- """Update session headers with authentication."""
47
+ def _build_headers(self) -> Dict[str, str]:
48
+ """Build default headers for requests."""
61
49
  headers = {
62
50
  "User-Agent": "lucidic-sdk/2.0",
63
51
  "Content-Type": "application/json"
@@ -69,24 +57,91 @@ class HttpClient:
69
57
  if self.config.agent_id:
70
58
  headers["x-agent-id"] = self.config.agent_id
71
59
 
72
- self.session.headers.update(headers)
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
73
96
 
74
- def _verify_api_key(self) -> None:
75
- """Verify the API key with the backend."""
76
- debug("[HTTP] Verifying API key")
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
77
127
  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}")
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 ====================
87
142
 
88
143
  def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
89
- """Make a GET request.
144
+ """Make a synchronous GET request.
90
145
 
91
146
  Args:
92
147
  endpoint: API endpoint (without base URL)
@@ -98,7 +153,7 @@ class HttpClient:
98
153
  return self.request("GET", endpoint, params=params)
99
154
 
100
155
  def post(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
101
- """Make a POST request.
156
+ """Make a synchronous POST request.
102
157
 
103
158
  Args:
104
159
  endpoint: API endpoint (without base URL)
@@ -107,15 +162,11 @@ class HttpClient:
107
162
  Returns:
108
163
  Response data as dictionary
109
164
  """
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()
165
+ data = self._add_timestamp(data)
115
166
  return self.request("POST", endpoint, json=data)
116
167
 
117
168
  def put(self, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
118
- """Make a PUT request.
169
+ """Make a synchronous PUT request.
119
170
 
120
171
  Args:
121
172
  endpoint: API endpoint (without base URL)
@@ -124,15 +175,11 @@ class HttpClient:
124
175
  Returns:
125
176
  Response data as dictionary
126
177
  """
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()
178
+ data = self._add_timestamp(data)
132
179
  return self.request("PUT", endpoint, json=data)
133
180
 
134
181
  def delete(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
135
- """Make a DELETE request.
182
+ """Make a synchronous DELETE request.
136
183
 
137
184
  Args:
138
185
  endpoint: API endpoint (without base URL)
@@ -151,68 +198,160 @@ class HttpClient:
151
198
  json: Optional[Dict[str, Any]] = None,
152
199
  **kwargs
153
200
  ) -> Dict[str, Any]:
154
- """Make an HTTP request.
201
+ """Make a synchronous HTTP request.
155
202
 
156
203
  Args:
157
204
  method: HTTP method
158
205
  endpoint: API endpoint (without base URL)
159
206
  params: Query parameters
160
207
  json: Request body (for POST/PUT)
161
- **kwargs: Additional arguments for requests
208
+ **kwargs: Additional arguments for httpx
162
209
 
163
210
  Returns:
164
211
  Response data as dictionary
165
212
 
166
213
  Raises:
167
- requests.RequestException: On HTTP errors
214
+ httpx.HTTPError: On HTTP errors
168
215
  """
169
- url = f"{self.base_url}/{endpoint}"
216
+ url = f"/{endpoint}"
170
217
 
171
218
  # Log request details
172
- debug(f"[HTTP] {method} {url}")
219
+ debug(f"[HTTP] {method} {self.base_url}{url}")
173
220
  if params:
174
221
  debug(f"[HTTP] Query params: {mask_sensitive(params)}")
175
222
  if json:
176
223
  debug(f"[HTTP] Request body: {truncate_data(mask_sensitive(json))}")
177
224
 
178
- response = self.session.request(
225
+ response = self.sync_client.request(
179
226
  method=method,
180
227
  url=url,
181
228
  params=params,
182
229
  json=json,
183
- timeout=self.config.network.timeout,
184
230
  **kwargs
185
231
  )
186
232
 
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
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
195
243
 
196
- error(f"[HTTP] Error {response.status_code}: {error_msg}")
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.
197
251
 
198
- response.raise_for_status()
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.
199
264
 
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}
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.
210
277
 
211
- debug(f"[HTTP] Response ({response.status_code}): {truncate_data(data)}")
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.
212
296
 
213
- return data
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 ====================
214
330
 
215
331
  def close(self) -> None:
216
- """Close the HTTP session."""
217
- if self.session:
218
- self.session.close()
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)
lucidicai/sdk/init.py CHANGED
@@ -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
lucidicai/utils/images.py CHANGED
@@ -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()
lucidicai/utils/queue.py CHANGED
@@ -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
@@ -1,4 +1,4 @@
1
- lucidicai/__init__.py,sha256=WwZ3A73h0Ttk1xgiiCt9yM-zXD1vIvQNg6XLTbMYQLY,12757
1
+ lucidicai/__init__.py,sha256=vujMytXGPFrZRxIZtvJbo81f1AtgyZpMVIhLyUX32v0,12757
2
2
  lucidicai/action.py,sha256=sPRd1hTIVXDqnvG9ZXWEipUFh0bsXcE0Fm7RVqmVccM,237
3
3
  lucidicai/client.py,sha256=IIhlY6Mfwy47FeMxzpvIygCaqcI1FnqiXiVU6M4QEiE,22327
4
4
  lucidicai/constants.py,sha256=zN8O7TjoRHRlaGa9CZUWppS73rhzKGwaEkF9XMTV0Cg,1160
@@ -18,7 +18,7 @@ lucidicai/state.py,sha256=4Tb1X6l2or6w_e62FYSuEeghAv3xXm5gquKwzCpvdok,235
18
18
  lucidicai/step.py,sha256=_oBIyTBZBvNkUkYHIrwWd75KMSlMtR9Ws2Lo71Lyff8,2522
19
19
  lucidicai/streaming.py,sha256=QOLAzhwxetvx711J8VcphY5kXWPJz9XEBJrmHveRKMc,9796
20
20
  lucidicai/api/__init__.py,sha256=UOYuFZupG0TgzMAxbLNgpodDXhDRXBgMva8ZblgBN9Y,31
21
- lucidicai/api/client.py,sha256=czD3sg4wgyGQTVVlnSi3wpeCt90_D4eOuK8nBlpiv4U,7276
21
+ lucidicai/api/client.py,sha256=RsJXaQn3ccxPcCKYyxoAR5268AhUVkkx1SvTX5_pFbI,12089
22
22
  lucidicai/api/resources/__init__.py,sha256=Wc8-JfL82wkE7eB8PHplqYvaEG2oXNXXhRyEPeduJeE,27
23
23
  lucidicai/api/resources/dataset.py,sha256=6UnMUd-y__TOAjUJLjbc0lZJRTy_gHkyoE82OvjFoN4,5583
24
24
  lucidicai/api/resources/event.py,sha256=GyyNL3_k53EbmvTdgJEABexiuJnoX61hxWey7DYmlYY,2434
@@ -50,10 +50,10 @@ lucidicai/sdk/decorators.py,sha256=B5BXG9Sn5ruUkxFq10L1rrCR_wzYUPlYeu5aqyXetMM,8
50
50
  lucidicai/sdk/error_boundary.py,sha256=IPr5wS9rS7ZQNgEaBwK53UaixAm6L2rijKKFfxcxjUI,9190
51
51
  lucidicai/sdk/event.py,sha256=NiPcnPzYCU0VlFbBk93LD88wqAYmnglV64nQb2XteOs,3747
52
52
  lucidicai/sdk/event_builder.py,sha256=oMvt39m07ZLmPllJTWwRxpinJUz9_AD17yNE6wQRoDA,10423
53
- lucidicai/sdk/init.py,sha256=tfpTRZLT317xNC_GE2OKqRR02Nj3s3a12CJELc-vVAE,12923
53
+ lucidicai/sdk/init.py,sha256=lUEChS3YKiwKWFLZnJIs-VXuicLZPiBXr8_UexqjKhE,15204
54
54
  lucidicai/sdk/shutdown_manager.py,sha256=I5ylR96QHQ_SfP1euAiM0qQ-I7upCPMW1HUNvoj7hCw,12090
55
55
  lucidicai/sdk/features/__init__.py,sha256=23KUF2EZBzsaH9JUFDGNXZb_3PSfc35VZfD59gAfyR0,26
56
- lucidicai/sdk/features/dataset.py,sha256=qFGnu8Wm1yhaflBhtm-5veN-KaoxGLBL5xWEifkrsY0,19416
56
+ lucidicai/sdk/features/dataset.py,sha256=DfoxHrfN86Hu-H0vAO0OHGwaevaGaXm26Z5wpfuXDSY,11814
57
57
  lucidicai/sdk/features/feature_flag.py,sha256=SzuzHiVnbticD6Ojn0_i9xQKui2s9QUFPJ7LixzAtf4,13844
58
58
  lucidicai/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  lucidicai/telemetry/base_provider.py,sha256=nrZVr4Y9xcAiMn4uAN3t3k6DlHNTvlXrA4qQg7lANOQ,544
@@ -78,10 +78,10 @@ lucidicai/telemetry/utils/model_pricing.py,sha256=Dxi6e0WjcIyCTkVX7K7f0pJ5rPu7nS
78
78
  lucidicai/telemetry/utils/text_storage.py,sha256=L62MMJ8E23TDqDTUv2aRntdKMCItsXV7XjY6cFwx2DE,1503
79
79
  lucidicai/telemetry/utils/universal_image_interceptor.py,sha256=vARgMk1hVSF--zfi5b8qBpJJOESuD17YlH9xqxmB9Uw,15954
80
80
  lucidicai/utils/__init__.py,sha256=ZiGtmJaF0ph9iIFIgQiAreVuYM_1o7qu9VySK1NblTw,22
81
- lucidicai/utils/images.py,sha256=YHFjeKHRxzWu0IsuNwKw303egPsd99AShaD4WND1lJk,12325
81
+ lucidicai/utils/images.py,sha256=HzPpv-Xs9k9VaDD1daT5fdZvoNf5ke9-E1awmkzAADc,12322
82
82
  lucidicai/utils/logger.py,sha256=R3B3gSee64F6UVHUrShihBq_O7W7bgfrBiVDXTO3Isg,4777
83
- lucidicai/utils/queue.py,sha256=tZYPAUHRAK_uyE8Mk4PloObsBcfITurHwIlNHr3gMFU,17326
84
- lucidicai-2.1.2.dist-info/METADATA,sha256=ZcL5YaquJn3D5MyMf7UqZTzrP9m5E4XAhEJQJAuOXTs,902
85
- lucidicai-2.1.2.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
86
- lucidicai-2.1.2.dist-info/top_level.txt,sha256=vSSdM3lclF4I5tyVC0xxUk8eIRnnYXMe1hW-eO91HUo,10
87
- lucidicai-2.1.2.dist-info/RECORD,,
83
+ lucidicai/utils/queue.py,sha256=8DQwnGw7pINEJ0dNSkB0PhdPW-iBQQ-YZg23poe4umE,17323
84
+ lucidicai-2.1.3.dist-info/METADATA,sha256=A5SQWuHc48k5n0Dg9KFI52E2SLe0su9vqHEC8jWIi5A,902
85
+ lucidicai-2.1.3.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
86
+ lucidicai-2.1.3.dist-info/top_level.txt,sha256=vSSdM3lclF4I5tyVC0xxUk8eIRnnYXMe1hW-eO91HUo,10
87
+ lucidicai-2.1.3.dist-info/RECORD,,