pytokencalc 0.7.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.
- pytokencalc/__init__.py +111 -0
- pytokencalc/_budget_enforcement.py +265 -0
- pytokencalc/config.py +45 -0
- pytokencalc/cost_calculator.py +522 -0
- pytokencalc/cost_model.py +405 -0
- pytokencalc/cost_models.py +390 -0
- pytokencalc/database.py +571 -0
- pytokencalc/error_messages.py +150 -0
- pytokencalc/exceptions.py +55 -0
- pytokencalc/persistence.py +281 -0
- pytokencalc/pricing_manager.py +304 -0
- pytokencalc/tokenizers/__init__.py +21 -0
- pytokencalc/tokenizers/base.py +118 -0
- pytokencalc/tokenizers/cache.py +126 -0
- pytokencalc/tokenizers/huggingface_counter.py +155 -0
- pytokencalc/tokenizers/openai_counter.py +168 -0
- pytokencalc/tokenizers/registry.py +200 -0
- pytokencalc-0.7.0.dist-info/METADATA +482 -0
- pytokencalc-0.7.0.dist-info/RECORD +22 -0
- pytokencalc-0.7.0.dist-info/WHEEL +5 -0
- pytokencalc-0.7.0.dist-info/licenses/LICENSE +21 -0
- pytokencalc-0.7.0.dist-info/top_level.txt +1 -0
pytokencalc/__init__.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyTokenCalc v0.7: Multi-Provider LLM Token Counting & Cost Calculator
|
|
3
|
+
|
|
4
|
+
Unified token counting and cost calculation across 20+ cloud providers and 10+ open-source APIs.
|
|
5
|
+
The token counting core for OpenAnchor cost optimization middleware.
|
|
6
|
+
|
|
7
|
+
v0.6+ Multi-Provider Token Models:
|
|
8
|
+
- Provider-specific token counting (Claude simple, GPT-4o dual, Gemini character-based, etc.)
|
|
9
|
+
- Local tokenizers (tiktoken, HF transformers) - 60% of models
|
|
10
|
+
- Cloud APIs with aggressive caching (Anthropic, Google)
|
|
11
|
+
- Vision/multimodal support (images, PDFs)
|
|
12
|
+
|
|
13
|
+
v0.5 Core Features (Backwards Compatible):
|
|
14
|
+
- CostCalculator: Calculate cost for any provider/model/tokens
|
|
15
|
+
- CostDatabase: Track operations and aggregate costs
|
|
16
|
+
- PricingManager: Provider pricing + daily updates
|
|
17
|
+
- Budget Enforcement: Hard limits to prevent cost overruns
|
|
18
|
+
|
|
19
|
+
v0.7+ New Features:
|
|
20
|
+
- TokenCounterRegistry: Unified token counting interface
|
|
21
|
+
- Intelligent routing: auto-detect tokenizer per model
|
|
22
|
+
- In-memory caching: reduce API calls 70-80%
|
|
23
|
+
- Vision tokens: images, PDFs, multimodal
|
|
24
|
+
|
|
25
|
+
Core Team:
|
|
26
|
+
- PyTokenCalc: Token counting & cost calculation
|
|
27
|
+
- OpenAnchor: Cost optimization middleware
|
|
28
|
+
- PrismNote: OSS data science notebook
|
|
29
|
+
|
|
30
|
+
Repository: https://github.com/Mullassery/PyTokenCalc
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
__version__ = "0.7.0"
|
|
34
|
+
__author__ = "Georgi Mammen Mullassery"
|
|
35
|
+
|
|
36
|
+
# Core cost calculation classes (v0.5 and v0.6)
|
|
37
|
+
from .cost_calculator import CostCalculator, CostCalculatorV6
|
|
38
|
+
from .cost_model import Cost, ProviderType
|
|
39
|
+
from .cost_models import (
|
|
40
|
+
UsageData,
|
|
41
|
+
CostModel,
|
|
42
|
+
CostModelRegistry,
|
|
43
|
+
ClaudeTokenModel,
|
|
44
|
+
GPT4oTokenModel,
|
|
45
|
+
GeminiCharacterModel,
|
|
46
|
+
GroqSpeedTieredModel,
|
|
47
|
+
DeepInfraTokenModel,
|
|
48
|
+
TogetherAITokenModel,
|
|
49
|
+
)
|
|
50
|
+
from .pricing_manager import PricingManager
|
|
51
|
+
from .database import DatabaseManager
|
|
52
|
+
from .persistence import CostDatabase
|
|
53
|
+
|
|
54
|
+
# Tokenizers (v0.7+: Multi-provider token counting)
|
|
55
|
+
try:
|
|
56
|
+
from .tokenizers import (
|
|
57
|
+
TokenCounter,
|
|
58
|
+
TokenCountResult,
|
|
59
|
+
TokenCounterRegistry,
|
|
60
|
+
TokenCounterCache,
|
|
61
|
+
)
|
|
62
|
+
TOKENIZERS_AVAILABLE = True
|
|
63
|
+
except ImportError:
|
|
64
|
+
TOKENIZERS_AVAILABLE = False
|
|
65
|
+
|
|
66
|
+
# Safety feature: Hard budget enforcement
|
|
67
|
+
from ._budget_enforcement import (
|
|
68
|
+
BudgetEnforcer,
|
|
69
|
+
BudgetLimit,
|
|
70
|
+
BudgetStatus,
|
|
71
|
+
BudgetPeriod,
|
|
72
|
+
EnforcementAction,
|
|
73
|
+
BudgetExceededError,
|
|
74
|
+
get_global_enforcer,
|
|
75
|
+
set_budget_limit,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
# Core v0.5 (backwards compatible)
|
|
80
|
+
"CostCalculator",
|
|
81
|
+
"Cost",
|
|
82
|
+
"ProviderType",
|
|
83
|
+
"PricingManager",
|
|
84
|
+
"DatabaseManager",
|
|
85
|
+
"CostDatabase",
|
|
86
|
+
# v0.6+ multi-provider cost models
|
|
87
|
+
"CostCalculatorV6",
|
|
88
|
+
"UsageData",
|
|
89
|
+
"CostModel",
|
|
90
|
+
"CostModelRegistry",
|
|
91
|
+
"ClaudeTokenModel",
|
|
92
|
+
"GPT4oTokenModel",
|
|
93
|
+
"GeminiCharacterModel",
|
|
94
|
+
"GroqSpeedTieredModel",
|
|
95
|
+
"DeepInfraTokenModel",
|
|
96
|
+
"TogetherAITokenModel",
|
|
97
|
+
# v0.7+ token counting (if available)
|
|
98
|
+
"TokenCounter",
|
|
99
|
+
"TokenCountResult",
|
|
100
|
+
"TokenCounterRegistry",
|
|
101
|
+
"TokenCounterCache",
|
|
102
|
+
# Budget enforcement (safety feature)
|
|
103
|
+
"BudgetEnforcer",
|
|
104
|
+
"BudgetLimit",
|
|
105
|
+
"BudgetStatus",
|
|
106
|
+
"BudgetPeriod",
|
|
107
|
+
"EnforcementAction",
|
|
108
|
+
"BudgetExceededError",
|
|
109
|
+
"get_global_enforcer",
|
|
110
|
+
"set_budget_limit",
|
|
111
|
+
]
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hard budget enforcement and spending limits.
|
|
3
|
+
|
|
4
|
+
Prevents runaway costs by enforcing hard stops when budgets are exceeded.
|
|
5
|
+
This is a CRITICAL safety feature to prevent agent-driven cost explosions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional, Callable, List
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BudgetPeriod(Enum):
|
|
18
|
+
"""Budget enforcement period."""
|
|
19
|
+
HOURLY = "hourly"
|
|
20
|
+
DAILY = "daily"
|
|
21
|
+
MONTHLY = "monthly"
|
|
22
|
+
YEARLY = "yearly"
|
|
23
|
+
TOTAL = "total" # Total lifetime budget
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EnforcementAction(Enum):
|
|
27
|
+
"""Action to take when budget is exceeded."""
|
|
28
|
+
ALERT = "alert" # Log warning, continue
|
|
29
|
+
THROTTLE = "throttle" # Reduce rate, continue
|
|
30
|
+
STOP = "stop" # Hard stop, raise exception
|
|
31
|
+
WEBHOOK = "webhook" # Call external webhook
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class BudgetLimit:
|
|
36
|
+
"""Budget limit definition."""
|
|
37
|
+
limit_usd: float
|
|
38
|
+
period: BudgetPeriod
|
|
39
|
+
action: EnforcementAction
|
|
40
|
+
warning_threshold: float = 0.80 # Alert at 80% of budget
|
|
41
|
+
description: str = ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class BudgetStatus:
|
|
46
|
+
"""Current budget status."""
|
|
47
|
+
period: BudgetPeriod
|
|
48
|
+
limit_usd: float
|
|
49
|
+
spent_usd: float
|
|
50
|
+
remaining_usd: float
|
|
51
|
+
percent_used: float
|
|
52
|
+
is_exceeded: bool
|
|
53
|
+
is_warning: bool
|
|
54
|
+
timestamp: datetime
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BudgetEnforcer:
|
|
58
|
+
"""
|
|
59
|
+
Enforces hard budget limits on API spending.
|
|
60
|
+
|
|
61
|
+
Prevents runaway costs from:
|
|
62
|
+
- Agentic loops (5-30x normal token consumption)
|
|
63
|
+
- Context window bloat (re-sending conversation history)
|
|
64
|
+
- Concurrent requests (multiple agents running simultaneously)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self):
|
|
68
|
+
self.limits: List[BudgetLimit] = []
|
|
69
|
+
self.spent_tracking = {
|
|
70
|
+
BudgetPeriod.HOURLY: 0.0,
|
|
71
|
+
BudgetPeriod.DAILY: 0.0,
|
|
72
|
+
BudgetPeriod.MONTHLY: 0.0,
|
|
73
|
+
BudgetPeriod.YEARLY: 0.0,
|
|
74
|
+
BudgetPeriod.TOTAL: 0.0,
|
|
75
|
+
}
|
|
76
|
+
self.last_reset = {
|
|
77
|
+
BudgetPeriod.HOURLY: datetime.now(),
|
|
78
|
+
BudgetPeriod.DAILY: datetime.now(),
|
|
79
|
+
BudgetPeriod.MONTHLY: datetime.now(),
|
|
80
|
+
BudgetPeriod.YEARLY: datetime.now(),
|
|
81
|
+
}
|
|
82
|
+
self.webhook_callbacks: dict[EnforcementAction, List[Callable]] = {
|
|
83
|
+
action: [] for action in EnforcementAction
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
def add_limit(self, limit: BudgetLimit):
|
|
87
|
+
"""Add a budget limit."""
|
|
88
|
+
self.limits.append(limit)
|
|
89
|
+
logger.info(f"Added budget limit: {limit.limit_usd} USD per {limit.period.value}")
|
|
90
|
+
|
|
91
|
+
def add_webhook(self, action: EnforcementAction, callback: Callable):
|
|
92
|
+
"""Add callback for enforcement action."""
|
|
93
|
+
self.webhook_callbacks[action].append(callback)
|
|
94
|
+
|
|
95
|
+
def check_budget(self, cost_usd: float) -> BudgetStatus:
|
|
96
|
+
"""
|
|
97
|
+
Check if a cost can be incurred within budget.
|
|
98
|
+
|
|
99
|
+
Raises BudgetExceededError if cost exceeds hard limit.
|
|
100
|
+
"""
|
|
101
|
+
self._reset_periods()
|
|
102
|
+
|
|
103
|
+
# Check all applicable limits
|
|
104
|
+
exceeded_limits = []
|
|
105
|
+
for limit in self.limits:
|
|
106
|
+
new_spent = self.spent_tracking[limit.period] + cost_usd
|
|
107
|
+
is_exceeded = new_spent > limit.limit_usd
|
|
108
|
+
is_warning = new_spent > (limit.limit_usd * limit.warning_threshold)
|
|
109
|
+
|
|
110
|
+
if is_exceeded or is_warning:
|
|
111
|
+
status = BudgetStatus(
|
|
112
|
+
period=limit.period,
|
|
113
|
+
limit_usd=limit.limit_usd,
|
|
114
|
+
spent_usd=new_spent,
|
|
115
|
+
remaining_usd=max(0, limit.limit_usd - new_spent),
|
|
116
|
+
percent_used=(new_spent / limit.limit_usd) * 100,
|
|
117
|
+
is_exceeded=is_exceeded,
|
|
118
|
+
is_warning=is_warning and not is_exceeded,
|
|
119
|
+
timestamp=datetime.now()
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if is_exceeded:
|
|
123
|
+
exceeded_limits.append((limit, status))
|
|
124
|
+
self._execute_action(limit.action, status)
|
|
125
|
+
elif is_warning:
|
|
126
|
+
logger.warning(
|
|
127
|
+
f"Budget warning: {status.percent_used:.1f}% of {limit.period.value} budget used "
|
|
128
|
+
f"({status.spent_usd:.2f}$ of {status.limit_usd}$)"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# If any limit is exceeded, determine what to do
|
|
132
|
+
if exceeded_limits:
|
|
133
|
+
limit, status = exceeded_limits[0]
|
|
134
|
+
if limit.action == EnforcementAction.STOP:
|
|
135
|
+
raise BudgetExceededError(
|
|
136
|
+
f"Budget exceeded for {limit.period.value}: "
|
|
137
|
+
f"{status.spent_usd:.2f}$ spent of {status.limit_usd}$ limit"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Record the spend
|
|
141
|
+
for period in BudgetPeriod:
|
|
142
|
+
self.spent_tracking[period] += cost_usd
|
|
143
|
+
|
|
144
|
+
return status
|
|
145
|
+
|
|
146
|
+
def get_status(self) -> dict[BudgetPeriod, BudgetStatus]:
|
|
147
|
+
"""Get current budget status for all periods."""
|
|
148
|
+
self._reset_periods()
|
|
149
|
+
|
|
150
|
+
status = {}
|
|
151
|
+
for limit in self.limits:
|
|
152
|
+
spent = self.spent_tracking[limit.period]
|
|
153
|
+
status[limit.period] = BudgetStatus(
|
|
154
|
+
period=limit.period,
|
|
155
|
+
limit_usd=limit.limit_usd,
|
|
156
|
+
spent_usd=spent,
|
|
157
|
+
remaining_usd=max(0, limit.limit_usd - spent),
|
|
158
|
+
percent_used=(spent / limit.limit_usd) * 100,
|
|
159
|
+
is_exceeded=spent > limit.limit_usd,
|
|
160
|
+
is_warning=spent > (limit.limit_usd * limit.warning_threshold),
|
|
161
|
+
timestamp=datetime.now()
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return status
|
|
165
|
+
|
|
166
|
+
def reset_period(self, period: BudgetPeriod):
|
|
167
|
+
"""Manually reset a budget period."""
|
|
168
|
+
self.spent_tracking[period] = 0.0
|
|
169
|
+
self.last_reset[period] = datetime.now()
|
|
170
|
+
logger.info(f"Reset {period.value} budget")
|
|
171
|
+
|
|
172
|
+
def _reset_periods(self):
|
|
173
|
+
"""Reset periods that have elapsed."""
|
|
174
|
+
now = datetime.now()
|
|
175
|
+
|
|
176
|
+
# Hourly
|
|
177
|
+
if (now - self.last_reset[BudgetPeriod.HOURLY]).total_seconds() >= 3600:
|
|
178
|
+
self.spent_tracking[BudgetPeriod.HOURLY] = 0.0
|
|
179
|
+
self.last_reset[BudgetPeriod.HOURLY] = now
|
|
180
|
+
|
|
181
|
+
# Daily (reset at midnight)
|
|
182
|
+
if now.date() != self.last_reset[BudgetPeriod.DAILY].date():
|
|
183
|
+
self.spent_tracking[BudgetPeriod.DAILY] = 0.0
|
|
184
|
+
self.last_reset[BudgetPeriod.DAILY] = now
|
|
185
|
+
|
|
186
|
+
# Monthly (reset on 1st of month)
|
|
187
|
+
if now.month != self.last_reset[BudgetPeriod.MONTHLY].month:
|
|
188
|
+
self.spent_tracking[BudgetPeriod.MONTHLY] = 0.0
|
|
189
|
+
self.last_reset[BudgetPeriod.MONTHLY] = now
|
|
190
|
+
|
|
191
|
+
# Yearly (reset on Jan 1)
|
|
192
|
+
if now.year != self.last_reset[BudgetPeriod.YEARLY].year:
|
|
193
|
+
self.spent_tracking[BudgetPeriod.YEARLY] = 0.0
|
|
194
|
+
self.last_reset[BudgetPeriod.YEARLY] = now
|
|
195
|
+
|
|
196
|
+
def _execute_action(self, action: EnforcementAction, status: BudgetStatus):
|
|
197
|
+
"""Execute the enforcement action."""
|
|
198
|
+
if action == EnforcementAction.ALERT:
|
|
199
|
+
logger.warning(f"Budget alert: {status.percent_used:.1f}% used ({status.spent_usd:.2f}$)")
|
|
200
|
+
|
|
201
|
+
elif action == EnforcementAction.THROTTLE:
|
|
202
|
+
logger.info(f"Throttling requests - budget at {status.percent_used:.1f}%")
|
|
203
|
+
|
|
204
|
+
elif action == EnforcementAction.STOP:
|
|
205
|
+
logger.error(
|
|
206
|
+
f"BUDGET EXCEEDED: {status.spent_usd:.2f}$ spent of {status.limit_usd}$ "
|
|
207
|
+
f"limit for {status.period.value}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
elif action == EnforcementAction.WEBHOOK:
|
|
211
|
+
for callback in self.webhook_callbacks[action]:
|
|
212
|
+
try:
|
|
213
|
+
callback(status)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.error(f"Webhook callback failed: {e}")
|
|
216
|
+
|
|
217
|
+
def suggest_budget_allocation(self, monthly_budget: float) -> dict:
|
|
218
|
+
"""
|
|
219
|
+
Suggest budget allocation across periods based on typical usage patterns.
|
|
220
|
+
|
|
221
|
+
Helps teams avoid accidental overspend.
|
|
222
|
+
"""
|
|
223
|
+
return {
|
|
224
|
+
"monthly_total": monthly_budget,
|
|
225
|
+
"daily_suggested": monthly_budget / 30,
|
|
226
|
+
"hourly_suggested": monthly_budget / (30 * 24),
|
|
227
|
+
"warning_threshold_pct": 80,
|
|
228
|
+
"hard_stop_action": "STOP",
|
|
229
|
+
"recommendation": (
|
|
230
|
+
"Set a daily limit at 3-5% of monthly budget (1/30) to catch runaway costs early. "
|
|
231
|
+
"Adjust if your usage is bursty. Set action=STOP for automated enforcement."
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class BudgetExceededError(Exception):
|
|
237
|
+
"""Raised when budget limit is exceeded with STOP action."""
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# Global budget enforcer instance (optional)
|
|
242
|
+
_global_enforcer: Optional[BudgetEnforcer] = None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_global_enforcer() -> BudgetEnforcer:
|
|
246
|
+
"""Get or create global budget enforcer."""
|
|
247
|
+
global _global_enforcer
|
|
248
|
+
if _global_enforcer is None:
|
|
249
|
+
_global_enforcer = BudgetEnforcer()
|
|
250
|
+
return _global_enforcer
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def set_budget_limit(
|
|
254
|
+
limit_usd: float,
|
|
255
|
+
period: BudgetPeriod = BudgetPeriod.DAILY,
|
|
256
|
+
action: EnforcementAction = EnforcementAction.STOP
|
|
257
|
+
):
|
|
258
|
+
"""Convenience function to set a budget limit."""
|
|
259
|
+
enforcer = get_global_enforcer()
|
|
260
|
+
limit = BudgetLimit(
|
|
261
|
+
limit_usd=limit_usd,
|
|
262
|
+
period=period,
|
|
263
|
+
action=action
|
|
264
|
+
)
|
|
265
|
+
enforcer.add_limit(limit)
|
pytokencalc/config.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Secure configuration management using environment variables."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
class Config:
|
|
8
|
+
"""Application configuration from environment variables."""
|
|
9
|
+
|
|
10
|
+
# Server
|
|
11
|
+
SERVER_HOST = os.getenv("PYCOSTAUDIT_HOST", "localhost")
|
|
12
|
+
SERVER_PORT = int(os.getenv("PYCOSTAUDIT_PORT", "8000"))
|
|
13
|
+
DEBUG = os.getenv("PYCOSTAUDIT_DEBUG", "false").lower() == "true"
|
|
14
|
+
|
|
15
|
+
# Database
|
|
16
|
+
DATABASE_URL = os.getenv("PYTOKENCALC_DATABASE_URL", "sqlite:///~/.pytokencalc/costs.db")
|
|
17
|
+
|
|
18
|
+
# SMTP (Email)
|
|
19
|
+
SMTP_HOST = os.getenv("PYCOSTAUDIT_SMTP_HOST", "smtp.gmail.com")
|
|
20
|
+
SMTP_PORT = int(os.getenv("PYCOSTAUDIT_SMTP_PORT", "587"))
|
|
21
|
+
SMTP_USER = os.getenv("PYCOSTAUDIT_SMTP_USER")
|
|
22
|
+
SMTP_PASSWORD = os.getenv("PYCOSTAUDIT_SMTP_PASSWORD")
|
|
23
|
+
FROM_EMAIL = os.getenv("PYCOSTAUDIT_FROM_EMAIL", SMTP_USER)
|
|
24
|
+
|
|
25
|
+
# Slack
|
|
26
|
+
SLACK_WEBHOOK_URL = os.getenv("PYCOSTAUDIT_SLACK_WEBHOOK_URL")
|
|
27
|
+
|
|
28
|
+
# Twilio (SMS)
|
|
29
|
+
TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
|
|
30
|
+
TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
|
|
31
|
+
TWILIO_FROM_NUMBER = os.getenv("TWILIO_PHONE_NUMBER")
|
|
32
|
+
|
|
33
|
+
# Telemetry
|
|
34
|
+
JAEGER_HOST = os.getenv("JAEGER_HOST", "localhost")
|
|
35
|
+
JAEGER_PORT = int(os.getenv("JAEGER_PORT", "6831"))
|
|
36
|
+
PROMETHEUS_PORT = int(os.getenv("PROMETHEUS_PORT", "8000"))
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def validate():
|
|
40
|
+
"""Validate required configuration."""
|
|
41
|
+
if not Config.SMTP_USER and not Config.SLACK_WEBHOOK_URL:
|
|
42
|
+
print("Warning: No SMTP or Slack credentials configured")
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
config = Config()
|