aiqa-client 0.3.5__tar.gz → 0.3.6__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 (21) hide show
  1. {aiqa_client-0.3.5/aiqa_client.egg-info → aiqa_client-0.3.6}/PKG-INFO +28 -1
  2. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/README.md +27 -0
  3. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa/__init__.py +3 -2
  4. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa/aiqa_exporter.py +12 -0
  5. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa/client.py +126 -12
  6. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa/tracing.py +26 -14
  7. {aiqa_client-0.3.5 → aiqa_client-0.3.6/aiqa_client.egg-info}/PKG-INFO +28 -1
  8. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/pyproject.toml +1 -1
  9. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/LICENSE +0 -0
  10. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/MANIFEST.in +0 -0
  11. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa/experiment_runner.py +0 -0
  12. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa/object_serialiser.py +0 -0
  13. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa/py.typed +0 -0
  14. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa/test_experiment_runner.py +0 -0
  15. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa/test_tracing.py +0 -0
  16. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa_client.egg-info/SOURCES.txt +0 -0
  17. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa_client.egg-info/dependency_links.txt +0 -0
  18. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa_client.egg-info/requires.txt +0 -0
  19. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/aiqa_client.egg-info/top_level.txt +0 -0
  20. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/setup.cfg +0 -0
  21. {aiqa_client-0.3.5 → aiqa_client-0.3.6}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiqa-client
3
- Version: 0.3.5
3
+ Version: 0.3.6
4
4
  Summary: OpenTelemetry-based Python client for tracing functions and sending traces to the AIQA server
5
5
  Author-email: AIQA <info@aiqa.dev>
6
6
  License: MIT
@@ -67,6 +67,9 @@ export AIQA_SERVER_URL="http://localhost:3000"
67
67
  export AIQA_API_KEY="your-api-key"
68
68
  ```
69
69
 
70
+ **Note:** If `AIQA_SERVER_URL` or `AIQA_API_KEY` are not set, tracing will be automatically disabled. You'll see one warning message at the start, and your application will continue to run without tracing.
71
+ You can check if tracing is enabled via `get_aiqa_client().enabled`.
72
+
70
73
  ## Usage
71
74
 
72
75
  ### Basic Usage
@@ -141,6 +144,30 @@ async def main():
141
144
  asyncio.run(main())
142
145
  ```
143
146
 
147
+ ### Enabling/Disabling Tracing
148
+
149
+ You can programmatically enable or disable tracing:
150
+
151
+ ```python
152
+ from aiqa import get_aiqa_client
153
+
154
+ client = get_aiqa_client()
155
+
156
+ # Disable tracing (spans won't be created or exported)
157
+ client.set_enabled(False)
158
+
159
+ # Re-enable tracing
160
+ client.set_enabled(True)
161
+
162
+ # Check if tracing is enabled
163
+ if client.enabled:
164
+ print("Tracing is enabled")
165
+ ```
166
+
167
+ When tracing is disabled:
168
+ - Spans are not created (functions execute normally without tracing overhead)
169
+ - Spans are not exported to the server
170
+
144
171
  ### Setting Span Attributes and Names
145
172
 
146
173
  ```python
@@ -30,6 +30,9 @@ export AIQA_SERVER_URL="http://localhost:3000"
30
30
  export AIQA_API_KEY="your-api-key"
31
31
  ```
32
32
 
33
+ **Note:** If `AIQA_SERVER_URL` or `AIQA_API_KEY` are not set, tracing will be automatically disabled. You'll see one warning message at the start, and your application will continue to run without tracing.
34
+ You can check if tracing is enabled via `get_aiqa_client().enabled`.
35
+
33
36
  ## Usage
34
37
 
35
38
  ### Basic Usage
@@ -104,6 +107,30 @@ async def main():
104
107
  asyncio.run(main())
105
108
  ```
106
109
 
110
+ ### Enabling/Disabling Tracing
111
+
112
+ You can programmatically enable or disable tracing:
113
+
114
+ ```python
115
+ from aiqa import get_aiqa_client
116
+
117
+ client = get_aiqa_client()
118
+
119
+ # Disable tracing (spans won't be created or exported)
120
+ client.set_enabled(False)
121
+
122
+ # Re-enable tracing
123
+ client.set_enabled(True)
124
+
125
+ # Check if tracing is enabled
126
+ if client.enabled:
127
+ print("Tracing is enabled")
128
+ ```
129
+
130
+ When tracing is disabled:
131
+ - Spans are not created (functions execute normally without tracing overhead)
132
+ - Spans are not exported to the server
133
+
107
134
  ### Setting Span Attributes and Names
108
135
 
109
136
  ```python
@@ -37,10 +37,10 @@ from .tracing import (
37
37
  set_component_tag,
38
38
  get_span,
39
39
  )
40
- from .client import get_aiqa_client
40
+ from .client import get_aiqa_client, set_enabled
41
41
  from .experiment_runner import ExperimentRunner
42
42
 
43
- __version__ = "0.3.5"
43
+ __version__ = "0.3.6"
44
44
 
45
45
  __all__ = [
46
46
  "WithTracing",
@@ -52,6 +52,7 @@ __all__ = [
52
52
  "get_provider",
53
53
  "get_exporter",
54
54
  "get_aiqa_client",
55
+ "set_enabled",
55
56
  "ExperimentRunner",
56
57
  "get_active_trace_id",
57
58
  "get_span_id",
@@ -71,6 +71,18 @@ class AIQASpanExporter(SpanExporter):
71
71
  if not spans:
72
72
  logger.debug("export() called with empty spans list")
73
73
  return SpanExportResult.SUCCESS
74
+
75
+ # Check if AIQA tracing is enabled
76
+ try:
77
+ from .client import get_aiqa_client
78
+ client = get_aiqa_client()
79
+ if not client.enabled:
80
+ logger.debug(f"AIQA export() skipped: tracing is disabled")
81
+ return SpanExportResult.SUCCESS
82
+ except Exception:
83
+ # If we can't check enabled status, proceed (fail open)
84
+ pass
85
+
74
86
  logger.debug(f"AIQA export() called with {len(spans)} spans")
75
87
  # Serialize and add to buffer, deduplicating by (traceId, spanId)
76
88
  with self.buffer_lock:
@@ -2,6 +2,7 @@
2
2
  import os
3
3
  import logging
4
4
  from functools import lru_cache
5
+ from typing import Optional
5
6
  from opentelemetry import trace
6
7
  from opentelemetry.sdk.trace import TracerProvider
7
8
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
@@ -31,10 +32,79 @@ from .aiqa_exporter import AIQASpanExporter
31
32
 
32
33
  AIQA_TRACER_NAME = "aiqa-tracer"
33
34
 
34
- client = {
35
- "provider": None,
36
- "exporter": None,
37
- }
35
+
36
+ class AIQAClient:
37
+ """
38
+ Singleton client for AIQA tracing.
39
+
40
+ This class manages the tracing provider, exporter, and enabled state.
41
+ Access via get_aiqa_client() which returns the singleton instance.
42
+ """
43
+ _instance: Optional['AIQAClient'] = None
44
+
45
+ def __new__(cls):
46
+ if cls._instance is None:
47
+ cls._instance = super().__new__(cls)
48
+ cls._instance._provider: Optional[TracerProvider] = None
49
+ cls._instance._exporter: Optional[AIQASpanExporter] = None
50
+ cls._instance._enabled: bool = True
51
+ cls._instance._initialized: bool = False
52
+ return cls._instance
53
+
54
+ @property
55
+ def provider(self) -> Optional[TracerProvider]:
56
+ """Get the tracer provider."""
57
+ return self._provider
58
+
59
+ @provider.setter
60
+ def provider(self, value: Optional[TracerProvider]) -> None:
61
+ """Set the tracer provider."""
62
+ self._provider = value
63
+
64
+ @property
65
+ def exporter(self) -> Optional[AIQASpanExporter]:
66
+ """Get the span exporter."""
67
+ return self._exporter
68
+
69
+ @exporter.setter
70
+ def exporter(self, value: Optional[AIQASpanExporter]) -> None:
71
+ """Set the span exporter."""
72
+ self._exporter = value
73
+
74
+ @property
75
+ def enabled(self) -> bool:
76
+ """Check if tracing is enabled."""
77
+ return self._enabled
78
+
79
+ @enabled.setter
80
+ def enabled(self, value: bool) -> None:
81
+ """Set the enabled state."""
82
+ self._enabled = value
83
+
84
+ def set_enabled(self, enabled: bool) -> None:
85
+ """
86
+ Enable or disable AIQA tracing.
87
+
88
+ When disabled:
89
+ - Tracing does not create spans
90
+ - Export does not send spans
91
+
92
+ Args:
93
+ enabled: True to enable tracing, False to disable
94
+ """
95
+ self._enabled = enabled
96
+ if enabled:
97
+ logger.info("AIQA tracing enabled")
98
+ else:
99
+ logger.info("AIQA tracing disabled")
100
+
101
+ def is_enabled(self) -> bool:
102
+ """Check if tracing is enabled."""
103
+ return self._enabled
104
+
105
+
106
+ # Global singleton instance (for backward compatibility with direct access)
107
+ client: AIQAClient = AIQAClient()
38
108
 
39
109
  # Component tag to add to all spans (can be set via AIQA_COMPONENT_TAG env var or programmatically)
40
110
  _component_tag: str = ""
@@ -52,14 +122,17 @@ def set_component_tag(tag: str | None) -> None:
52
122
 
53
123
 
54
124
  @lru_cache(maxsize=1)
55
- def get_aiqa_client():
125
+ def get_aiqa_client() -> AIQAClient:
56
126
  """
57
- Initialize and return the AIQA client.
127
+ Initialize and return the AIQA client singleton.
58
128
 
59
129
  This function must be called before using any AIQA tracing functionality to ensure
60
130
  that environment variables (such as AIQA_SERVER_URL, AIQA_API_KEY, AIQA_COMPONENT_TAG)
61
131
  are properly loaded and the tracing system is initialized.
62
132
 
133
+ The client object manages the tracing system state. Tracing is done by the WithTracing
134
+ decorator. Experiments are run by the ExperimentRunner class.
135
+
63
136
  The function is idempotent - calling it multiple times is safe and will only
64
137
  initialize once.
65
138
 
@@ -67,7 +140,7 @@ def get_aiqa_client():
67
140
  from aiqa import get_aiqa_client, WithTracing
68
141
 
69
142
  # Initialize client (loads env vars)
70
- get_aiqa_client()
143
+ client = get_aiqa_client()
71
144
 
72
145
  @WithTracing
73
146
  def my_function():
@@ -79,12 +152,32 @@ def get_aiqa_client():
79
152
  except Exception as e:
80
153
  logger.error(f"Failed to initialize AIQA tracing: {e}")
81
154
  logger.warning("AIQA tracing is disabled. Your application will continue to run without tracing.")
82
- # optionally return a richer client object; for now you just need init
83
155
  return client
84
156
 
85
157
  def _init_tracing():
86
158
  """Initialize tracing system and load configuration from environment variables."""
159
+ global client
160
+ if client._initialized:
161
+ return
162
+
87
163
  try:
164
+ # Check for required environment variables
165
+ server_url = os.getenv("AIQA_SERVER_URL")
166
+ api_key = os.getenv("AIQA_API_KEY")
167
+
168
+ if not server_url or not api_key:
169
+ client.enabled = False
170
+ missing_vars = []
171
+ if not server_url:
172
+ missing_vars.append("AIQA_SERVER_URL")
173
+ if not api_key:
174
+ missing_vars.append("AIQA_API_KEY")
175
+ logger.warning(
176
+ f"AIQA tracing is disabled: missing required environment variables: {', '.join(missing_vars)}"
177
+ )
178
+ client._initialized = True
179
+ return
180
+
88
181
  # Initialize component tag from environment variable
89
182
  set_component_tag(os.getenv("AIQA_COMPONENT_TAG", None))
90
183
 
@@ -114,15 +207,15 @@ def _init_tracing():
114
207
 
115
208
  # Idempotently add your processor
116
209
  _attach_aiqa_processor(provider)
117
- global client
118
- client["provider"] = provider
210
+ client.provider = provider
119
211
 
120
212
  # Log successful initialization
121
- server_url = os.getenv("AIQA_SERVER_URL", "not configured")
122
213
  logger.info(f"AIQA initialized and tracing (sampling rate: {sampling_rate:.2f}, server: {server_url})")
214
+ client._initialized = True
123
215
 
124
216
  except Exception as e:
125
217
  logger.error(f"Error initializing AIQA tracing: {e}")
218
+ client._initialized = True # Mark as initialized even on error to prevent retry loops
126
219
  raise
127
220
 
128
221
  def _attach_aiqa_processor(provider: TracerProvider):
@@ -140,7 +233,7 @@ def _attach_aiqa_processor(provider: TracerProvider):
140
233
  )
141
234
  provider.add_span_processor(BatchSpanProcessor(exporter))
142
235
  global client
143
- client["exporter"] = exporter
236
+ client.exporter = exporter
144
237
  logger.debug("AIQA span processor attached successfully")
145
238
  except Exception as e:
146
239
  logger.error(f"Error attaching AIQA span processor: {e}")
@@ -148,6 +241,27 @@ def _attach_aiqa_processor(provider: TracerProvider):
148
241
  raise
149
242
 
150
243
 
244
+ def set_enabled(enabled: bool) -> None:
245
+ """
246
+ Enable or disable AIQA tracing.
247
+
248
+ When disabled:
249
+ - Tracing does not create spans
250
+ - Export does not send spans
251
+
252
+ Args:
253
+ enabled: True to enable tracing, False to disable
254
+
255
+ Example:
256
+ from aiqa import get_aiqa_client
257
+
258
+ client = get_aiqa_client()
259
+ client.set_enabled(False) # Disable tracing
260
+ """
261
+ client = get_aiqa_client()
262
+ client.set_enabled(enabled)
263
+
264
+
151
265
  def get_aiqa_tracer():
152
266
  """
153
267
  Get the AIQA tracer with version from __init__.py __version__.
@@ -29,10 +29,10 @@ async def flush_tracing() -> None:
29
29
  This flushes both the BatchSpanProcessor and the exporter buffer.
30
30
  """
31
31
  client = get_aiqa_client()
32
- if client.get("provider"):
33
- client["provider"].force_flush() # Synchronous method
34
- if client.get("exporter"):
35
- await client["exporter"].flush()
32
+ if client.provider:
33
+ client.provider.force_flush() # Synchronous method
34
+ if client.exporter:
35
+ await client.exporter.flush()
36
36
 
37
37
 
38
38
  async def shutdown_tracing() -> None:
@@ -42,10 +42,10 @@ async def shutdown_tracing() -> None:
42
42
  """
43
43
  try:
44
44
  client = get_aiqa_client()
45
- if client.get("provider"):
46
- client["provider"].shutdown() # Synchronous method
47
- if client.get("exporter"):
48
- client["exporter"].shutdown() # Synchronous method
45
+ if client.provider:
46
+ client.provider.shutdown() # Synchronous method
47
+ if client.exporter:
48
+ client.exporter.shutdown() # Synchronous method
49
49
  except Exception as e:
50
50
  logger.error(f"Error shutting down tracing: {e}")
51
51
 
@@ -640,7 +640,10 @@ def WithTracing(
640
640
  def _execute_with_span_sync(executor: Callable[[], Any], input_data: Any) -> Any:
641
641
  """Execute sync function within span context, handling input/output and exceptions."""
642
642
  # Ensure tracer provider is initialized before creating spans
643
- get_aiqa_client()
643
+ client = get_aiqa_client()
644
+ if not client.enabled:
645
+ return executor()
646
+
644
647
  with tracer.start_as_current_span(fn_name) as span:
645
648
  if not _setup_span(span, input_data):
646
649
  return executor()
@@ -656,7 +659,10 @@ def WithTracing(
656
659
  async def _execute_with_span_async(executor: Callable[[], Any], input_data: Any) -> Any:
657
660
  """Execute async function within span context, handling input/output and exceptions."""
658
661
  # Ensure tracer provider is initialized before creating spans
659
- get_aiqa_client()
662
+ client = get_aiqa_client()
663
+ if not client.enabled:
664
+ return await executor()
665
+
660
666
  with tracer.start_as_current_span(fn_name) as span:
661
667
  if not _setup_span(span, input_data):
662
668
  return await executor()
@@ -675,7 +681,10 @@ def WithTracing(
675
681
  def _execute_generator_sync(executor: Callable[[], Any], input_data: Any) -> Any:
676
682
  """Execute sync generator function, returning a traced generator."""
677
683
  # Ensure tracer provider is initialized before creating spans
678
- get_aiqa_client()
684
+ client = get_aiqa_client()
685
+ if not client.enabled:
686
+ return executor()
687
+
679
688
  # Create span but don't use 'with' - span will be closed by TracedGenerator
680
689
  span = tracer.start_span(fn_name)
681
690
  token = trace.context_api.attach(trace.context_api.set_span_in_context(span))
@@ -698,7 +707,10 @@ def WithTracing(
698
707
  async def _execute_generator_async(executor: Callable[[], Any], input_data: Any) -> Any:
699
708
  """Execute async generator function, returning a traced async generator."""
700
709
  # Ensure tracer provider is initialized before creating spans
701
- get_aiqa_client()
710
+ client = get_aiqa_client()
711
+ if not client.enabled:
712
+ return await executor()
713
+
702
714
  # Create span but don't use 'with' - span will be closed by TracedAsyncGenerator
703
715
  span = tracer.start_span(fn_name)
704
716
  token = trace.context_api.attach(trace.context_api.set_span_in_context(span))
@@ -960,12 +972,12 @@ def set_component_tag(tag: str) -> None:
960
972
  def get_provider() -> Optional[TracerProvider]:
961
973
  """Get the tracer provider for advanced usage."""
962
974
  client = get_aiqa_client()
963
- return client.get("provider")
975
+ return client.provider
964
976
 
965
977
  def get_exporter() -> Optional[AIQASpanExporter]:
966
978
  """Get the exporter for advanced usage."""
967
979
  client = get_aiqa_client()
968
- return client.get("exporter")
980
+ return client.exporter
969
981
 
970
982
 
971
983
  def get_active_trace_id() -> Optional[str]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiqa-client
3
- Version: 0.3.5
3
+ Version: 0.3.6
4
4
  Summary: OpenTelemetry-based Python client for tracing functions and sending traces to the AIQA server
5
5
  Author-email: AIQA <info@aiqa.dev>
6
6
  License: MIT
@@ -67,6 +67,9 @@ export AIQA_SERVER_URL="http://localhost:3000"
67
67
  export AIQA_API_KEY="your-api-key"
68
68
  ```
69
69
 
70
+ **Note:** If `AIQA_SERVER_URL` or `AIQA_API_KEY` are not set, tracing will be automatically disabled. You'll see one warning message at the start, and your application will continue to run without tracing.
71
+ You can check if tracing is enabled via `get_aiqa_client().enabled`.
72
+
70
73
  ## Usage
71
74
 
72
75
  ### Basic Usage
@@ -141,6 +144,30 @@ async def main():
141
144
  asyncio.run(main())
142
145
  ```
143
146
 
147
+ ### Enabling/Disabling Tracing
148
+
149
+ You can programmatically enable or disable tracing:
150
+
151
+ ```python
152
+ from aiqa import get_aiqa_client
153
+
154
+ client = get_aiqa_client()
155
+
156
+ # Disable tracing (spans won't be created or exported)
157
+ client.set_enabled(False)
158
+
159
+ # Re-enable tracing
160
+ client.set_enabled(True)
161
+
162
+ # Check if tracing is enabled
163
+ if client.enabled:
164
+ print("Tracing is enabled")
165
+ ```
166
+
167
+ When tracing is disabled:
168
+ - Spans are not created (functions execute normally without tracing overhead)
169
+ - Spans are not exported to the server
170
+
144
171
  ### Setting Span Attributes and Names
145
172
 
146
173
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aiqa-client"
7
- version = "0.3.5"
7
+ version = "0.3.6"
8
8
  description = "OpenTelemetry-based Python client for tracing functions and sending traces to the AIQA server"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
File without changes
File without changes
File without changes
File without changes
File without changes