ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0a4__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 +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- 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 +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- 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 +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- 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 +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0a4.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0a4.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0a4.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 -1251
- 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 -243
- ccproxy/services/codex_detection_service.py +0 -252
- 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.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.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.7.dist-info → ccproxy_api-0.2.0a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
"""Centralized async task management for lifecycle control and resource cleanup.
|
|
2
|
+
|
|
3
|
+
This module provides a centralized task manager that tracks all spawned async tasks,
|
|
4
|
+
handles proper cancellation on shutdown, and provides exception handling for
|
|
5
|
+
background tasks to prevent resource leaks and unhandled exceptions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import contextlib
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from collections.abc import Awaitable, Callable
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Optional, TypeVar
|
|
14
|
+
|
|
15
|
+
from ccproxy.core.logging import TraceBoundLogger, get_logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING: # pragma: no cover - import for type checking only
|
|
19
|
+
from ccproxy.services.container import ServiceContainer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
24
|
+
logger: TraceBoundLogger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TaskInfo:
|
|
28
|
+
"""Information about a managed task."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
task: asyncio.Task[Any],
|
|
33
|
+
name: str,
|
|
34
|
+
created_at: float,
|
|
35
|
+
creator: str | None = None,
|
|
36
|
+
cleanup_callback: Callable[[], None] | None = None,
|
|
37
|
+
):
|
|
38
|
+
self.task = task
|
|
39
|
+
self.name = name
|
|
40
|
+
self.created_at = created_at
|
|
41
|
+
self.creator = creator
|
|
42
|
+
self.cleanup_callback = cleanup_callback
|
|
43
|
+
self.task_id = str(uuid.uuid4())
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def age_seconds(self) -> float:
|
|
47
|
+
"""Get the age of the task in seconds."""
|
|
48
|
+
return time.time() - self.created_at
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def is_done(self) -> bool:
|
|
52
|
+
"""Check if the task is done."""
|
|
53
|
+
return self.task.done()
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_cancelled(self) -> bool:
|
|
57
|
+
"""Check if the task was cancelled."""
|
|
58
|
+
return self.task.cancelled()
|
|
59
|
+
|
|
60
|
+
def get_exception(self) -> BaseException | None:
|
|
61
|
+
"""Get the exception if the task failed."""
|
|
62
|
+
if self.task.done() and not self.task.cancelled():
|
|
63
|
+
try:
|
|
64
|
+
return self.task.exception()
|
|
65
|
+
except asyncio.InvalidStateError:
|
|
66
|
+
return None
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AsyncTaskManager:
|
|
71
|
+
"""Centralized manager for async tasks with lifecycle control.
|
|
72
|
+
|
|
73
|
+
This class provides:
|
|
74
|
+
- Task registration and tracking
|
|
75
|
+
- Automatic cleanup of completed tasks
|
|
76
|
+
- Graceful shutdown with cancellation
|
|
77
|
+
- Exception handling for background tasks
|
|
78
|
+
- Task monitoring and statistics
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
cleanup_interval: float = 30.0,
|
|
84
|
+
shutdown_timeout: float = 30.0,
|
|
85
|
+
max_tasks: int = 1000,
|
|
86
|
+
):
|
|
87
|
+
"""Initialize the task manager.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
cleanup_interval: Interval for cleaning up completed tasks (seconds)
|
|
91
|
+
shutdown_timeout: Timeout for graceful shutdown (seconds)
|
|
92
|
+
max_tasks: Maximum number of tasks to track (prevents memory leaks)
|
|
93
|
+
"""
|
|
94
|
+
self.cleanup_interval = cleanup_interval
|
|
95
|
+
self.shutdown_timeout = shutdown_timeout
|
|
96
|
+
self.max_tasks = max_tasks
|
|
97
|
+
|
|
98
|
+
self._tasks: dict[str, TaskInfo] = {}
|
|
99
|
+
self._lock = asyncio.Lock()
|
|
100
|
+
self._shutdown_event = asyncio.Event()
|
|
101
|
+
self._cleanup_task: asyncio.Task[None] | None = None
|
|
102
|
+
self._started = False
|
|
103
|
+
|
|
104
|
+
async def start(self) -> None:
|
|
105
|
+
"""Start the task manager and its cleanup task."""
|
|
106
|
+
if self._started:
|
|
107
|
+
logger.warning("task_manager_already_started")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
self._started = True
|
|
111
|
+
logger.debug("task_manager_starting", cleanup_interval=self.cleanup_interval)
|
|
112
|
+
|
|
113
|
+
# Start cleanup task
|
|
114
|
+
self._cleanup_task = asyncio.create_task(
|
|
115
|
+
self._cleanup_loop(), name="task_manager_cleanup"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
logger.debug("task_manager_started")
|
|
119
|
+
|
|
120
|
+
async def stop(self) -> None:
|
|
121
|
+
"""Stop the task manager and cancel all managed tasks."""
|
|
122
|
+
if not self._started:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
logger.debug("task_manager_stopping", active_tasks=len(self._tasks))
|
|
126
|
+
self._shutdown_event.set()
|
|
127
|
+
|
|
128
|
+
# Stop cleanup task first
|
|
129
|
+
if self._cleanup_task and not self._cleanup_task.done():
|
|
130
|
+
self._cleanup_task.cancel()
|
|
131
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
132
|
+
await self._cleanup_task
|
|
133
|
+
|
|
134
|
+
# Cancel all managed tasks
|
|
135
|
+
await self._cancel_all_tasks()
|
|
136
|
+
|
|
137
|
+
# Clear task registry
|
|
138
|
+
async with self._lock:
|
|
139
|
+
self._tasks.clear()
|
|
140
|
+
|
|
141
|
+
self._started = False
|
|
142
|
+
logger.debug("task_manager_stopped")
|
|
143
|
+
|
|
144
|
+
async def create_task(
|
|
145
|
+
self,
|
|
146
|
+
coro: Awaitable[T],
|
|
147
|
+
*,
|
|
148
|
+
name: str | None = None,
|
|
149
|
+
creator: str | None = None,
|
|
150
|
+
cleanup_callback: Callable[[], None] | None = None,
|
|
151
|
+
) -> asyncio.Task[T]:
|
|
152
|
+
"""Create a managed task.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
coro: Coroutine to execute
|
|
156
|
+
name: Optional name for the task (auto-generated if None)
|
|
157
|
+
creator: Optional creator identifier for debugging
|
|
158
|
+
cleanup_callback: Optional callback to run when task completes
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
The created task
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
RuntimeError: If task manager is not started or has too many tasks
|
|
165
|
+
"""
|
|
166
|
+
if not self._started:
|
|
167
|
+
raise RuntimeError("Task manager is not started")
|
|
168
|
+
|
|
169
|
+
# Check task limit
|
|
170
|
+
if len(self._tasks) >= self.max_tasks:
|
|
171
|
+
logger.warning(
|
|
172
|
+
"task_manager_at_capacity",
|
|
173
|
+
current_tasks=len(self._tasks),
|
|
174
|
+
max_tasks=self.max_tasks,
|
|
175
|
+
)
|
|
176
|
+
# Clean up completed tasks to make room
|
|
177
|
+
await self._cleanup_completed_tasks()
|
|
178
|
+
|
|
179
|
+
if len(self._tasks) >= self.max_tasks:
|
|
180
|
+
raise RuntimeError(f"Task manager at capacity ({self.max_tasks} tasks)")
|
|
181
|
+
|
|
182
|
+
# Generate name if not provided
|
|
183
|
+
if name is None:
|
|
184
|
+
name = f"managed_task_{len(self._tasks)}"
|
|
185
|
+
|
|
186
|
+
# Create the task with exception handling
|
|
187
|
+
task = asyncio.create_task(
|
|
188
|
+
self._wrap_with_exception_handling(coro, name),
|
|
189
|
+
name=name,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Register the task
|
|
193
|
+
task_info = TaskInfo(
|
|
194
|
+
task=task,
|
|
195
|
+
name=name,
|
|
196
|
+
created_at=time.time(),
|
|
197
|
+
creator=creator,
|
|
198
|
+
cleanup_callback=cleanup_callback,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async with self._lock:
|
|
202
|
+
self._tasks[task_info.task_id] = task_info
|
|
203
|
+
|
|
204
|
+
# Add done callback for automatic cleanup
|
|
205
|
+
task.add_done_callback(lambda t: self._schedule_cleanup_callback(task_info))
|
|
206
|
+
|
|
207
|
+
logger.debug(
|
|
208
|
+
"task_created",
|
|
209
|
+
task_id=task_info.task_id,
|
|
210
|
+
task_name=name,
|
|
211
|
+
creator=creator,
|
|
212
|
+
total_tasks=len(self._tasks),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return task
|
|
216
|
+
|
|
217
|
+
async def _wrap_with_exception_handling(
|
|
218
|
+
self, coro: Awaitable[T], task_name: str
|
|
219
|
+
) -> T:
|
|
220
|
+
"""Wrap coroutine with exception handling."""
|
|
221
|
+
try:
|
|
222
|
+
return await coro
|
|
223
|
+
except asyncio.CancelledError:
|
|
224
|
+
logger.debug("task_cancelled", task_name=task_name)
|
|
225
|
+
raise
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(
|
|
228
|
+
"task_exception",
|
|
229
|
+
task_name=task_name,
|
|
230
|
+
error=str(e),
|
|
231
|
+
error_type=type(e).__name__,
|
|
232
|
+
exc_info=True,
|
|
233
|
+
)
|
|
234
|
+
raise
|
|
235
|
+
|
|
236
|
+
def _schedule_cleanup_callback(self, task_info: TaskInfo) -> None:
|
|
237
|
+
"""Schedule cleanup callback for completed task."""
|
|
238
|
+
try:
|
|
239
|
+
# Run cleanup callback if provided
|
|
240
|
+
if task_info.cleanup_callback:
|
|
241
|
+
task_info.cleanup_callback()
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.warning(
|
|
244
|
+
"task_cleanup_callback_failed",
|
|
245
|
+
task_id=task_info.task_id,
|
|
246
|
+
task_name=task_info.name,
|
|
247
|
+
error=str(e),
|
|
248
|
+
exc_info=True,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
async def _cleanup_loop(self) -> None:
|
|
252
|
+
"""Background loop for cleaning up completed tasks."""
|
|
253
|
+
logger.debug("task_cleanup_loop_started")
|
|
254
|
+
|
|
255
|
+
while not self._shutdown_event.is_set():
|
|
256
|
+
try:
|
|
257
|
+
await asyncio.wait_for(
|
|
258
|
+
self._shutdown_event.wait(), timeout=self.cleanup_interval
|
|
259
|
+
)
|
|
260
|
+
break # Shutdown event set
|
|
261
|
+
except TimeoutError:
|
|
262
|
+
pass # Continue with cleanup
|
|
263
|
+
|
|
264
|
+
await self._cleanup_completed_tasks()
|
|
265
|
+
|
|
266
|
+
logger.debug("task_cleanup_loop_stopped")
|
|
267
|
+
|
|
268
|
+
async def _cleanup_completed_tasks(self) -> None:
|
|
269
|
+
"""Clean up completed tasks from the registry."""
|
|
270
|
+
completed_tasks = []
|
|
271
|
+
|
|
272
|
+
async with self._lock:
|
|
273
|
+
for task_id, task_info in list(self._tasks.items()):
|
|
274
|
+
if task_info.is_done:
|
|
275
|
+
completed_tasks.append((task_id, task_info))
|
|
276
|
+
del self._tasks[task_id]
|
|
277
|
+
|
|
278
|
+
if completed_tasks:
|
|
279
|
+
logger.debug(
|
|
280
|
+
"tasks_cleaned_up",
|
|
281
|
+
completed_count=len(completed_tasks),
|
|
282
|
+
remaining_tasks=len(self._tasks),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Log any task exceptions
|
|
286
|
+
for task_id, task_info in completed_tasks:
|
|
287
|
+
if task_info.get_exception():
|
|
288
|
+
logger.warning(
|
|
289
|
+
"completed_task_had_exception",
|
|
290
|
+
task_id=task_id,
|
|
291
|
+
task_name=task_info.name,
|
|
292
|
+
exception=str(task_info.get_exception()),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
async def _cancel_all_tasks(self) -> None:
|
|
296
|
+
"""Cancel all managed tasks with timeout."""
|
|
297
|
+
if not self._tasks:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
logger.debug("cancelling_all_tasks", task_count=len(self._tasks))
|
|
301
|
+
|
|
302
|
+
# Cancel all tasks
|
|
303
|
+
tasks_to_cancel = []
|
|
304
|
+
async with self._lock:
|
|
305
|
+
for task_info in self._tasks.values():
|
|
306
|
+
if not task_info.is_done:
|
|
307
|
+
task_info.task.cancel()
|
|
308
|
+
tasks_to_cancel.append(task_info.task)
|
|
309
|
+
|
|
310
|
+
if not tasks_to_cancel:
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
# Wait for cancellation with timeout
|
|
314
|
+
try:
|
|
315
|
+
await asyncio.wait_for(
|
|
316
|
+
asyncio.gather(*tasks_to_cancel, return_exceptions=True),
|
|
317
|
+
timeout=self.shutdown_timeout,
|
|
318
|
+
)
|
|
319
|
+
logger.debug("all_tasks_cancelled_gracefully")
|
|
320
|
+
except TimeoutError:
|
|
321
|
+
logger.warning(
|
|
322
|
+
"task_cancellation_timeout",
|
|
323
|
+
timeout=self.shutdown_timeout,
|
|
324
|
+
remaining_tasks=sum(1 for t in tasks_to_cancel if not t.done()),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
async def get_task_stats(self) -> dict[str, Any]:
|
|
328
|
+
"""Get statistics about managed tasks."""
|
|
329
|
+
async with self._lock:
|
|
330
|
+
active_tasks = sum(1 for t in self._tasks.values() if not t.is_done)
|
|
331
|
+
cancelled_tasks = sum(1 for t in self._tasks.values() if t.is_cancelled)
|
|
332
|
+
failed_tasks = sum(
|
|
333
|
+
1
|
|
334
|
+
for t in self._tasks.values()
|
|
335
|
+
if t.is_done and not t.is_cancelled and t.get_exception()
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
"total_tasks": len(self._tasks),
|
|
340
|
+
"active_tasks": active_tasks,
|
|
341
|
+
"cancelled_tasks": cancelled_tasks,
|
|
342
|
+
"failed_tasks": failed_tasks,
|
|
343
|
+
"completed_tasks": len(self._tasks) - active_tasks,
|
|
344
|
+
"started": self._started,
|
|
345
|
+
"max_tasks": self.max_tasks,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async def list_active_tasks(self) -> list[dict[str, Any]]:
|
|
349
|
+
"""Get list of active tasks with details."""
|
|
350
|
+
active_tasks = []
|
|
351
|
+
|
|
352
|
+
async with self._lock:
|
|
353
|
+
for task_info in self._tasks.values():
|
|
354
|
+
if not task_info.is_done:
|
|
355
|
+
active_tasks.append(
|
|
356
|
+
{
|
|
357
|
+
"task_id": task_info.task_id,
|
|
358
|
+
"name": task_info.name,
|
|
359
|
+
"creator": task_info.creator,
|
|
360
|
+
"age_seconds": task_info.age_seconds,
|
|
361
|
+
"created_at": task_info.created_at,
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return active_tasks
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def is_started(self) -> bool:
|
|
369
|
+
"""Check if the task manager is started."""
|
|
370
|
+
return self._started
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# Dependency-injected access helpers
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _resolve_task_manager(
|
|
377
|
+
*,
|
|
378
|
+
container: Optional["ServiceContainer"] = None,
|
|
379
|
+
task_manager: Optional["AsyncTaskManager"] = None,
|
|
380
|
+
) -> "AsyncTaskManager":
|
|
381
|
+
"""Resolve the async task manager instance using dependency injection.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
container: Optional service container to resolve the manager from
|
|
385
|
+
task_manager: Optional explicit manager instance (takes precedence)
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
AsyncTaskManager instance
|
|
389
|
+
|
|
390
|
+
Raises:
|
|
391
|
+
RuntimeError: If the manager cannot be resolved
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
if task_manager is not None:
|
|
395
|
+
return task_manager
|
|
396
|
+
|
|
397
|
+
from ccproxy.services.container import ServiceContainer as _ServiceContainer
|
|
398
|
+
|
|
399
|
+
if container is not None:
|
|
400
|
+
resolved_container: _ServiceContainer = container
|
|
401
|
+
else:
|
|
402
|
+
resolved_container_maybe = _ServiceContainer.get_current(strict=False)
|
|
403
|
+
if resolved_container_maybe is None:
|
|
404
|
+
raise RuntimeError(
|
|
405
|
+
"ServiceContainer is not available; provide a container or task manager"
|
|
406
|
+
)
|
|
407
|
+
resolved_container = resolved_container_maybe
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
return resolved_container.get_async_task_manager()
|
|
411
|
+
except Exception as exc:
|
|
412
|
+
raise RuntimeError(
|
|
413
|
+
"AsyncTaskManager is not registered in the provided ServiceContainer"
|
|
414
|
+
) from exc
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
async def create_managed_task(
|
|
418
|
+
coro: Awaitable[T],
|
|
419
|
+
*,
|
|
420
|
+
name: str | None = None,
|
|
421
|
+
creator: str | None = None,
|
|
422
|
+
cleanup_callback: Callable[[], None] | None = None,
|
|
423
|
+
container: Optional["ServiceContainer"] = None,
|
|
424
|
+
task_manager: Optional["AsyncTaskManager"] = None,
|
|
425
|
+
) -> asyncio.Task[T]:
|
|
426
|
+
"""Create a managed task using the dependency-injected task manager.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
coro: Coroutine to execute
|
|
430
|
+
name: Optional name for the task
|
|
431
|
+
creator: Optional creator identifier
|
|
432
|
+
cleanup_callback: Optional cleanup callback
|
|
433
|
+
container: Optional service container for resolving the task manager
|
|
434
|
+
task_manager: Optional explicit task manager instance
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
The created managed task
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
manager = _resolve_task_manager(container=container, task_manager=task_manager)
|
|
441
|
+
return await manager.create_task(
|
|
442
|
+
coro, name=name, creator=creator, cleanup_callback=cleanup_callback
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
async def start_task_manager(
|
|
447
|
+
*,
|
|
448
|
+
container: Optional["ServiceContainer"] = None,
|
|
449
|
+
task_manager: Optional["AsyncTaskManager"] = None,
|
|
450
|
+
) -> None:
|
|
451
|
+
"""Start the dependency-injected task manager."""
|
|
452
|
+
|
|
453
|
+
manager = _resolve_task_manager(container=container, task_manager=task_manager)
|
|
454
|
+
await manager.start()
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
async def stop_task_manager(
|
|
458
|
+
*,
|
|
459
|
+
container: Optional["ServiceContainer"] = None,
|
|
460
|
+
task_manager: Optional["AsyncTaskManager"] = None,
|
|
461
|
+
) -> None:
|
|
462
|
+
"""Stop the dependency-injected task manager."""
|
|
463
|
+
|
|
464
|
+
manager = _resolve_task_manager(container=container, task_manager=task_manager)
|
|
465
|
+
await manager.stop()
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def create_fire_and_forget_task(
|
|
469
|
+
coro: Awaitable[T],
|
|
470
|
+
*,
|
|
471
|
+
name: str | None = None,
|
|
472
|
+
creator: str | None = None,
|
|
473
|
+
container: Optional["ServiceContainer"] = None,
|
|
474
|
+
task_manager: Optional["AsyncTaskManager"] = None,
|
|
475
|
+
) -> None:
|
|
476
|
+
"""Create a fire-and-forget managed task from a synchronous context.
|
|
477
|
+
|
|
478
|
+
This function schedules a coroutine to run as a managed task without
|
|
479
|
+
needing to await it. Useful for calling from synchronous functions
|
|
480
|
+
that need to schedule background work.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
coro: Coroutine to execute
|
|
484
|
+
name: Optional name for the task
|
|
485
|
+
creator: Optional creator identifier
|
|
486
|
+
container: Optional service container to resolve the task manager
|
|
487
|
+
task_manager: Optional explicit task manager instance
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
manager = _resolve_task_manager(container=container, task_manager=task_manager)
|
|
491
|
+
|
|
492
|
+
if not manager.is_started:
|
|
493
|
+
# If task manager isn't started, fall back to regular asyncio.create_task
|
|
494
|
+
logger.warning(
|
|
495
|
+
"task_manager_not_started_fire_and_forget",
|
|
496
|
+
name=name,
|
|
497
|
+
creator=creator,
|
|
498
|
+
)
|
|
499
|
+
asyncio.create_task(coro, name=name) # type: ignore[arg-type]
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
# Schedule the task creation as a fire-and-forget operation
|
|
503
|
+
async def _create_managed_task() -> None:
|
|
504
|
+
try:
|
|
505
|
+
await manager.create_task(coro, name=name, creator=creator)
|
|
506
|
+
except Exception as e:
|
|
507
|
+
logger.error(
|
|
508
|
+
"fire_and_forget_task_creation_failed",
|
|
509
|
+
name=name,
|
|
510
|
+
creator=creator,
|
|
511
|
+
error=str(e),
|
|
512
|
+
exc_info=True,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Use asyncio.create_task to schedule the managed task creation
|
|
516
|
+
asyncio.create_task(_create_managed_task(), name=f"create_{name or 'unnamed'}")
|
ccproxy/core/async_utils.py
CHANGED
|
@@ -5,7 +5,9 @@ import re
|
|
|
5
5
|
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
|
|
6
6
|
from contextlib import asynccontextmanager, contextmanager
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any, TypeVar
|
|
8
|
+
from typing import Any, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
from ccproxy.core.logging import get_logger
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
T = TypeVar("T")
|
|
@@ -45,7 +47,13 @@ def get_package_dir() -> Path:
|
|
|
45
47
|
package_dir = Path(spec.origin).parent.parent.resolve()
|
|
46
48
|
else:
|
|
47
49
|
package_dir = Path(__file__).parent.parent.parent.resolve()
|
|
48
|
-
except
|
|
50
|
+
except (AttributeError, ImportError, ModuleNotFoundError) as e:
|
|
51
|
+
logger = get_logger(__name__)
|
|
52
|
+
logger.debug("package_dir_fallback", error=str(e), exc_info=e)
|
|
53
|
+
package_dir = Path(__file__).parent.parent.parent.resolve()
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger = get_logger(__name__)
|
|
56
|
+
logger.debug("package_dir_unexpected_error", error=str(e), exc_info=e)
|
|
49
57
|
package_dir = Path(__file__).parent.parent.parent.resolve()
|
|
50
58
|
|
|
51
59
|
return package_dir
|
|
@@ -100,7 +108,11 @@ async def safe_await(awaitable: Awaitable[T], timeout: float | None = None) -> T
|
|
|
100
108
|
return await awaitable
|
|
101
109
|
except TimeoutError:
|
|
102
110
|
return None
|
|
103
|
-
except
|
|
111
|
+
except asyncio.CancelledError:
|
|
112
|
+
return None
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger = get_logger(__name__)
|
|
115
|
+
logger.debug("awaitable_silent_error", error=str(e), exc_info=e)
|
|
104
116
|
return None
|
|
105
117
|
|
|
106
118
|
|
|
@@ -215,7 +227,11 @@ async def wait_for_condition(
|
|
|
215
227
|
result = await result
|
|
216
228
|
if result:
|
|
217
229
|
return True
|
|
218
|
-
except
|
|
230
|
+
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
231
|
+
return False
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger = get_logger(__name__)
|
|
234
|
+
logger.debug("condition_check_error", error=str(e), exc_info=e)
|
|
219
235
|
pass
|
|
220
236
|
|
|
221
237
|
if asyncio.get_event_loop().time() - start_time > timeout:
|
|
@@ -254,7 +270,7 @@ async def async_cache_result(
|
|
|
254
270
|
if cache_key in _cache:
|
|
255
271
|
cached_time, cached_result = _cache[cache_key]
|
|
256
272
|
if current_time - cached_time < cache_duration:
|
|
257
|
-
return cached_result
|
|
273
|
+
return cast(T, cached_result)
|
|
258
274
|
|
|
259
275
|
# Compute and cache the result
|
|
260
276
|
result = await func(*args, **kwargs)
|
|
@@ -467,10 +483,14 @@ def validate_config_with_schema(
|
|
|
467
483
|
import tempfile
|
|
468
484
|
|
|
469
485
|
# Import tomllib for Python 3.11+ or fallback to tomli
|
|
486
|
+
# Avoid name redefinition warnings by selecting a loader function.
|
|
470
487
|
try:
|
|
471
|
-
import tomllib
|
|
488
|
+
import tomllib as _tomllib
|
|
489
|
+
|
|
490
|
+
toml_load = _tomllib.load
|
|
472
491
|
except ImportError:
|
|
473
|
-
|
|
492
|
+
_tomli = __import__("tomli")
|
|
493
|
+
toml_load = _tomli.load
|
|
474
494
|
|
|
475
495
|
config_path = Path()
|
|
476
496
|
|
|
@@ -483,7 +503,7 @@ def validate_config_with_schema(
|
|
|
483
503
|
if suffix == ".toml":
|
|
484
504
|
# Read and parse TOML - let TOML parse errors bubble up
|
|
485
505
|
with config_path.open("rb") as f:
|
|
486
|
-
toml_data =
|
|
506
|
+
toml_data = toml_load(f)
|
|
487
507
|
|
|
488
508
|
# Get or generate schema
|
|
489
509
|
if schema_path:
|
|
@@ -528,6 +548,16 @@ def validate_config_with_schema(
|
|
|
528
548
|
"check-jsonschema command not found. "
|
|
529
549
|
"Install with: pip install check-jsonschema"
|
|
530
550
|
) from e
|
|
551
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
|
552
|
+
# Clean up temporary files in case of error
|
|
553
|
+
Path(temp_schema_path).unlink(missing_ok=True)
|
|
554
|
+
Path(temp_json_path).unlink(missing_ok=True)
|
|
555
|
+
raise ValueError(f"Schema validation subprocess error: {e}") from e
|
|
556
|
+
except (OSError, PermissionError) as e:
|
|
557
|
+
# Clean up temporary files in case of error
|
|
558
|
+
Path(temp_schema_path).unlink(missing_ok=True)
|
|
559
|
+
Path(temp_json_path).unlink(missing_ok=True)
|
|
560
|
+
raise ValueError(f"File operation error during validation: {e}") from e
|
|
531
561
|
except Exception as e:
|
|
532
562
|
# Clean up temporary files in case of error
|
|
533
563
|
Path(temp_schema_path).unlink(missing_ok=True)
|
|
@@ -577,6 +607,14 @@ def validate_config_with_schema(
|
|
|
577
607
|
"check-jsonschema command not found. "
|
|
578
608
|
"Install with: pip install check-jsonschema"
|
|
579
609
|
) from e
|
|
610
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
|
611
|
+
if cleanup_schema:
|
|
612
|
+
Path(temp_schema_path).unlink(missing_ok=True)
|
|
613
|
+
raise ValueError(f"Schema validation subprocess error: {e}") from e
|
|
614
|
+
except (OSError, PermissionError) as e:
|
|
615
|
+
if cleanup_schema:
|
|
616
|
+
Path(temp_schema_path).unlink(missing_ok=True)
|
|
617
|
+
raise ValueError(f"File operation error during validation: {e}") from e
|
|
580
618
|
except Exception as e:
|
|
581
619
|
if cleanup_schema:
|
|
582
620
|
Path(temp_schema_path).unlink(missing_ok=True)
|
|
@@ -595,13 +633,8 @@ def generate_json_schema() -> dict[str, Any]:
|
|
|
595
633
|
Returns:
|
|
596
634
|
JSON Schema dictionary
|
|
597
635
|
|
|
598
|
-
Raises:
|
|
599
|
-
ImportError: If required dependencies are not available
|
|
600
636
|
"""
|
|
601
|
-
|
|
602
|
-
from ccproxy.config.settings import Settings
|
|
603
|
-
except ImportError as e:
|
|
604
|
-
raise ImportError(f"Required dependencies not available: {e}") from e
|
|
637
|
+
from ccproxy.config.settings import Settings
|
|
605
638
|
|
|
606
639
|
schema = Settings.model_json_schema()
|
|
607
640
|
|