revenium-python-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- revenium_middleware/__init__.py +184 -0
- revenium_middleware/_core/__init__.py +65 -0
- revenium_middleware/_core/config.py +165 -0
- revenium_middleware/_core/context.py +109 -0
- revenium_middleware/_core/decorators.py +202 -0
- revenium_middleware/_core/metering.py +207 -0
- revenium_middleware/_core/prompt_extraction.py +55 -0
- revenium_middleware/_core/subscriber.py +51 -0
- revenium_middleware/_core/trace_fields.py +265 -0
- revenium_middleware/anthropic/__init__.py +108 -0
- revenium_middleware/anthropic/bedrock_adapter.py +753 -0
- revenium_middleware/anthropic/config.py +29 -0
- revenium_middleware/anthropic/middleware.py +1070 -0
- revenium_middleware/anthropic/prompt_extractor.py +178 -0
- revenium_middleware/anthropic/provider.py +141 -0
- revenium_middleware/anthropic/summary_printer.py +286 -0
- revenium_middleware/anthropic/trace_fields.py +158 -0
- revenium_middleware/google/__init__.py +114 -0
- revenium_middleware/google/common/__init__.py +127 -0
- revenium_middleware/google/common/exceptions.py +137 -0
- revenium_middleware/google/common/protocols.py +192 -0
- revenium_middleware/google/common/summary_printer.py +271 -0
- revenium_middleware/google/common/trace_fields.py +205 -0
- revenium_middleware/google/common/types.py +208 -0
- revenium_middleware/google/common/utils.py +1111 -0
- revenium_middleware/google/config.py +64 -0
- revenium_middleware/google/google_ai/__init__.py +53 -0
- revenium_middleware/google/google_ai/middleware.py +667 -0
- revenium_middleware/google/google_ai/provider.py +135 -0
- revenium_middleware/google/prompt_extractor.py +396 -0
- revenium_middleware/google/vertex_ai/__init__.py +56 -0
- revenium_middleware/google/vertex_ai/middleware.py +1162 -0
- revenium_middleware/google/vertex_ai/provider.py +99 -0
- revenium_middleware/litellm/__init__.py +25 -0
- revenium_middleware/litellm/client/__init__.py +81 -0
- revenium_middleware/litellm/client/config.py +53 -0
- revenium_middleware/litellm/client/context.py +198 -0
- revenium_middleware/litellm/client/decorators.py +912 -0
- revenium_middleware/litellm/client/hooks.py +192 -0
- revenium_middleware/litellm/client/integrations/__init__.py +26 -0
- revenium_middleware/litellm/client/integrations/crewai.py +446 -0
- revenium_middleware/litellm/client/middleware.py +321 -0
- revenium_middleware/litellm/client/summary_printer.py +314 -0
- revenium_middleware/litellm/client/trace_fields.py +51 -0
- revenium_middleware/litellm/client/validation.py +207 -0
- revenium_middleware/litellm/proxy/__init__.py +25 -0
- revenium_middleware/litellm/proxy/middleware.py +217 -0
- revenium_middleware/ollama/__init__.py +28 -0
- revenium_middleware/ollama/middleware.py +569 -0
- revenium_middleware/ollama/trace_fields.py +63 -0
- revenium_middleware/openai/__init__.py +23 -0
- revenium_middleware/openai/azure_config.py +169 -0
- revenium_middleware/openai/azure_model_resolver.py +219 -0
- revenium_middleware/openai/config.py +45 -0
- revenium_middleware/openai/exceptions.py +115 -0
- revenium_middleware/openai/langchain/__init__.py +114 -0
- revenium_middleware/openai/langchain/_utils.py +129 -0
- revenium_middleware/openai/langchain/unified_handler.py +526 -0
- revenium_middleware/openai/middleware.py +1451 -0
- revenium_middleware/openai/prompt_extractor.py +173 -0
- revenium_middleware/openai/provider.py +170 -0
- revenium_middleware/openai/summary_printer.py +292 -0
- revenium_middleware/openai/trace_fields.py +98 -0
- revenium_middleware/perplexity/__init__.py +97 -0
- revenium_middleware/perplexity/middleware.py +379 -0
- revenium_middleware/perplexity/perplexity_sdk.py +256 -0
- revenium_middleware/perplexity/provider.py +84 -0
- revenium_middleware/perplexity/trace_fields.py +25 -0
- revenium_python_sdk-0.1.0.dist-info/METADATA +252 -0
- revenium_python_sdk-0.1.0.dist-info/RECORD +73 -0
- revenium_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- revenium_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- revenium_python_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Revenium Middleware — the economic system of record for AI usage.
|
|
3
|
+
|
|
4
|
+
This package provides core metering functionality and provider-specific
|
|
5
|
+
middleware for deeply attributed AI usage metrics.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ReadableFormatter(logging.Formatter):
|
|
14
|
+
"""
|
|
15
|
+
Custom formatter that improves readability of log messages.
|
|
16
|
+
- Adds visual separators for important events
|
|
17
|
+
- Uses colors if terminal supports them
|
|
18
|
+
- Keeps messages clean and scannable
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# ANSI color codes
|
|
22
|
+
COLORS = {
|
|
23
|
+
'RESET': '\033[0m',
|
|
24
|
+
'BOLD': '\033[1m',
|
|
25
|
+
'DIM': '\033[2m',
|
|
26
|
+
'GREEN': '\033[92m',
|
|
27
|
+
'YELLOW': '\033[93m',
|
|
28
|
+
'BLUE': '\033[94m',
|
|
29
|
+
'CYAN': '\033[96m',
|
|
30
|
+
'RED': '\033[91m',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Box drawing characters
|
|
34
|
+
SEPARATOR = "─" * 70
|
|
35
|
+
|
|
36
|
+
def __init__(self, *args, **kwargs):
|
|
37
|
+
super().__init__(*args, **kwargs)
|
|
38
|
+
self.use_colors = self._supports_color()
|
|
39
|
+
|
|
40
|
+
def _supports_color(self):
|
|
41
|
+
"""Check if terminal supports ANSI colors."""
|
|
42
|
+
return hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
|
|
43
|
+
|
|
44
|
+
def _colorize(self, text, color):
|
|
45
|
+
"""Apply color to text if terminal supports it."""
|
|
46
|
+
if self.use_colors and color in self.COLORS:
|
|
47
|
+
return f"{self.COLORS[color]}{text}{self.COLORS['RESET']}"
|
|
48
|
+
return text
|
|
49
|
+
|
|
50
|
+
def _truncate_long_objects(self, message, max_length=200):
|
|
51
|
+
"""Truncate very long object representations to keep logs readable."""
|
|
52
|
+
# Check if message contains object representations
|
|
53
|
+
if '(' in message and ')' in message and len(message) > max_length:
|
|
54
|
+
# Look for patterns like "ClassName(field=value, ...)"
|
|
55
|
+
# Match object representations
|
|
56
|
+
pattern = r'(\w+)\(([^)]{100,})\)'
|
|
57
|
+
|
|
58
|
+
def replacer(match):
|
|
59
|
+
class_name = match.group(1)
|
|
60
|
+
content = match.group(2)
|
|
61
|
+
# Truncate the content
|
|
62
|
+
if len(content) > 150:
|
|
63
|
+
truncated = content[:150] + "..."
|
|
64
|
+
return f"{class_name}({truncated})"
|
|
65
|
+
return match.group(0)
|
|
66
|
+
|
|
67
|
+
message = re.sub(pattern, replacer, message)
|
|
68
|
+
|
|
69
|
+
return message
|
|
70
|
+
|
|
71
|
+
def format(self, record):
|
|
72
|
+
# Format the base message
|
|
73
|
+
message = record.getMessage()
|
|
74
|
+
|
|
75
|
+
# Truncate very long object representations in DEBUG logs
|
|
76
|
+
if record.levelname == 'DEBUG':
|
|
77
|
+
message = self._truncate_long_objects(message)
|
|
78
|
+
|
|
79
|
+
# Add visual enhancements for key events
|
|
80
|
+
# Check log level first to ensure it takes precedence
|
|
81
|
+
if record.levelname == 'ERROR':
|
|
82
|
+
message = self._colorize(f"[ERROR] {message}", 'RED')
|
|
83
|
+
elif record.levelname == 'WARNING':
|
|
84
|
+
message = self._colorize(f"[WARNING] {message}", 'YELLOW')
|
|
85
|
+
elif "SUCCESS" in message or "successful" in message.lower():
|
|
86
|
+
message = self._colorize(f"[SUCCESS] {message}", 'GREEN')
|
|
87
|
+
elif "FAILURE" in message:
|
|
88
|
+
message = self._colorize(f"[ERROR] {message}", 'RED')
|
|
89
|
+
elif "Shutdown complete" in message:
|
|
90
|
+
# Make shutdown completion more visible
|
|
91
|
+
separator = self._colorize(self.SEPARATOR, 'GREEN')
|
|
92
|
+
success_msg = self._colorize(
|
|
93
|
+
"[COMPLETE] All operations finished successfully", 'GREEN'
|
|
94
|
+
)
|
|
95
|
+
message = f"\n{separator}\n{success_msg}\n{separator}"
|
|
96
|
+
elif "Shutdown initiated" in message:
|
|
97
|
+
separator = self._colorize(self.SEPARATOR, 'CYAN')
|
|
98
|
+
shutdown_msg = self._colorize('[SHUTDOWN] ' + message, 'CYAN')
|
|
99
|
+
message = f"\n{separator}\n{shutdown_msg}"
|
|
100
|
+
|
|
101
|
+
# Format timestamp and level
|
|
102
|
+
if record.levelname == 'DEBUG':
|
|
103
|
+
level = self._colorize('DEBUG', 'DIM')
|
|
104
|
+
elif record.levelname == 'INFO':
|
|
105
|
+
level = self._colorize('INFO', 'BLUE')
|
|
106
|
+
elif record.levelname == 'WARNING':
|
|
107
|
+
level = self._colorize('WARN', 'YELLOW')
|
|
108
|
+
elif record.levelname == 'ERROR':
|
|
109
|
+
level = self._colorize('ERROR', 'RED')
|
|
110
|
+
else:
|
|
111
|
+
level = record.levelname
|
|
112
|
+
|
|
113
|
+
# Build the final log message
|
|
114
|
+
timestamp = self.formatTime(record, '%H:%M:%S')
|
|
115
|
+
return f"{timestamp} [{level}] {message}"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Set up logger
|
|
119
|
+
logger = logging.getLogger("revenium_middleware")
|
|
120
|
+
log_level = os.environ.get("REVENIUM_LOG_LEVEL", "INFO").upper()
|
|
121
|
+
try:
|
|
122
|
+
logger.setLevel(getattr(logging, log_level))
|
|
123
|
+
except AttributeError:
|
|
124
|
+
logger.setLevel(logging.INFO)
|
|
125
|
+
logger.warning(f"Invalid log level: {log_level}, defaulting to INFO")
|
|
126
|
+
|
|
127
|
+
# Configure a handler with the readable formatter if none exists
|
|
128
|
+
if not logger.handlers and not logging.root.handlers:
|
|
129
|
+
handler = logging.StreamHandler()
|
|
130
|
+
formatter = ReadableFormatter()
|
|
131
|
+
handler.setFormatter(formatter)
|
|
132
|
+
logger.addHandler(handler)
|
|
133
|
+
|
|
134
|
+
# Allow propagation to root logger for testing
|
|
135
|
+
logger.propagate = True
|
|
136
|
+
|
|
137
|
+
# Re-export everything from _core for backward compatibility.
|
|
138
|
+
# Existing imports like `from revenium_middleware import client` keep working.
|
|
139
|
+
from ._core import ( # noqa: E402
|
|
140
|
+
client,
|
|
141
|
+
run_async_in_thread,
|
|
142
|
+
shutdown_event,
|
|
143
|
+
revenium_meter,
|
|
144
|
+
revenium_metadata,
|
|
145
|
+
track_usage,
|
|
146
|
+
is_inside_decorated_function,
|
|
147
|
+
get_function_metadata,
|
|
148
|
+
set_decorated_context,
|
|
149
|
+
clear_decorated_context,
|
|
150
|
+
get_injected_metadata,
|
|
151
|
+
set_injected_metadata,
|
|
152
|
+
clear_injected_metadata,
|
|
153
|
+
merge_metadata,
|
|
154
|
+
is_selective_metering_enabled,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Re-export tool metering utilities from revenium_metering (v6.8.2+)
|
|
158
|
+
from revenium_metering import meter_tool, report_tool_call, configure # noqa: E402
|
|
159
|
+
|
|
160
|
+
__all__ = [
|
|
161
|
+
# Metering exports
|
|
162
|
+
"client",
|
|
163
|
+
"run_async_in_thread",
|
|
164
|
+
"shutdown_event",
|
|
165
|
+
# Decorator exports
|
|
166
|
+
"revenium_meter",
|
|
167
|
+
"revenium_metadata",
|
|
168
|
+
"track_usage",
|
|
169
|
+
# Context management exports
|
|
170
|
+
"is_inside_decorated_function",
|
|
171
|
+
"get_function_metadata",
|
|
172
|
+
"set_decorated_context",
|
|
173
|
+
"clear_decorated_context",
|
|
174
|
+
"get_injected_metadata",
|
|
175
|
+
"set_injected_metadata",
|
|
176
|
+
"clear_injected_metadata",
|
|
177
|
+
"merge_metadata",
|
|
178
|
+
# Config exports
|
|
179
|
+
"is_selective_metering_enabled",
|
|
180
|
+
# Tool metering exports (from revenium_metering)
|
|
181
|
+
"meter_tool",
|
|
182
|
+
"report_tool_call",
|
|
183
|
+
"configure",
|
|
184
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core functionality for Revenium middleware.
|
|
3
|
+
|
|
4
|
+
This subpackage contains the foundational components shared across all
|
|
5
|
+
provider-specific middleware implementations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .metering import run_async_in_thread, shutdown_event, client
|
|
9
|
+
from .context import (
|
|
10
|
+
is_inside_decorated_function,
|
|
11
|
+
get_function_metadata,
|
|
12
|
+
set_decorated_context,
|
|
13
|
+
clear_decorated_context,
|
|
14
|
+
get_injected_metadata,
|
|
15
|
+
set_injected_metadata,
|
|
16
|
+
clear_injected_metadata,
|
|
17
|
+
merge_metadata,
|
|
18
|
+
)
|
|
19
|
+
from .decorators import revenium_meter, revenium_metadata, track_usage
|
|
20
|
+
from .config import is_selective_metering_enabled
|
|
21
|
+
from .trace_fields import (
|
|
22
|
+
get_environment,
|
|
23
|
+
get_region,
|
|
24
|
+
get_credential_alias,
|
|
25
|
+
get_trace_type,
|
|
26
|
+
get_trace_name,
|
|
27
|
+
get_parent_transaction_id,
|
|
28
|
+
get_transaction_name,
|
|
29
|
+
get_retry_number,
|
|
30
|
+
validate_trace_type,
|
|
31
|
+
validate_trace_name,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Metering
|
|
36
|
+
"client",
|
|
37
|
+
"run_async_in_thread",
|
|
38
|
+
"shutdown_event",
|
|
39
|
+
# Decorators
|
|
40
|
+
"revenium_meter",
|
|
41
|
+
"revenium_metadata",
|
|
42
|
+
"track_usage",
|
|
43
|
+
# Context management
|
|
44
|
+
"is_inside_decorated_function",
|
|
45
|
+
"get_function_metadata",
|
|
46
|
+
"set_decorated_context",
|
|
47
|
+
"clear_decorated_context",
|
|
48
|
+
"get_injected_metadata",
|
|
49
|
+
"set_injected_metadata",
|
|
50
|
+
"clear_injected_metadata",
|
|
51
|
+
"merge_metadata",
|
|
52
|
+
# Config
|
|
53
|
+
"is_selective_metering_enabled",
|
|
54
|
+
# Trace fields
|
|
55
|
+
"get_environment",
|
|
56
|
+
"get_region",
|
|
57
|
+
"get_credential_alias",
|
|
58
|
+
"get_trace_type",
|
|
59
|
+
"get_trace_name",
|
|
60
|
+
"get_parent_transaction_id",
|
|
61
|
+
"get_transaction_name",
|
|
62
|
+
"get_retry_number",
|
|
63
|
+
"validate_trace_type",
|
|
64
|
+
"validate_trace_name",
|
|
65
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared configuration constants and settings for Revenium middleware.
|
|
3
|
+
|
|
4
|
+
This module is the single source of truth for configuration values shared
|
|
5
|
+
across all provider middlewares. Provider-specific config files extend
|
|
6
|
+
this via class inheritance and re-export symbols for backward compatibility.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from typing import Set, Literal, Union, Optional
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Type alias for summary format
|
|
16
|
+
SummaryFormat = Literal["human", "json"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Config:
|
|
20
|
+
"""Shared configuration constants for all Revenium middleware providers."""
|
|
21
|
+
|
|
22
|
+
# Threading and async timeouts
|
|
23
|
+
THREAD_JOIN_TIMEOUT: float = 5.0
|
|
24
|
+
API_REQUEST_TIMEOUT: float = 30.0
|
|
25
|
+
BACKGROUND_THREAD_TIMEOUT: float = 5.0
|
|
26
|
+
|
|
27
|
+
# Cache settings
|
|
28
|
+
PROVIDER_CACHE_TTL: int = 3600 # 1 hour
|
|
29
|
+
MODEL_CACHE_TTL: int = 3600 # 1 hour
|
|
30
|
+
|
|
31
|
+
# Logging and security
|
|
32
|
+
MAX_LOG_STRING_LENGTH: int = 100
|
|
33
|
+
MAX_SANITIZATION_DEPTH: int = 3
|
|
34
|
+
|
|
35
|
+
# Stream processing
|
|
36
|
+
STREAM_CHUNK_BUFFER_SIZE: int = 1000
|
|
37
|
+
STREAM_TIMEOUT: float = 30.0
|
|
38
|
+
|
|
39
|
+
# Retry and resilience
|
|
40
|
+
MAX_RETRY_ATTEMPTS: int = 3
|
|
41
|
+
RETRY_BACKOFF_FACTOR: float = 1.5
|
|
42
|
+
INITIAL_RETRY_DELAY: float = 0.1
|
|
43
|
+
|
|
44
|
+
# Core environment variable names
|
|
45
|
+
ENV_REVENIUM_API_KEY: str = "REVENIUM_METERING_API_KEY"
|
|
46
|
+
ENV_LOG_LEVEL: str = "REVENIUM_LOG_LEVEL"
|
|
47
|
+
|
|
48
|
+
# Trace visualization environment variables
|
|
49
|
+
ENV_REVENIUM_ENVIRONMENT: str = "REVENIUM_ENVIRONMENT"
|
|
50
|
+
ENV_ENVIRONMENT: str = "ENVIRONMENT"
|
|
51
|
+
ENV_DEPLOYMENT_ENV: str = "DEPLOYMENT_ENV"
|
|
52
|
+
ENV_REVENIUM_REGION: str = "REVENIUM_REGION"
|
|
53
|
+
ENV_REVENIUM_CREDENTIAL_ALIAS: str = "REVENIUM_CREDENTIAL_ALIAS"
|
|
54
|
+
ENV_REVENIUM_TRACE_TYPE: str = "REVENIUM_TRACE_TYPE"
|
|
55
|
+
ENV_REVENIUM_TRACE_NAME: str = "REVENIUM_TRACE_NAME"
|
|
56
|
+
ENV_REVENIUM_PARENT_TRANSACTION_ID: str = "REVENIUM_PARENT_TRANSACTION_ID"
|
|
57
|
+
ENV_REVENIUM_TRANSACTION_NAME: str = "REVENIUM_TRANSACTION_NAME"
|
|
58
|
+
ENV_REVENIUM_RETRY_NUMBER: str = "REVENIUM_RETRY_NUMBER"
|
|
59
|
+
|
|
60
|
+
# Prompt capture settings
|
|
61
|
+
ENV_REVENIUM_CAPTURE_PROMPTS: str = "REVENIUM_CAPTURE_PROMPTS"
|
|
62
|
+
CAPTURE_PROMPTS: bool = os.getenv("REVENIUM_CAPTURE_PROMPTS", "false").lower() in ("true", "1", "yes", "on")
|
|
63
|
+
MAX_PROMPT_LENGTH: int = 50_000 # Maximum characters per prompt field
|
|
64
|
+
|
|
65
|
+
# Terminal summary settings
|
|
66
|
+
ENV_REVENIUM_PRINT_SUMMARY: str = "REVENIUM_PRINT_SUMMARY"
|
|
67
|
+
ENV_REVENIUM_TEAM_ID: str = "REVENIUM_TEAM_ID"
|
|
68
|
+
ENV_REVENIUM_BASE_URL: str = "REVENIUM_METERING_BASE_URL"
|
|
69
|
+
SUMMARY_RETRY_ATTEMPTS: int = 3
|
|
70
|
+
SUMMARY_RETRY_DELAY: float = 2.0
|
|
71
|
+
SUMMARY_API_TIMEOUT: float = 5.0
|
|
72
|
+
DEFAULT_BASE_URL: str = "https://api.revenium.ai"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SecurityConfig:
|
|
76
|
+
"""Security-related configuration shared across all providers."""
|
|
77
|
+
|
|
78
|
+
SENSITIVE_FIELDS: Set[str] = {
|
|
79
|
+
'api_key', 'subscriber_credential', 'subscriber_email', 'messages',
|
|
80
|
+
'input', 'content', 'prompt', 'text', 'data', 'authorization',
|
|
81
|
+
'x-api-key', 'bearer', 'token', 'password', 'secret', 'key',
|
|
82
|
+
'credential', 'auth', 'private',
|
|
83
|
+
'system_prompt', 'input_messages', 'output_response'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
SENSITIVE_PATTERNS: Set[str] = {
|
|
87
|
+
'sk-', 'pk-', 'Bearer ', 'Basic ', 'Token '
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def is_selective_metering_enabled() -> bool:
|
|
92
|
+
"""
|
|
93
|
+
Check if selective metering is enabled.
|
|
94
|
+
|
|
95
|
+
When enabled, only functions decorated with @revenium_meter will be metered.
|
|
96
|
+
When disabled (default), all API calls are metered automatically.
|
|
97
|
+
|
|
98
|
+
The setting is controlled by the REVENIUM_SELECTIVE_METERING environment variable.
|
|
99
|
+
Accepted values for enabled: "true", "1", "yes", "on" (case-insensitive)
|
|
100
|
+
"""
|
|
101
|
+
env_value = os.environ.get("REVENIUM_SELECTIVE_METERING", "false").lower()
|
|
102
|
+
return env_value in ("true", "1", "yes", "on")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_config_value(key: str, default: any = None) -> any:
|
|
106
|
+
"""Get configuration value from environment or use default."""
|
|
107
|
+
return os.getenv(key, default)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_debug_enabled() -> bool:
|
|
111
|
+
"""Check if debug logging is enabled."""
|
|
112
|
+
log_level = get_config_value(Config.ENV_LOG_LEVEL, "INFO").upper()
|
|
113
|
+
return log_level == "DEBUG"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_timeout_config() -> dict:
|
|
117
|
+
"""Get base timeout-related configuration."""
|
|
118
|
+
return {
|
|
119
|
+
'thread_join': Config.THREAD_JOIN_TIMEOUT,
|
|
120
|
+
'api_request': Config.API_REQUEST_TIMEOUT,
|
|
121
|
+
'background_thread': Config.BACKGROUND_THREAD_TIMEOUT,
|
|
122
|
+
'stream': Config.STREAM_TIMEOUT,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def parse_print_summary_value(value: Optional[str]) -> Union[bool, SummaryFormat]:
|
|
127
|
+
"""
|
|
128
|
+
Parse REVENIUM_PRINT_SUMMARY environment variable value.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
False if disabled, 'human' or 'json' if enabled
|
|
132
|
+
"""
|
|
133
|
+
if value is None:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
value_lower = value.lower().strip()
|
|
137
|
+
|
|
138
|
+
if value_lower in ('false', '0', 'no', 'off', 'disabled', ''):
|
|
139
|
+
return False
|
|
140
|
+
elif value_lower in ('true', '1', 'yes', 'on', 'enabled', 'human'):
|
|
141
|
+
return 'human'
|
|
142
|
+
elif value_lower == 'json':
|
|
143
|
+
return 'json'
|
|
144
|
+
else:
|
|
145
|
+
logger.warning(
|
|
146
|
+
f"Invalid REVENIUM_PRINT_SUMMARY value '{value}'. "
|
|
147
|
+
f"Expected 'true', 'human', 'json', or 'false'. Defaulting to disabled."
|
|
148
|
+
)
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_print_summary_config() -> Union[bool, SummaryFormat]:
|
|
153
|
+
"""Get print summary configuration from environment."""
|
|
154
|
+
value = get_config_value(Config.ENV_REVENIUM_PRINT_SUMMARY)
|
|
155
|
+
return parse_print_summary_value(value)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_team_id() -> Optional[str]:
|
|
159
|
+
"""Get Revenium team ID from environment."""
|
|
160
|
+
return get_config_value(Config.ENV_REVENIUM_TEAM_ID)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_base_url() -> str:
|
|
164
|
+
"""Get Revenium base URL from environment (defaults to https://api.revenium.ai)."""
|
|
165
|
+
return get_config_value(Config.ENV_REVENIUM_BASE_URL, Config.DEFAULT_BASE_URL)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context tracking for selective metering and metadata injection with decorators.
|
|
3
|
+
|
|
4
|
+
This module provides thread-safe and async-safe context tracking to determine
|
|
5
|
+
whether code is currently executing inside a decorated function that should be metered,
|
|
6
|
+
and to store metadata that should be injected into API calls.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import contextvars
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
|
|
12
|
+
# Context variable to track if we're inside a decorated function
|
|
13
|
+
_decorated_function_context: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
|
14
|
+
'revenium_decorated_function', default=False
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Context variable to store metadata from the current decorated function
|
|
18
|
+
_function_metadata_context: contextvars.ContextVar[Optional[Dict[str, Any]]] = contextvars.ContextVar(
|
|
19
|
+
'revenium_function_metadata', default=None
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Context variable to store injected metadata from @revenium_metadata decorator
|
|
23
|
+
_injected_metadata_context: contextvars.ContextVar[Optional[Dict[str, Any]]] = contextvars.ContextVar(
|
|
24
|
+
'revenium_injected_metadata', default=None
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_inside_decorated_function() -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Check if code is currently executing inside a decorated function.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
True if inside a decorated function, False otherwise
|
|
34
|
+
"""
|
|
35
|
+
return _decorated_function_context.get()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_function_metadata() -> Optional[Dict[str, Any]]:
|
|
39
|
+
"""
|
|
40
|
+
Get metadata from the current decorated function context.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Dictionary of metadata or None if not in decorated function
|
|
44
|
+
"""
|
|
45
|
+
return _function_metadata_context.get()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def set_decorated_context(is_decorated: bool, metadata: Optional[Dict[str, Any]] = None) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Set the decorated function context.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
is_decorated: Whether we're inside a decorated function
|
|
54
|
+
metadata: Optional metadata from the decorator
|
|
55
|
+
"""
|
|
56
|
+
_decorated_function_context.set(is_decorated)
|
|
57
|
+
_function_metadata_context.set(metadata)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def clear_decorated_context() -> None:
|
|
61
|
+
"""Clear the decorated function context."""
|
|
62
|
+
_decorated_function_context.set(False)
|
|
63
|
+
_function_metadata_context.set(None)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_injected_metadata() -> Optional[Dict[str, Any]]:
|
|
67
|
+
"""
|
|
68
|
+
Get metadata from the current @revenium_metadata decorator context.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dictionary of injected metadata or None if not in decorated function
|
|
72
|
+
"""
|
|
73
|
+
return _injected_metadata_context.get()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def set_injected_metadata(metadata: Optional[Dict[str, Any]]) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Set the injected metadata context.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
metadata: Dictionary of metadata to inject into API calls
|
|
82
|
+
"""
|
|
83
|
+
_injected_metadata_context.set(metadata)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def clear_injected_metadata() -> None:
|
|
87
|
+
"""Clear the injected metadata context."""
|
|
88
|
+
_injected_metadata_context.set(None)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def merge_metadata(api_metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
92
|
+
"""
|
|
93
|
+
Merge injected metadata with API-level metadata.
|
|
94
|
+
|
|
95
|
+
API-level metadata takes precedence over injected metadata.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
api_metadata: Metadata passed directly to the API call
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Merged metadata dictionary with API-level metadata taking precedence
|
|
102
|
+
"""
|
|
103
|
+
injected = get_injected_metadata() or {}
|
|
104
|
+
api = api_metadata or {}
|
|
105
|
+
|
|
106
|
+
# Start with injected metadata, then override with API-level metadata
|
|
107
|
+
merged = {**injected, **api}
|
|
108
|
+
return merged
|
|
109
|
+
|