ccproxy-api 0.1.7__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 +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.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.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 -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.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Centralized HTML templates for OAuth responses."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastapi.responses import HTMLResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OAuthProvider(Enum):
|
|
10
|
+
"""OAuth provider types."""
|
|
11
|
+
|
|
12
|
+
CLAUDE = "Claude"
|
|
13
|
+
OPENAI = "OpenAI"
|
|
14
|
+
GENERIC = "OAuth Provider"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OAuthTemplates:
|
|
18
|
+
"""Centralized HTML templates for OAuth responses.
|
|
19
|
+
|
|
20
|
+
This class provides consistent HTML responses across all OAuth providers,
|
|
21
|
+
reducing code duplication and ensuring a uniform user experience.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Base HTML template with common styling
|
|
25
|
+
_BASE_TEMPLATE = """
|
|
26
|
+
<!DOCTYPE html>
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<head>
|
|
29
|
+
<meta charset="UTF-8">
|
|
30
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
31
|
+
<title>{title}</title>
|
|
32
|
+
<style>
|
|
33
|
+
body {{
|
|
34
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
35
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
36
|
+
display: flex;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
align-items: center;
|
|
39
|
+
height: 100vh;
|
|
40
|
+
margin: 0;
|
|
41
|
+
padding: 20px;
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
}}
|
|
44
|
+
.container {{
|
|
45
|
+
background: white;
|
|
46
|
+
border-radius: 12px;
|
|
47
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
48
|
+
padding: 40px;
|
|
49
|
+
max-width: 500px;
|
|
50
|
+
width: 100%;
|
|
51
|
+
text-align: center;
|
|
52
|
+
}}
|
|
53
|
+
h1 {{
|
|
54
|
+
color: {header_color};
|
|
55
|
+
margin: 0 0 20px 0;
|
|
56
|
+
font-size: 28px;
|
|
57
|
+
font-weight: 600;
|
|
58
|
+
}}
|
|
59
|
+
.icon {{
|
|
60
|
+
font-size: 48px;
|
|
61
|
+
margin-bottom: 20px;
|
|
62
|
+
}}
|
|
63
|
+
p {{
|
|
64
|
+
color: #4a5568;
|
|
65
|
+
font-size: 16px;
|
|
66
|
+
line-height: 1.6;
|
|
67
|
+
margin: 10px 0;
|
|
68
|
+
}}
|
|
69
|
+
.error-detail {{
|
|
70
|
+
background-color: #fef2f2;
|
|
71
|
+
border: 1px solid #fecaca;
|
|
72
|
+
border-radius: 6px;
|
|
73
|
+
padding: 12px;
|
|
74
|
+
margin-top: 20px;
|
|
75
|
+
color: #991b1b;
|
|
76
|
+
font-family: 'Courier New', Courier, monospace;
|
|
77
|
+
font-size: 14px;
|
|
78
|
+
text-align: left;
|
|
79
|
+
word-wrap: break-word;
|
|
80
|
+
}}
|
|
81
|
+
.success-message {{
|
|
82
|
+
background-color: #f0fdf4;
|
|
83
|
+
border: 1px solid #86efac;
|
|
84
|
+
border-radius: 6px;
|
|
85
|
+
padding: 12px;
|
|
86
|
+
margin-top: 20px;
|
|
87
|
+
color: #166534;
|
|
88
|
+
}}
|
|
89
|
+
.countdown {{
|
|
90
|
+
color: #6b7280;
|
|
91
|
+
font-size: 14px;
|
|
92
|
+
margin-top: 20px;
|
|
93
|
+
}}
|
|
94
|
+
.action-hint {{
|
|
95
|
+
color: #9ca3af;
|
|
96
|
+
font-size: 14px;
|
|
97
|
+
margin-top: 15px;
|
|
98
|
+
}}
|
|
99
|
+
</style>
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
<div class="container">
|
|
103
|
+
{content}
|
|
104
|
+
</div>
|
|
105
|
+
{script}
|
|
106
|
+
</body>
|
|
107
|
+
</html>
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# Success content template
|
|
111
|
+
_SUCCESS_CONTENT = """
|
|
112
|
+
<div class="icon">✅</div>
|
|
113
|
+
<h1>Authentication Successful!</h1>
|
|
114
|
+
<p>You have successfully authenticated with {provider}.</p>
|
|
115
|
+
<div class="success-message">
|
|
116
|
+
Your credentials have been saved securely.
|
|
117
|
+
</div>
|
|
118
|
+
<p class="action-hint">You can close this window and return to the terminal.</p>
|
|
119
|
+
<div class="countdown" id="countdown">This window will close automatically in 3 seconds...</div>
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
# Error content template
|
|
123
|
+
_ERROR_CONTENT = """
|
|
124
|
+
<div class="icon">❌</div>
|
|
125
|
+
<h1>{title}</h1>
|
|
126
|
+
<p>{message}</p>
|
|
127
|
+
{error_detail}
|
|
128
|
+
<p class="action-hint">You can close this window and try again.</p>
|
|
129
|
+
<div class="countdown" id="countdown">This window will close automatically in 5 seconds...</div>
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
# Auto-close script
|
|
133
|
+
_AUTO_CLOSE_SCRIPT = """
|
|
134
|
+
<script>
|
|
135
|
+
let seconds = {seconds};
|
|
136
|
+
const countdownEl = document.getElementById('countdown');
|
|
137
|
+
|
|
138
|
+
const updateCountdown = () => {{
|
|
139
|
+
if (seconds > 0) {{
|
|
140
|
+
countdownEl.textContent = `This window will close automatically in ${{seconds}} second${{seconds === 1 ? '' : 's'}}...`;
|
|
141
|
+
seconds--;
|
|
142
|
+
setTimeout(updateCountdown, 1000);
|
|
143
|
+
}} else {{
|
|
144
|
+
countdownEl.textContent = 'Closing window...';
|
|
145
|
+
window.close();
|
|
146
|
+
}}
|
|
147
|
+
}};
|
|
148
|
+
|
|
149
|
+
updateCountdown();
|
|
150
|
+
|
|
151
|
+
// Try to close even if countdown doesn't work
|
|
152
|
+
setTimeout(() => {{
|
|
153
|
+
window.close();
|
|
154
|
+
}}, {milliseconds});
|
|
155
|
+
</script>
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def success(
|
|
160
|
+
cls,
|
|
161
|
+
provider: OAuthProvider = OAuthProvider.GENERIC,
|
|
162
|
+
auto_close_seconds: int = 3,
|
|
163
|
+
**kwargs: Any,
|
|
164
|
+
) -> HTMLResponse:
|
|
165
|
+
"""Generate success HTML response.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
provider: OAuth provider name
|
|
169
|
+
auto_close_seconds: Seconds before auto-closing window
|
|
170
|
+
**kwargs: Additional template variables
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
HTML response for successful authentication
|
|
174
|
+
"""
|
|
175
|
+
content = cls._SUCCESS_CONTENT.format(provider=provider.value, **kwargs)
|
|
176
|
+
|
|
177
|
+
script = cls._AUTO_CLOSE_SCRIPT.format(
|
|
178
|
+
seconds=auto_close_seconds, milliseconds=auto_close_seconds * 1000
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
html = cls._BASE_TEMPLATE.format(
|
|
182
|
+
title="Authentication Successful",
|
|
183
|
+
header_color="#10b981",
|
|
184
|
+
content=content,
|
|
185
|
+
script=script,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return HTMLResponse(content=html, status_code=200)
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def error(
|
|
192
|
+
cls,
|
|
193
|
+
error_message: str,
|
|
194
|
+
title: str = "Authentication Failed",
|
|
195
|
+
error_detail: str | None = None,
|
|
196
|
+
status_code: int = 400,
|
|
197
|
+
auto_close_seconds: int = 5,
|
|
198
|
+
**kwargs: Any,
|
|
199
|
+
) -> HTMLResponse:
|
|
200
|
+
"""Generate error HTML response.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
error_message: Main error message to display
|
|
204
|
+
title: Page and header title
|
|
205
|
+
error_detail: Optional detailed error information
|
|
206
|
+
status_code: HTTP status code
|
|
207
|
+
auto_close_seconds: Seconds before auto-closing window
|
|
208
|
+
**kwargs: Additional template variables
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
HTML response for failed authentication
|
|
212
|
+
"""
|
|
213
|
+
error_detail_html = ""
|
|
214
|
+
if error_detail:
|
|
215
|
+
# Sanitize error detail to prevent XSS
|
|
216
|
+
safe_detail = cls._sanitize_html(error_detail)
|
|
217
|
+
error_detail_html = f'<div class="error-detail">{safe_detail}</div>'
|
|
218
|
+
|
|
219
|
+
content = cls._ERROR_CONTENT.format(
|
|
220
|
+
title=title,
|
|
221
|
+
message=error_message,
|
|
222
|
+
error_detail=error_detail_html,
|
|
223
|
+
**kwargs,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
script = cls._AUTO_CLOSE_SCRIPT.format(
|
|
227
|
+
seconds=auto_close_seconds, milliseconds=auto_close_seconds * 1000
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
html = cls._BASE_TEMPLATE.format(
|
|
231
|
+
title=title, header_color="#ef4444", content=content, script=script
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return HTMLResponse(content=html, status_code=status_code)
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def callback_error(
|
|
238
|
+
cls,
|
|
239
|
+
error: str | None = None,
|
|
240
|
+
error_description: str | None = None,
|
|
241
|
+
provider: OAuthProvider = OAuthProvider.GENERIC,
|
|
242
|
+
**kwargs: Any,
|
|
243
|
+
) -> HTMLResponse:
|
|
244
|
+
"""Generate error response for OAuth callback errors.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
error: OAuth error code
|
|
248
|
+
error_description: OAuth error description
|
|
249
|
+
provider: OAuth provider name
|
|
250
|
+
**kwargs: Additional template variables
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
HTML response for callback errors
|
|
254
|
+
"""
|
|
255
|
+
if error == "access_denied":
|
|
256
|
+
return cls.error(
|
|
257
|
+
error_message=f"You denied access to {provider.value}.",
|
|
258
|
+
title="Access Denied",
|
|
259
|
+
error_detail=error_description,
|
|
260
|
+
**kwargs,
|
|
261
|
+
)
|
|
262
|
+
elif error == "invalid_request":
|
|
263
|
+
return cls.error(
|
|
264
|
+
error_message="The authentication request was invalid.",
|
|
265
|
+
title="Invalid Request",
|
|
266
|
+
error_detail=error_description
|
|
267
|
+
or "The OAuth request parameters were incorrect.",
|
|
268
|
+
**kwargs,
|
|
269
|
+
)
|
|
270
|
+
elif error == "unauthorized_client":
|
|
271
|
+
return cls.error(
|
|
272
|
+
error_message="This application is not authorized.",
|
|
273
|
+
title="Unauthorized Application",
|
|
274
|
+
error_detail=error_description
|
|
275
|
+
or "The client is not authorized to use this grant type.",
|
|
276
|
+
**kwargs,
|
|
277
|
+
)
|
|
278
|
+
elif error == "unsupported_response_type":
|
|
279
|
+
return cls.error(
|
|
280
|
+
error_message="The authorization server does not support this response type.",
|
|
281
|
+
title="Unsupported Response Type",
|
|
282
|
+
error_detail=error_description,
|
|
283
|
+
**kwargs,
|
|
284
|
+
)
|
|
285
|
+
elif error == "invalid_scope":
|
|
286
|
+
return cls.error(
|
|
287
|
+
error_message="The requested scope is invalid or unknown.",
|
|
288
|
+
title="Invalid Scope",
|
|
289
|
+
error_detail=error_description,
|
|
290
|
+
**kwargs,
|
|
291
|
+
)
|
|
292
|
+
elif error == "server_error":
|
|
293
|
+
return cls.error(
|
|
294
|
+
error_message=f"The {provider.value} server encountered an error.",
|
|
295
|
+
title="Server Error",
|
|
296
|
+
error_detail=error_description or "Please try again later.",
|
|
297
|
+
status_code=500,
|
|
298
|
+
**kwargs,
|
|
299
|
+
)
|
|
300
|
+
elif error == "temporarily_unavailable":
|
|
301
|
+
return cls.error(
|
|
302
|
+
error_message=f"The {provider.value} service is temporarily unavailable.",
|
|
303
|
+
title="Service Unavailable",
|
|
304
|
+
error_detail=error_description or "Please try again later.",
|
|
305
|
+
status_code=503,
|
|
306
|
+
**kwargs,
|
|
307
|
+
)
|
|
308
|
+
else:
|
|
309
|
+
# Generic error
|
|
310
|
+
return cls.error(
|
|
311
|
+
error_message=error_description
|
|
312
|
+
or error
|
|
313
|
+
or "An unknown error occurred.",
|
|
314
|
+
title="Authentication Error",
|
|
315
|
+
error_detail=f"Error code: {error}" if error else None,
|
|
316
|
+
**kwargs,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def _sanitize_html(cls, text: str) -> str:
|
|
321
|
+
"""Sanitize text for safe HTML display.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
text: Text to sanitize
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Sanitized text safe for HTML display
|
|
328
|
+
"""
|
|
329
|
+
# Basic HTML entity escaping
|
|
330
|
+
replacements = {
|
|
331
|
+
"&": "&",
|
|
332
|
+
"<": "<",
|
|
333
|
+
">": ">",
|
|
334
|
+
'"': """,
|
|
335
|
+
"'": "'",
|
|
336
|
+
"/": "/",
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for char, entity in replacements.items():
|
|
340
|
+
text = text.replace(char, entity)
|
|
341
|
+
|
|
342
|
+
return text
|
ccproxy/auth/storage/__init__.py
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"""Token storage implementations for authentication."""
|
|
2
2
|
|
|
3
|
-
from ccproxy.auth.storage.base import TokenStorage
|
|
4
|
-
from ccproxy.auth.storage.json_file import JsonFileTokenStorage
|
|
5
|
-
from ccproxy.auth.storage.keyring import KeyringTokenStorage
|
|
3
|
+
from ccproxy.auth.storage.base import BaseJsonStorage, TokenStorage
|
|
6
4
|
|
|
7
5
|
|
|
8
6
|
__all__ = [
|
|
9
7
|
"TokenStorage",
|
|
10
|
-
"
|
|
11
|
-
"KeyringTokenStorage",
|
|
8
|
+
"BaseJsonStorage",
|
|
12
9
|
]
|
ccproxy/auth/storage/base.py
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
"""Abstract base class for token storage."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
3
7
|
from abc import ABC, abstractmethod
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Generic, TypeVar
|
|
4
11
|
|
|
5
|
-
from ccproxy.auth.
|
|
12
|
+
from ccproxy.auth.exceptions import CredentialsInvalidError, CredentialsStorageError
|
|
13
|
+
from ccproxy.auth.models.credentials import BaseCredentials
|
|
14
|
+
from ccproxy.core.logging import get_logger
|
|
6
15
|
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
CredentialsT = TypeVar("CredentialsT", bound=BaseCredentials)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TokenStorage(ABC, Generic[CredentialsT]):
|
|
23
|
+
"""Abstract interface for token storage operations.
|
|
24
|
+
|
|
25
|
+
This is a generic interface that can work with any credential type
|
|
26
|
+
that extends BaseModel (e.g., ClaudeCredentials, OpenAICredentials).
|
|
27
|
+
"""
|
|
10
28
|
|
|
11
29
|
@abstractmethod
|
|
12
|
-
async def load(self) ->
|
|
30
|
+
async def load(self) -> CredentialsT | None:
|
|
13
31
|
"""Load credentials from storage.
|
|
14
32
|
|
|
15
33
|
Returns:
|
|
@@ -18,7 +36,7 @@ class TokenStorage(ABC):
|
|
|
18
36
|
pass
|
|
19
37
|
|
|
20
38
|
@abstractmethod
|
|
21
|
-
async def save(self, credentials:
|
|
39
|
+
async def save(self, credentials: CredentialsT) -> bool:
|
|
22
40
|
"""Save credentials to storage.
|
|
23
41
|
|
|
24
42
|
Args:
|
|
@@ -55,3 +73,259 @@ class TokenStorage(ABC):
|
|
|
55
73
|
Human-readable description of where credentials are stored
|
|
56
74
|
"""
|
|
57
75
|
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BaseJsonStorage(TokenStorage[CredentialsT], Generic[CredentialsT]):
|
|
79
|
+
"""Base class for JSON file storage implementations.
|
|
80
|
+
|
|
81
|
+
This class provides common JSON read/write operations with error handling,
|
|
82
|
+
atomic writes, and proper permission management.
|
|
83
|
+
|
|
84
|
+
This is a generic class that can work with any credential type.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, file_path: Path, enable_backups: bool = True):
|
|
88
|
+
"""Initialize JSON storage.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
file_path: Path to JSON file for storage
|
|
92
|
+
enable_backups: Whether to create backups before overwriting
|
|
93
|
+
"""
|
|
94
|
+
self.file_path = file_path
|
|
95
|
+
self.enable_backups = enable_backups
|
|
96
|
+
|
|
97
|
+
async def _read_json(self) -> dict[str, Any]:
|
|
98
|
+
"""Read JSON data from file with error handling.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Parsed JSON data or empty dict if file doesn't exist
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
CredentialsInvalidError: If JSON is invalid
|
|
105
|
+
CredentialsStorageError: If file cannot be read
|
|
106
|
+
"""
|
|
107
|
+
if not await self.exists():
|
|
108
|
+
return {}
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
# Run file I/O in thread pool to avoid blocking
|
|
112
|
+
def read_file() -> dict[str, Any]:
|
|
113
|
+
with self.file_path.open("r") as f:
|
|
114
|
+
return json.load(f) # type: ignore[no-any-return]
|
|
115
|
+
|
|
116
|
+
data = await asyncio.to_thread(read_file)
|
|
117
|
+
return data
|
|
118
|
+
|
|
119
|
+
except json.JSONDecodeError as e:
|
|
120
|
+
logger.warning(
|
|
121
|
+
"json_decode_error",
|
|
122
|
+
path=str(self.file_path),
|
|
123
|
+
error=str(e),
|
|
124
|
+
line=e.lineno,
|
|
125
|
+
category="auth",
|
|
126
|
+
)
|
|
127
|
+
raise CredentialsInvalidError(
|
|
128
|
+
f"Invalid JSON in {self.file_path}: {e}"
|
|
129
|
+
) from e
|
|
130
|
+
|
|
131
|
+
except FileNotFoundError:
|
|
132
|
+
# File was deleted between exists() check and read
|
|
133
|
+
return {}
|
|
134
|
+
|
|
135
|
+
except PermissionError as e:
|
|
136
|
+
logger.error(
|
|
137
|
+
"permission_denied",
|
|
138
|
+
path=str(self.file_path),
|
|
139
|
+
error=str(e),
|
|
140
|
+
exc_info=e,
|
|
141
|
+
)
|
|
142
|
+
raise CredentialsStorageError(f"Permission denied: {self.file_path}") from e
|
|
143
|
+
|
|
144
|
+
except OSError as e:
|
|
145
|
+
logger.error(
|
|
146
|
+
"file_read_error",
|
|
147
|
+
path=str(self.file_path),
|
|
148
|
+
error=str(e),
|
|
149
|
+
exc_info=e,
|
|
150
|
+
)
|
|
151
|
+
raise CredentialsStorageError(f"Error reading {self.file_path}: {e}") from e
|
|
152
|
+
|
|
153
|
+
async def _create_backup(self) -> bool:
|
|
154
|
+
"""Create a timestamped backup of the current file.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
True if backup was created successfully, False otherwise
|
|
158
|
+
"""
|
|
159
|
+
if not await self.exists():
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
# Generate backup filename with timestamp
|
|
164
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
165
|
+
backup_name = f"{self.file_path.name}.{timestamp}.bak"
|
|
166
|
+
backup_path = self.file_path.parent / backup_name
|
|
167
|
+
|
|
168
|
+
# Copy file to backup location
|
|
169
|
+
await asyncio.to_thread(shutil.copy2, self.file_path, backup_path)
|
|
170
|
+
|
|
171
|
+
logger.debug(
|
|
172
|
+
"backup_created",
|
|
173
|
+
original=str(self.file_path),
|
|
174
|
+
backup=str(backup_path),
|
|
175
|
+
category="auth",
|
|
176
|
+
)
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.warning(
|
|
181
|
+
"backup_failed",
|
|
182
|
+
path=str(self.file_path),
|
|
183
|
+
error=str(e),
|
|
184
|
+
exc_info=e,
|
|
185
|
+
category="auth",
|
|
186
|
+
)
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
async def _write_json(self, data: dict[str, Any]) -> None:
|
|
190
|
+
"""Write JSON data to file atomically with error handling.
|
|
191
|
+
|
|
192
|
+
This method performs atomic writes by writing to a temporary file
|
|
193
|
+
first, then renaming it to the target file. If backups are enabled
|
|
194
|
+
and the file exists, a backup will be created before overwriting.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
data: Data to write as JSON
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
CredentialsStorageError: If file cannot be written
|
|
201
|
+
"""
|
|
202
|
+
# Create backup if enabled and file exists
|
|
203
|
+
if self.enable_backups and await self.exists():
|
|
204
|
+
await self._create_backup()
|
|
205
|
+
|
|
206
|
+
temp_path = self.file_path.with_suffix(".tmp")
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
# Ensure parent directory exists
|
|
210
|
+
await asyncio.to_thread(
|
|
211
|
+
self.file_path.parent.mkdir,
|
|
212
|
+
parents=True,
|
|
213
|
+
exist_ok=True,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Run file I/O in thread pool to avoid blocking
|
|
217
|
+
def write_file() -> None:
|
|
218
|
+
# Write to temporary file
|
|
219
|
+
with temp_path.open("w") as f:
|
|
220
|
+
json.dump(data, f, indent=2)
|
|
221
|
+
|
|
222
|
+
# Set restrictive permissions (read/write for owner only)
|
|
223
|
+
temp_path.chmod(0o600)
|
|
224
|
+
|
|
225
|
+
# Atomic rename
|
|
226
|
+
temp_path.replace(self.file_path)
|
|
227
|
+
|
|
228
|
+
await asyncio.to_thread(write_file)
|
|
229
|
+
|
|
230
|
+
logger.debug(
|
|
231
|
+
"json_write_success",
|
|
232
|
+
path=str(self.file_path),
|
|
233
|
+
size=len(json.dumps(data)),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
except (TypeError, ValueError) as e:
|
|
237
|
+
logger.error(
|
|
238
|
+
"json_encode_error",
|
|
239
|
+
path=str(self.file_path),
|
|
240
|
+
error=str(e),
|
|
241
|
+
exc_info=e,
|
|
242
|
+
)
|
|
243
|
+
raise CredentialsStorageError(f"Failed to encode JSON: {e}") from e
|
|
244
|
+
|
|
245
|
+
except PermissionError as e:
|
|
246
|
+
logger.error(
|
|
247
|
+
"permission_denied",
|
|
248
|
+
path=str(self.file_path),
|
|
249
|
+
error=str(e),
|
|
250
|
+
exc_info=e,
|
|
251
|
+
)
|
|
252
|
+
raise CredentialsStorageError(f"Permission denied: {self.file_path}") from e
|
|
253
|
+
|
|
254
|
+
except OSError as e:
|
|
255
|
+
logger.error(
|
|
256
|
+
"file_write_error",
|
|
257
|
+
path=str(self.file_path),
|
|
258
|
+
error=str(e),
|
|
259
|
+
exc_info=e,
|
|
260
|
+
)
|
|
261
|
+
raise CredentialsStorageError(f"Error writing {self.file_path}: {e}") from e
|
|
262
|
+
|
|
263
|
+
finally:
|
|
264
|
+
# Clean up temp file if it exists
|
|
265
|
+
if temp_path.exists():
|
|
266
|
+
with contextlib.suppress(Exception):
|
|
267
|
+
temp_path.unlink()
|
|
268
|
+
|
|
269
|
+
async def exists(self) -> bool:
|
|
270
|
+
"""Check if credentials file exists.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
True if file exists, False otherwise
|
|
274
|
+
"""
|
|
275
|
+
# Run file system check in thread pool for consistency
|
|
276
|
+
file_exists = await asyncio.to_thread(
|
|
277
|
+
lambda: self.file_path.exists() and self.file_path.is_file()
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
logger.debug(
|
|
281
|
+
"auth_file_existence_check",
|
|
282
|
+
file_path=str(self.file_path),
|
|
283
|
+
exists=file_exists,
|
|
284
|
+
category="auth",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return file_exists
|
|
288
|
+
|
|
289
|
+
async def delete(self) -> bool:
|
|
290
|
+
"""Delete credentials file.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
True if deleted successfully, False if file didn't exist
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
CredentialsStorageError: If file cannot be deleted
|
|
297
|
+
"""
|
|
298
|
+
try:
|
|
299
|
+
if await self.exists():
|
|
300
|
+
await asyncio.to_thread(self.file_path.unlink)
|
|
301
|
+
logger.debug("file_deleted", path=str(self.file_path))
|
|
302
|
+
return True
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
except PermissionError as e:
|
|
306
|
+
logger.error(
|
|
307
|
+
"permission_denied",
|
|
308
|
+
path=str(self.file_path),
|
|
309
|
+
error=str(e),
|
|
310
|
+
exc_info=e,
|
|
311
|
+
)
|
|
312
|
+
raise CredentialsStorageError(f"Permission denied: {self.file_path}") from e
|
|
313
|
+
|
|
314
|
+
except OSError as e:
|
|
315
|
+
logger.error(
|
|
316
|
+
"file_delete_error",
|
|
317
|
+
path=str(self.file_path),
|
|
318
|
+
error=str(e),
|
|
319
|
+
exc_info=e,
|
|
320
|
+
)
|
|
321
|
+
raise CredentialsStorageError(
|
|
322
|
+
f"Error deleting {self.file_path}: {e}"
|
|
323
|
+
) from e
|
|
324
|
+
|
|
325
|
+
def get_location(self) -> str:
|
|
326
|
+
"""Get the storage location description.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Path to the JSON file
|
|
330
|
+
"""
|
|
331
|
+
return str(self.file_path)
|