ccproxy-api 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.
- ccproxy/__init__.py +4 -0
- ccproxy/__main__.py +7 -0
- ccproxy/_version.py +21 -0
- ccproxy/adapters/__init__.py +11 -0
- ccproxy/adapters/base.py +80 -0
- ccproxy/adapters/openai/__init__.py +43 -0
- ccproxy/adapters/openai/adapter.py +915 -0
- ccproxy/adapters/openai/models.py +412 -0
- ccproxy/adapters/openai/streaming.py +449 -0
- ccproxy/api/__init__.py +28 -0
- ccproxy/api/app.py +225 -0
- ccproxy/api/dependencies.py +140 -0
- ccproxy/api/middleware/__init__.py +11 -0
- ccproxy/api/middleware/auth.py +0 -0
- ccproxy/api/middleware/cors.py +55 -0
- ccproxy/api/middleware/errors.py +703 -0
- ccproxy/api/middleware/headers.py +51 -0
- ccproxy/api/middleware/logging.py +175 -0
- ccproxy/api/middleware/request_id.py +69 -0
- ccproxy/api/middleware/server_header.py +62 -0
- ccproxy/api/responses.py +84 -0
- ccproxy/api/routes/__init__.py +16 -0
- ccproxy/api/routes/claude.py +181 -0
- ccproxy/api/routes/health.py +489 -0
- ccproxy/api/routes/metrics.py +1033 -0
- ccproxy/api/routes/proxy.py +238 -0
- ccproxy/auth/__init__.py +75 -0
- ccproxy/auth/bearer.py +68 -0
- ccproxy/auth/credentials_adapter.py +93 -0
- ccproxy/auth/dependencies.py +229 -0
- ccproxy/auth/exceptions.py +79 -0
- ccproxy/auth/manager.py +102 -0
- ccproxy/auth/models.py +118 -0
- ccproxy/auth/oauth/__init__.py +26 -0
- ccproxy/auth/oauth/models.py +49 -0
- ccproxy/auth/oauth/routes.py +396 -0
- ccproxy/auth/oauth/storage.py +0 -0
- ccproxy/auth/storage/__init__.py +12 -0
- ccproxy/auth/storage/base.py +57 -0
- ccproxy/auth/storage/json_file.py +159 -0
- ccproxy/auth/storage/keyring.py +192 -0
- ccproxy/claude_sdk/__init__.py +20 -0
- ccproxy/claude_sdk/client.py +169 -0
- ccproxy/claude_sdk/converter.py +331 -0
- ccproxy/claude_sdk/options.py +120 -0
- ccproxy/cli/__init__.py +14 -0
- ccproxy/cli/commands/__init__.py +8 -0
- ccproxy/cli/commands/auth.py +553 -0
- ccproxy/cli/commands/config/__init__.py +14 -0
- ccproxy/cli/commands/config/commands.py +766 -0
- ccproxy/cli/commands/config/schema_commands.py +119 -0
- ccproxy/cli/commands/serve.py +630 -0
- ccproxy/cli/docker/__init__.py +34 -0
- ccproxy/cli/docker/adapter_factory.py +157 -0
- ccproxy/cli/docker/params.py +278 -0
- ccproxy/cli/helpers.py +144 -0
- ccproxy/cli/main.py +193 -0
- ccproxy/cli/options/__init__.py +14 -0
- ccproxy/cli/options/claude_options.py +216 -0
- ccproxy/cli/options/core_options.py +40 -0
- ccproxy/cli/options/security_options.py +48 -0
- ccproxy/cli/options/server_options.py +117 -0
- ccproxy/config/__init__.py +40 -0
- ccproxy/config/auth.py +154 -0
- ccproxy/config/claude.py +124 -0
- ccproxy/config/cors.py +79 -0
- ccproxy/config/discovery.py +87 -0
- ccproxy/config/docker_settings.py +265 -0
- ccproxy/config/loader.py +108 -0
- ccproxy/config/observability.py +158 -0
- ccproxy/config/pricing.py +88 -0
- ccproxy/config/reverse_proxy.py +31 -0
- ccproxy/config/scheduler.py +89 -0
- ccproxy/config/security.py +14 -0
- ccproxy/config/server.py +81 -0
- ccproxy/config/settings.py +534 -0
- ccproxy/config/validators.py +231 -0
- ccproxy/core/__init__.py +274 -0
- ccproxy/core/async_utils.py +675 -0
- ccproxy/core/constants.py +97 -0
- ccproxy/core/errors.py +256 -0
- ccproxy/core/http.py +328 -0
- ccproxy/core/http_transformers.py +428 -0
- ccproxy/core/interfaces.py +247 -0
- ccproxy/core/logging.py +189 -0
- ccproxy/core/middleware.py +114 -0
- ccproxy/core/proxy.py +143 -0
- ccproxy/core/system.py +38 -0
- ccproxy/core/transformers.py +259 -0
- ccproxy/core/types.py +129 -0
- ccproxy/core/validators.py +288 -0
- ccproxy/docker/__init__.py +67 -0
- ccproxy/docker/adapter.py +588 -0
- ccproxy/docker/docker_path.py +207 -0
- ccproxy/docker/middleware.py +103 -0
- ccproxy/docker/models.py +228 -0
- ccproxy/docker/protocol.py +192 -0
- ccproxy/docker/stream_process.py +264 -0
- ccproxy/docker/validators.py +173 -0
- ccproxy/models/__init__.py +123 -0
- ccproxy/models/errors.py +42 -0
- ccproxy/models/messages.py +243 -0
- ccproxy/models/requests.py +85 -0
- ccproxy/models/responses.py +227 -0
- ccproxy/models/types.py +102 -0
- ccproxy/observability/__init__.py +51 -0
- ccproxy/observability/access_logger.py +400 -0
- ccproxy/observability/context.py +447 -0
- ccproxy/observability/metrics.py +539 -0
- ccproxy/observability/pushgateway.py +366 -0
- ccproxy/observability/sse_events.py +303 -0
- ccproxy/observability/stats_printer.py +755 -0
- ccproxy/observability/storage/__init__.py +1 -0
- ccproxy/observability/storage/duckdb_simple.py +665 -0
- ccproxy/observability/storage/models.py +55 -0
- ccproxy/pricing/__init__.py +19 -0
- ccproxy/pricing/cache.py +212 -0
- ccproxy/pricing/loader.py +267 -0
- ccproxy/pricing/models.py +106 -0
- ccproxy/pricing/updater.py +309 -0
- ccproxy/scheduler/__init__.py +39 -0
- ccproxy/scheduler/core.py +335 -0
- ccproxy/scheduler/exceptions.py +34 -0
- ccproxy/scheduler/manager.py +186 -0
- ccproxy/scheduler/registry.py +150 -0
- ccproxy/scheduler/tasks.py +484 -0
- ccproxy/services/__init__.py +10 -0
- ccproxy/services/claude_sdk_service.py +614 -0
- ccproxy/services/credentials/__init__.py +55 -0
- ccproxy/services/credentials/config.py +105 -0
- ccproxy/services/credentials/manager.py +562 -0
- ccproxy/services/credentials/oauth_client.py +482 -0
- ccproxy/services/proxy_service.py +1536 -0
- ccproxy/static/.keep +0 -0
- ccproxy/testing/__init__.py +34 -0
- ccproxy/testing/config.py +148 -0
- ccproxy/testing/content_generation.py +197 -0
- ccproxy/testing/mock_responses.py +262 -0
- ccproxy/testing/response_handlers.py +161 -0
- ccproxy/testing/scenarios.py +241 -0
- ccproxy/utils/__init__.py +6 -0
- ccproxy/utils/cost_calculator.py +210 -0
- ccproxy/utils/streaming_metrics.py +199 -0
- ccproxy_api-0.1.0.dist-info/METADATA +253 -0
- ccproxy_api-0.1.0.dist-info/RECORD +148 -0
- ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
- ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
- ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""Base scheduled task classes and task implementations."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import random
|
|
6
|
+
import time
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import structlog
|
|
11
|
+
|
|
12
|
+
from .exceptions import TaskExecutionError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = structlog.get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseScheduledTask(ABC):
|
|
19
|
+
"""
|
|
20
|
+
Abstract base class for all scheduled tasks.
|
|
21
|
+
|
|
22
|
+
Provides common functionality for task lifecycle management, error handling,
|
|
23
|
+
and exponential backoff for failed executions.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
name: str,
|
|
29
|
+
interval_seconds: float,
|
|
30
|
+
enabled: bool = True,
|
|
31
|
+
max_backoff_seconds: float = 300.0,
|
|
32
|
+
jitter_factor: float = 0.25,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Initialize scheduled task.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
name: Human-readable task name
|
|
39
|
+
interval_seconds: Interval between task executions in seconds
|
|
40
|
+
enabled: Whether the task is enabled
|
|
41
|
+
max_backoff_seconds: Maximum backoff delay for failed tasks
|
|
42
|
+
jitter_factor: Jitter factor for backoff randomization (0.0-1.0)
|
|
43
|
+
"""
|
|
44
|
+
self.name = name
|
|
45
|
+
self.interval_seconds = max(1.0, interval_seconds)
|
|
46
|
+
self.enabled = enabled
|
|
47
|
+
self.max_backoff_seconds = max_backoff_seconds
|
|
48
|
+
self.jitter_factor = min(1.0, max(0.0, jitter_factor))
|
|
49
|
+
|
|
50
|
+
self._consecutive_failures = 0
|
|
51
|
+
self._last_run_time: float = 0
|
|
52
|
+
self._running = False
|
|
53
|
+
self._task: asyncio.Task[Any] | None = None
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
async def run(self) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Execute the scheduled task.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
True if execution was successful, False otherwise
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
async def setup(self) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Perform any setup required before task execution starts.
|
|
68
|
+
|
|
69
|
+
Called once when the task is first started. Override if needed.
|
|
70
|
+
Default implementation does nothing.
|
|
71
|
+
"""
|
|
72
|
+
# Default implementation - subclasses can override if needed
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
async def cleanup(self) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Perform any cleanup required after task execution stops.
|
|
78
|
+
|
|
79
|
+
Called once when the task is stopped. Override if needed.
|
|
80
|
+
Default implementation does nothing.
|
|
81
|
+
"""
|
|
82
|
+
# Default implementation - subclasses can override if needed
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
def calculate_next_delay(self) -> float:
|
|
86
|
+
"""
|
|
87
|
+
Calculate the delay before the next task execution.
|
|
88
|
+
|
|
89
|
+
Returns exponential backoff delay for failed tasks, or normal interval
|
|
90
|
+
for successful tasks, with optional jitter.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Delay in seconds before next execution
|
|
94
|
+
"""
|
|
95
|
+
if self._consecutive_failures == 0:
|
|
96
|
+
base_delay = self.interval_seconds
|
|
97
|
+
else:
|
|
98
|
+
# Exponential backoff: interval * (2 ^ failures)
|
|
99
|
+
base_delay = self.interval_seconds * (2**self._consecutive_failures)
|
|
100
|
+
base_delay = min(base_delay, self.max_backoff_seconds)
|
|
101
|
+
|
|
102
|
+
# Add jitter to prevent thundering herd
|
|
103
|
+
if self.jitter_factor > 0:
|
|
104
|
+
jitter = base_delay * self.jitter_factor * (random.random() - 0.5)
|
|
105
|
+
base_delay += jitter
|
|
106
|
+
|
|
107
|
+
return max(1.0, base_delay)
|
|
108
|
+
|
|
109
|
+
async def start(self) -> None:
|
|
110
|
+
"""Start the scheduled task execution loop."""
|
|
111
|
+
if self._running or not self.enabled:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
self._running = True
|
|
115
|
+
logger.debug("task_starting", task_name=self.name)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
await self.setup()
|
|
119
|
+
self._task = asyncio.create_task(self._run_loop())
|
|
120
|
+
logger.debug("task_started", task_name=self.name)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
self._running = False
|
|
123
|
+
logger.error(
|
|
124
|
+
"task_start_failed",
|
|
125
|
+
task_name=self.name,
|
|
126
|
+
error=str(e),
|
|
127
|
+
error_type=type(e).__name__,
|
|
128
|
+
)
|
|
129
|
+
raise
|
|
130
|
+
|
|
131
|
+
async def stop(self) -> None:
|
|
132
|
+
"""Stop the scheduled task execution loop."""
|
|
133
|
+
if not self._running:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
self._running = False
|
|
137
|
+
logger.debug("task_stopping", task_name=self.name)
|
|
138
|
+
|
|
139
|
+
# Cancel the running task
|
|
140
|
+
if self._task and not self._task.done():
|
|
141
|
+
self._task.cancel()
|
|
142
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
143
|
+
await self._task
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
await self.cleanup()
|
|
147
|
+
logger.debug("task_stopped", task_name=self.name)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error(
|
|
150
|
+
"task_cleanup_failed",
|
|
151
|
+
task_name=self.name,
|
|
152
|
+
error=str(e),
|
|
153
|
+
error_type=type(e).__name__,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
async def _run_loop(self) -> None:
|
|
157
|
+
"""Main execution loop for the scheduled task."""
|
|
158
|
+
while self._running:
|
|
159
|
+
try:
|
|
160
|
+
start_time = time.time()
|
|
161
|
+
|
|
162
|
+
# Execute the task
|
|
163
|
+
success = await self.run()
|
|
164
|
+
|
|
165
|
+
execution_time = time.time() - start_time
|
|
166
|
+
|
|
167
|
+
if success:
|
|
168
|
+
self._consecutive_failures = 0
|
|
169
|
+
logger.debug(
|
|
170
|
+
"task_execution_success",
|
|
171
|
+
task_name=self.name,
|
|
172
|
+
execution_time=execution_time,
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
self._consecutive_failures += 1
|
|
176
|
+
logger.warning(
|
|
177
|
+
"task_execution_failed",
|
|
178
|
+
task_name=self.name,
|
|
179
|
+
consecutive_failures=self._consecutive_failures,
|
|
180
|
+
execution_time=execution_time,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
self._last_run_time = time.time()
|
|
184
|
+
|
|
185
|
+
# Calculate delay before next execution
|
|
186
|
+
delay = self.calculate_next_delay()
|
|
187
|
+
|
|
188
|
+
if not success and self._consecutive_failures > 1:
|
|
189
|
+
logger.info(
|
|
190
|
+
"task_backoff_delay",
|
|
191
|
+
task_name=self.name,
|
|
192
|
+
consecutive_failures=self._consecutive_failures,
|
|
193
|
+
delay=delay,
|
|
194
|
+
max_backoff=self.max_backoff_seconds,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Wait for next execution or cancellation
|
|
198
|
+
await asyncio.sleep(delay)
|
|
199
|
+
|
|
200
|
+
except asyncio.CancelledError:
|
|
201
|
+
logger.debug("task_cancelled", task_name=self.name)
|
|
202
|
+
break
|
|
203
|
+
except Exception as e:
|
|
204
|
+
self._consecutive_failures += 1
|
|
205
|
+
logger.error(
|
|
206
|
+
"task_execution_error",
|
|
207
|
+
task_name=self.name,
|
|
208
|
+
error=str(e),
|
|
209
|
+
error_type=type(e).__name__,
|
|
210
|
+
consecutive_failures=self._consecutive_failures,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Use backoff delay for exceptions too
|
|
214
|
+
backoff_delay = self.calculate_next_delay()
|
|
215
|
+
await asyncio.sleep(backoff_delay)
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def is_running(self) -> bool:
|
|
219
|
+
"""Check if the task is currently running."""
|
|
220
|
+
return self._running
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def consecutive_failures(self) -> int:
|
|
224
|
+
"""Get the number of consecutive failures."""
|
|
225
|
+
return self._consecutive_failures
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def last_run_time(self) -> float:
|
|
229
|
+
"""Get the timestamp of the last execution."""
|
|
230
|
+
return self._last_run_time
|
|
231
|
+
|
|
232
|
+
def get_status(self) -> dict[str, Any]:
|
|
233
|
+
"""
|
|
234
|
+
Get current task status information.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Dictionary with task status details
|
|
238
|
+
"""
|
|
239
|
+
return {
|
|
240
|
+
"name": self.name,
|
|
241
|
+
"enabled": self.enabled,
|
|
242
|
+
"running": self.is_running,
|
|
243
|
+
"interval_seconds": self.interval_seconds,
|
|
244
|
+
"consecutive_failures": self.consecutive_failures,
|
|
245
|
+
"last_run_time": self.last_run_time,
|
|
246
|
+
"next_delay": self.calculate_next_delay() if self.is_running else None,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class PushgatewayTask(BaseScheduledTask):
|
|
251
|
+
"""Task for pushing metrics to Pushgateway periodically."""
|
|
252
|
+
|
|
253
|
+
def __init__(
|
|
254
|
+
self,
|
|
255
|
+
name: str,
|
|
256
|
+
interval_seconds: float,
|
|
257
|
+
enabled: bool = True,
|
|
258
|
+
max_backoff_seconds: float = 300.0,
|
|
259
|
+
):
|
|
260
|
+
"""
|
|
261
|
+
Initialize pushgateway task.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
name: Task name
|
|
265
|
+
interval_seconds: Interval between pushgateway operations
|
|
266
|
+
enabled: Whether task is enabled
|
|
267
|
+
max_backoff_seconds: Maximum backoff delay for failures
|
|
268
|
+
"""
|
|
269
|
+
super().__init__(
|
|
270
|
+
name=name,
|
|
271
|
+
interval_seconds=interval_seconds,
|
|
272
|
+
enabled=enabled,
|
|
273
|
+
max_backoff_seconds=max_backoff_seconds,
|
|
274
|
+
)
|
|
275
|
+
self._metrics_instance: Any | None = None
|
|
276
|
+
|
|
277
|
+
async def setup(self) -> None:
|
|
278
|
+
"""Initialize metrics instance for pushgateway operations."""
|
|
279
|
+
try:
|
|
280
|
+
from ccproxy.observability.metrics import get_metrics
|
|
281
|
+
|
|
282
|
+
self._metrics_instance = get_metrics()
|
|
283
|
+
logger.debug("pushgateway_task_setup_complete", task_name=self.name)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
logger.error(
|
|
286
|
+
"pushgateway_task_setup_failed",
|
|
287
|
+
task_name=self.name,
|
|
288
|
+
error=str(e),
|
|
289
|
+
error_type=type(e).__name__,
|
|
290
|
+
)
|
|
291
|
+
raise
|
|
292
|
+
|
|
293
|
+
async def run(self) -> bool:
|
|
294
|
+
"""Execute pushgateway metrics push."""
|
|
295
|
+
try:
|
|
296
|
+
if not self._metrics_instance:
|
|
297
|
+
logger.warning("pushgateway_no_metrics_instance", task_name=self.name)
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
if not self._metrics_instance.is_pushgateway_enabled():
|
|
301
|
+
logger.debug("pushgateway_disabled", task_name=self.name)
|
|
302
|
+
return True # Not an error, just disabled
|
|
303
|
+
|
|
304
|
+
success = bool(self._metrics_instance.push_to_gateway())
|
|
305
|
+
|
|
306
|
+
if success:
|
|
307
|
+
logger.debug("pushgateway_push_success", task_name=self.name)
|
|
308
|
+
else:
|
|
309
|
+
logger.warning("pushgateway_push_failed", task_name=self.name)
|
|
310
|
+
|
|
311
|
+
return success
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
logger.error(
|
|
315
|
+
"pushgateway_task_error",
|
|
316
|
+
task_name=self.name,
|
|
317
|
+
error=str(e),
|
|
318
|
+
error_type=type(e).__name__,
|
|
319
|
+
)
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class StatsPrintingTask(BaseScheduledTask):
|
|
324
|
+
"""Task for printing stats summary periodically."""
|
|
325
|
+
|
|
326
|
+
def __init__(
|
|
327
|
+
self,
|
|
328
|
+
name: str,
|
|
329
|
+
interval_seconds: float,
|
|
330
|
+
enabled: bool = True,
|
|
331
|
+
):
|
|
332
|
+
"""
|
|
333
|
+
Initialize stats printing task.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
name: Task name
|
|
337
|
+
interval_seconds: Interval between stats printing
|
|
338
|
+
enabled: Whether task is enabled
|
|
339
|
+
"""
|
|
340
|
+
super().__init__(
|
|
341
|
+
name=name,
|
|
342
|
+
interval_seconds=interval_seconds,
|
|
343
|
+
enabled=enabled,
|
|
344
|
+
)
|
|
345
|
+
self._stats_collector_instance: Any | None = None
|
|
346
|
+
self._metrics_instance: Any | None = None
|
|
347
|
+
|
|
348
|
+
async def setup(self) -> None:
|
|
349
|
+
"""Initialize stats collector and metrics instances."""
|
|
350
|
+
try:
|
|
351
|
+
from ccproxy.config.settings import get_settings
|
|
352
|
+
from ccproxy.observability.metrics import get_metrics
|
|
353
|
+
from ccproxy.observability.stats_printer import get_stats_collector
|
|
354
|
+
|
|
355
|
+
self._metrics_instance = get_metrics()
|
|
356
|
+
settings = get_settings()
|
|
357
|
+
self._stats_collector_instance = get_stats_collector(
|
|
358
|
+
settings=settings.observability,
|
|
359
|
+
metrics_instance=self._metrics_instance,
|
|
360
|
+
)
|
|
361
|
+
logger.debug("stats_printing_task_setup_complete", task_name=self.name)
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.error(
|
|
364
|
+
"stats_printing_task_setup_failed",
|
|
365
|
+
task_name=self.name,
|
|
366
|
+
error=str(e),
|
|
367
|
+
error_type=type(e).__name__,
|
|
368
|
+
)
|
|
369
|
+
raise
|
|
370
|
+
|
|
371
|
+
async def run(self) -> bool:
|
|
372
|
+
"""Execute stats printing."""
|
|
373
|
+
try:
|
|
374
|
+
if not self._stats_collector_instance:
|
|
375
|
+
logger.warning("stats_printing_no_collector", task_name=self.name)
|
|
376
|
+
return False
|
|
377
|
+
|
|
378
|
+
await self._stats_collector_instance.print_stats()
|
|
379
|
+
logger.debug("stats_printing_success", task_name=self.name)
|
|
380
|
+
return True
|
|
381
|
+
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.error(
|
|
384
|
+
"stats_printing_task_error",
|
|
385
|
+
task_name=self.name,
|
|
386
|
+
error=str(e),
|
|
387
|
+
error_type=type(e).__name__,
|
|
388
|
+
)
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class PricingCacheUpdateTask(BaseScheduledTask):
|
|
393
|
+
"""Task for updating pricing cache periodically."""
|
|
394
|
+
|
|
395
|
+
def __init__(
|
|
396
|
+
self,
|
|
397
|
+
name: str,
|
|
398
|
+
interval_seconds: float,
|
|
399
|
+
enabled: bool = True,
|
|
400
|
+
force_refresh_on_startup: bool = False,
|
|
401
|
+
pricing_updater: Any | None = None,
|
|
402
|
+
):
|
|
403
|
+
"""
|
|
404
|
+
Initialize pricing cache update task.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
name: Task name
|
|
408
|
+
interval_seconds: Interval between pricing updates
|
|
409
|
+
enabled: Whether task is enabled
|
|
410
|
+
force_refresh_on_startup: Whether to force refresh on first run
|
|
411
|
+
pricing_updater: Injected pricing updater instance
|
|
412
|
+
"""
|
|
413
|
+
super().__init__(
|
|
414
|
+
name=name,
|
|
415
|
+
interval_seconds=interval_seconds,
|
|
416
|
+
enabled=enabled,
|
|
417
|
+
)
|
|
418
|
+
self.force_refresh_on_startup = force_refresh_on_startup
|
|
419
|
+
self._pricing_updater = pricing_updater
|
|
420
|
+
self._first_run = True
|
|
421
|
+
|
|
422
|
+
async def setup(self) -> None:
|
|
423
|
+
"""Initialize pricing updater instance if not injected."""
|
|
424
|
+
if self._pricing_updater is None:
|
|
425
|
+
try:
|
|
426
|
+
from ccproxy.config.pricing import PricingSettings
|
|
427
|
+
from ccproxy.pricing.cache import PricingCache
|
|
428
|
+
from ccproxy.pricing.updater import PricingUpdater
|
|
429
|
+
|
|
430
|
+
# Create pricing components with dependency injection
|
|
431
|
+
settings = PricingSettings()
|
|
432
|
+
cache = PricingCache(settings)
|
|
433
|
+
self._pricing_updater = PricingUpdater(cache, settings)
|
|
434
|
+
logger.debug("pricing_update_task_setup_complete", task_name=self.name)
|
|
435
|
+
except Exception as e:
|
|
436
|
+
logger.error(
|
|
437
|
+
"pricing_update_task_setup_failed",
|
|
438
|
+
task_name=self.name,
|
|
439
|
+
error=str(e),
|
|
440
|
+
error_type=type(e).__name__,
|
|
441
|
+
)
|
|
442
|
+
raise
|
|
443
|
+
else:
|
|
444
|
+
logger.debug(
|
|
445
|
+
"pricing_update_task_using_injected_updater", task_name=self.name
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
async def run(self) -> bool:
|
|
449
|
+
"""Execute pricing cache update."""
|
|
450
|
+
try:
|
|
451
|
+
if not self._pricing_updater:
|
|
452
|
+
logger.warning("pricing_update_no_updater", task_name=self.name)
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
# Force refresh on first run if configured
|
|
456
|
+
force_refresh = self._first_run and self.force_refresh_on_startup
|
|
457
|
+
self._first_run = False
|
|
458
|
+
|
|
459
|
+
if force_refresh:
|
|
460
|
+
logger.info("pricing_update_force_refresh_startup", task_name=self.name)
|
|
461
|
+
refresh_result = await self._pricing_updater.force_refresh()
|
|
462
|
+
success = bool(refresh_result)
|
|
463
|
+
else:
|
|
464
|
+
# Regular update check
|
|
465
|
+
pricing_data = await self._pricing_updater.get_current_pricing(
|
|
466
|
+
force_refresh=False
|
|
467
|
+
)
|
|
468
|
+
success = pricing_data is not None
|
|
469
|
+
|
|
470
|
+
if success:
|
|
471
|
+
logger.debug("pricing_update_success", task_name=self.name)
|
|
472
|
+
else:
|
|
473
|
+
logger.warning("pricing_update_failed", task_name=self.name)
|
|
474
|
+
|
|
475
|
+
return success
|
|
476
|
+
|
|
477
|
+
except Exception as e:
|
|
478
|
+
logger.error(
|
|
479
|
+
"pricing_update_task_error",
|
|
480
|
+
task_name=self.name,
|
|
481
|
+
error=str(e),
|
|
482
|
+
error_type=type(e).__name__,
|
|
483
|
+
)
|
|
484
|
+
return False
|