revenium-python-sdk 0.1.9__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.9 → revenium_python_sdk-0.2.0}/PKG-INFO +48 -3
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/README.md +41 -1
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/pyproject.toml +10 -2
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/__init__.py +9 -3
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/__init__.py +5 -1
- {revenium_python_sdk-0.1.9 → 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.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/metering.py +78 -1
- revenium_python_sdk-0.2.0/revenium_middleware/_core/metering_buffer.py +327 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/metering_submission.py +17 -3
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/LICENSE +21 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/__init__.py +112 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_base_client.py +1963 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_client.py +513 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_compat.py +219 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_constants.py +14 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_exceptions.py +108 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_files.py +123 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_models.py +805 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_qs.py +150 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_resource.py +43 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_response.py +832 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_streaming.py +333 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_types.py +219 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/__init__.py +57 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_logs.py +25 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_proxy.py +65 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_reflection.py +42 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_resources_proxy.py +24 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_streams.py +12 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_sync.py +86 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_transform.py +447 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_typing.py +151 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_utils/_utils.py +422 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/_version.py +4 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/context.py +234 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/decorator.py +438 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/py.typed +0 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/resources/__init__.py +47 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/resources/ai.py +1870 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/resources/apis.py +459 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/resources/events.py +262 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/__init__.py +12 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/ai_create_audio_params.py +203 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/ai_create_completion_params.py +268 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/ai_create_image_params.py +173 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/ai_create_video_params.py +176 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/api_meter_request_params.py +86 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/api_meter_response_params.py +37 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/event_create_params.py +59 -0
- revenium_python_sdk-0.2.0/revenium_middleware/_metering/types/metering_response_resource.py +49 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/agentic_outcomes.py +2 -2
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/bedrock_adapter.py +14 -4
- {revenium_python_sdk-0.1.9 → 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.9 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/_metering.py +2 -2
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/utils.py +4 -4
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/google_ai/middleware.py +15 -3
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/vertex_ai/middleware.py +18 -4
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/middleware.py +27 -17
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/proxy/middleware.py +3 -3
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/ollama/middleware.py +53 -24
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/middleware.py +128 -78
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/middleware.py +2 -1
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/perplexity_sdk.py +2 -1
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/PKG-INFO +48 -3
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/SOURCES.txt +44 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/requires.txt +6 -1
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/LICENSE +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/config.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/context.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/decorators.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/exceptions.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/fields.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/patch_registry.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/prompt_extraction.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/subscriber.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/config.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/prompt_extractor.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/provider.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/anthropic/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/config.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/middleware.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/fal/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/exceptions.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/protocols.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/common/types.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/config.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/google_ai/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/google_ai/provider.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/prompt_extractor.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/vertex_ai/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/google/vertex_ai/provider.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/config.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/context.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/decorators.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/hooks.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/integrations/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/integrations/crewai.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/client/validation.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/litellm/proxy/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/ollama/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/ollama/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/azure_config.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/azure_model_resolver.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/config.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/exceptions.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/langchain/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/langchain/_utils.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/langchain/unified_handler.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/prompt_extractor.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/provider.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/openai/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/provider.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/perplexity/trace_fields.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/webhooks/__init__.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_middleware/webhooks/_verify.py +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/dependency_links.txt +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/revenium_python_sdk.egg-info/top_level.txt +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/setup.cfg +0 -0
- {revenium_python_sdk-0.1.9 → revenium_python_sdk-0.2.0}/tests/test_metering.py +0 -0
|
@@ -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
|
|
@@ -22,7 +22,12 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
22
22
|
Requires-Python: >=3.8
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist:
|
|
25
|
+
Requires-Dist: anyio<5,>=3.5.0
|
|
26
|
+
Requires-Dist: distro<2,>=1.7.0
|
|
27
|
+
Requires-Dist: httpx<1,>=0.23.0
|
|
28
|
+
Requires-Dist: pydantic<3,>=1.9.0
|
|
29
|
+
Requires-Dist: sniffio
|
|
30
|
+
Requires-Dist: typing-extensions<5,>=4.10
|
|
26
31
|
Provides-Extra: openai
|
|
27
32
|
Requires-Dist: wrapt>=1.14.0; extra == "openai"
|
|
28
33
|
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
@@ -1109,7 +1114,7 @@ def free_feature(prompt: str) -> str:
|
|
|
1109
1114
|
|
|
1110
1115
|
## Tool Metering
|
|
1111
1116
|
|
|
1112
|
-
The `@meter_tool` decorator lets you meter arbitrary tool/function calls (web scrapers, database lookups, API fetchers, image generators, etc.) alongside your automatic LLM API metering.
|
|
1117
|
+
The `@meter_tool` decorator lets you meter arbitrary tool/function calls (web scrapers, database lookups, API fetchers, image generators, etc.) alongside your automatic LLM API metering.
|
|
1113
1118
|
|
|
1114
1119
|
```python
|
|
1115
1120
|
import os
|
|
@@ -1311,6 +1316,44 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
|
|
|
1311
1316
|
|----------|-------------|
|
|
1312
1317
|
| `REVENIUM_METERING_API_KEY` | Your Revenium API key (starts with `hak_` or `rev_`) |
|
|
1313
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
|
+
|
|
1314
1357
|
### Optional Environment Variables
|
|
1315
1358
|
|
|
1316
1359
|
| Variable | Default | Description |
|
|
@@ -1329,6 +1372,8 @@ See [`examples/openai/openai_blocking_demo.py`](examples/openai/openai_blocking_
|
|
|
1329
1372
|
| `REVENIUM_TRANSACTION_NAME` | - | Human-friendly operation name |
|
|
1330
1373
|
| `REVENIUM_RETRY_NUMBER` | - | Retry attempt number |
|
|
1331
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 |
|
|
1332
1377
|
|
|
1333
1378
|
### Provider-Specific Environment Variables
|
|
1334
1379
|
|
|
@@ -1021,7 +1021,7 @@ def free_feature(prompt: str) -> str:
|
|
|
1021
1021
|
|
|
1022
1022
|
## Tool Metering
|
|
1023
1023
|
|
|
1024
|
-
The `@meter_tool` decorator lets you meter arbitrary tool/function calls (web scrapers, database lookups, API fetchers, image generators, etc.) alongside your automatic LLM API metering.
|
|
1024
|
+
The `@meter_tool` decorator lets you meter arbitrary tool/function calls (web scrapers, database lookups, API fetchers, image generators, etc.) alongside your automatic LLM API metering.
|
|
1025
1025
|
|
|
1026
1026
|
```python
|
|
1027
1027
|
import os
|
|
@@ -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"
|
|
@@ -13,7 +13,12 @@ authors = [
|
|
|
13
13
|
{ name = "Revenium", email = "support@revenium.io" }
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
|
-
"
|
|
16
|
+
"anyio<5,>=3.5.0",
|
|
17
|
+
"distro<2,>=1.7.0",
|
|
18
|
+
"httpx<1,>=0.23.0",
|
|
19
|
+
"pydantic<3,>=1.9.0",
|
|
20
|
+
"sniffio",
|
|
21
|
+
"typing-extensions<5,>=4.10",
|
|
17
22
|
]
|
|
18
23
|
keywords = ["sdk", "ai", "llm", "middleware", "metering", "revenium", "openai", "anthropic", "google", "ollama", "litellm", "perplexity", "fal"]
|
|
19
24
|
classifiers = [
|
|
@@ -123,3 +128,6 @@ addopts = "-m 'not e2e and not integration'"
|
|
|
123
128
|
|
|
124
129
|
[tool.setuptools.packages.find]
|
|
125
130
|
include = ["revenium_middleware*"]
|
|
131
|
+
|
|
132
|
+
[tool.setuptools.package-data]
|
|
133
|
+
"revenium_middleware._metering" = ["py.typed", "LICENSE"]
|
|
@@ -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,
|
|
@@ -157,8 +160,8 @@ from ._core import ( # noqa: E402
|
|
|
157
160
|
is_selective_metering_enabled,
|
|
158
161
|
)
|
|
159
162
|
|
|
160
|
-
# Re-export tool metering utilities from
|
|
161
|
-
from
|
|
163
|
+
# Re-export tool metering utilities from the in-package metering module (BACK-2151)
|
|
164
|
+
from ._metering import meter_tool, report_tool_call, configure # noqa: E402
|
|
162
165
|
|
|
163
166
|
# Agentic-outcome client (used by examples/agentic_outcomes/ pack)
|
|
164
167
|
from .agentic_outcomes import AgenticOutcomeClient, AgenticOutcomeSettings # noqa: E402
|
|
@@ -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
|
|
@@ -186,7 +192,7 @@ __all__ = [
|
|
|
186
192
|
"set_idempotency_key",
|
|
187
193
|
# Config exports
|
|
188
194
|
"is_selective_metering_enabled",
|
|
189
|
-
# Tool metering exports
|
|
195
|
+
# Tool metering exports
|
|
190
196
|
"meter_tool",
|
|
191
197
|
"report_tool_call",
|
|
192
198
|
"configure",
|
{revenium_python_sdk-0.1.9 → 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.9 → 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.9 → revenium_python_sdk-0.2.0}/revenium_middleware/_core/metering.py
RENAMED
|
@@ -7,7 +7,7 @@ import contextvars
|
|
|
7
7
|
import atexit
|
|
8
8
|
import signal
|
|
9
9
|
from typing import Literal, Awaitable, Any, Optional, Callable
|
|
10
|
-
from
|
|
10
|
+
from revenium_middleware._metering import ReveniumMetering
|
|
11
11
|
from revenium_middleware._core.config import validate_api_key
|
|
12
12
|
|
|
13
13
|
# Get the logger that was configured in __init__.py
|
|
@@ -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:
|