ccproxy-api 0.1.6__py3-none-any.whl → 0.2.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/api/__init__.py +1 -15
- ccproxy/api/app.py +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +95 -342
- ccproxy/utils/version_checker.py +279 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1231
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -269
- ccproxy/services/codex_detection_service.py +0 -263
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
ccproxy/scheduler/tasks.py
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
"""Base scheduled task classes and task implementations."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import contextlib
|
|
5
4
|
import random
|
|
6
5
|
import time
|
|
7
6
|
from abc import ABC, abstractmethod
|
|
8
|
-
from datetime import UTC
|
|
7
|
+
from datetime import UTC, datetime
|
|
9
8
|
from typing import Any
|
|
10
9
|
|
|
11
10
|
import structlog
|
|
11
|
+
from packaging import version as pkg_version
|
|
12
|
+
|
|
13
|
+
from ccproxy.core.async_task_manager import create_managed_task
|
|
14
|
+
from ccproxy.scheduler.errors import SchedulerError
|
|
15
|
+
from ccproxy.utils.version_checker import (
|
|
16
|
+
VersionCheckState,
|
|
17
|
+
commit_refs_match,
|
|
18
|
+
compare_versions,
|
|
19
|
+
extract_commit_from_version,
|
|
20
|
+
fetch_latest_branch_commit,
|
|
21
|
+
fetch_latest_github_version,
|
|
22
|
+
get_branch_override,
|
|
23
|
+
get_current_version,
|
|
24
|
+
get_version_check_state_path,
|
|
25
|
+
load_check_state,
|
|
26
|
+
resolve_branch_for_commit,
|
|
27
|
+
save_check_state,
|
|
28
|
+
)
|
|
12
29
|
|
|
13
30
|
|
|
14
31
|
logger = structlog.get_logger(__name__)
|
|
@@ -50,6 +67,7 @@ class BaseScheduledTask(ABC):
|
|
|
50
67
|
self._last_run_time: float = 0
|
|
51
68
|
self._running = False
|
|
52
69
|
self._task: asyncio.Task[Any] | None = None
|
|
70
|
+
self._stop_complete: asyncio.Event | None = None
|
|
53
71
|
|
|
54
72
|
@abstractmethod
|
|
55
73
|
async def run(self) -> bool:
|
|
@@ -111,12 +129,27 @@ class BaseScheduledTask(ABC):
|
|
|
111
129
|
return
|
|
112
130
|
|
|
113
131
|
self._running = True
|
|
132
|
+
self._stop_complete = asyncio.Event()
|
|
114
133
|
logger.debug("task_starting", task_name=self.name)
|
|
115
134
|
|
|
116
135
|
try:
|
|
117
136
|
await self.setup()
|
|
118
|
-
self._task =
|
|
137
|
+
self._task = await create_managed_task(
|
|
138
|
+
self._run_loop(),
|
|
139
|
+
name=f"scheduled_task_{self.name}",
|
|
140
|
+
creator="BaseScheduledTask",
|
|
141
|
+
)
|
|
119
142
|
logger.debug("task_started", task_name=self.name)
|
|
143
|
+
except SchedulerError as e:
|
|
144
|
+
self._running = False
|
|
145
|
+
logger.error(
|
|
146
|
+
"task_start_scheduler_error",
|
|
147
|
+
task_name=self.name,
|
|
148
|
+
error=str(e),
|
|
149
|
+
error_type=type(e).__name__,
|
|
150
|
+
exc_info=e,
|
|
151
|
+
)
|
|
152
|
+
raise
|
|
120
153
|
except Exception as e:
|
|
121
154
|
self._running = False
|
|
122
155
|
logger.error(
|
|
@@ -124,6 +157,7 @@ class BaseScheduledTask(ABC):
|
|
|
124
157
|
task_name=self.name,
|
|
125
158
|
error=str(e),
|
|
126
159
|
error_type=type(e).__name__,
|
|
160
|
+
exc_info=e,
|
|
127
161
|
)
|
|
128
162
|
raise
|
|
129
163
|
|
|
@@ -135,21 +169,55 @@ class BaseScheduledTask(ABC):
|
|
|
135
169
|
self._running = False
|
|
136
170
|
logger.debug("task_stopping", task_name=self.name)
|
|
137
171
|
|
|
138
|
-
# Cancel the running task
|
|
172
|
+
# Cancel the running task and wait for it to complete
|
|
139
173
|
if self._task and not self._task.done():
|
|
140
174
|
self._task.cancel()
|
|
141
|
-
|
|
175
|
+
try:
|
|
176
|
+
# Wait for the task to complete cancellation
|
|
142
177
|
await self._task
|
|
178
|
+
except asyncio.CancelledError:
|
|
179
|
+
# Expected when task is cancelled
|
|
180
|
+
pass
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.warning(
|
|
183
|
+
"task_stop_unexpected_error",
|
|
184
|
+
task_name=self.name,
|
|
185
|
+
error=str(e),
|
|
186
|
+
error_type=type(e).__name__,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Ensure the task reference is cleared
|
|
190
|
+
self._task = None
|
|
191
|
+
|
|
192
|
+
# Wait for the completion event to be signaled
|
|
193
|
+
if self._stop_complete is not None:
|
|
194
|
+
try:
|
|
195
|
+
await asyncio.wait_for(self._stop_complete.wait(), timeout=1.0)
|
|
196
|
+
except TimeoutError:
|
|
197
|
+
logger.warning(
|
|
198
|
+
"task_stop_completion_timeout",
|
|
199
|
+
task_name=self.name,
|
|
200
|
+
message="Task stop completion event not signaled within timeout",
|
|
201
|
+
)
|
|
143
202
|
|
|
144
203
|
try:
|
|
145
204
|
await self.cleanup()
|
|
146
205
|
logger.debug("task_stopped", task_name=self.name)
|
|
206
|
+
except SchedulerError as e:
|
|
207
|
+
logger.error(
|
|
208
|
+
"task_cleanup_scheduler_error",
|
|
209
|
+
task_name=self.name,
|
|
210
|
+
error=str(e),
|
|
211
|
+
error_type=type(e).__name__,
|
|
212
|
+
exc_info=e,
|
|
213
|
+
)
|
|
147
214
|
except Exception as e:
|
|
148
215
|
logger.error(
|
|
149
216
|
"task_cleanup_failed",
|
|
150
217
|
task_name=self.name,
|
|
151
218
|
error=str(e),
|
|
152
219
|
error_type=type(e).__name__,
|
|
220
|
+
exc_info=e,
|
|
153
221
|
)
|
|
154
222
|
|
|
155
223
|
async def _run_loop(self) -> None:
|
|
@@ -199,6 +267,32 @@ class BaseScheduledTask(ABC):
|
|
|
199
267
|
except asyncio.CancelledError:
|
|
200
268
|
logger.debug("task_cancelled", task_name=self.name)
|
|
201
269
|
break
|
|
270
|
+
except TimeoutError as e:
|
|
271
|
+
self._consecutive_failures += 1
|
|
272
|
+
logger.error(
|
|
273
|
+
"task_execution_timeout_error",
|
|
274
|
+
task_name=self.name,
|
|
275
|
+
error=str(e),
|
|
276
|
+
error_type=type(e).__name__,
|
|
277
|
+
consecutive_failures=self._consecutive_failures,
|
|
278
|
+
exc_info=e,
|
|
279
|
+
)
|
|
280
|
+
# Use backoff delay for exceptions too
|
|
281
|
+
backoff_delay = self.calculate_next_delay()
|
|
282
|
+
await asyncio.sleep(backoff_delay)
|
|
283
|
+
except SchedulerError as e:
|
|
284
|
+
self._consecutive_failures += 1
|
|
285
|
+
logger.error(
|
|
286
|
+
"task_execution_scheduler_error",
|
|
287
|
+
task_name=self.name,
|
|
288
|
+
error=str(e),
|
|
289
|
+
error_type=type(e).__name__,
|
|
290
|
+
consecutive_failures=self._consecutive_failures,
|
|
291
|
+
exc_info=e,
|
|
292
|
+
)
|
|
293
|
+
# Use backoff delay for exceptions too
|
|
294
|
+
backoff_delay = self.calculate_next_delay()
|
|
295
|
+
await asyncio.sleep(backoff_delay)
|
|
202
296
|
except Exception as e:
|
|
203
297
|
self._consecutive_failures += 1
|
|
204
298
|
logger.error(
|
|
@@ -207,12 +301,16 @@ class BaseScheduledTask(ABC):
|
|
|
207
301
|
error=str(e),
|
|
208
302
|
error_type=type(e).__name__,
|
|
209
303
|
consecutive_failures=self._consecutive_failures,
|
|
304
|
+
exc_info=e,
|
|
210
305
|
)
|
|
211
|
-
|
|
212
306
|
# Use backoff delay for exceptions too
|
|
213
307
|
backoff_delay = self.calculate_next_delay()
|
|
214
308
|
await asyncio.sleep(backoff_delay)
|
|
215
309
|
|
|
310
|
+
# Signal that the task has completed
|
|
311
|
+
if self._stop_complete is not None:
|
|
312
|
+
self._stop_complete.set()
|
|
313
|
+
|
|
216
314
|
@property
|
|
217
315
|
def is_running(self) -> bool:
|
|
218
316
|
"""Check if the task is currently running."""
|
|
@@ -246,243 +344,6 @@ class BaseScheduledTask(ABC):
|
|
|
246
344
|
}
|
|
247
345
|
|
|
248
346
|
|
|
249
|
-
class PushgatewayTask(BaseScheduledTask):
|
|
250
|
-
"""Task for pushing metrics to Pushgateway periodically."""
|
|
251
|
-
|
|
252
|
-
def __init__(
|
|
253
|
-
self,
|
|
254
|
-
name: str,
|
|
255
|
-
interval_seconds: float,
|
|
256
|
-
enabled: bool = True,
|
|
257
|
-
max_backoff_seconds: float = 300.0,
|
|
258
|
-
):
|
|
259
|
-
"""
|
|
260
|
-
Initialize pushgateway task.
|
|
261
|
-
|
|
262
|
-
Args:
|
|
263
|
-
name: Task name
|
|
264
|
-
interval_seconds: Interval between pushgateway operations
|
|
265
|
-
enabled: Whether task is enabled
|
|
266
|
-
max_backoff_seconds: Maximum backoff delay for failures
|
|
267
|
-
"""
|
|
268
|
-
super().__init__(
|
|
269
|
-
name=name,
|
|
270
|
-
interval_seconds=interval_seconds,
|
|
271
|
-
enabled=enabled,
|
|
272
|
-
max_backoff_seconds=max_backoff_seconds,
|
|
273
|
-
)
|
|
274
|
-
self._metrics_instance: Any | None = None
|
|
275
|
-
|
|
276
|
-
async def setup(self) -> None:
|
|
277
|
-
"""Initialize metrics instance for pushgateway operations."""
|
|
278
|
-
try:
|
|
279
|
-
from ccproxy.observability.metrics import get_metrics
|
|
280
|
-
|
|
281
|
-
self._metrics_instance = get_metrics()
|
|
282
|
-
logger.debug("pushgateway_task_setup_complete", task_name=self.name)
|
|
283
|
-
except Exception as e:
|
|
284
|
-
logger.error(
|
|
285
|
-
"pushgateway_task_setup_failed",
|
|
286
|
-
task_name=self.name,
|
|
287
|
-
error=str(e),
|
|
288
|
-
error_type=type(e).__name__,
|
|
289
|
-
)
|
|
290
|
-
raise
|
|
291
|
-
|
|
292
|
-
async def run(self) -> bool:
|
|
293
|
-
"""Execute pushgateway metrics push."""
|
|
294
|
-
try:
|
|
295
|
-
if not self._metrics_instance:
|
|
296
|
-
logger.warning("pushgateway_no_metrics_instance", task_name=self.name)
|
|
297
|
-
return False
|
|
298
|
-
|
|
299
|
-
if not self._metrics_instance.is_pushgateway_enabled():
|
|
300
|
-
logger.debug("pushgateway_disabled", task_name=self.name)
|
|
301
|
-
return True # Not an error, just disabled
|
|
302
|
-
|
|
303
|
-
success = bool(self._metrics_instance.push_to_gateway())
|
|
304
|
-
|
|
305
|
-
if success:
|
|
306
|
-
logger.debug("pushgateway_push_success", task_name=self.name)
|
|
307
|
-
else:
|
|
308
|
-
logger.warning("pushgateway_push_failed", task_name=self.name)
|
|
309
|
-
|
|
310
|
-
return success
|
|
311
|
-
|
|
312
|
-
except Exception as e:
|
|
313
|
-
logger.error(
|
|
314
|
-
"pushgateway_task_error",
|
|
315
|
-
task_name=self.name,
|
|
316
|
-
error=str(e),
|
|
317
|
-
error_type=type(e).__name__,
|
|
318
|
-
)
|
|
319
|
-
return False
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
class StatsPrintingTask(BaseScheduledTask):
|
|
323
|
-
"""Task for printing stats summary periodically."""
|
|
324
|
-
|
|
325
|
-
def __init__(
|
|
326
|
-
self,
|
|
327
|
-
name: str,
|
|
328
|
-
interval_seconds: float,
|
|
329
|
-
enabled: bool = True,
|
|
330
|
-
):
|
|
331
|
-
"""
|
|
332
|
-
Initialize stats printing task.
|
|
333
|
-
|
|
334
|
-
Args:
|
|
335
|
-
name: Task name
|
|
336
|
-
interval_seconds: Interval between stats printing
|
|
337
|
-
enabled: Whether task is enabled
|
|
338
|
-
"""
|
|
339
|
-
super().__init__(
|
|
340
|
-
name=name,
|
|
341
|
-
interval_seconds=interval_seconds,
|
|
342
|
-
enabled=enabled,
|
|
343
|
-
)
|
|
344
|
-
self._stats_collector_instance: Any | None = None
|
|
345
|
-
self._metrics_instance: Any | None = None
|
|
346
|
-
|
|
347
|
-
async def setup(self) -> None:
|
|
348
|
-
"""Initialize stats collector and metrics instances."""
|
|
349
|
-
try:
|
|
350
|
-
from ccproxy.config.settings import get_settings
|
|
351
|
-
from ccproxy.observability.metrics import get_metrics
|
|
352
|
-
from ccproxy.observability.stats_printer import get_stats_collector
|
|
353
|
-
|
|
354
|
-
self._metrics_instance = get_metrics()
|
|
355
|
-
settings = get_settings()
|
|
356
|
-
self._stats_collector_instance = get_stats_collector(
|
|
357
|
-
settings=settings.observability,
|
|
358
|
-
metrics_instance=self._metrics_instance,
|
|
359
|
-
)
|
|
360
|
-
logger.debug("stats_printing_task_setup_complete", task_name=self.name)
|
|
361
|
-
except Exception as e:
|
|
362
|
-
logger.error(
|
|
363
|
-
"stats_printing_task_setup_failed",
|
|
364
|
-
task_name=self.name,
|
|
365
|
-
error=str(e),
|
|
366
|
-
error_type=type(e).__name__,
|
|
367
|
-
)
|
|
368
|
-
raise
|
|
369
|
-
|
|
370
|
-
async def run(self) -> bool:
|
|
371
|
-
"""Execute stats printing."""
|
|
372
|
-
try:
|
|
373
|
-
if not self._stats_collector_instance:
|
|
374
|
-
logger.warning("stats_printing_no_collector", task_name=self.name)
|
|
375
|
-
return False
|
|
376
|
-
|
|
377
|
-
await self._stats_collector_instance.print_stats()
|
|
378
|
-
logger.debug("stats_printing_success", task_name=self.name)
|
|
379
|
-
return True
|
|
380
|
-
|
|
381
|
-
except Exception as e:
|
|
382
|
-
logger.error(
|
|
383
|
-
"stats_printing_task_error",
|
|
384
|
-
task_name=self.name,
|
|
385
|
-
error=str(e),
|
|
386
|
-
error_type=type(e).__name__,
|
|
387
|
-
)
|
|
388
|
-
return False
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
class PricingCacheUpdateTask(BaseScheduledTask):
|
|
392
|
-
"""Task for updating pricing cache periodically."""
|
|
393
|
-
|
|
394
|
-
def __init__(
|
|
395
|
-
self,
|
|
396
|
-
name: str,
|
|
397
|
-
interval_seconds: float,
|
|
398
|
-
enabled: bool = True,
|
|
399
|
-
force_refresh_on_startup: bool = False,
|
|
400
|
-
pricing_updater: Any | None = None,
|
|
401
|
-
):
|
|
402
|
-
"""
|
|
403
|
-
Initialize pricing cache update task.
|
|
404
|
-
|
|
405
|
-
Args:
|
|
406
|
-
name: Task name
|
|
407
|
-
interval_seconds: Interval between pricing updates
|
|
408
|
-
enabled: Whether task is enabled
|
|
409
|
-
force_refresh_on_startup: Whether to force refresh on first run
|
|
410
|
-
pricing_updater: Injected pricing updater instance
|
|
411
|
-
"""
|
|
412
|
-
super().__init__(
|
|
413
|
-
name=name,
|
|
414
|
-
interval_seconds=interval_seconds,
|
|
415
|
-
enabled=enabled,
|
|
416
|
-
)
|
|
417
|
-
self.force_refresh_on_startup = force_refresh_on_startup
|
|
418
|
-
self._pricing_updater = pricing_updater
|
|
419
|
-
self._first_run = True
|
|
420
|
-
|
|
421
|
-
async def setup(self) -> None:
|
|
422
|
-
"""Initialize pricing updater instance if not injected."""
|
|
423
|
-
if self._pricing_updater is None:
|
|
424
|
-
try:
|
|
425
|
-
from ccproxy.config.pricing import PricingSettings
|
|
426
|
-
from ccproxy.pricing.cache import PricingCache
|
|
427
|
-
from ccproxy.pricing.updater import PricingUpdater
|
|
428
|
-
|
|
429
|
-
# Create pricing components with dependency injection
|
|
430
|
-
settings = PricingSettings()
|
|
431
|
-
cache = PricingCache(settings)
|
|
432
|
-
self._pricing_updater = PricingUpdater(cache, settings)
|
|
433
|
-
logger.debug("pricing_update_task_setup_complete", task_name=self.name)
|
|
434
|
-
except Exception as e:
|
|
435
|
-
logger.error(
|
|
436
|
-
"pricing_update_task_setup_failed",
|
|
437
|
-
task_name=self.name,
|
|
438
|
-
error=str(e),
|
|
439
|
-
error_type=type(e).__name__,
|
|
440
|
-
)
|
|
441
|
-
raise
|
|
442
|
-
else:
|
|
443
|
-
logger.debug(
|
|
444
|
-
"pricing_update_task_using_injected_updater", task_name=self.name
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
async def run(self) -> bool:
|
|
448
|
-
"""Execute pricing cache update."""
|
|
449
|
-
try:
|
|
450
|
-
if not self._pricing_updater:
|
|
451
|
-
logger.warning("pricing_update_no_updater", task_name=self.name)
|
|
452
|
-
return False
|
|
453
|
-
|
|
454
|
-
# Force refresh on first run if configured
|
|
455
|
-
force_refresh = self._first_run and self.force_refresh_on_startup
|
|
456
|
-
self._first_run = False
|
|
457
|
-
|
|
458
|
-
if force_refresh:
|
|
459
|
-
logger.info("pricing_update_force_refresh_startup", task_name=self.name)
|
|
460
|
-
refresh_result = await self._pricing_updater.force_refresh()
|
|
461
|
-
success = bool(refresh_result)
|
|
462
|
-
else:
|
|
463
|
-
# Regular update check
|
|
464
|
-
pricing_data = await self._pricing_updater.get_current_pricing(
|
|
465
|
-
force_refresh=False
|
|
466
|
-
)
|
|
467
|
-
success = pricing_data is not None
|
|
468
|
-
|
|
469
|
-
if success:
|
|
470
|
-
logger.debug("pricing_update_success", task_name=self.name)
|
|
471
|
-
else:
|
|
472
|
-
logger.warning("pricing_update_failed", task_name=self.name)
|
|
473
|
-
|
|
474
|
-
return success
|
|
475
|
-
|
|
476
|
-
except Exception as e:
|
|
477
|
-
logger.error(
|
|
478
|
-
"pricing_update_task_error",
|
|
479
|
-
task_name=self.name,
|
|
480
|
-
error=str(e),
|
|
481
|
-
error_type=type(e).__name__,
|
|
482
|
-
)
|
|
483
|
-
return False
|
|
484
|
-
|
|
485
|
-
|
|
486
347
|
class PoolStatsTask(BaseScheduledTask):
|
|
487
348
|
"""Task for displaying pool statistics periodically."""
|
|
488
349
|
|
|
@@ -601,6 +462,7 @@ class PoolStatsTask(BaseScheduledTask):
|
|
|
601
462
|
task_name=self.name,
|
|
602
463
|
error=str(e),
|
|
603
464
|
error_type=type(e).__name__,
|
|
465
|
+
exc_info=e,
|
|
604
466
|
)
|
|
605
467
|
return False
|
|
606
468
|
|
|
@@ -613,7 +475,9 @@ class VersionUpdateCheckTask(BaseScheduledTask):
|
|
|
613
475
|
name: str,
|
|
614
476
|
interval_seconds: float,
|
|
615
477
|
enabled: bool = True,
|
|
616
|
-
|
|
478
|
+
version_check_cache_ttl_hours: float = 1.0,
|
|
479
|
+
*,
|
|
480
|
+
skip_first_scheduled_run: bool = True,
|
|
617
481
|
):
|
|
618
482
|
"""
|
|
619
483
|
Initialize version update check task.
|
|
@@ -622,110 +486,278 @@ class VersionUpdateCheckTask(BaseScheduledTask):
|
|
|
622
486
|
name: Task name
|
|
623
487
|
interval_seconds: Interval between version checks
|
|
624
488
|
enabled: Whether task is enabled
|
|
625
|
-
|
|
489
|
+
version_check_cache_ttl_hours: Maximum cache age (hours) used at startup before contacting GitHub
|
|
490
|
+
skip_first_scheduled_run: If True, first scheduled loop execution is skipped
|
|
626
491
|
"""
|
|
627
492
|
super().__init__(
|
|
628
493
|
name=name,
|
|
629
494
|
interval_seconds=interval_seconds,
|
|
630
495
|
enabled=enabled,
|
|
631
496
|
)
|
|
632
|
-
self.
|
|
497
|
+
self.version_check_cache_ttl_hours = version_check_cache_ttl_hours
|
|
498
|
+
# Mark first scheduled execution; allow skipping to avoid duplicate run after startup
|
|
633
499
|
self._first_run = True
|
|
500
|
+
self._skip_first_run = skip_first_scheduled_run
|
|
501
|
+
|
|
502
|
+
def _log_version_comparison(
|
|
503
|
+
self, current_version: str, latest_version: str, *, source: str | None = None
|
|
504
|
+
) -> None:
|
|
505
|
+
"""
|
|
506
|
+
Log version comparison results with appropriate warning level.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
current_version: Current version string
|
|
510
|
+
latest_version: Latest version string
|
|
511
|
+
"""
|
|
512
|
+
if compare_versions(current_version, latest_version):
|
|
513
|
+
logger.warning(
|
|
514
|
+
"version_update_available",
|
|
515
|
+
task_name=self.name,
|
|
516
|
+
current_version=current_version,
|
|
517
|
+
latest_version=latest_version,
|
|
518
|
+
source=source,
|
|
519
|
+
description=(f"New version available: {latest_version}"),
|
|
520
|
+
)
|
|
521
|
+
else:
|
|
522
|
+
logger.debug(
|
|
523
|
+
"version_check_complete_no_update",
|
|
524
|
+
task_name=self.name,
|
|
525
|
+
current_version=current_version,
|
|
526
|
+
latest_version=latest_version,
|
|
527
|
+
source=source,
|
|
528
|
+
description=(
|
|
529
|
+
f"No update: latest_version={latest_version} "
|
|
530
|
+
f"current_version={current_version}"
|
|
531
|
+
),
|
|
532
|
+
)
|
|
634
533
|
|
|
635
534
|
async def run(self) -> bool:
|
|
636
535
|
"""Execute version update check."""
|
|
637
536
|
try:
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
compare_versions,
|
|
643
|
-
fetch_latest_github_version,
|
|
644
|
-
get_current_version,
|
|
645
|
-
get_version_check_state_path,
|
|
646
|
-
load_check_state,
|
|
647
|
-
save_check_state,
|
|
537
|
+
logger.debug(
|
|
538
|
+
"version_check_task_run_start",
|
|
539
|
+
task_name=self.name,
|
|
540
|
+
first_run=self._first_run,
|
|
648
541
|
)
|
|
649
|
-
|
|
650
542
|
state_path = get_version_check_state_path()
|
|
651
543
|
current_time = datetime.now(UTC)
|
|
652
544
|
|
|
653
|
-
#
|
|
654
|
-
if self._first_run:
|
|
545
|
+
# Skip first scheduled run to avoid duplicate check after startup
|
|
546
|
+
if self._first_run and self._skip_first_run:
|
|
655
547
|
self._first_run = False
|
|
656
|
-
|
|
548
|
+
logger.debug(
|
|
549
|
+
"version_check_first_run_skipped",
|
|
550
|
+
task_name=self.name,
|
|
551
|
+
message="Skipping first scheduled run since startup check already completed",
|
|
552
|
+
)
|
|
553
|
+
return True
|
|
657
554
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
# Check age of last check
|
|
662
|
-
time_diff = current_time - existing_state.last_check_at
|
|
663
|
-
age_hours = time_diff.total_seconds() / 3600
|
|
555
|
+
# Determine freshness window using configured cache TTL
|
|
556
|
+
# Applies to both startup and scheduled runs to avoid unnecessary network calls
|
|
557
|
+
max_age_hours = self.version_check_cache_ttl_hours
|
|
664
558
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
559
|
+
# Load previous state if available
|
|
560
|
+
prev_state: VersionCheckState | None = await load_check_state(state_path)
|
|
561
|
+
|
|
562
|
+
current_version = get_current_version()
|
|
563
|
+
current_commit = extract_commit_from_version(current_version)
|
|
564
|
+
|
|
565
|
+
if prev_state is not None:
|
|
566
|
+
invalidation_reason: str | None = None
|
|
567
|
+
if (
|
|
568
|
+
prev_state.running_version is not None
|
|
569
|
+
and prev_state.running_version != current_version
|
|
570
|
+
):
|
|
571
|
+
invalidation_reason = "version"
|
|
572
|
+
elif (
|
|
573
|
+
prev_state.running_commit is not None
|
|
574
|
+
and current_commit is not None
|
|
575
|
+
and not commit_refs_match(prev_state.running_commit, current_commit)
|
|
576
|
+
):
|
|
577
|
+
invalidation_reason = "commit"
|
|
578
|
+
|
|
579
|
+
if invalidation_reason is not None:
|
|
580
|
+
logger.debug(
|
|
581
|
+
"version_check_cache_invalidated",
|
|
582
|
+
task_name=self.name,
|
|
583
|
+
reason=invalidation_reason,
|
|
584
|
+
cached_running_version=prev_state.running_version,
|
|
585
|
+
cached_running_commit=prev_state.running_commit,
|
|
586
|
+
current_version=current_version,
|
|
587
|
+
current_commit=current_commit,
|
|
588
|
+
)
|
|
589
|
+
prev_state = None
|
|
590
|
+
|
|
591
|
+
latest_version: str | None = None
|
|
592
|
+
latest_branch_commit: str | None = None
|
|
593
|
+
source: str | None = None
|
|
594
|
+
|
|
595
|
+
# If we have a recent state within the freshness window, avoid network call
|
|
596
|
+
if prev_state is not None:
|
|
597
|
+
age_hours = (
|
|
598
|
+
current_time - prev_state.last_check_at
|
|
599
|
+
).total_seconds() / 3600.0
|
|
600
|
+
if age_hours < max_age_hours:
|
|
601
|
+
logger.debug(
|
|
602
|
+
"version_check_cache_fresh",
|
|
603
|
+
task_name=self.name,
|
|
604
|
+
age_hours=round(age_hours, 3),
|
|
605
|
+
max_age_hours=max_age_hours,
|
|
606
|
+
)
|
|
607
|
+
latest_version = prev_state.latest_version_found
|
|
608
|
+
latest_branch_commit = prev_state.latest_branch_commit
|
|
609
|
+
source = "cache"
|
|
610
|
+
else:
|
|
611
|
+
logger.debug(
|
|
612
|
+
"version_check_cache_stale",
|
|
613
|
+
task_name=self.name,
|
|
614
|
+
age_hours=round(age_hours, 3),
|
|
615
|
+
max_age_hours=max_age_hours,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
current_version_parsed = pkg_version.parse(current_version)
|
|
619
|
+
branch_name: str | None = None
|
|
620
|
+
|
|
621
|
+
if current_version_parsed.is_devrelease and current_commit is not None:
|
|
622
|
+
branch_name = get_branch_override()
|
|
623
|
+
if branch_name is None and prev_state is not None:
|
|
624
|
+
branch_name = prev_state.latest_branch_name
|
|
625
|
+
if branch_name is None:
|
|
626
|
+
branch_name = await resolve_branch_for_commit(current_commit)
|
|
627
|
+
|
|
628
|
+
if branch_name is not None:
|
|
629
|
+
if source == "cache" and (
|
|
630
|
+
prev_state is None
|
|
631
|
+
or prev_state.latest_branch_name != branch_name
|
|
632
|
+
or not prev_state.latest_branch_commit
|
|
633
|
+
):
|
|
634
|
+
latest_branch_commit = None
|
|
635
|
+
source = None
|
|
636
|
+
|
|
637
|
+
if latest_branch_commit is None:
|
|
638
|
+
latest_branch_commit = await fetch_latest_branch_commit(branch_name)
|
|
639
|
+
if latest_branch_commit is None:
|
|
640
|
+
logger.warning(
|
|
641
|
+
"version_check_branch_fetch_failed",
|
|
642
|
+
task_name=self.name,
|
|
643
|
+
branch=branch_name,
|
|
644
|
+
)
|
|
645
|
+
return False
|
|
646
|
+
|
|
647
|
+
await save_check_state(
|
|
648
|
+
state_path,
|
|
649
|
+
VersionCheckState(
|
|
650
|
+
last_check_at=current_time,
|
|
651
|
+
latest_version_found=(
|
|
652
|
+
latest_version
|
|
653
|
+
or (
|
|
654
|
+
prev_state.latest_version_found
|
|
655
|
+
if prev_state is not None
|
|
656
|
+
else None
|
|
657
|
+
)
|
|
658
|
+
),
|
|
659
|
+
latest_branch_name=branch_name,
|
|
660
|
+
latest_branch_commit=latest_branch_commit,
|
|
661
|
+
running_version=current_version,
|
|
662
|
+
running_commit=current_commit,
|
|
663
|
+
),
|
|
664
|
+
)
|
|
665
|
+
source = "network"
|
|
666
|
+
|
|
667
|
+
if current_commit is None:
|
|
668
|
+
logger.debug(
|
|
669
|
+
"branch_revision_no_commit_to_compare",
|
|
670
|
+
task_name=self.name,
|
|
671
|
+
branch=branch_name,
|
|
672
|
+
source=source,
|
|
673
|
+
)
|
|
674
|
+
else:
|
|
675
|
+
update_available = not commit_refs_match(
|
|
676
|
+
current_commit, latest_branch_commit
|
|
677
|
+
)
|
|
678
|
+
if update_available:
|
|
679
|
+
logger.warning(
|
|
680
|
+
"branch_revision_update_available",
|
|
669
681
|
task_name=self.name,
|
|
670
|
-
|
|
671
|
-
|
|
682
|
+
branch=branch_name,
|
|
683
|
+
current_commit=current_commit,
|
|
684
|
+
latest_commit=latest_branch_commit,
|
|
685
|
+
source=source,
|
|
686
|
+
description=(
|
|
687
|
+
"New commits available for branch "
|
|
688
|
+
f"{branch_name}: {latest_branch_commit}"
|
|
689
|
+
),
|
|
672
690
|
)
|
|
673
691
|
else:
|
|
674
692
|
logger.debug(
|
|
675
|
-
"
|
|
693
|
+
"branch_revision_up_to_date",
|
|
676
694
|
task_name=self.name,
|
|
677
|
-
|
|
678
|
-
|
|
695
|
+
branch=branch_name,
|
|
696
|
+
current_commit=current_commit,
|
|
697
|
+
source=source,
|
|
679
698
|
)
|
|
680
|
-
return True # Skip this run
|
|
681
|
-
else:
|
|
682
|
-
# No previous state, run check
|
|
683
|
-
should_run_startup_check = True
|
|
684
|
-
logger.debug("version_check_startup_no_state", task_name=self.name)
|
|
685
|
-
|
|
686
|
-
if not should_run_startup_check:
|
|
687
|
-
return True
|
|
688
|
-
|
|
689
|
-
# Fetch latest version from GitHub
|
|
690
|
-
latest_version = await fetch_latest_github_version()
|
|
691
|
-
if latest_version is None:
|
|
692
|
-
logger.warning("version_check_fetch_failed", task_name=self.name)
|
|
693
|
-
return False
|
|
694
|
-
|
|
695
|
-
# Get current version
|
|
696
|
-
current_version = get_current_version()
|
|
697
|
-
|
|
698
|
-
# Save state
|
|
699
|
-
new_state = VersionCheckState(
|
|
700
|
-
last_check_at=current_time,
|
|
701
|
-
latest_version_found=latest_version,
|
|
702
|
-
)
|
|
703
|
-
await save_check_state(state_path, new_state)
|
|
704
|
-
|
|
705
|
-
# Compare versions
|
|
706
|
-
if compare_versions(current_version, latest_version):
|
|
707
|
-
logger.info(
|
|
708
|
-
"version_update_available",
|
|
709
|
-
task_name=self.name,
|
|
710
|
-
current_version=current_version,
|
|
711
|
-
latest_version=latest_version,
|
|
712
|
-
message=f"New version {latest_version} available! You are running {current_version}",
|
|
713
|
-
)
|
|
714
699
|
else:
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
700
|
+
if latest_version is None:
|
|
701
|
+
latest_version = await fetch_latest_github_version()
|
|
702
|
+
if latest_version is None:
|
|
703
|
+
logger.warning(
|
|
704
|
+
"version_check_fetch_failed", task_name=self.name
|
|
705
|
+
)
|
|
706
|
+
return False
|
|
707
|
+
await save_check_state(
|
|
708
|
+
state_path,
|
|
709
|
+
VersionCheckState(
|
|
710
|
+
last_check_at=current_time,
|
|
711
|
+
latest_version_found=latest_version,
|
|
712
|
+
latest_branch_name=(
|
|
713
|
+
prev_state.latest_branch_name
|
|
714
|
+
if prev_state is not None
|
|
715
|
+
else None
|
|
716
|
+
),
|
|
717
|
+
latest_branch_commit=(
|
|
718
|
+
prev_state.latest_branch_commit
|
|
719
|
+
if prev_state is not None
|
|
720
|
+
else None
|
|
721
|
+
),
|
|
722
|
+
running_version=current_version,
|
|
723
|
+
running_commit=current_commit,
|
|
724
|
+
),
|
|
725
|
+
)
|
|
726
|
+
source = "network"
|
|
727
|
+
self._log_version_comparison(
|
|
728
|
+
current_version, latest_version, source=source
|
|
720
729
|
)
|
|
721
730
|
|
|
731
|
+
# Mark first run as complete
|
|
732
|
+
if self._first_run:
|
|
733
|
+
self._first_run = False
|
|
734
|
+
|
|
722
735
|
return True
|
|
723
736
|
|
|
737
|
+
except ImportError as e:
|
|
738
|
+
logger.error(
|
|
739
|
+
"version_check_task_import_error",
|
|
740
|
+
task_name=self.name,
|
|
741
|
+
error=str(e),
|
|
742
|
+
error_type=type(e).__name__,
|
|
743
|
+
exc_info=e,
|
|
744
|
+
)
|
|
745
|
+
return False
|
|
746
|
+
|
|
724
747
|
except Exception as e:
|
|
725
748
|
logger.error(
|
|
726
749
|
"version_check_task_error",
|
|
727
750
|
task_name=self.name,
|
|
728
751
|
error=str(e),
|
|
729
752
|
error_type=type(e).__name__,
|
|
753
|
+
exc_info=e,
|
|
730
754
|
)
|
|
731
755
|
return False
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
# Test helper task exposed for tests that import from this module
|
|
759
|
+
class MockScheduledTask(BaseScheduledTask):
|
|
760
|
+
"""Minimal mock task used by tests for registration and lifecycle checks."""
|
|
761
|
+
|
|
762
|
+
async def run(self) -> bool:
|
|
763
|
+
return True
|