thinkingsdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. thinkingsdk/__init__.py +443 -0
  2. thinkingsdk/_version.py +12 -0
  3. thinkingsdk/async_instrumentation.py +366 -0
  4. thinkingsdk/auto_instrument.py +115 -0
  5. thinkingsdk/background_sender.py +387 -0
  6. thinkingsdk/config.py +158 -0
  7. thinkingsdk/config_loader.py +316 -0
  8. thinkingsdk/context.py +279 -0
  9. thinkingsdk/custom_events.py +363 -0
  10. thinkingsdk/enhanced_context.py +239 -0
  11. thinkingsdk/enhanced_queue.py +80 -0
  12. thinkingsdk/event_deduplicator.py +338 -0
  13. thinkingsdk/event_queue.py +116 -0
  14. thinkingsdk/exception_chain.py +301 -0
  15. thinkingsdk/instrumentation.py +1139 -0
  16. thinkingsdk/integrations/__init__.py +84 -0
  17. thinkingsdk/integrations/console.py +60 -0
  18. thinkingsdk/integrations/django.py +328 -0
  19. thinkingsdk/integrations/fastapi.py +434 -0
  20. thinkingsdk/integrations/flask.py +323 -0
  21. thinkingsdk/integrations/logging.py +173 -0
  22. thinkingsdk/integrations/middleware_base.py +102 -0
  23. thinkingsdk/integrations/psycopg2.py +243 -0
  24. thinkingsdk/integrations/pymongo.py +223 -0
  25. thinkingsdk/integrations/redis_integration.py +244 -0
  26. thinkingsdk/integrations/sqlalchemy.py +219 -0
  27. thinkingsdk/integrations/stdlib.py +138 -0
  28. thinkingsdk/performance_monitor.py +314 -0
  29. thinkingsdk/pii_scrubber.py +250 -0
  30. thinkingsdk/strategic_sampling.py +357 -0
  31. thinkingsdk/thinkingsdk.yaml +119 -0
  32. thinkingsdk/validate.py +487 -0
  33. thinkingsdk-0.1.0.dist-info/METADATA +165 -0
  34. thinkingsdk-0.1.0.dist-info/RECORD +38 -0
  35. thinkingsdk-0.1.0.dist-info/WHEEL +5 -0
  36. thinkingsdk-0.1.0.dist-info/entry_points.txt +2 -0
  37. thinkingsdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  38. thinkingsdk-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,443 @@
1
+ # thinkingsdk/__init__.py
2
+ """
3
+ Production-grade ThinkingSDK client. Usage inside user code:
4
+
5
+ import thinkingsdk as thinking
6
+
7
+ # Basic usage
8
+ thinking.start(api_key="sk_live_XXXX")
9
+
10
+ # Advanced usage with configuration
11
+ config = {
12
+ 'instrumentation': {'sample_rate': 0.5, 'capture_returns': True},
13
+ 'sender': {'batch_size': 100, 'retry_attempts': 5},
14
+ 'queue': {'maxsize': 20000}
15
+ }
16
+ thinking.start(api_key="sk_live_XXXX", config=config)
17
+
18
+ # Get statistics
19
+ stats = thinking.get_stats()
20
+
21
+ # Clean shutdown
22
+ thinking.stop()
23
+ """
24
+
25
+ import os
26
+ import logging
27
+ import atexit
28
+ from typing import Dict, Any, Optional, List
29
+
30
+ from ._version import __version__, __version_info__
31
+ from .instrumentation import RuntimeInstrumentation
32
+ from .background_sender import BackgroundSender
33
+ from .event_queue import EventQueue
34
+ from .config import Config
35
+ from .config_loader import ConfigLoader
36
+ from .context import context, set_context, clear_context, add_context
37
+ from .event_deduplicator import EventDeduplicator
38
+ from .pii_scrubber import PIIScrubber
39
+ from .custom_events import BreadcrumbTracker, CustomEventTracker, Timer
40
+ from .enhanced_queue import EnhancedEventQueue
41
+
42
+ # Module-level state
43
+ _instrumentation: Optional[RuntimeInstrumentation] = None
44
+ _sender: Optional[BackgroundSender] = None
45
+ _queue: Optional[EventQueue] = None
46
+ _config: Optional[Config] = None
47
+ _deduplicator: Optional[EventDeduplicator] = None
48
+ _pii_scrubber: Optional[PIIScrubber] = None
49
+ _breadcrumb_tracker: Optional[BreadcrumbTracker] = None
50
+ _custom_event_tracker: Optional[CustomEventTracker] = None
51
+ _integrations: Optional[List] = None
52
+
53
+ #TODO: rethink hard stopping exceptions for all methods (e.g. raise Exceptions)
54
+ # This is a production-grade SDK, so we want to avoid raising exceptions
55
+ # unless absolutely necessary. Most methods will log errors instead of raising.
56
+ # However, some critical methods like start() and stop() will raise if called incorrectly.
57
+ # This should not hinder normal usage, but rather help catch misconfigurations early.
58
+
59
+ def start(
60
+ api_key: Optional[str] = None,
61
+ server_url: Optional[str] = None,
62
+ config: Optional[Dict[str, Any]] = None,
63
+ config_file: Optional[str] = None,
64
+ enable_logging: Optional[bool] = None
65
+ ) -> None:
66
+ """
67
+ Start ThinkingSDK instrumentation and background sending.
68
+
69
+ Args:
70
+ api_key: API key for authentication (overrides config file)
71
+ server_url: Base URL of the ThinkingSDK server (overrides config file)
72
+ config: Optional configuration dictionary (overrides config file)
73
+ config_file: Path to YAML config file (default: searches for thinkingsdk.yaml)
74
+ enable_logging: Enable logging for debugging (overrides config file)
75
+
76
+ Priority: function args > config dict > config file > defaults
77
+
78
+ Raises:
79
+ RuntimeError: If SDK is already started
80
+ """
81
+ global _instrumentation, _sender, _queue, _config
82
+
83
+ # Check for disable flag - this takes precedence over everything
84
+ if os.environ.get('THINKINGSDK_DISABLE_AUTO'):
85
+ if enable_logging:
86
+ logging.info("ThinkingSDK disabled by THINKINGSDK_DISABLE_AUTO environment variable")
87
+ return
88
+
89
+ if _sender is not None:
90
+ raise RuntimeError("ThinkingSDK is already started. Call stop() first.")
91
+
92
+ # Load configuration from YAML file
93
+ config_loader = ConfigLoader(config_file)
94
+
95
+ # Check if SDK is enabled
96
+ if not config_loader.is_enabled():
97
+ if enable_logging:
98
+ logging.debug("ThinkingSDK is disabled in configuration")
99
+ return
100
+
101
+ # Build final configuration (priority: args > config dict > yaml file)
102
+ final_config = config_loader.config.copy()
103
+ if config:
104
+ # Merge user-provided config
105
+ for key, value in config.items():
106
+ if isinstance(value, dict) and key in final_config:
107
+ final_config[key].update(value)
108
+ else:
109
+ final_config[key] = value
110
+
111
+ # Override with function arguments if provided
112
+ if api_key is None:
113
+ api_key = config_loader.get_api_key()
114
+ if server_url is None:
115
+ server_url = config_loader.get("server_url", "https://api.thinkingsdk.ai")
116
+ if enable_logging is None:
117
+ enable_logging = config_loader.get("debug", False)
118
+
119
+ # Initialize configuration
120
+ _config = Config(final_config)
121
+
122
+ # Set up logging if requested
123
+ if enable_logging or _config.is_logging_enabled():
124
+ logging.basicConfig(
125
+ level=getattr(logging, _config.get_log_level().upper()),
126
+ format='%(asctime)s - ThinkingSDK - %(levelname)s - %(message)s'
127
+ )
128
+
129
+ #TODO: is _queue or enhanced_queue both needed? which one should be used?
130
+ try:
131
+ # Create components
132
+ _queue = EventQueue(**_config.get_queue_config())
133
+
134
+ # Create PII scrubber if privacy is enabled
135
+ global _pii_scrubber
136
+ privacy_config = final_config.get('privacy', {})
137
+ if privacy_config.get('sanitize_data', True):
138
+ _pii_scrubber = PIIScrubber(privacy_config)
139
+
140
+ # Create deduplicator for efficiency
141
+ global _deduplicator
142
+ _deduplicator = EventDeduplicator(final_config.get('deduplication', {}))
143
+
144
+ # Create breadcrumb tracker
145
+ global _breadcrumb_tracker
146
+ _breadcrumb_tracker = BreadcrumbTracker(max_breadcrumbs=100)
147
+
148
+ # Setup semantic event integrations for breadcrumbs
149
+ global _integrations
150
+ _integrations = []
151
+
152
+ # Add standard library integration (HTTP, subprocess)
153
+ from .integrations.stdlib import StdlibIntegration
154
+ stdlib_integration = StdlibIntegration()
155
+ stdlib_integration.setup_once()
156
+ _integrations.append(stdlib_integration)
157
+
158
+ # Add logging integration
159
+ from .integrations.logging import LoggingIntegration
160
+ logging_integration = LoggingIntegration(level=logging.DEBUG)
161
+ logging_integration.setup_once()
162
+ _integrations.append(logging_integration)
163
+
164
+ # Add console integration (print statements)
165
+ from .integrations.console import ConsoleIntegration
166
+ console_integration = ConsoleIntegration(capture_print=True)
167
+ console_integration.setup_once()
168
+ _integrations.append(console_integration)
169
+
170
+ # Try to add database integrations if available
171
+ try:
172
+ import sqlalchemy
173
+ from .integrations.sqlalchemy import SQLAlchemyIntegration
174
+ sqlalchemy_integration = SQLAlchemyIntegration(capture_params=False)
175
+ sqlalchemy_integration.setup_once()
176
+ _integrations.append(sqlalchemy_integration)
177
+ except ImportError:
178
+ pass # SQLAlchemy not installed
179
+
180
+ try:
181
+ import psycopg2
182
+ from .integrations.psycopg2 import Psycopg2Integration
183
+ psycopg2_integration = Psycopg2Integration(capture_params=False)
184
+ psycopg2_integration.setup_once()
185
+ _integrations.append(psycopg2_integration)
186
+ except ImportError:
187
+ pass # psycopg2 not installed
188
+
189
+ try:
190
+ import pymongo
191
+ from .integrations.pymongo import PyMongoIntegration
192
+ pymongo_integration = PyMongoIntegration(sanitize_queries=True)
193
+ pymongo_integration.setup_once()
194
+ _integrations.append(pymongo_integration)
195
+ except (ImportError, Exception) as e:
196
+ if enable_logging:
197
+ logging.debug(f"Skipping pymongo integration: {e}")
198
+ pass # pymongo not installed or incompatible
199
+
200
+ try:
201
+ import redis
202
+ from .integrations.redis_integration import RedisIntegration
203
+ redis_integration = RedisIntegration(max_data_size=100)
204
+ redis_integration.setup_once()
205
+ _integrations.append(redis_integration)
206
+ except ImportError:
207
+ pass # redis not installed
208
+
209
+ # Create custom event tracker
210
+ global _custom_event_tracker
211
+ _custom_event_tracker = CustomEventTracker(_queue, _breadcrumb_tracker)
212
+
213
+ # Create enhanced queue that uses deduplicator and PII scrubber
214
+ enhanced_queue = EnhancedEventQueue(_queue, _deduplicator, _pii_scrubber)
215
+
216
+ _instrumentation = RuntimeInstrumentation(enhanced_queue, _config.get_instrumentation_config())
217
+ _sender = BackgroundSender(enhanced_queue, api_key, server_url, _config.get_sender_config())
218
+
219
+ # Start instrumentation and background sender
220
+ _instrumentation.setup_hooks()
221
+ _sender.start()
222
+
223
+ # Register automatic cleanup on Python exit
224
+ atexit.register(_cleanup_on_exit)
225
+
226
+ if enable_logging or _config.is_logging_enabled():
227
+ logging.debug("ThinkingSDK started successfully")
228
+
229
+ except Exception as e:
230
+ # Clean up on failure
231
+ stop()
232
+ raise RuntimeError(f"Failed to start ThinkingSDK: {e}") from e
233
+
234
+
235
+ def _cleanup_on_exit() -> None:
236
+ """
237
+ Automatic cleanup function called on Python exit.
238
+ Ensures events are flushed before the program terminates.
239
+ """
240
+ try:
241
+ # Silently cleanup - no output to maintain transparency
242
+ # Only cleanup if SDK is still running
243
+ if _sender is not None and _instrumentation is not None:
244
+ if _config and (_config.is_logging_enabled()):
245
+ logging.debug("ThinkingSDK: Automatic cleanup on exit - flushing events...")
246
+ stop(timeout=3.0) # Shorter timeout for exit handler
247
+ if _config and (_config.is_logging_enabled()):
248
+ logging.debug("ThinkingSDK: Exit cleanup completed")
249
+ except Exception as e:
250
+ # Don't let exit handler exceptions crash the program
251
+ if _config and (_config.is_logging_enabled()):
252
+ logging.debug(f"ThinkingSDK: Exit cleanup failed: {e}")
253
+
254
+
255
+ def stop(timeout: float = 5.0) -> None:
256
+ """
257
+ Stop ThinkingSDK instrumentation and background sending.
258
+ TODO: empty the queue gracefully before stopping
259
+ Args:
260
+ timeout: Maximum time to wait for graceful shutdown (seconds)
261
+ """
262
+ global _instrumentation, _sender, _queue, _config
263
+
264
+ # Unregister exit handler to prevent duplicate cleanup
265
+ try:
266
+ atexit.unregister(_cleanup_on_exit)
267
+ except ValueError:
268
+ pass # Already unregistered
269
+
270
+ if _config and (_config.is_logging_enabled()):
271
+ logging.debug("Stopping ThinkingSDK...")
272
+
273
+ # Clean up instrumentation
274
+ if _instrumentation:
275
+ try:
276
+ _instrumentation.cleanup_hooks()
277
+ except Exception as e:
278
+ if _config and _config.is_logging_enabled():
279
+ logging.error(f"Error cleaning up instrumentation: {e}")
280
+ finally:
281
+ _instrumentation = None
282
+
283
+ # Stop background sender
284
+ if _sender:
285
+ try:
286
+ _sender.stop(timeout=timeout)
287
+ except Exception as e:
288
+ if _config and _config.is_logging_enabled():
289
+ logging.error(f"Error stopping sender: {e}")
290
+ finally:
291
+ _sender = None
292
+
293
+ # Clear remaining state
294
+ _queue = None
295
+ _config = None
296
+
297
+ if _config and _config.is_logging_enabled():
298
+ logging.debug("ThinkingSDK stopped")
299
+
300
+
301
+ def get_stats() -> Dict[str, Any]:
302
+ """
303
+ Get statistics about the current ThinkingSDK session.
304
+
305
+ Returns:
306
+ Dictionary containing statistics from all components
307
+
308
+ Raises:
309
+ RuntimeError: If SDK is not started
310
+ """
311
+ if not _sender:
312
+ raise RuntimeError("ThinkingSDK is not started. Call start() first.")
313
+
314
+ stats = {
315
+ 'sdk_active': True,
316
+ 'config': _config.to_dict() if _config else {},
317
+ }
318
+
319
+ # Add component statistics
320
+ if _queue:
321
+ stats['queue'] = _queue.get_stats()
322
+
323
+ if _instrumentation:
324
+ stats['instrumentation'] = _instrumentation.get_stats()
325
+
326
+ if _sender:
327
+ stats['sender'] = _sender.get_stats()
328
+
329
+ return stats
330
+
331
+
332
+ def is_active() -> bool:
333
+ """
334
+ Check if ThinkingSDK is currently active.
335
+
336
+ Returns:
337
+ True if SDK is started and running
338
+ """
339
+ return _sender is not None and _instrumentation is not None
340
+
341
+
342
+ def track_event(event_name: str, data: Optional[Dict[str, Any]] = None, level: str = "info") -> None:
343
+ """
344
+ Track a custom business event.
345
+
346
+ Args:
347
+ event_name: Name of the event (e.g., "payment_processed")
348
+ data: Event data/metadata
349
+ level: Event level (debug, info, warning, error, critical)
350
+
351
+ Raises:
352
+ RuntimeError: If SDK is not started
353
+ """
354
+ if not _custom_event_tracker:
355
+ raise RuntimeError("ThinkingSDK is not started. Call start() first.")
356
+
357
+ _custom_event_tracker.track_event(event_name, data, level)
358
+
359
+
360
+ def track_metric(metric_name: str, value: float, unit: str = "none", tags: Optional[Dict[str, str]] = None) -> None:
361
+ """
362
+ Track a numeric metric.
363
+
364
+ Args:
365
+ metric_name: Name of the metric
366
+ value: Numeric value
367
+ unit: Unit of measurement
368
+ tags: Additional tags
369
+
370
+ Raises:
371
+ RuntimeError: If SDK is not started
372
+ """
373
+ if not _custom_event_tracker:
374
+ raise RuntimeError("ThinkingSDK is not started. Call start() first.")
375
+
376
+ _custom_event_tracker.track_metric(metric_name, value, unit, tags)
377
+
378
+
379
+ def add_breadcrumb(message: str, category: str = "default", level: str = "info", data: Optional[Dict[str, Any]] = None) -> None:
380
+ """
381
+ Add a breadcrumb to the trail.
382
+
383
+ Args:
384
+ message: Breadcrumb message
385
+ category: Category (navigation, http, console, user, etc.)
386
+ level: Severity level
387
+ data: Additional data
388
+
389
+ Raises:
390
+ RuntimeError: If SDK is not started
391
+ """
392
+ if not _custom_event_tracker:
393
+ raise RuntimeError("ThinkingSDK is not started. Call start() first.")
394
+
395
+ _custom_event_tracker.add_breadcrumb(message, category, level, data)
396
+
397
+
398
+ def mark_feature_usage(feature_name: str, metadata: Optional[Dict[str, Any]] = None) -> None:
399
+ """
400
+ Track feature usage for product analytics.
401
+
402
+ Args:
403
+ feature_name: Name of the feature
404
+ metadata: Additional metadata
405
+
406
+ Raises:
407
+ RuntimeError: If SDK is not started
408
+ """
409
+ if not _custom_event_tracker:
410
+ raise RuntimeError("ThinkingSDK is not started. Call start() first.")
411
+
412
+ _custom_event_tracker.mark_feature_usage(feature_name, metadata)
413
+
414
+
415
+ def timer(operation_name: str, tags: Optional[Dict[str, str]] = None):
416
+ """
417
+ Create a timer context manager for timing operations.
418
+
419
+ Args:
420
+ operation_name: Name of the operation to time
421
+ tags: Additional tags
422
+
423
+ Returns:
424
+ Timer context manager
425
+
426
+ Example:
427
+ with thinking.timer("database_query"):
428
+ results = db.query("SELECT * FROM users")
429
+ """
430
+ if not _custom_event_tracker:
431
+ raise RuntimeError("ThinkingSDK is not started. Call start() first.")
432
+
433
+ return Timer(_custom_event_tracker, operation_name, tags)
434
+
435
+
436
+ # Expose key classes for advanced usage
437
+ __all__ = [
438
+ '__version__', '__version_info__',
439
+ 'start', 'stop', 'get_stats', 'is_active',
440
+ 'context', 'set_context', 'clear_context', 'add_context',
441
+ 'track_event', 'track_metric', 'add_breadcrumb', 'mark_feature_usage', 'timer',
442
+ 'RuntimeInstrumentation', 'BackgroundSender', 'EventQueue', 'Config'
443
+ ]
@@ -0,0 +1,12 @@
1
+ """Version information for ThinkingSDK client."""
2
+
3
+ __version__ = "0.1.0"
4
+ __version_info__ = (0, 1, 0)
5
+
6
+ def get_version():
7
+ """Return the version string."""
8
+ return __version__
9
+
10
+ def get_version_info():
11
+ """Return version as a tuple of integers."""
12
+ return __version_info__