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,134 @@
|
|
|
1
|
+
"""Generic storage implementation using Pydantic validation."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import SecretStr, TypeAdapter
|
|
8
|
+
|
|
9
|
+
from ccproxy.auth.models.credentials import BaseCredentials
|
|
10
|
+
from ccproxy.auth.storage.base import BaseJsonStorage
|
|
11
|
+
from ccproxy.core.logging import get_logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T", bound=BaseCredentials)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GenericJsonStorage(BaseJsonStorage[T]):
|
|
20
|
+
"""Generic storage implementation using Pydantic validation.
|
|
21
|
+
|
|
22
|
+
This replaces provider-specific storage classes with a single
|
|
23
|
+
implementation that handles any Pydantic model.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, file_path: Path, model_class: type[T]):
|
|
27
|
+
"""Initialize generic storage.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
file_path: Path to JSON file
|
|
31
|
+
model_class: Pydantic model class for validation
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(file_path)
|
|
34
|
+
self.model_class = model_class
|
|
35
|
+
self.type_adapter = TypeAdapter(model_class)
|
|
36
|
+
|
|
37
|
+
async def load(self) -> T | None:
|
|
38
|
+
"""Load and validate credentials with Pydantic.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Validated model instance or None if file doesn't exist
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
data = await self._read_json()
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
# File doesn't exist - this is normal for uninitialized credentials
|
|
47
|
+
logger.debug(
|
|
48
|
+
"credential_file_not_found",
|
|
49
|
+
path=str(self.file_path),
|
|
50
|
+
category="auth",
|
|
51
|
+
)
|
|
52
|
+
return None
|
|
53
|
+
except Exception as e:
|
|
54
|
+
# Handle JSON decode errors and other file read issues with clear warning
|
|
55
|
+
error_type = type(e).__name__
|
|
56
|
+
logger.warning(
|
|
57
|
+
"credential_file_read_failed",
|
|
58
|
+
error_type=error_type,
|
|
59
|
+
error=str(e),
|
|
60
|
+
exc_info=e,
|
|
61
|
+
path=str(self.file_path),
|
|
62
|
+
category="auth",
|
|
63
|
+
)
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
if not data:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# Pydantic handles all validation and conversion
|
|
71
|
+
return self.type_adapter.validate_python(data)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
# Log validation errors with clean warning (not error)
|
|
74
|
+
error_type = type(e).__name__
|
|
75
|
+
logger.warning(
|
|
76
|
+
"credential_validation_failed",
|
|
77
|
+
error_type=error_type,
|
|
78
|
+
error=str(e),
|
|
79
|
+
exc_info=e,
|
|
80
|
+
model=self.model_class.__name__,
|
|
81
|
+
path=str(self.file_path),
|
|
82
|
+
category="auth",
|
|
83
|
+
)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
async def save(self, obj: T) -> bool:
|
|
87
|
+
"""Save model using Pydantic serialization.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
obj: Pydantic model instance to save
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if saved successfully
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
# Preserve original JSON structure using aliases
|
|
97
|
+
# Use dump_python without mode="json" to get actual values
|
|
98
|
+
data = self.type_adapter.dump_python(
|
|
99
|
+
obj,
|
|
100
|
+
by_alias=True, # Use field aliases from original models
|
|
101
|
+
exclude_none=True,
|
|
102
|
+
)
|
|
103
|
+
# Convert SecretStr values to their actual values
|
|
104
|
+
data = self._unmask_secrets(data)
|
|
105
|
+
await self._write_json(data)
|
|
106
|
+
return True
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(
|
|
109
|
+
"Failed to save credentials",
|
|
110
|
+
error=str(e),
|
|
111
|
+
exc_info=e,
|
|
112
|
+
model=self.model_class.__name__,
|
|
113
|
+
)
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def _unmask_secrets(self, data: Any) -> Any:
|
|
117
|
+
"""Recursively unmask SecretStr values in data structure.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
data: Data structure potentially containing SecretStr values
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Data with SecretStr values replaced by their actual values
|
|
124
|
+
"""
|
|
125
|
+
if isinstance(data, dict):
|
|
126
|
+
return {k: self._unmask_secrets(v) for k, v in data.items()}
|
|
127
|
+
elif isinstance(data, list):
|
|
128
|
+
return [self._unmask_secrets(item) for item in data]
|
|
129
|
+
elif isinstance(data, SecretStr):
|
|
130
|
+
return data.get_secret_value()
|
|
131
|
+
elif isinstance(data, datetime):
|
|
132
|
+
return data.isoformat()
|
|
133
|
+
else:
|
|
134
|
+
return data
|
ccproxy/cli/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .commands.serve import api
|
|
1
|
+
from .commands.serve import api
|
|
2
2
|
from .helpers import get_rich_toolkit
|
|
3
3
|
from .main import app, app_main, main, version_callback
|
|
4
4
|
|
|
@@ -8,7 +8,6 @@ __all__ = [
|
|
|
8
8
|
"main",
|
|
9
9
|
"version_callback",
|
|
10
10
|
"api",
|
|
11
|
-
"claude",
|
|
12
11
|
"app_main",
|
|
13
12
|
"get_rich_toolkit",
|
|
14
13
|
]
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Generic Pydantic model introspection and display utility for settings help."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import enum
|
|
7
|
+
import inspect
|
|
8
|
+
from typing import Any, get_args, get_origin
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, SecretStr
|
|
11
|
+
from pydantic.fields import FieldInfo
|
|
12
|
+
from rich.box import HEAVY_HEAD
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
ELLIPSIS = "…"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_model(obj: Any) -> bool:
|
|
23
|
+
"""Check if an object is a Pydantic BaseModel subclass."""
|
|
24
|
+
return inspect.isclass(obj) and issubclass(obj, BaseModel)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _typename(tp: Any) -> str:
|
|
28
|
+
"""Return human-friendly type name for Unions, Literals, containers, etc."""
|
|
29
|
+
origin = get_origin(tp)
|
|
30
|
+
if origin is None:
|
|
31
|
+
if isinstance(tp, type):
|
|
32
|
+
try:
|
|
33
|
+
if issubclass(tp, enum.Enum):
|
|
34
|
+
return f"Enum[{tp.__name__}]"
|
|
35
|
+
except TypeError:
|
|
36
|
+
pass
|
|
37
|
+
name_str: str = tp.__name__
|
|
38
|
+
return name_str
|
|
39
|
+
return str(tp)
|
|
40
|
+
|
|
41
|
+
args_str = ", ".join(_typename(a) for a in get_args(tp))
|
|
42
|
+
name = getattr(origin, "__name__", str(origin))
|
|
43
|
+
|
|
44
|
+
# Handle Union types (including | syntax)
|
|
45
|
+
if name in {"Union", "types.UnionType", "UnionType"}:
|
|
46
|
+
return " | ".join(_typename(a) for a in get_args(tp))
|
|
47
|
+
|
|
48
|
+
# Handle common generic types
|
|
49
|
+
if name in {"list", "List"}:
|
|
50
|
+
return f"list[{args_str}]"
|
|
51
|
+
if name in {"dict", "Dict"}:
|
|
52
|
+
return f"dict[{args_str}]"
|
|
53
|
+
if name in {"Annotated"}:
|
|
54
|
+
# Show inner type for Annotated
|
|
55
|
+
inner_args = get_args(tp)
|
|
56
|
+
return _typename(inner_args[0]) if inner_args else "Any"
|
|
57
|
+
|
|
58
|
+
result: str = f"{name}[{args_str}]" if args_str else name
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_secret_field(name: str) -> bool:
|
|
63
|
+
"""Check if a field name suggests it contains secret data."""
|
|
64
|
+
secret_patterns = ["token", "key", "secret", "password", "credential", "auth"]
|
|
65
|
+
return any(pattern in name.lower() for pattern in secret_patterns)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _render_value(val: Any, width: int = 22, field_name: str = "") -> str:
|
|
69
|
+
"""Render a value for display with truncation and secret masking."""
|
|
70
|
+
# Handle None
|
|
71
|
+
if val is None:
|
|
72
|
+
return "—"
|
|
73
|
+
|
|
74
|
+
# Mask secrets
|
|
75
|
+
if isinstance(val, SecretStr):
|
|
76
|
+
return "***"
|
|
77
|
+
if field_name and _is_secret_field(field_name):
|
|
78
|
+
return "***" if val else "—"
|
|
79
|
+
|
|
80
|
+
# Handle enums
|
|
81
|
+
if isinstance(val, enum.Enum):
|
|
82
|
+
return str(val.value)
|
|
83
|
+
|
|
84
|
+
# Handle Pydantic models - show class name only
|
|
85
|
+
if isinstance(val, BaseModel):
|
|
86
|
+
return val.__class__.__name__
|
|
87
|
+
|
|
88
|
+
# Handle dataclasses
|
|
89
|
+
if dataclasses.is_dataclass(val):
|
|
90
|
+
return val.__class__.__name__
|
|
91
|
+
|
|
92
|
+
# Convert to string and truncate if needed
|
|
93
|
+
s = repr(val)
|
|
94
|
+
if len(s) > width:
|
|
95
|
+
return s[: width - 1] + ELLIPSIS
|
|
96
|
+
return s
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _default_for_field(field: FieldInfo) -> Any:
|
|
100
|
+
"""Extract default value or factory from a Pydantic field."""
|
|
101
|
+
from pydantic_core import PydanticUndefined
|
|
102
|
+
|
|
103
|
+
# Check if field has a default value
|
|
104
|
+
if hasattr(field, "default") and field.default is not PydanticUndefined:
|
|
105
|
+
return field.default
|
|
106
|
+
|
|
107
|
+
# Check if field has a default_factory
|
|
108
|
+
if hasattr(field, "default_factory") and field.default_factory is not None:
|
|
109
|
+
factory = field.default_factory
|
|
110
|
+
if callable(factory):
|
|
111
|
+
try:
|
|
112
|
+
return factory() # type: ignore[call-arg]
|
|
113
|
+
except Exception:
|
|
114
|
+
return "<factory>"
|
|
115
|
+
return "<factory>"
|
|
116
|
+
|
|
117
|
+
# Field is required
|
|
118
|
+
return "required"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _choices_from_type(tp: Any) -> list[str]:
|
|
122
|
+
"""Extract enum/literal choices from a type annotation."""
|
|
123
|
+
origin = get_origin(tp)
|
|
124
|
+
|
|
125
|
+
# Handle direct enum types
|
|
126
|
+
if origin is None:
|
|
127
|
+
try:
|
|
128
|
+
if inspect.isclass(tp) and issubclass(tp, enum.Enum):
|
|
129
|
+
return [str(m.value) for m in tp]
|
|
130
|
+
except TypeError:
|
|
131
|
+
pass
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
# Handle Literal types
|
|
135
|
+
if hasattr(origin, "__name__") and origin.__name__ in {"Literal"}:
|
|
136
|
+
return [repr(x) for x in get_args(tp)]
|
|
137
|
+
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _is_field_hidden(field: FieldInfo) -> bool:
|
|
142
|
+
"""Check if a field should be hidden from display.
|
|
143
|
+
|
|
144
|
+
Fields can be hidden by setting:
|
|
145
|
+
- field.exclude = True
|
|
146
|
+
- field.json_schema_extra = {"config_example_hidden": True}
|
|
147
|
+
"""
|
|
148
|
+
# Check if field is explicitly excluded
|
|
149
|
+
if field.exclude:
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
# Check json_schema_extra for config_example_hidden flag
|
|
153
|
+
json_schema_extra = field.json_schema_extra
|
|
154
|
+
if json_schema_extra:
|
|
155
|
+
# Handle both dict and callable forms
|
|
156
|
+
if callable(json_schema_extra):
|
|
157
|
+
try:
|
|
158
|
+
# Pydantic v2 callable signature: (schema_dict, handler)
|
|
159
|
+
extra_dict = json_schema_extra({})
|
|
160
|
+
if isinstance(extra_dict, dict):
|
|
161
|
+
return bool(extra_dict.get("config_example_hidden", False))
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
elif isinstance(json_schema_extra, dict):
|
|
165
|
+
return bool(json_schema_extra.get("config_example_hidden", False))
|
|
166
|
+
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def build_table_for_model(
|
|
171
|
+
model_cls: type[BaseModel],
|
|
172
|
+
instance: BaseModel | None = None,
|
|
173
|
+
*,
|
|
174
|
+
title: str | None = None,
|
|
175
|
+
show_value: bool = True,
|
|
176
|
+
) -> Table:
|
|
177
|
+
"""Build a Rich table for a Pydantic model with field information.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
model_cls: The Pydantic model class to display
|
|
181
|
+
instance: Optional instance to show actual values
|
|
182
|
+
title: Optional table title (defaults to model class name)
|
|
183
|
+
show_value: Whether to include the Value column (useful for schema-only display)
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
A Rich Table ready for printing
|
|
187
|
+
"""
|
|
188
|
+
table = Table(
|
|
189
|
+
title=title or model_cls.__name__,
|
|
190
|
+
box=HEAVY_HEAD,
|
|
191
|
+
show_lines=False,
|
|
192
|
+
header_style="bold",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
table.add_column("Field", style="bold")
|
|
196
|
+
table.add_column("Type", style="cyan")
|
|
197
|
+
if show_value:
|
|
198
|
+
table.add_column("Value", style="green")
|
|
199
|
+
table.add_column("Default", style="yellow")
|
|
200
|
+
table.add_column("Description", style="dim")
|
|
201
|
+
|
|
202
|
+
schema = model_cls.model_json_schema()
|
|
203
|
+
required_fields = set(schema.get("required", []))
|
|
204
|
+
|
|
205
|
+
for field_name, field in model_cls.model_fields.items():
|
|
206
|
+
# Skip hidden fields
|
|
207
|
+
if _is_field_hidden(field):
|
|
208
|
+
continue
|
|
209
|
+
prop = schema.get("properties", {}).get(field_name, {})
|
|
210
|
+
|
|
211
|
+
# Get type string
|
|
212
|
+
tp_str = (
|
|
213
|
+
_typename(field.annotation)
|
|
214
|
+
if field.annotation is not None
|
|
215
|
+
else prop.get("type", "object")
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Get default value
|
|
219
|
+
default = _default_for_field(field)
|
|
220
|
+
|
|
221
|
+
# Get actual value if instance provided
|
|
222
|
+
if show_value and instance is not None:
|
|
223
|
+
val = getattr(
|
|
224
|
+
instance, field_name, default if default != "required" else None
|
|
225
|
+
)
|
|
226
|
+
value_str = _render_value(val, width=22, field_name=field_name)
|
|
227
|
+
else:
|
|
228
|
+
value_str = None
|
|
229
|
+
|
|
230
|
+
# Get description
|
|
231
|
+
desc = prop.get("description", "") or field.description or ""
|
|
232
|
+
|
|
233
|
+
# Add choices to description if available
|
|
234
|
+
choices = _choices_from_type(field.annotation)
|
|
235
|
+
if choices and len(choices) <= 5: # Only show if reasonable number
|
|
236
|
+
choices_str = ", ".join(choices[:5])
|
|
237
|
+
if len(choices) > 5:
|
|
238
|
+
choices_str += "..."
|
|
239
|
+
desc = f"{desc} Choices: {choices_str}".strip()
|
|
240
|
+
|
|
241
|
+
# Mark required fields with *
|
|
242
|
+
display_name = f"{field_name}*" if field_name in required_fields else field_name
|
|
243
|
+
|
|
244
|
+
# Build row
|
|
245
|
+
if show_value and value_str is not None:
|
|
246
|
+
table.add_row(
|
|
247
|
+
display_name,
|
|
248
|
+
tp_str,
|
|
249
|
+
value_str,
|
|
250
|
+
_render_value(default, width=22),
|
|
251
|
+
desc,
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
table.add_row(
|
|
255
|
+
display_name,
|
|
256
|
+
tp_str,
|
|
257
|
+
_render_value(default, width=22),
|
|
258
|
+
desc,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return table
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def collect_nested_models(model_cls: type[BaseModel]) -> list[type[BaseModel]]:
|
|
265
|
+
"""Recursively find all nested BaseModel types in a model's fields.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
model_cls: The Pydantic model class to scan
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List of unique BaseModel subclasses found, sorted by name
|
|
272
|
+
"""
|
|
273
|
+
nested_models: set[type[BaseModel]] = set()
|
|
274
|
+
|
|
275
|
+
def walk(tp: Any) -> None:
|
|
276
|
+
"""Recursively walk type annotations to find BaseModel subclasses."""
|
|
277
|
+
origin = get_origin(tp)
|
|
278
|
+
|
|
279
|
+
if origin is None:
|
|
280
|
+
# Direct type - check if it's a BaseModel
|
|
281
|
+
try:
|
|
282
|
+
if inspect.isclass(tp) and issubclass(tp, BaseModel):
|
|
283
|
+
nested_models.add(tp)
|
|
284
|
+
# Recursively scan this model's fields
|
|
285
|
+
for field in tp.model_fields.values():
|
|
286
|
+
if field.annotation is not None:
|
|
287
|
+
walk(field.annotation)
|
|
288
|
+
except TypeError:
|
|
289
|
+
pass
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
# Generic type - walk the type arguments
|
|
293
|
+
for arg in get_args(tp):
|
|
294
|
+
walk(arg)
|
|
295
|
+
|
|
296
|
+
# Scan all fields in the model
|
|
297
|
+
for field in model_cls.model_fields.values():
|
|
298
|
+
if field.annotation is not None:
|
|
299
|
+
walk(field.annotation)
|
|
300
|
+
|
|
301
|
+
# Exclude the model itself
|
|
302
|
+
nested_models.discard(model_cls)
|
|
303
|
+
|
|
304
|
+
# Return sorted by name for stable output
|
|
305
|
+
return sorted(nested_models, key=lambda c: c.__name__)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def print_settings_help(
|
|
309
|
+
model_cls: type[BaseModel],
|
|
310
|
+
instance: BaseModel | None = None,
|
|
311
|
+
*,
|
|
312
|
+
title_left: str = "",
|
|
313
|
+
version: str | None = None,
|
|
314
|
+
enabled: bool | None = None,
|
|
315
|
+
) -> None:
|
|
316
|
+
"""Print comprehensive settings help for a Pydantic model.
|
|
317
|
+
|
|
318
|
+
Displays:
|
|
319
|
+
1. Main table with all fields (with values if instance provided)
|
|
320
|
+
2. Nested Configuration Types section with schema tables for each nested model
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
model_cls: The Pydantic model class to display
|
|
324
|
+
instance: Optional instance to show actual values
|
|
325
|
+
title_left: Optional prefix for the title
|
|
326
|
+
version: Optional version string to display
|
|
327
|
+
enabled: Optional enabled status to display
|
|
328
|
+
"""
|
|
329
|
+
# Build header
|
|
330
|
+
header = title_left + model_cls.__name__ if title_left else model_cls.__name__
|
|
331
|
+
suffix = []
|
|
332
|
+
if version:
|
|
333
|
+
suffix.append(f"v{version}")
|
|
334
|
+
if enabled is not None:
|
|
335
|
+
suffix.append("enabled" if enabled else "disabled")
|
|
336
|
+
if suffix:
|
|
337
|
+
header += " (" + ", ".join(suffix) + ")"
|
|
338
|
+
|
|
339
|
+
# Print main table
|
|
340
|
+
console.print(f"\n{header}", style="bold")
|
|
341
|
+
console.print(
|
|
342
|
+
build_table_for_model(model_cls, instance, show_value=instance is not None)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Print nested types
|
|
346
|
+
nested = collect_nested_models(model_cls)
|
|
347
|
+
if nested:
|
|
348
|
+
console.print("\n[bold cyan]Nested Configuration Types:[/bold cyan]\n")
|
|
349
|
+
for nested_cls in nested:
|
|
350
|
+
console.print(build_table_for_model(nested_cls, show_value=False))
|
|
351
|
+
console.print()
|