lucidicai 2.0.1__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {lucidicai-2.0.1 → lucidicai-2.1.0}/PKG-INFO +1 -1
  2. {lucidicai-2.0.1 → lucidicai-2.1.0}/README.md +3 -3
  3. lucidicai-2.1.0/lucidicai/__init__.py +392 -0
  4. lucidicai-2.1.0/lucidicai/api/__init__.py +1 -0
  5. lucidicai-2.1.0/lucidicai/api/client.py +218 -0
  6. lucidicai-2.1.0/lucidicai/api/resources/__init__.py +1 -0
  7. lucidicai-2.1.0/lucidicai/api/resources/dataset.py +192 -0
  8. lucidicai-2.1.0/lucidicai/api/resources/event.py +88 -0
  9. lucidicai-2.1.0/lucidicai/api/resources/session.py +126 -0
  10. lucidicai-2.1.0/lucidicai/core/__init__.py +1 -0
  11. lucidicai-2.1.0/lucidicai/core/config.py +223 -0
  12. {lucidicai-2.0.1/lucidicai → lucidicai-2.1.0/lucidicai/core}/errors.py +21 -15
  13. lucidicai-2.1.0/lucidicai/core/types.py +35 -0
  14. lucidicai-2.1.0/lucidicai/sdk/__init__.py +1 -0
  15. {lucidicai-2.0.1/lucidicai → lucidicai-2.1.0/lucidicai/sdk}/decorators.py +68 -27
  16. lucidicai-2.1.0/lucidicai/sdk/error_boundary.py +299 -0
  17. lucidicai-2.1.0/lucidicai/sdk/event.py +122 -0
  18. lucidicai-2.1.0/lucidicai/sdk/event_builder.py +304 -0
  19. lucidicai-2.1.0/lucidicai/sdk/features/__init__.py +1 -0
  20. lucidicai-2.1.0/lucidicai/sdk/features/dataset.py +605 -0
  21. lucidicai-2.1.0/lucidicai/sdk/features/feature_flag.py +383 -0
  22. lucidicai-2.1.0/lucidicai/sdk/init.py +271 -0
  23. lucidicai-2.1.0/lucidicai/sdk/shutdown_manager.py +302 -0
  24. lucidicai-2.1.0/lucidicai/telemetry/context_bridge.py +82 -0
  25. {lucidicai-2.0.1 → lucidicai-2.1.0}/lucidicai/telemetry/context_capture_processor.py +25 -9
  26. {lucidicai-2.0.1 → lucidicai-2.1.0}/lucidicai/telemetry/litellm_bridge.py +18 -24
  27. {lucidicai-2.0.1 → lucidicai-2.1.0}/lucidicai/telemetry/lucidic_exporter.py +51 -36
  28. lucidicai-2.1.0/lucidicai/utils/__init__.py +1 -0
  29. lucidicai-2.1.0/lucidicai/utils/images.py +337 -0
  30. lucidicai-2.1.0/lucidicai/utils/logger.py +168 -0
  31. lucidicai-2.1.0/lucidicai/utils/queue.py +393 -0
  32. {lucidicai-2.0.1 → lucidicai-2.1.0}/lucidicai.egg-info/PKG-INFO +1 -1
  33. lucidicai-2.1.0/lucidicai.egg-info/SOURCES.txt +43 -0
  34. {lucidicai-2.0.1 → lucidicai-2.1.0}/setup.py +1 -1
  35. lucidicai-2.0.1/lucidicai/__init__.py +0 -917
  36. lucidicai-2.0.1/lucidicai/client.py +0 -510
  37. lucidicai-2.0.1/lucidicai/constants.py +0 -29
  38. lucidicai-2.0.1/lucidicai/event.py +0 -53
  39. lucidicai-2.0.1/lucidicai/event_queue.py +0 -466
  40. lucidicai-2.0.1/lucidicai/image_upload.py +0 -111
  41. lucidicai-2.0.1/lucidicai/lru.py +0 -19
  42. lucidicai-2.0.1/lucidicai/session.py +0 -51
  43. lucidicai-2.0.1/lucidicai/singleton.py +0 -53
  44. lucidicai-2.0.1/lucidicai/streaming.py +0 -225
  45. lucidicai-2.0.1/lucidicai/telemetry/utils/image_storage.py +0 -45
  46. lucidicai-2.0.1/lucidicai/telemetry/utils/text_storage.py +0 -53
  47. lucidicai-2.0.1/lucidicai/telemetry/utils/universal_image_interceptor.py +0 -365
  48. lucidicai-2.0.1/lucidicai.egg-info/SOURCES.txt +0 -32
  49. {lucidicai-2.0.1/lucidicai → lucidicai-2.1.0/lucidicai/sdk}/context.py +0 -0
  50. {lucidicai-2.0.1 → lucidicai-2.1.0}/lucidicai/telemetry/__init__.py +0 -0
  51. {lucidicai-2.0.1 → lucidicai-2.1.0}/lucidicai/telemetry/extract.py +0 -0
  52. {lucidicai-2.0.1 → lucidicai-2.1.0}/lucidicai/telemetry/openai_agents_instrumentor.py +0 -0
  53. {lucidicai-2.0.1 → lucidicai-2.1.0}/lucidicai/telemetry/telemetry_init.py +0 -0
  54. {lucidicai-2.0.1 → lucidicai-2.1.0}/lucidicai/telemetry/utils/__init__.py +0 -0
  55. {lucidicai-2.0.1/lucidicai → lucidicai-2.1.0/lucidicai/telemetry/utils}/model_pricing.py +0 -0
  56. {lucidicai-2.0.1 → lucidicai-2.1.0}/lucidicai.egg-info/dependency_links.txt +0 -0
  57. {lucidicai-2.0.1 → lucidicai-2.1.0}/lucidicai.egg-info/requires.txt +0 -0
  58. {lucidicai-2.0.1 → lucidicai-2.1.0}/lucidicai.egg-info/top_level.txt +0 -0
  59. {lucidicai-2.0.1 → lucidicai-2.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lucidicai
3
- Version: 2.0.1
3
+ Version: 2.1.0
4
4
  Summary: Lucidic AI Python SDK
5
5
  Author: Andy Liang
6
6
  Author-email: andy@lucidic.ai
@@ -83,7 +83,7 @@ lai.init(
83
83
  auto_end=True, # Optional: Auto-end session on exit (default: True)
84
84
  masking_function=my_mask_func, # Optional: Custom PII masking
85
85
  tags=["customer-support", "v1.2"], # Optional: Session tags
86
- rubrics=[...], # Optional: Evaluation criteria
86
+ evaluators=[...], # Optional: Evaluation criteria
87
87
  experiment_id="...", # Optional: Link to experiment
88
88
  capture_uncaught=True # Optional: Capture crash events (default: True)
89
89
  )
@@ -413,8 +413,8 @@ Create experiments to group and analyze multiple sessions:
413
413
  # Create an experiment
414
414
  experiment_id = lai.create_experiment(
415
415
  experiment_name="Prompt Optimization Test",
416
- pass_fail_rubrics=["response_quality", "latency"],
417
- score_rubrics=["coherence", "relevance"],
416
+ LLM_boolean_evaluators=["response_quality", "latency"],
417
+ LLM_numeric_evaluators=["coherence", "relevance"],
418
418
  description="Testing different prompt strategies",
419
419
  tags=["A/B-test", "prompts"]
420
420
  )
@@ -0,0 +1,392 @@
1
+ """Lucidic AI SDK - Clean Export-Only Entry Point
2
+
3
+ This file only contains exports, with all logic moved to appropriate modules.
4
+ """
5
+
6
+ # Import core modules
7
+ from .sdk import init as init_module
8
+ from .sdk import event as event_module
9
+ from .sdk import error_boundary
10
+ from .core.config import get_config
11
+
12
+ # Import raw functions
13
+ from .sdk.init import (
14
+ init as _init,
15
+ get_session_id as _get_session_id,
16
+ clear_state as _clear_state,
17
+ )
18
+
19
+ from .sdk.event import (
20
+ create_event as _create_event,
21
+ create_error_event as _create_error_event,
22
+ flush as _flush,
23
+ )
24
+
25
+ # Context management exports
26
+ from .sdk.context import (
27
+ set_active_session,
28
+ clear_active_session,
29
+ bind_session,
30
+ bind_session_async,
31
+ session,
32
+ session_async,
33
+ run_session,
34
+ run_in_session,
35
+ current_session_id,
36
+ current_parent_event_id,
37
+ )
38
+
39
+ # Decorators
40
+ from .sdk.decorators import event, event as step # step is deprecated alias
41
+
42
+ # Error types
43
+ from .core.errors import (
44
+ LucidicError,
45
+ LucidicNotInitializedError,
46
+ APIKeyVerificationError,
47
+ InvalidOperationError,
48
+ PromptError,
49
+ FeatureFlagError,
50
+ )
51
+
52
+ # Import functions that need to be implemented
53
+ def _update_session(
54
+ task=None,
55
+ session_eval=None,
56
+ session_eval_reason=None,
57
+ is_successful=None,
58
+ is_successful_reason=None
59
+ ):
60
+ """Update the current session."""
61
+ from .sdk.init import get_resources, get_session_id
62
+
63
+ session_id = get_session_id()
64
+ if not session_id:
65
+ return
66
+
67
+ resources = get_resources()
68
+ if resources and 'sessions' in resources:
69
+ updates = {}
70
+ if task is not None:
71
+ updates['task'] = task
72
+ if session_eval is not None:
73
+ updates['session_eval'] = session_eval
74
+ if session_eval_reason is not None:
75
+ updates['session_eval_reason'] = session_eval_reason
76
+ if is_successful is not None:
77
+ updates['is_successful'] = is_successful
78
+ if is_successful_reason is not None:
79
+ updates['is_successful_reason'] = is_successful_reason
80
+
81
+ if updates:
82
+ resources['sessions'].update_session(session_id, updates)
83
+
84
+
85
+ def _end_session(
86
+ session_eval=None,
87
+ session_eval_reason=None,
88
+ is_successful=None,
89
+ is_successful_reason=None,
90
+ wait_for_flush=True
91
+ ):
92
+ """End the current session."""
93
+ from .sdk.init import get_resources, get_session_id, get_event_queue
94
+
95
+ session_id = get_session_id()
96
+ if not session_id:
97
+ return
98
+
99
+ # Flush events if requested
100
+ if wait_for_flush:
101
+ flush(timeout_seconds=5.0)
102
+
103
+ # End session via API
104
+ resources = get_resources()
105
+ if resources and 'sessions' in resources:
106
+ resources['sessions'].end_session(
107
+ session_id,
108
+ is_successful=is_successful,
109
+ session_eval=session_eval,
110
+ is_successful_reason=is_successful_reason,
111
+ session_eval_reason=session_eval_reason
112
+ )
113
+
114
+ # Clear session context
115
+ clear_active_session()
116
+
117
+
118
+ def _get_session():
119
+ """Get the current session object."""
120
+ from .sdk.init import get_session_id
121
+ return get_session_id()
122
+
123
+
124
+ def _create_experiment(
125
+ experiment_name,
126
+ LLM_boolean_evaluators=None,
127
+ LLM_numeric_evaluators=None,
128
+ description=None,
129
+ tags=None,
130
+ api_key=None,
131
+ agent_id=None,
132
+ ):
133
+ """Create a new experiment."""
134
+ from .sdk.init import get_http
135
+ from .core.config import SDKConfig, get_config
136
+
137
+ # Get or create HTTP client
138
+ http = get_http()
139
+ config = get_config()
140
+
141
+ if not http:
142
+ config = SDKConfig.from_env(api_key=api_key, agent_id=agent_id)
143
+ from .api.client import HttpClient
144
+ http = HttpClient(config)
145
+
146
+ # Use provided agent_id or fall back to config
147
+ final_agent_id = agent_id or config.agent_id
148
+ if not final_agent_id:
149
+ raise ValueError("Agent ID is required for creating experiments")
150
+
151
+ evaluator_names = []
152
+ if LLM_boolean_evaluators:
153
+ evaluator_names.extend(LLM_boolean_evaluators)
154
+ if LLM_numeric_evaluators:
155
+ evaluator_names.extend(LLM_numeric_evaluators)
156
+
157
+ # Create experiment via API (matching TypeScript exactly)
158
+ response = http.post('createexperiment', {
159
+ 'agent_id': final_agent_id,
160
+ 'experiment_name': experiment_name,
161
+ 'description': description or '',
162
+ 'tags': tags or [],
163
+ 'evaluator_names': evaluator_names
164
+ })
165
+
166
+ return response.get('experiment_id')
167
+
168
+
169
+ def _get_prompt(
170
+ prompt_name,
171
+ variables=None,
172
+ cache_ttl=300,
173
+ label='production'
174
+ ):
175
+ """Get a prompt from the prompt database."""
176
+ from .sdk.init import get_http
177
+
178
+ http = get_http()
179
+ if not http:
180
+ return ""
181
+
182
+ # Get prompt from API
183
+ try:
184
+ response = http.get('getprompt', {
185
+ 'prompt_name': prompt_name,
186
+ 'label': label
187
+ })
188
+
189
+ # TypeScript SDK expects 'prompt_content' field
190
+ prompt = response.get('prompt_content', '')
191
+
192
+ # Replace variables if provided
193
+ if variables:
194
+ for key, value in variables.items():
195
+ prompt = prompt.replace(f"{{{key}}}", str(value))
196
+
197
+ return prompt
198
+ except Exception:
199
+ return ""
200
+
201
+
202
+ def _get_dataset(dataset_id, api_key=None, agent_id=None):
203
+ """Get a dataset by ID."""
204
+ from .sdk.features.dataset import get_dataset as __get_dataset
205
+ return __get_dataset(dataset_id, api_key, agent_id)
206
+
207
+
208
+ def _get_dataset_items(dataset_id, api_key=None, agent_id=None):
209
+ """Get dataset items."""
210
+ from .sdk.features.dataset import get_dataset_items as __get_dataset_items
211
+ return __get_dataset_items(dataset_id, api_key, agent_id)
212
+
213
+
214
+ def _list_datasets(api_key=None, agent_id=None):
215
+ """List all datasets."""
216
+ from .sdk.features.dataset import list_datasets as __list_datasets
217
+ return __list_datasets(api_key, agent_id)
218
+
219
+
220
+ def _create_dataset(name, description=None, tags=None, suggested_flag_config=None, api_key=None, agent_id=None):
221
+ """Create a new dataset."""
222
+ from .sdk.features.dataset import create_dataset as __create_dataset
223
+ return __create_dataset(name, description, tags, suggested_flag_config, api_key, agent_id)
224
+
225
+
226
+ def _update_dataset(dataset_id, name=None, description=None, tags=None, suggested_flag_config=None, api_key=None, agent_id=None):
227
+ """Update dataset metadata."""
228
+ from .sdk.features.dataset import update_dataset as __update_dataset
229
+ return __update_dataset(dataset_id, name, description, tags, suggested_flag_config, api_key, agent_id)
230
+
231
+
232
+ def _delete_dataset(dataset_id, api_key=None, agent_id=None):
233
+ """Delete a dataset."""
234
+ from .sdk.features.dataset import delete_dataset as __delete_dataset
235
+ return __delete_dataset(dataset_id, api_key, agent_id)
236
+
237
+
238
+ def _create_dataset_item(dataset_id, name, input_data, expected_output=None, description=None, tags=None, metadata=None, flag_overrides=None, api_key=None, agent_id=None):
239
+ """Create a dataset item."""
240
+ from .sdk.features.dataset import create_dataset_item as __create_dataset_item
241
+ return __create_dataset_item(dataset_id, name, input_data, expected_output, description, tags, metadata, flag_overrides, api_key, agent_id)
242
+
243
+
244
+ def _get_dataset_item(dataset_id, item_id, api_key=None, agent_id=None):
245
+ """Get a specific dataset item."""
246
+ from .sdk.features.dataset import get_dataset_item as __get_dataset_item
247
+ return __get_dataset_item(dataset_id, item_id, api_key, agent_id)
248
+
249
+
250
+ def _update_dataset_item(dataset_id, item_id, name=None, input_data=None, expected_output=None, description=None, tags=None, metadata=None, flag_overrides=None, api_key=None, agent_id=None):
251
+ """Update a dataset item."""
252
+ from .sdk.features.dataset import update_dataset_item as __update_dataset_item
253
+ return __update_dataset_item(dataset_id, item_id, name, input_data, expected_output, description, tags, metadata, flag_overrides, api_key, agent_id)
254
+
255
+
256
+ def _delete_dataset_item(dataset_id, item_id, api_key=None, agent_id=None):
257
+ """Delete a dataset item."""
258
+ from .sdk.features.dataset import delete_dataset_item as __delete_dataset_item
259
+ return __delete_dataset_item(dataset_id, item_id, api_key, agent_id)
260
+
261
+
262
+ def _list_dataset_item_sessions(dataset_id, item_id, api_key=None, agent_id=None):
263
+ """List all sessions for a dataset item."""
264
+ from .sdk.features.dataset import list_dataset_item_sessions as __list_dataset_item_sessions
265
+ return __list_dataset_item_sessions(dataset_id, item_id, api_key, agent_id)
266
+
267
+
268
+ # Feature flags
269
+ from .sdk.features.feature_flag import (
270
+ get_feature_flag,
271
+ get_bool_flag,
272
+ get_int_flag,
273
+ get_float_flag,
274
+ get_string_flag,
275
+ get_json_flag,
276
+ clear_feature_flag_cache,
277
+ )
278
+
279
+ # Error boundary utilities
280
+ is_silent_mode = error_boundary.is_silent_mode
281
+ get_error_history = error_boundary.get_error_history
282
+ clear_error_history = error_boundary.clear_error_history
283
+
284
+ # Version
285
+ __version__ = "2.0.0"
286
+
287
+ # Apply error boundary wrapping to all SDK functions
288
+ from .sdk.error_boundary import wrap_sdk_function
289
+
290
+ # Wrap main SDK functions
291
+ init = wrap_sdk_function(_init, "init")
292
+ get_session_id = wrap_sdk_function(_get_session_id, "init")
293
+ clear_state = wrap_sdk_function(_clear_state, "init")
294
+ create_event = wrap_sdk_function(_create_event, "event")
295
+ create_error_event = wrap_sdk_function(_create_error_event, "event")
296
+ flush = wrap_sdk_function(_flush, "event")
297
+
298
+ # Wrap session functions
299
+ update_session = wrap_sdk_function(_update_session, "session")
300
+ end_session = wrap_sdk_function(_end_session, "session")
301
+ get_session = wrap_sdk_function(_get_session, "session")
302
+
303
+ # Wrap feature functions
304
+ create_experiment = wrap_sdk_function(_create_experiment, "experiment")
305
+ get_prompt = wrap_sdk_function(_get_prompt, "prompt")
306
+
307
+ # Dataset management - complete CRUD
308
+ list_datasets = wrap_sdk_function(_list_datasets, "dataset")
309
+ create_dataset = wrap_sdk_function(_create_dataset, "dataset")
310
+ get_dataset = wrap_sdk_function(_get_dataset, "dataset")
311
+ update_dataset = wrap_sdk_function(_update_dataset, "dataset")
312
+ delete_dataset = wrap_sdk_function(_delete_dataset, "dataset")
313
+
314
+ # Dataset item management
315
+ create_dataset_item = wrap_sdk_function(_create_dataset_item, "dataset")
316
+ get_dataset_item = wrap_sdk_function(_get_dataset_item, "dataset")
317
+ update_dataset_item = wrap_sdk_function(_update_dataset_item, "dataset")
318
+ delete_dataset_item = wrap_sdk_function(_delete_dataset_item, "dataset")
319
+ get_dataset_items = wrap_sdk_function(_get_dataset_items, "dataset")
320
+ list_dataset_item_sessions = wrap_sdk_function(_list_dataset_item_sessions, "dataset")
321
+
322
+ # All exports
323
+ __all__ = [
324
+ # Main functions
325
+ 'init',
326
+ 'get_session_id',
327
+ 'clear_state',
328
+ 'update_session',
329
+ 'end_session',
330
+ 'get_session',
331
+ 'create_event',
332
+ 'create_error_event',
333
+ 'flush',
334
+
335
+ # Decorators
336
+ 'event',
337
+ 'step',
338
+
339
+ # Features
340
+ 'create_experiment',
341
+ 'get_prompt',
342
+
343
+ # Dataset management
344
+ 'list_datasets',
345
+ 'create_dataset',
346
+ 'get_dataset',
347
+ 'update_dataset',
348
+ 'delete_dataset',
349
+ 'create_dataset_item',
350
+ 'get_dataset_item',
351
+ 'update_dataset_item',
352
+ 'delete_dataset_item',
353
+ 'get_dataset_items',
354
+ 'list_dataset_item_sessions',
355
+
356
+ # Feature flags
357
+ 'get_feature_flag',
358
+ 'get_bool_flag',
359
+ 'get_int_flag',
360
+ 'get_float_flag',
361
+ 'get_string_flag',
362
+ 'get_json_flag',
363
+ 'clear_feature_flag_cache',
364
+
365
+ # Context management
366
+ 'set_active_session',
367
+ 'clear_active_session',
368
+ 'bind_session',
369
+ 'bind_session_async',
370
+ 'session',
371
+ 'session_async',
372
+ 'run_session',
373
+ 'run_in_session',
374
+ 'current_session_id',
375
+ 'current_parent_event_id',
376
+
377
+ # Error types
378
+ 'LucidicError',
379
+ 'LucidicNotInitializedError',
380
+ 'APIKeyVerificationError',
381
+ 'InvalidOperationError',
382
+ 'PromptError',
383
+ 'FeatureFlagError',
384
+
385
+ # Error boundary
386
+ 'is_silent_mode',
387
+ 'get_error_history',
388
+ 'clear_error_history',
389
+
390
+ # Version
391
+ '__version__',
392
+ ]
@@ -0,0 +1 @@
1
+ """API client and resources."""
@@ -0,0 +1,218 @@
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()
@@ -0,0 +1 @@
1
+ """API resource modules."""