revenium-python-sdk 0.1.10__tar.gz → 0.2.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.
- {revenium_python_sdk-0.1.10/revenium_python_sdk.egg-info → revenium_python_sdk-0.2.0}/PKG-INFO +41 -1
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/README.md +40 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/pyproject.toml +1 -1
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/__init__.py +6 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/__init__.py +5 -1
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/enforcement.py +91 -23
- revenium_python_sdk-0.2.0/revenium_middleware/_core/log_sanitize.py +100 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/metering.py +77 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_core/metering_buffer.py +327 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/metering_submission.py +17 -3
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/decorator.py +101 -45
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/bedrock_adapter.py +14 -4
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/middleware.py +195 -20
- revenium_python_sdk-0.2.0/revenium_middleware/anthropic/stream_create.py +176 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/_metering.py +2 -2
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/utils.py +4 -4
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/google_ai/middleware.py +15 -3
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/vertex_ai/middleware.py +18 -4
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/middleware.py +27 -17
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/proxy/middleware.py +3 -3
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/ollama/middleware.py +53 -24
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/middleware.py +128 -78
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/middleware.py +2 -1
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/perplexity_sdk.py +2 -1
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0/revenium_python_sdk.egg-info}/PKG-INFO +41 -1
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/SOURCES.txt +3 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/LICENSE +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/config.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/context.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/decorators.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/exceptions.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/fields.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/patch_registry.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/prompt_extraction.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/subscriber.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/LICENSE +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_base_client.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_client.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_compat.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_constants.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_exceptions.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_files.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_models.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_qs.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_resource.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_response.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_streaming.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_types.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_logs.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_proxy.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_reflection.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_resources_proxy.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_streams.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_sync.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_transform.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_typing.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_utils/_utils.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/_version.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/context.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/py.typed +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/resources/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/resources/ai.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/resources/apis.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/resources/events.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/ai_create_audio_params.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/ai_create_completion_params.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/ai_create_image_params.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/ai_create_video_params.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/api_meter_request_params.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/api_meter_response_params.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/event_create_params.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_metering/types/metering_response_resource.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/agentic_outcomes.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/config.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/prompt_extractor.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/provider.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/config.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/middleware.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/exceptions.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/protocols.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/types.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/config.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/google_ai/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/google_ai/provider.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/prompt_extractor.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/vertex_ai/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/google/vertex_ai/provider.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/config.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/context.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/decorators.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/hooks.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/integrations/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/integrations/crewai.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/validation.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/proxy/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/ollama/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/ollama/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/azure_config.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/azure_model_resolver.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/config.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/exceptions.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/langchain/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/langchain/_utils.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/langchain/unified_handler.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/prompt_extractor.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/provider.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/provider.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/webhooks/__init__.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/webhooks/_verify.py +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/dependency_links.txt +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/requires.txt +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/top_level.txt +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/setup.cfg +0 -0
- {revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/tests/test_metering.py +0 -0
{revenium_python_sdk-0.1.10/revenium_python_sdk.egg-info → revenium_python_sdk-0.2.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: revenium-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: The official Revenium Python SDK — unified AI metering middleware for OpenAI, Anthropic, Google, Ollama, LiteLLM, Perplexity, and fal.ai.
|
|
5
5
|
Author-email: Revenium <support@revenium.io>
|
|
6
6
|
License: MIT
|
|
@@ -1316,6 +1316,44 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
|
|
|
1316
1316
|
|----------|-------------|
|
|
1317
1317
|
| `REVENIUM_METERING_API_KEY` | Your Revenium API key (starts with `hak_` or `rev_`) |
|
|
1318
1318
|
|
|
1319
|
+
### Configuring After Import
|
|
1320
|
+
|
|
1321
|
+
The metering client is normally built from the environment when
|
|
1322
|
+
`revenium_middleware` is first imported. If your credentials only become
|
|
1323
|
+
available later (a secrets-vault bootstrap, framework settings hooks, import
|
|
1324
|
+
ordering), you don't need to restart: as soon as `REVENIUM_METERING_API_KEY`
|
|
1325
|
+
appears in the environment, the next metered call picks it up automatically.
|
|
1326
|
+
You can also configure programmatically at any time:
|
|
1327
|
+
|
|
1328
|
+
```python
|
|
1329
|
+
import revenium_middleware
|
|
1330
|
+
|
|
1331
|
+
revenium_middleware.initialize_metering(
|
|
1332
|
+
api_key="hak_your_key", # defaults to REVENIUM_METERING_API_KEY
|
|
1333
|
+
base_url="https://api.revenium.ai", # defaults to REVENIUM_METERING_BASE_URL
|
|
1334
|
+
)
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
`initialize_metering()` returns `True` when metering is enabled after the
|
|
1338
|
+
call; invoke it with no arguments to re-read the environment.
|
|
1339
|
+
|
|
1340
|
+
### Delivery Resilience (Store-and-Forward)
|
|
1341
|
+
|
|
1342
|
+
Metering events that still fail after the client's own retries (network
|
|
1343
|
+
outages, 5xx, rate limiting) are not lost: they are held in a bounded
|
|
1344
|
+
in-memory buffer and replayed automatically in the background every 30
|
|
1345
|
+
seconds, reusing each event's original `Idempotency-Key` so replays can
|
|
1346
|
+
never double-bill. Permanent failures (401/403/404/422) are never buffered.
|
|
1347
|
+
The buffer holds up to 1000 events (oldest evicted first) for at most 24
|
|
1348
|
+
hours, and is drained on graceful shutdown. Inspect it programmatically:
|
|
1349
|
+
|
|
1350
|
+
```python
|
|
1351
|
+
from revenium_middleware import get_buffer_stats
|
|
1352
|
+
|
|
1353
|
+
print(get_buffer_stats())
|
|
1354
|
+
# {'size': 0, 'max_size': 1000, 'total_buffered': 3, 'total_replayed': 3, ...}
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1319
1357
|
### Optional Environment Variables
|
|
1320
1358
|
|
|
1321
1359
|
| Variable | Default | Description |
|
|
@@ -1334,6 +1372,8 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
|
|
|
1334
1372
|
| `REVENIUM_TRANSACTION_NAME` | - | Human-friendly operation name |
|
|
1335
1373
|
| `REVENIUM_RETRY_NUMBER` | - | Retry attempt number |
|
|
1336
1374
|
| `REVENIUM_BEDROCK_DISABLE` | - | Set to `1` to disable Bedrock auto-detection |
|
|
1375
|
+
| `REVENIUM_BUFFER_MAX_SIZE` | `1000` | Store-and-forward buffer capacity (oldest events evicted when full) |
|
|
1376
|
+
| `REVENIUM_BUFFER_FLUSH_INTERVAL` | `30` | Seconds between automatic replay attempts for buffered events |
|
|
1337
1377
|
|
|
1338
1378
|
### Provider-Specific Environment Variables
|
|
1339
1379
|
|
|
@@ -1223,6 +1223,44 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
|
|
|
1223
1223
|
|----------|-------------|
|
|
1224
1224
|
| `REVENIUM_METERING_API_KEY` | Your Revenium API key (starts with `hak_` or `rev_`) |
|
|
1225
1225
|
|
|
1226
|
+
### Configuring After Import
|
|
1227
|
+
|
|
1228
|
+
The metering client is normally built from the environment when
|
|
1229
|
+
`revenium_middleware` is first imported. If your credentials only become
|
|
1230
|
+
available later (a secrets-vault bootstrap, framework settings hooks, import
|
|
1231
|
+
ordering), you don't need to restart: as soon as `REVENIUM_METERING_API_KEY`
|
|
1232
|
+
appears in the environment, the next metered call picks it up automatically.
|
|
1233
|
+
You can also configure programmatically at any time:
|
|
1234
|
+
|
|
1235
|
+
```python
|
|
1236
|
+
import revenium_middleware
|
|
1237
|
+
|
|
1238
|
+
revenium_middleware.initialize_metering(
|
|
1239
|
+
api_key="hak_your_key", # defaults to REVENIUM_METERING_API_KEY
|
|
1240
|
+
base_url="https://api.revenium.ai", # defaults to REVENIUM_METERING_BASE_URL
|
|
1241
|
+
)
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
`initialize_metering()` returns `True` when metering is enabled after the
|
|
1245
|
+
call; invoke it with no arguments to re-read the environment.
|
|
1246
|
+
|
|
1247
|
+
### Delivery Resilience (Store-and-Forward)
|
|
1248
|
+
|
|
1249
|
+
Metering events that still fail after the client's own retries (network
|
|
1250
|
+
outages, 5xx, rate limiting) are not lost: they are held in a bounded
|
|
1251
|
+
in-memory buffer and replayed automatically in the background every 30
|
|
1252
|
+
seconds, reusing each event's original `Idempotency-Key` so replays can
|
|
1253
|
+
never double-bill. Permanent failures (401/403/404/422) are never buffered.
|
|
1254
|
+
The buffer holds up to 1000 events (oldest evicted first) for at most 24
|
|
1255
|
+
hours, and is drained on graceful shutdown. Inspect it programmatically:
|
|
1256
|
+
|
|
1257
|
+
```python
|
|
1258
|
+
from revenium_middleware import get_buffer_stats
|
|
1259
|
+
|
|
1260
|
+
print(get_buffer_stats())
|
|
1261
|
+
# {'size': 0, 'max_size': 1000, 'total_buffered': 3, 'total_replayed': 3, ...}
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1226
1264
|
### Optional Environment Variables
|
|
1227
1265
|
|
|
1228
1266
|
| Variable | Default | Description |
|
|
@@ -1241,6 +1279,8 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
|
|
|
1241
1279
|
| `REVENIUM_TRANSACTION_NAME` | - | Human-friendly operation name |
|
|
1242
1280
|
| `REVENIUM_RETRY_NUMBER` | - | Retry attempt number |
|
|
1243
1281
|
| `REVENIUM_BEDROCK_DISABLE` | - | Set to `1` to disable Bedrock auto-detection |
|
|
1282
|
+
| `REVENIUM_BUFFER_MAX_SIZE` | `1000` | Store-and-forward buffer capacity (oldest events evicted when full) |
|
|
1283
|
+
| `REVENIUM_BUFFER_FLUSH_INTERVAL` | `30` | Seconds between automatic replay attempts for buffered events |
|
|
1244
1284
|
|
|
1245
1285
|
### Provider-Specific Environment Variables
|
|
1246
1286
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "revenium-python-sdk"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "The official Revenium Python SDK — unified AI metering middleware for OpenAI, Anthropic, Google, Ollama, LiteLLM, Perplexity, and fal.ai."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -138,6 +138,9 @@ logger.propagate = True
|
|
|
138
138
|
# Existing imports like `from revenium_middleware import client` keep working.
|
|
139
139
|
from ._core import ( # noqa: E402
|
|
140
140
|
client,
|
|
141
|
+
get_buffer_stats,
|
|
142
|
+
get_client,
|
|
143
|
+
initialize_metering,
|
|
141
144
|
run_async_in_thread,
|
|
142
145
|
shutdown_event,
|
|
143
146
|
revenium_meter,
|
|
@@ -166,6 +169,9 @@ from .agentic_outcomes import AgenticOutcomeClient, AgenticOutcomeSettings # no
|
|
|
166
169
|
__all__ = [
|
|
167
170
|
# Metering exports
|
|
168
171
|
"client",
|
|
172
|
+
"get_buffer_stats",
|
|
173
|
+
"get_client",
|
|
174
|
+
"initialize_metering",
|
|
169
175
|
"run_async_in_thread",
|
|
170
176
|
"shutdown_event",
|
|
171
177
|
# Decorator exports
|
{revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/__init__.py
RENAMED
|
@@ -5,7 +5,8 @@ This subpackage contains the foundational components shared across all
|
|
|
5
5
|
provider-specific middleware implementations.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .metering import run_async_in_thread, shutdown_event, client
|
|
8
|
+
from .metering import run_async_in_thread, shutdown_event, client, get_client, initialize_metering
|
|
9
|
+
from .metering_buffer import get_buffer_stats
|
|
9
10
|
from .metering_submission import submit_ai_event
|
|
10
11
|
from .exceptions import BudgetExceededError
|
|
11
12
|
from .enforcement import check_enforcement, is_circuit_breaker_enabled, stop_polling
|
|
@@ -50,6 +51,9 @@ from .trace_fields import (
|
|
|
50
51
|
__all__ = [
|
|
51
52
|
# Metering
|
|
52
53
|
"client",
|
|
54
|
+
"get_client",
|
|
55
|
+
"initialize_metering",
|
|
56
|
+
"get_buffer_stats",
|
|
53
57
|
"run_async_in_thread",
|
|
54
58
|
"shutdown_event",
|
|
55
59
|
"submit_ai_event",
|
{revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/enforcement.py
RENAMED
|
@@ -10,11 +10,13 @@ Opt-in via ``REVENIUM_CIRCUIT_BREAKER_ENABLED``. Disabled by default so the
|
|
|
10
10
|
SDK stays no-op for callers who haven't enrolled in cost controls.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import datetime
|
|
13
14
|
import json
|
|
14
15
|
import logging
|
|
15
16
|
import os
|
|
16
17
|
import threading
|
|
17
18
|
import time
|
|
19
|
+
from email.utils import parsedate_to_datetime
|
|
18
20
|
from typing import List, Optional, Tuple
|
|
19
21
|
from urllib.parse import quote, urlparse
|
|
20
22
|
|
|
@@ -27,6 +29,14 @@ logger = logging.getLogger("revenium_middleware.extension")
|
|
|
27
29
|
|
|
28
30
|
_DEFAULT_POLL_INTERVAL = 60 # seconds between background refreshes
|
|
29
31
|
_CACHE_TTL = 120 # seconds before a cached rule is considered stale
|
|
32
|
+
|
|
33
|
+
# Retry posture for the rule fetch (fleet backoff consistency): transient
|
|
34
|
+
# failures ride out a spike instead of leaving spend controls on stale
|
|
35
|
+
# limits until the next poll interval.
|
|
36
|
+
_FETCH_MAX_ATTEMPTS = 5
|
|
37
|
+
_FETCH_BACKOFF_INITIAL = 0.5 # seconds; doubles per attempt
|
|
38
|
+
_FETCH_BACKOFF_CAP = 8.0 # seconds; also clamps Retry-After
|
|
39
|
+
_FETCH_RETRYABLE_STATUS = frozenset({408, 429}) # plus any 5xx
|
|
30
40
|
_RULES_CACHE_FILENAME = "revenium_enforcement_rules.json"
|
|
31
41
|
|
|
32
42
|
_cached_rules: List[dict] = []
|
|
@@ -142,6 +152,37 @@ def _persist_cache_to_disk(rules: list) -> None:
|
|
|
142
152
|
logger.debug("Failed to write enforcement cache to %s", path, exc_info=True)
|
|
143
153
|
|
|
144
154
|
|
|
155
|
+
def _sleep(seconds: float) -> bool:
|
|
156
|
+
"""Backoff wait, interruptible by shutdown so the poller exits promptly.
|
|
157
|
+
|
|
158
|
+
Returns True when shutdown was signalled, so callers can abort their
|
|
159
|
+
retry loop instead of burning the remaining attempts with zero delay.
|
|
160
|
+
"""
|
|
161
|
+
return _stop_event.wait(seconds)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _is_retryable_status(status_code: int) -> bool:
|
|
165
|
+
return status_code in _FETCH_RETRYABLE_STATUS or status_code >= 500
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _retry_after_seconds(response: "httpx.Response") -> Optional[float]:
|
|
169
|
+
"""Parse Retry-After (delta-seconds or HTTP-date), clamped to the cap."""
|
|
170
|
+
raw = response.headers.get("Retry-After")
|
|
171
|
+
if not raw:
|
|
172
|
+
return None
|
|
173
|
+
try:
|
|
174
|
+
delay = float(raw)
|
|
175
|
+
except ValueError:
|
|
176
|
+
try:
|
|
177
|
+
when = parsedate_to_datetime(raw)
|
|
178
|
+
if when.tzinfo is None:
|
|
179
|
+
when = when.replace(tzinfo=datetime.timezone.utc)
|
|
180
|
+
delay = (when - datetime.datetime.now(datetime.timezone.utc)).total_seconds()
|
|
181
|
+
except Exception:
|
|
182
|
+
return None
|
|
183
|
+
return max(0.0, min(delay, _FETCH_BACKOFF_CAP))
|
|
184
|
+
|
|
185
|
+
|
|
145
186
|
def _fetch_rules() -> Optional[list]:
|
|
146
187
|
"""Fetch current enforcement rules from the Revenium API.
|
|
147
188
|
|
|
@@ -170,29 +211,56 @@ def _fetch_rules() -> Optional[list]:
|
|
|
170
211
|
# containing '/', '..', or '?' cannot retarget the request to a
|
|
171
212
|
# different endpoint on the same origin.
|
|
172
213
|
safe_team_id = quote(team_id, safe="")
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
timeout=10
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
214
|
+
url = f"{base_url}/v2/api/ai/enforcement-rules/{safe_team_id}"
|
|
215
|
+
for attempt in range(_FETCH_MAX_ATTEMPTS):
|
|
216
|
+
backoff = min(_FETCH_BACKOFF_INITIAL * (2 ** attempt), _FETCH_BACKOFF_CAP)
|
|
217
|
+
try:
|
|
218
|
+
response = httpx.get(url, headers={"x-api-key": api_key}, timeout=10)
|
|
219
|
+
except (httpx.TimeoutException, httpx.TransportError):
|
|
220
|
+
if attempt == _FETCH_MAX_ATTEMPTS - 1:
|
|
221
|
+
break
|
|
222
|
+
if _sleep(backoff):
|
|
223
|
+
break
|
|
224
|
+
continue
|
|
225
|
+
except Exception:
|
|
226
|
+
logger.debug("Failed to fetch enforcement rules, falling open", exc_info=True)
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
if _is_retryable_status(response.status_code):
|
|
230
|
+
if attempt == _FETCH_MAX_ATTEMPTS - 1:
|
|
231
|
+
break
|
|
232
|
+
delay = _retry_after_seconds(response)
|
|
233
|
+
if _sleep(delay if delay is not None else backoff):
|
|
234
|
+
break
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
# 204 No Content == no rules configured for this team; cache empty list
|
|
239
|
+
if response.status_code == 204:
|
|
240
|
+
return []
|
|
241
|
+
response.raise_for_status()
|
|
242
|
+
data = response.json()
|
|
243
|
+
# Server currently returns ``{"rules": [...], "compiledAt": ...}`` but
|
|
244
|
+
# accept a bare list too so a future schema change does not silently
|
|
245
|
+
# AttributeError its way into a stale cache.
|
|
246
|
+
if isinstance(data, list):
|
|
247
|
+
return data
|
|
248
|
+
if isinstance(data, dict):
|
|
249
|
+
return data.get("rules", [])
|
|
250
|
+
logger.warning("Unexpected enforcement response shape: %r", type(data).__name__)
|
|
251
|
+
return None
|
|
252
|
+
except Exception:
|
|
253
|
+
logger.debug("Failed to fetch enforcement rules, falling open", exc_info=True)
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
# Deliberate fail-open: an enforcement-refresh outage must never become a
|
|
257
|
+
# customer traffic outage. The caller preserves the previous cache and the
|
|
258
|
+
# next poll cycle tries again.
|
|
259
|
+
logger.warning(
|
|
260
|
+
"Enforcement rule fetch exhausted %d attempts; failing open on the previous cache",
|
|
261
|
+
_FETCH_MAX_ATTEMPTS,
|
|
262
|
+
)
|
|
263
|
+
return None
|
|
196
264
|
|
|
197
265
|
|
|
198
266
|
def _refresh_cache() -> None:
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared log sanitization for all Revenium provider middlewares.
|
|
3
|
+
|
|
4
|
+
Debug logging must never emit raw request kwargs: auth material (API keys,
|
|
5
|
+
bearer tokens, headers) and prompt/message content (potential PII) are
|
|
6
|
+
redacted before anything hits application logs.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
logger.debug("Calling wrapped function with args: %s, kwargs: %s",
|
|
10
|
+
sanitize_for_logging(args), sanitize_for_logging(kwargs))
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
__all__ = ["sanitize_for_logging"]
|
|
16
|
+
|
|
17
|
+
# Maximum recursion depth when sanitizing nested structures.
|
|
18
|
+
MAX_SANITIZATION_DEPTH: int = 8
|
|
19
|
+
|
|
20
|
+
# Strings longer than this are truncated (they may embed sensitive data).
|
|
21
|
+
MAX_LOG_STRING_LENGTH: int = 100
|
|
22
|
+
|
|
23
|
+
# Case-insensitive substring markers: any dict key containing one of these
|
|
24
|
+
# has its value replaced with "[REDACTED]".
|
|
25
|
+
SENSITIVE_KEY_MARKERS = frozenset(
|
|
26
|
+
{
|
|
27
|
+
"api_key",
|
|
28
|
+
"apikey",
|
|
29
|
+
"api-key",
|
|
30
|
+
"secret",
|
|
31
|
+
"token",
|
|
32
|
+
"password",
|
|
33
|
+
"credential",
|
|
34
|
+
"authorization",
|
|
35
|
+
"auth",
|
|
36
|
+
"bearer",
|
|
37
|
+
"private",
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Case-insensitive exact-match keys whose values carry prompt/message content
|
|
42
|
+
# (potential PII). Values are summarized without recursing into them.
|
|
43
|
+
CONTENT_FIELD_NAMES = frozenset(
|
|
44
|
+
{
|
|
45
|
+
"messages",
|
|
46
|
+
"prompt",
|
|
47
|
+
"system",
|
|
48
|
+
"input",
|
|
49
|
+
"contents",
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
_REDACTED = "[REDACTED]"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _summarize_content(value: Any) -> str:
|
|
57
|
+
"""Summarize prompt/message content without exposing (or recursing into) it."""
|
|
58
|
+
if isinstance(value, (list, tuple)):
|
|
59
|
+
return f"[REDACTED: {len(value)} items]"
|
|
60
|
+
return _REDACTED
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def sanitize_for_logging(data: Any, max_depth: int = MAX_SANITIZATION_DEPTH) -> Any:
|
|
64
|
+
"""
|
|
65
|
+
Sanitize data for secure logging.
|
|
66
|
+
|
|
67
|
+
- Values under keys containing a sensitive marker (api_key, token,
|
|
68
|
+
authorization, ...) are replaced with "[REDACTED]".
|
|
69
|
+
- Values under prompt/message content keys (messages, prompt, system,
|
|
70
|
+
input, contents) are summarized without recursing into them.
|
|
71
|
+
- Long strings are truncated to avoid leaking embedded payloads.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
data: Data to sanitize (dict, list/tuple, or primitive)
|
|
75
|
+
max_depth: Maximum recursion depth to prevent runaway recursion
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Sanitized data safe for logging
|
|
79
|
+
"""
|
|
80
|
+
if max_depth <= 0:
|
|
81
|
+
return "[MAX_DEPTH_REACHED]"
|
|
82
|
+
|
|
83
|
+
if isinstance(data, dict):
|
|
84
|
+
sanitized = {}
|
|
85
|
+
for key, value in data.items():
|
|
86
|
+
key_lower = str(key).lower()
|
|
87
|
+
if key_lower in CONTENT_FIELD_NAMES:
|
|
88
|
+
sanitized[key] = _summarize_content(value)
|
|
89
|
+
elif any(marker in key_lower for marker in SENSITIVE_KEY_MARKERS):
|
|
90
|
+
sanitized[key] = _REDACTED
|
|
91
|
+
else:
|
|
92
|
+
sanitized[key] = sanitize_for_logging(value, max_depth - 1)
|
|
93
|
+
return sanitized
|
|
94
|
+
elif isinstance(data, (list, tuple)):
|
|
95
|
+
return [sanitize_for_logging(item, max_depth - 1) for item in data]
|
|
96
|
+
elif isinstance(data, str) and len(data) > MAX_LOG_STRING_LENGTH:
|
|
97
|
+
# Truncate very long strings that might contain sensitive data
|
|
98
|
+
return data[:MAX_LOG_STRING_LENGTH] + "...[TRUNCATED]"
|
|
99
|
+
else:
|
|
100
|
+
return data
|
{revenium_python_sdk-0.1.10 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/metering.py
RENAMED
|
@@ -55,6 +55,73 @@ api_key = os.environ.get("REVENIUM_METERING_API_KEY")
|
|
|
55
55
|
_base_url_raw = os.environ.get("REVENIUM_METERING_BASE_URL")
|
|
56
56
|
client = _build_metering_client(api_key, _base_url_raw)
|
|
57
57
|
|
|
58
|
+
# Remembers a malformed env key so the lazy path doesn't re-raise per call.
|
|
59
|
+
_last_failed_key: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _sync_client_reexports(new_client: Optional[ReveniumMetering]) -> None:
|
|
63
|
+
"""Point the well-known re-export attributes at ``new_client``.
|
|
64
|
+
|
|
65
|
+
Takes the client explicitly (rather than reading the module-level global)
|
|
66
|
+
so concurrent rebuilds cannot interleave and leave the re-exports pointing
|
|
67
|
+
at a different instance than the caller just assigned. Dynamic readers
|
|
68
|
+
(``revenium_middleware.client``, ``revenium_middleware._core.client``)
|
|
69
|
+
observe the new client. Modules that bound the name via
|
|
70
|
+
``from revenium_middleware import client`` at import time keep their
|
|
71
|
+
snapshot -- code paths that matter resolve through ``get_client()``
|
|
72
|
+
instead.
|
|
73
|
+
"""
|
|
74
|
+
import sys
|
|
75
|
+
for module_name in ("revenium_middleware", "revenium_middleware._core"):
|
|
76
|
+
module = sys.modules.get(module_name)
|
|
77
|
+
if module is not None and hasattr(module, "client"):
|
|
78
|
+
module.client = new_client
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def initialize_metering(api_key: Optional[str] = None, base_url: Optional[str] = None) -> bool:
|
|
82
|
+
"""(Re)build the metering client from explicit values or the environment.
|
|
83
|
+
|
|
84
|
+
Call this when credentials only become available after the first
|
|
85
|
+
``revenium_middleware`` import (vault/parameter-store bootstraps, framework
|
|
86
|
+
settings hooks) or to reconfigure at runtime. Explicit arguments take
|
|
87
|
+
precedence over ``REVENIUM_METERING_API_KEY`` / ``REVENIUM_METERING_BASE_URL``.
|
|
88
|
+
|
|
89
|
+
Returns True when metering is enabled after the call. Raises ``ValueError``
|
|
90
|
+
for a malformed API key -- explicit configuration fails loudly, unlike the
|
|
91
|
+
lazy ``get_client()`` path, which logs a warning and leaves metering
|
|
92
|
+
disabled (best-effort by design).
|
|
93
|
+
"""
|
|
94
|
+
global client
|
|
95
|
+
key = api_key if api_key is not None else os.environ.get("REVENIUM_METERING_API_KEY")
|
|
96
|
+
url = base_url if base_url is not None else os.environ.get("REVENIUM_METERING_BASE_URL")
|
|
97
|
+
new_client = _build_metering_client(key, url)
|
|
98
|
+
client = new_client
|
|
99
|
+
_sync_client_reexports(new_client)
|
|
100
|
+
return new_client is not None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_client() -> Optional[ReveniumMetering]:
|
|
104
|
+
"""Return the metering client, retrying the environment when unset.
|
|
105
|
+
|
|
106
|
+
Covers env vars populated after import: as soon as
|
|
107
|
+
``REVENIUM_METERING_API_KEY`` appears, the next metering event builds the
|
|
108
|
+
client instead of silently no-opping forever.
|
|
109
|
+
|
|
110
|
+
Best-effort by design: a malformed env key is logged once and metering
|
|
111
|
+
stays disabled, whereas the explicit ``initialize_metering()`` raises
|
|
112
|
+
``ValueError`` so programmatic misconfiguration fails loudly.
|
|
113
|
+
"""
|
|
114
|
+
global _last_failed_key
|
|
115
|
+
if client is None:
|
|
116
|
+
env_key = os.environ.get("REVENIUM_METERING_API_KEY")
|
|
117
|
+
if env_key and env_key != _last_failed_key:
|
|
118
|
+
try:
|
|
119
|
+
initialize_metering()
|
|
120
|
+
except ValueError as e:
|
|
121
|
+
logger.warning("Deferred metering initialization failed: %s", e)
|
|
122
|
+
_last_failed_key = env_key
|
|
123
|
+
return client
|
|
124
|
+
|
|
58
125
|
_threads_lock = threading.Lock()
|
|
59
126
|
active_threads = []
|
|
60
127
|
shutdown_event = threading.Event()
|
|
@@ -78,6 +145,16 @@ def handle_exit(signum=None, frame=None):
|
|
|
78
145
|
shutdown_event.set()
|
|
79
146
|
|
|
80
147
|
|
|
148
|
+
# Drain the store-and-forward buffer (bounded) before joining metering
|
|
149
|
+
# threads, so events that already exhausted retries get a final attempt.
|
|
150
|
+
try:
|
|
151
|
+
from revenium_middleware._core import metering_buffer
|
|
152
|
+
|
|
153
|
+
if metering_buffer._buffer is not None:
|
|
154
|
+
metering_buffer._buffer.flush(deadline_seconds=5.0)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.debug("Metering buffer drain during shutdown failed: %s", e)
|
|
157
|
+
|
|
81
158
|
with _threads_lock:
|
|
82
159
|
threads_to_join = list(active_threads)
|
|
83
160
|
for thread in threads_to_join:
|