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,604 @@
|
|
|
1
|
+
"""Plugin discovery system for finding and loading plugins.
|
|
2
|
+
|
|
3
|
+
This module provides mechanisms to discover plugins from the filesystem
|
|
4
|
+
and dynamically load their factories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
import importlib.machinery
|
|
9
|
+
import importlib.util
|
|
10
|
+
import logging
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
from typing import Any, cast
|
|
15
|
+
|
|
16
|
+
import structlog
|
|
17
|
+
|
|
18
|
+
from ccproxy.config import Settings
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
# Python 3.10+
|
|
23
|
+
from importlib.metadata import EntryPoint, entry_points
|
|
24
|
+
except ImportError: # pragma: no cover
|
|
25
|
+
entry_points = None # type: ignore[assignment]
|
|
26
|
+
EntryPoint = Any # type: ignore[misc,assignment]
|
|
27
|
+
|
|
28
|
+
from .interfaces import PluginFactory
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
logger = structlog.get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_logger(context: str, plugin_name: str | None = None) -> Any:
|
|
35
|
+
"""Return a structlog logger bound with shared plugin metadata."""
|
|
36
|
+
|
|
37
|
+
bound = logger.bind(type=context, category="plugin")
|
|
38
|
+
if plugin_name:
|
|
39
|
+
bound = bound.bind(name=plugin_name)
|
|
40
|
+
return bound
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _log_missing_dependency(
|
|
44
|
+
*, plugin_name: str, error: ModuleNotFoundError, context: str
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Log a structured warning for a missing plugin dependency."""
|
|
47
|
+
|
|
48
|
+
missing_dependency = getattr(error, "name", None)
|
|
49
|
+
if not missing_dependency:
|
|
50
|
+
missing_dependency = str(error).removeprefix("No module named ").strip("'\"")
|
|
51
|
+
|
|
52
|
+
event_name = "plugin_dependency_missing"
|
|
53
|
+
log_payload = {"dependency": missing_dependency, "details": context}
|
|
54
|
+
|
|
55
|
+
_get_logger(context=context, plugin_name=plugin_name).warning(
|
|
56
|
+
event_name,
|
|
57
|
+
**log_payload,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
logging.warning("%s %s", event_name, log_payload)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_combined_plugin_denylist(
|
|
64
|
+
disabled_plugins: Iterable[str] | None,
|
|
65
|
+
plugin_configs: dict[str, Any] | None,
|
|
66
|
+
) -> set[str]:
|
|
67
|
+
"""Merge explicit and per-plugin disabled settings into a single deny list."""
|
|
68
|
+
|
|
69
|
+
combined = set(disabled_plugins or [])
|
|
70
|
+
|
|
71
|
+
if not plugin_configs:
|
|
72
|
+
return combined
|
|
73
|
+
|
|
74
|
+
for plugin_name, config in plugin_configs.items():
|
|
75
|
+
if not isinstance(config, dict):
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
enabled_flag = config.get("enabled")
|
|
79
|
+
if enabled_flag is False:
|
|
80
|
+
combined.add(plugin_name)
|
|
81
|
+
|
|
82
|
+
return combined
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class PluginDiscovery:
|
|
86
|
+
"""Discovers and loads plugins from the filesystem."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, plugins_dirs: Iterable[Path]):
|
|
89
|
+
"""Initialize plugin discovery.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
plugins_dirs: Ordered directories containing plugin packages
|
|
93
|
+
"""
|
|
94
|
+
seen: set[Path] = set()
|
|
95
|
+
ordered: list[Path] = []
|
|
96
|
+
for directory in plugins_dirs:
|
|
97
|
+
path = Path(directory)
|
|
98
|
+
resolved = path.resolve()
|
|
99
|
+
if resolved in seen:
|
|
100
|
+
continue
|
|
101
|
+
seen.add(resolved)
|
|
102
|
+
ordered.append(path)
|
|
103
|
+
self.plugin_dirs = ordered
|
|
104
|
+
self.discovered_plugins: dict[str, Path] = {}
|
|
105
|
+
|
|
106
|
+
def discover_plugins(self) -> dict[str, Path]:
|
|
107
|
+
"""Discover all plugins in the plugins directory.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Dictionary mapping plugin names to their paths
|
|
111
|
+
"""
|
|
112
|
+
self.discovered_plugins.clear()
|
|
113
|
+
|
|
114
|
+
logger_fs = _get_logger("filesystem")
|
|
115
|
+
discovered: list[str] = []
|
|
116
|
+
missing_dirs: list[str] = []
|
|
117
|
+
|
|
118
|
+
for base_dir in self.plugin_dirs:
|
|
119
|
+
if not base_dir.exists():
|
|
120
|
+
missing_dirs.append(str(base_dir))
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
for item in sorted(base_dir.iterdir()):
|
|
124
|
+
if not item.is_dir() or item.name.startswith("_"):
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
plugin_file = item / "plugin.py"
|
|
128
|
+
if not plugin_file.exists():
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if item.name in self.discovered_plugins:
|
|
132
|
+
_get_logger("filesystem", item.name).debug(
|
|
133
|
+
"plugin_duplicate_ignored",
|
|
134
|
+
original=str(self.discovered_plugins[item.name]),
|
|
135
|
+
ignored=str(plugin_file),
|
|
136
|
+
)
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
self.discovered_plugins[item.name] = plugin_file
|
|
140
|
+
discovered.append(item.name)
|
|
141
|
+
|
|
142
|
+
plugin_logger = _get_logger("filesystem", item.name)
|
|
143
|
+
plugin_trace = getattr(plugin_logger, "trace", plugin_logger.debug)
|
|
144
|
+
plugin_trace(
|
|
145
|
+
"plugin_found",
|
|
146
|
+
path=str(plugin_file),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if missing_dirs:
|
|
150
|
+
logger_fs.warning(
|
|
151
|
+
"plugins_directories_missing",
|
|
152
|
+
paths=missing_dirs,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Single consolidated log for all discoveries
|
|
156
|
+
logger_fs.info(
|
|
157
|
+
"plugins_discovered",
|
|
158
|
+
count=len(discovered),
|
|
159
|
+
names=discovered if discovered else [],
|
|
160
|
+
directories=[str(path) for path in self.plugin_dirs],
|
|
161
|
+
)
|
|
162
|
+
return self.discovered_plugins
|
|
163
|
+
|
|
164
|
+
def load_plugin_factory(self, name: str) -> PluginFactory | None:
|
|
165
|
+
"""Load a plugin factory by name.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
name: Plugin name
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Plugin factory or None if not found or failed to load
|
|
172
|
+
"""
|
|
173
|
+
logger_fs = _get_logger("filesystem", name)
|
|
174
|
+
if name not in self.discovered_plugins:
|
|
175
|
+
logger_fs.warning("plugin_not_discovered")
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
plugin_path = self.discovered_plugins[name]
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
plugin_dir = plugin_path.parent
|
|
182
|
+
|
|
183
|
+
# Ensure the namespace package includes this plugin directory so
|
|
184
|
+
# relative imports like 'from .config import ...' resolve when the
|
|
185
|
+
# plugin lives outside the main repository (e.g., ~/.config/ccproxy/plugins).
|
|
186
|
+
try:
|
|
187
|
+
import ccproxy.plugins as builtin_plugins
|
|
188
|
+
|
|
189
|
+
if hasattr(builtin_plugins, "__path__"):
|
|
190
|
+
location = str(plugin_dir)
|
|
191
|
+
if location not in builtin_plugins.__path__:
|
|
192
|
+
builtin_plugins.__path__.append(location)
|
|
193
|
+
except ModuleNotFoundError: # pragma: no cover - defensive
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
module_name = f"ccproxy.plugins.{name}.plugin"
|
|
197
|
+
package_name = f"ccproxy.plugins.{name}"
|
|
198
|
+
|
|
199
|
+
# Reload package/module to pick up filesystem changes
|
|
200
|
+
import sys
|
|
201
|
+
|
|
202
|
+
package_module: None | ModuleType = None
|
|
203
|
+
for candidate in (module_name, package_name):
|
|
204
|
+
if candidate in sys.modules:
|
|
205
|
+
sys.modules.pop(candidate)
|
|
206
|
+
|
|
207
|
+
init_file = plugin_dir / "__init__.py"
|
|
208
|
+
if init_file.exists():
|
|
209
|
+
package_spec = importlib.util.spec_from_file_location(
|
|
210
|
+
package_name,
|
|
211
|
+
init_file,
|
|
212
|
+
submodule_search_locations=[str(plugin_dir)],
|
|
213
|
+
)
|
|
214
|
+
if package_spec and package_spec.loader:
|
|
215
|
+
package_module = importlib.util.module_from_spec(package_spec)
|
|
216
|
+
sys.modules[package_name] = package_module
|
|
217
|
+
package_spec.loader.exec_module(package_module)
|
|
218
|
+
|
|
219
|
+
package_module = sys.modules.get(package_name)
|
|
220
|
+
if package_module is None:
|
|
221
|
+
if init_file.exists():
|
|
222
|
+
package_spec = importlib.util.spec_from_file_location(
|
|
223
|
+
package_name,
|
|
224
|
+
init_file,
|
|
225
|
+
submodule_search_locations=[str(plugin_dir)],
|
|
226
|
+
)
|
|
227
|
+
if package_spec and package_spec.loader:
|
|
228
|
+
package_module = importlib.util.module_from_spec(package_spec)
|
|
229
|
+
package_module.__path__ = [str(plugin_dir)]
|
|
230
|
+
sys.modules[package_name] = package_module
|
|
231
|
+
package_spec.loader.exec_module(package_module)
|
|
232
|
+
else: # pragma: no cover - defensive
|
|
233
|
+
package_module = importlib.util.module_from_spec(
|
|
234
|
+
importlib.machinery.ModuleSpec(
|
|
235
|
+
package_name, loader=None, is_package=True
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
package_module.__path__ = [str(plugin_dir)]
|
|
239
|
+
sys.modules[package_name] = package_module
|
|
240
|
+
else:
|
|
241
|
+
package_module = importlib.util.module_from_spec(
|
|
242
|
+
importlib.machinery.ModuleSpec(
|
|
243
|
+
package_name, loader=None, is_package=True
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
package_module.__path__ = [str(plugin_dir)]
|
|
247
|
+
sys.modules[package_name] = package_module
|
|
248
|
+
else:
|
|
249
|
+
package_module.__file__ = (
|
|
250
|
+
str(init_file) if init_file.exists() else package_module.__file__
|
|
251
|
+
)
|
|
252
|
+
package_module.__path__ = [str(plugin_dir)]
|
|
253
|
+
|
|
254
|
+
if package_name in sys.modules:
|
|
255
|
+
package_module = sys.modules[package_name]
|
|
256
|
+
package_module.__file__ = (
|
|
257
|
+
str(init_file)
|
|
258
|
+
if init_file.exists()
|
|
259
|
+
else getattr(package_module, "__file__", None)
|
|
260
|
+
)
|
|
261
|
+
package_module.__path__ = [str(plugin_dir)]
|
|
262
|
+
|
|
263
|
+
spec = importlib.util.spec_from_file_location(
|
|
264
|
+
module_name,
|
|
265
|
+
plugin_path,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if not spec or not spec.loader:
|
|
269
|
+
logger_fs.error("plugin_spec_creation_failed")
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
module = importlib.util.module_from_spec(spec)
|
|
273
|
+
sys.modules[module_name] = module
|
|
274
|
+
spec.loader.exec_module(module)
|
|
275
|
+
|
|
276
|
+
# Get the factory from the module
|
|
277
|
+
if not hasattr(module, "factory"):
|
|
278
|
+
logger_fs.error(
|
|
279
|
+
"plugin_factory_not_found",
|
|
280
|
+
msg="Module must export 'factory' variable",
|
|
281
|
+
)
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
factory = module.factory
|
|
285
|
+
|
|
286
|
+
if not isinstance(factory, PluginFactory):
|
|
287
|
+
logger_fs.error(
|
|
288
|
+
"plugin_factory_invalid_type",
|
|
289
|
+
type=type(factory).__name__,
|
|
290
|
+
)
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
logger_fs.debug(
|
|
294
|
+
"plugin_factory_loaded",
|
|
295
|
+
version=factory.get_manifest().version,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return factory
|
|
299
|
+
|
|
300
|
+
except ModuleNotFoundError as exc:
|
|
301
|
+
_log_missing_dependency(
|
|
302
|
+
plugin_name=name,
|
|
303
|
+
error=exc,
|
|
304
|
+
context="filesystem",
|
|
305
|
+
)
|
|
306
|
+
return None
|
|
307
|
+
except Exception as e:
|
|
308
|
+
logger_fs.error(
|
|
309
|
+
"plugin_load_failed",
|
|
310
|
+
error=str(e),
|
|
311
|
+
exc_info=e,
|
|
312
|
+
)
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
def load_all_factories(
|
|
316
|
+
self, plugin_filter: "PluginFilter | None" = None
|
|
317
|
+
) -> dict[str, PluginFactory]:
|
|
318
|
+
"""Load all discovered plugin factories.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Dictionary mapping plugin names to their factories
|
|
322
|
+
"""
|
|
323
|
+
logger_fs = _get_logger("filesystem")
|
|
324
|
+
factories: dict[str, PluginFactory] = {}
|
|
325
|
+
|
|
326
|
+
skipped_names: list[str] = []
|
|
327
|
+
|
|
328
|
+
for name in self.discovered_plugins:
|
|
329
|
+
if plugin_filter and not plugin_filter.is_enabled(name):
|
|
330
|
+
skipped_names.append(name)
|
|
331
|
+
continue
|
|
332
|
+
factory = self.load_plugin_factory(name)
|
|
333
|
+
if factory:
|
|
334
|
+
factories[name] = factory
|
|
335
|
+
|
|
336
|
+
if skipped_names:
|
|
337
|
+
logger_fs.debug("plugin_skipped_before_load", names=skipped_names)
|
|
338
|
+
|
|
339
|
+
logger_fs.debug(
|
|
340
|
+
"plugin_factories_loaded",
|
|
341
|
+
count=len(factories),
|
|
342
|
+
names=list(factories.keys()),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
return factories
|
|
346
|
+
|
|
347
|
+
def load_entry_point_factories(
|
|
348
|
+
self,
|
|
349
|
+
skip_names: set[str] | None = None,
|
|
350
|
+
plugin_filter: "PluginFilter | None" = None,
|
|
351
|
+
) -> dict[str, PluginFactory]:
|
|
352
|
+
"""Load plugin factories from installed entry points.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Dictionary mapping plugin names to their factories
|
|
356
|
+
"""
|
|
357
|
+
factories: dict[str, PluginFactory] = {}
|
|
358
|
+
logger_ep = _get_logger("entrypoint")
|
|
359
|
+
if entry_points is None:
|
|
360
|
+
logger_ep.debug("entry_points_not_available")
|
|
361
|
+
return factories
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
groups = entry_points()
|
|
365
|
+
eps = []
|
|
366
|
+
# importlib.metadata API differences across Python versions
|
|
367
|
+
if hasattr(groups, "select"):
|
|
368
|
+
eps = list(groups.select(group="ccproxy.plugins"))
|
|
369
|
+
else: # pragma: no cover
|
|
370
|
+
eps = list(groups.get("ccproxy.plugins", []))
|
|
371
|
+
|
|
372
|
+
skip_logged: set[str] = set()
|
|
373
|
+
filtered_skipped: list[str] = []
|
|
374
|
+
for ep in eps:
|
|
375
|
+
name = ep.name
|
|
376
|
+
# Skip entry points that collide with existing filesystem plugins
|
|
377
|
+
if skip_names and name in skip_names:
|
|
378
|
+
if name not in skip_logged:
|
|
379
|
+
_get_logger("entrypoint", name).debug(
|
|
380
|
+
"entry_point_skipped_preexisting_filesystem"
|
|
381
|
+
)
|
|
382
|
+
skip_logged.add(name)
|
|
383
|
+
continue
|
|
384
|
+
# Skip duplicates within entry points themselves
|
|
385
|
+
if name in factories:
|
|
386
|
+
if name not in skip_logged:
|
|
387
|
+
_get_logger("entrypoint", name).debug(
|
|
388
|
+
"entry_point_duplicate_ignored"
|
|
389
|
+
)
|
|
390
|
+
skip_logged.add(name)
|
|
391
|
+
continue
|
|
392
|
+
if plugin_filter and not plugin_filter.is_enabled(name):
|
|
393
|
+
filtered_skipped.append(name)
|
|
394
|
+
continue
|
|
395
|
+
try:
|
|
396
|
+
# Primary load
|
|
397
|
+
obj = ep.load()
|
|
398
|
+
except ModuleNotFoundError as exc:
|
|
399
|
+
_log_missing_dependency(
|
|
400
|
+
plugin_name=name,
|
|
401
|
+
error=exc,
|
|
402
|
+
context="entrypoint",
|
|
403
|
+
)
|
|
404
|
+
continue
|
|
405
|
+
except Exception as e:
|
|
406
|
+
# Fallback: import module and get 'factory'
|
|
407
|
+
try:
|
|
408
|
+
module_name = getattr(ep, "module", None)
|
|
409
|
+
if not module_name:
|
|
410
|
+
value = getattr(ep, "value", "")
|
|
411
|
+
module_name = value.split(":")[0] if ":" in value else None
|
|
412
|
+
if not module_name:
|
|
413
|
+
raise e
|
|
414
|
+
mod = importlib.import_module(module_name)
|
|
415
|
+
if hasattr(mod, "factory"):
|
|
416
|
+
obj = mod.factory
|
|
417
|
+
else:
|
|
418
|
+
raise e
|
|
419
|
+
except ModuleNotFoundError as exc2:
|
|
420
|
+
_log_missing_dependency(
|
|
421
|
+
plugin_name=name,
|
|
422
|
+
error=exc2,
|
|
423
|
+
context="entrypoint_fallback",
|
|
424
|
+
)
|
|
425
|
+
continue
|
|
426
|
+
except Exception as e2:
|
|
427
|
+
_get_logger("entrypoint", name).error(
|
|
428
|
+
"entry_point_load_failed", error=str(e2), exc_info=e2
|
|
429
|
+
)
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
factory: PluginFactory | None = None
|
|
433
|
+
|
|
434
|
+
# If the object already looks like a factory (duck typing)
|
|
435
|
+
if hasattr(obj, "get_manifest") and hasattr(obj, "create_runtime"):
|
|
436
|
+
factory = cast(PluginFactory, obj)
|
|
437
|
+
# If it's callable, try to call to get a factory
|
|
438
|
+
elif callable(obj):
|
|
439
|
+
try:
|
|
440
|
+
maybe = obj()
|
|
441
|
+
if hasattr(maybe, "get_manifest") and hasattr(
|
|
442
|
+
maybe, "create_runtime"
|
|
443
|
+
):
|
|
444
|
+
factory = cast(PluginFactory, maybe)
|
|
445
|
+
except Exception:
|
|
446
|
+
factory = None
|
|
447
|
+
|
|
448
|
+
if not factory:
|
|
449
|
+
_get_logger("entrypoint", name).warning(
|
|
450
|
+
"entry_point_not_factory", obj_type=type(obj).__name__
|
|
451
|
+
)
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
factories[name] = factory
|
|
455
|
+
# logger.debug(
|
|
456
|
+
# "entry_point_factory_loaded",
|
|
457
|
+
# name=name,
|
|
458
|
+
# version=factory.get_manifest().version,
|
|
459
|
+
# category="plugin",
|
|
460
|
+
# )
|
|
461
|
+
|
|
462
|
+
if filtered_skipped:
|
|
463
|
+
logger_ep.info("plugin_skipped_before_load", names=filtered_skipped)
|
|
464
|
+
except Exception as e: # pragma: no cover
|
|
465
|
+
logger_ep.error("entry_points_enumeration_failed", error=str(e), exc_info=e)
|
|
466
|
+
return factories
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class PluginFilter:
|
|
470
|
+
"""Filter plugins based on configuration."""
|
|
471
|
+
|
|
472
|
+
def __init__(
|
|
473
|
+
self,
|
|
474
|
+
enabled_plugins: list[str] | None = None,
|
|
475
|
+
disabled_plugins: Iterable[str] | None = None,
|
|
476
|
+
):
|
|
477
|
+
"""Initialize plugin filter.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
enabled_plugins: List of explicitly enabled plugins (None = all)
|
|
481
|
+
disabled_plugins: Precomputed deny list of disabled plugins
|
|
482
|
+
"""
|
|
483
|
+
self.enabled_plugins = set(enabled_plugins) if enabled_plugins else None
|
|
484
|
+
self.disabled_plugins = set(disabled_plugins or [])
|
|
485
|
+
|
|
486
|
+
def is_enabled(self, plugin_name: str) -> bool:
|
|
487
|
+
"""Check if a plugin is enabled using allow/deny-list precedence."""
|
|
488
|
+
|
|
489
|
+
# 1. If enabled_plugins is specified, ONLY those are allowed
|
|
490
|
+
if self.enabled_plugins is not None:
|
|
491
|
+
return plugin_name in self.enabled_plugins
|
|
492
|
+
|
|
493
|
+
# 2. Check disabled_plugins blacklist
|
|
494
|
+
return plugin_name not in self.disabled_plugins
|
|
495
|
+
|
|
496
|
+
def filter_factories(
|
|
497
|
+
self, factories: dict[str, PluginFactory]
|
|
498
|
+
) -> dict[str, PluginFactory]:
|
|
499
|
+
"""Filter plugin factories based on configuration.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
factories: All discovered factories
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Filtered factories
|
|
506
|
+
"""
|
|
507
|
+
logger_filter = _get_logger("filter")
|
|
508
|
+
filtered = {}
|
|
509
|
+
enabled_plugins = []
|
|
510
|
+
disabled_plugins = []
|
|
511
|
+
|
|
512
|
+
for name, factory in factories.items():
|
|
513
|
+
if self.is_enabled(name):
|
|
514
|
+
filtered[name] = factory
|
|
515
|
+
enabled_plugins.append(name)
|
|
516
|
+
else:
|
|
517
|
+
disabled_plugins.append(name)
|
|
518
|
+
_get_logger("filter", name).info("plugin_disabled")
|
|
519
|
+
|
|
520
|
+
# Debug logging for enabled and disabled plugins
|
|
521
|
+
logger_filter.debug(
|
|
522
|
+
"plugin_filter_summary",
|
|
523
|
+
enabled_plugins=sorted(enabled_plugins),
|
|
524
|
+
disabled_plugins=sorted(disabled_plugins),
|
|
525
|
+
enabled_count=len(enabled_plugins),
|
|
526
|
+
disabled_count=len(disabled_plugins),
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
return filtered
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def discover_and_load_plugins(settings: Settings) -> dict[str, PluginFactory]:
|
|
533
|
+
"""Discover and load all configured plugins.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
settings: Application settings
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Dictionary of loaded plugin factories
|
|
540
|
+
"""
|
|
541
|
+
plugin_dirs: list[Path]
|
|
542
|
+
# if len(settings.plugin_discovery.directories) > 0:
|
|
543
|
+
plugin_dirs = [Path(path) for path in settings.plugin_discovery.directories]
|
|
544
|
+
# else:
|
|
545
|
+
# plugin_dirs = [Path(__file__).parent.parent.parent / "plugins"]
|
|
546
|
+
|
|
547
|
+
logger_mgr = _get_logger("manager")
|
|
548
|
+
|
|
549
|
+
logger_mgr.debug(
|
|
550
|
+
"plugin_filesystem_directories",
|
|
551
|
+
directories=[str(path) for path in plugin_dirs],
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Discover plugins
|
|
555
|
+
discovery = PluginDiscovery(plugin_dirs)
|
|
556
|
+
|
|
557
|
+
combined_denylist = build_combined_plugin_denylist(
|
|
558
|
+
getattr(settings, "disabled_plugins", None),
|
|
559
|
+
getattr(settings, "plugins", None),
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
filter_config = PluginFilter(
|
|
563
|
+
enabled_plugins=getattr(settings, "enabled_plugins", None),
|
|
564
|
+
disabled_plugins=combined_denylist,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
# Determine whether to use local filesystem discovery
|
|
568
|
+
if settings.plugins_disable_local_discovery:
|
|
569
|
+
logger_mgr.info(
|
|
570
|
+
"plugins_local_discovery_disabled",
|
|
571
|
+
reason="settings.plugins_disable_local_discovery",
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
all_factories: dict[str, PluginFactory] = {}
|
|
575
|
+
|
|
576
|
+
filesystem_factories: dict[str, PluginFactory] = {}
|
|
577
|
+
filesystem_names: set[str] = set()
|
|
578
|
+
|
|
579
|
+
if not settings.plugins_disable_local_discovery:
|
|
580
|
+
discovery.discover_plugins()
|
|
581
|
+
filesystem_factories = discovery.load_all_factories(plugin_filter=filter_config)
|
|
582
|
+
filesystem_names = set(filesystem_factories.keys())
|
|
583
|
+
all_factories.update(filesystem_factories)
|
|
584
|
+
|
|
585
|
+
entry_point_factories = discovery.load_entry_point_factories(
|
|
586
|
+
skip_names=filesystem_names,
|
|
587
|
+
plugin_filter=filter_config,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
for name, factory in entry_point_factories.items():
|
|
591
|
+
if name in all_factories:
|
|
592
|
+
_get_logger("manager", name).debug("plugin_filesystem_override")
|
|
593
|
+
all_factories.setdefault(name, factory)
|
|
594
|
+
|
|
595
|
+
filtered_factories = filter_config.filter_factories(all_factories)
|
|
596
|
+
|
|
597
|
+
logger_mgr.info(
|
|
598
|
+
"plugins_ready",
|
|
599
|
+
discovered=len(all_factories),
|
|
600
|
+
enabled=len(filtered_factories),
|
|
601
|
+
names=list(filtered_factories.keys()),
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
return filtered_factories
|