python-ddd-framework 0.3.1__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.
- python_ddd_framework/__init__.py +449 -0
- python_ddd_framework/application/__init__.py +6 -0
- python_ddd_framework/application/build_spec.py +37 -0
- python_ddd_framework/application/builder.py +260 -0
- python_ddd_framework/application/composition.py +219 -0
- python_ddd_framework/application/runtime.py +549 -0
- python_ddd_framework/application/runtime_hooks.py +327 -0
- python_ddd_framework/application/state.py +216 -0
- python_ddd_framework/application_services/__init__.py +54 -0
- python_ddd_framework/application_services/catalog.py +500 -0
- python_ddd_framework/application_services/contracts.py +190 -0
- python_ddd_framework/application_services/dispatcher.py +423 -0
- python_ddd_framework/application_services/errors.py +89 -0
- python_ddd_framework/application_services/execution.py +199 -0
- python_ddd_framework/application_services/interceptors.py +44 -0
- python_ddd_framework/application_services/invocation.py +431 -0
- python_ddd_framework/application_services/policies.py +46 -0
- python_ddd_framework/application_services/seeding.py +72 -0
- python_ddd_framework/application_services/signature.py +49 -0
- python_ddd_framework/application_services/validation.py +84 -0
- python_ddd_framework/auditing/__init__.py +12 -0
- python_ddd_framework/auditing/contracts.py +39 -0
- python_ddd_framework/auditing/control.py +48 -0
- python_ddd_framework/auditing/sqlalchemy/__init__.py +6 -0
- python_ddd_framework/auditing/sqlalchemy/migrations/0001_auditing.py +45 -0
- python_ddd_framework/auditing/sqlalchemy/migrations/__init__.py +1 -0
- python_ddd_framework/auditing/sqlalchemy/models.py +31 -0
- python_ddd_framework/auditing/sqlalchemy/module.py +32 -0
- python_ddd_framework/auditing/sqlalchemy/store.py +63 -0
- python_ddd_framework/authorization/__init__.py +38 -0
- python_ddd_framework/authorization/catalog.py +63 -0
- python_ddd_framework/authorization/contracts.py +241 -0
- python_ddd_framework/authorization/definition_discovery.py +81 -0
- python_ddd_framework/authorization/definitions.py +43 -0
- python_ddd_framework/authorization/errors.py +40 -0
- python_ddd_framework/background_execution/__init__.py +17 -0
- python_ddd_framework/background_execution/application.py +41 -0
- python_ddd_framework/background_execution/child.py +123 -0
- python_ddd_framework/background_execution/contracts.py +48 -0
- python_ddd_framework/background_execution/lifecycle.py +22 -0
- python_ddd_framework/background_execution/local.py +127 -0
- python_ddd_framework/background_execution/locks.py +33 -0
- python_ddd_framework/background_execution/management.py +17 -0
- python_ddd_framework/background_execution/module.py +10 -0
- python_ddd_framework/background_execution/permissions.py +14 -0
- python_ddd_framework/background_execution/processes.py +331 -0
- python_ddd_framework/background_jobs/__init__.py +31 -0
- python_ddd_framework/background_jobs/catalog.py +340 -0
- python_ddd_framework/background_jobs/contracts.py +208 -0
- python_ddd_framework/background_jobs/declaration.py +67 -0
- python_ddd_framework/background_jobs/errors.py +28 -0
- python_ddd_framework/background_jobs/execution.py +203 -0
- python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
- python_ddd_framework/background_jobs/pgqueuer/__init__.py +6 -0
- python_ddd_framework/background_jobs/pgqueuer/enqueue.py +86 -0
- python_ddd_framework/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +95 -0
- python_ddd_framework/background_jobs/pgqueuer/migrations/__init__.py +1 -0
- python_ddd_framework/background_jobs/pgqueuer/module.py +106 -0
- python_ddd_framework/background_jobs/pgqueuer/options.py +82 -0
- python_ddd_framework/background_jobs/pgqueuer/runtime.py +479 -0
- python_ddd_framework/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
- python_ddd_framework/background_jobs/pgqueuer/supervision.py +201 -0
- python_ddd_framework/background_workers/__init__.py +22 -0
- python_ddd_framework/background_workers/catalog.py +118 -0
- python_ddd_framework/background_workers/contracts.py +152 -0
- python_ddd_framework/background_workers/errors.py +20 -0
- python_ddd_framework/background_workers/execution.py +133 -0
- python_ddd_framework/background_workers/runtime.py +219 -0
- python_ddd_framework/caching/__init__.py +11 -0
- python_ddd_framework/caching/catalog.py +73 -0
- python_ddd_framework/caching/contracts.py +73 -0
- python_ddd_framework/caching/errors.py +20 -0
- python_ddd_framework/cli/__init__.py +105 -0
- python_ddd_framework/cli/development.py +85 -0
- python_ddd_framework/cli/errors.py +5 -0
- python_ddd_framework/cli/inspection.py +96 -0
- python_ddd_framework/cli/project.py +69 -0
- python_ddd_framework/cli/runtime.py +56 -0
- python_ddd_framework/configuration/__init__.py +21 -0
- python_ddd_framework/configuration/composition.py +65 -0
- python_ddd_framework/configuration/contracts.py +68 -0
- python_ddd_framework/configuration/dotenv_source.py +51 -0
- python_ddd_framework/configuration/environment_source.py +38 -0
- python_ddd_framework/configuration/immutability.py +124 -0
- python_ddd_framework/configuration/input_shape.py +58 -0
- python_ddd_framework/configuration/merge.py +103 -0
- python_ddd_framework/configuration/root.py +215 -0
- python_ddd_framework/configuration/sources.py +519 -0
- python_ddd_framework/configuration/values.py +427 -0
- python_ddd_framework/configuration/yaml_source.py +51 -0
- python_ddd_framework/developer_kit/__init__.py +1 -0
- python_ddd_framework/developer_kit/generation.py +182 -0
- python_ddd_framework/developer_kit/project_metadata.py +45 -0
- python_ddd_framework/developer_kit/source.py +53 -0
- python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +70 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +41 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +20 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +83 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +84 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +35 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +56 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +14 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +73 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +25 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +36 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/orders.py.jinja +25 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +26 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +49 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
- python_ddd_framework/developer_kit/templates/project/cookiecutter.json +6 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +9 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +38 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +130 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/app.development.yaml.jinja +30 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +20 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +84 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +185 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +47 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +37 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +57 -0
- python_ddd_framework/developer_kit/wiring.py +162 -0
- python_ddd_framework/diagnostics/__init__.py +23 -0
- python_ddd_framework/diagnostics/journal.py +174 -0
- python_ddd_framework/diagnostics/model.py +83 -0
- python_ddd_framework/diagnostics/source.py +25 -0
- python_ddd_framework/distributed_lock/__init__.py +6 -0
- python_ddd_framework/distributed_lock/contracts.py +20 -0
- python_ddd_framework/distributed_lock/options.py +19 -0
- python_ddd_framework/domain/__init__.py +13 -0
- python_ddd_framework/domain/aggregates.py +64 -0
- python_ddd_framework/domain/entities.py +33 -0
- python_ddd_framework/domain/errors.py +12 -0
- python_ddd_framework/domain/value_objects.py +31 -0
- python_ddd_framework/errors/__init__.py +75 -0
- python_ddd_framework/errors/base.py +13 -0
- python_ddd_framework/errors/business.py +53 -0
- python_ddd_framework/errors/configuration.py +119 -0
- python_ddd_framework/errors/diagnostics.py +12 -0
- python_ddd_framework/errors/lifecycle.py +164 -0
- python_ddd_framework/errors/modularity.py +100 -0
- python_ddd_framework/errors/services.py +100 -0
- python_ddd_framework/events/__init__.py +25 -0
- python_ddd_framework/events/aggregate.py +39 -0
- python_ddd_framework/events/catalog.py +136 -0
- python_ddd_framework/events/contracts.py +39 -0
- python_ddd_framework/events/contribution.py +128 -0
- python_ddd_framework/events/discovery.py +105 -0
- python_ddd_framework/events/errors.py +48 -0
- python_ddd_framework/events/runtime.py +192 -0
- python_ddd_framework/fastapi/__init__.py +49 -0
- python_ddd_framework/fastapi/action.py +175 -0
- python_ddd_framework/fastapi/adapter.py +391 -0
- python_ddd_framework/fastapi/application_services.py +654 -0
- python_ddd_framework/fastapi/background.py +42 -0
- python_ddd_framework/fastapi/contracts.py +275 -0
- python_ddd_framework/fastapi/errors.py +32 -0
- python_ddd_framework/fastapi/filters.py +111 -0
- python_ddd_framework/fastapi/health.py +44 -0
- python_ddd_framework/fastapi/http.py +72 -0
- python_ddd_framework/fastapi/http_router.py +216 -0
- python_ddd_framework/fastapi/manual_action.py +105 -0
- python_ddd_framework/fastapi/middleware.py +93 -0
- python_ddd_framework/fastapi/parameters.py +97 -0
- python_ddd_framework/fastapi/realtime/__init__.py +13 -0
- python_ddd_framework/fastapi/realtime/authentication.py +181 -0
- python_ddd_framework/fastapi/realtime/connection.py +166 -0
- python_ddd_framework/fastapi/realtime/module.py +41 -0
- python_ddd_framework/fastapi/realtime/options.py +50 -0
- python_ddd_framework/fastapi/realtime/runtime.py +423 -0
- python_ddd_framework/fastapi/request_context.py +247 -0
- python_ddd_framework/fastapi/route_integrity.py +88 -0
- python_ddd_framework/fastapi/routing.py +424 -0
- python_ddd_framework/fastapi/server.py +182 -0
- python_ddd_framework/fastapi/settings.py +40 -0
- python_ddd_framework/fastapi/tracing.py +62 -0
- python_ddd_framework/fastapi/transfer.py +128 -0
- python_ddd_framework/fastapi/upload_limits.py +63 -0
- python_ddd_framework/fastapi/uploads.py +94 -0
- python_ddd_framework/hosted_services/__init__.py +29 -0
- python_ddd_framework/hosted_services/bridge.py +420 -0
- python_ddd_framework/hosted_services/catalog.py +265 -0
- python_ddd_framework/hosted_services/contracts.py +55 -0
- python_ddd_framework/hosted_services/errors.py +53 -0
- python_ddd_framework/hosted_services/options.py +17 -0
- python_ddd_framework/hosted_services/runtime.py +267 -0
- python_ddd_framework/hosted_services/state.py +40 -0
- python_ddd_framework/hosting/__init__.py +4 -0
- python_ddd_framework/hosting/instance.py +40 -0
- python_ddd_framework/identity/__init__.py +73 -0
- python_ddd_framework/identity/application.py +287 -0
- python_ddd_framework/identity/contracts.py +283 -0
- python_ddd_framework/identity/errors.py +28 -0
- python_ddd_framework/identity/http_api.py +90 -0
- python_ddd_framework/identity/module.py +49 -0
- python_ddd_framework/identity/passwords.py +34 -0
- python_ddd_framework/identity/permissions.py +14 -0
- python_ddd_framework/identity/services.py +108 -0
- python_ddd_framework/identity/sqlalchemy/__init__.py +5 -0
- python_ddd_framework/identity/sqlalchemy/migrations/0001_identity.py +143 -0
- python_ddd_framework/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
- python_ddd_framework/identity/sqlalchemy/migrations/__init__.py +1 -0
- python_ddd_framework/identity/sqlalchemy/models.py +85 -0
- python_ddd_framework/identity/sqlalchemy/module.py +62 -0
- python_ddd_framework/identity/sqlalchemy/stores.py +526 -0
- python_ddd_framework/identity/tokens.py +106 -0
- python_ddd_framework/invocation/__init__.py +3 -0
- python_ddd_framework/invocation/callables.py +171 -0
- python_ddd_framework/invocation/contracts.py +33 -0
- python_ddd_framework/invocation/entries.py +54 -0
- python_ddd_framework/invocation/function_runtime.py +48 -0
- python_ddd_framework/invocation/interception.py +193 -0
- python_ddd_framework/lifecycle/__init__.py +25 -0
- python_ddd_framework/lifecycle/composition.py +35 -0
- python_ddd_framework/lifecycle/context.py +31 -0
- python_ddd_framework/lifecycle/runtime.py +43 -0
- python_ddd_framework/lifecycle/state.py +20 -0
- python_ddd_framework/modularity/__init__.py +15 -0
- python_ddd_framework/modularity/contracts.py +91 -0
- python_ddd_framework/modularity/discovery.py +157 -0
- python_ddd_framework/modularity/graph.py +334 -0
- python_ddd_framework/modularity/registry.py +100 -0
- python_ddd_framework/modularity/selection.py +16 -0
- python_ddd_framework/notifications/__init__.py +19 -0
- python_ddd_framework/notifications/catalog.py +46 -0
- python_ddd_framework/notifications/contracts.py +107 -0
- python_ddd_framework/observability/__init__.py +5 -0
- python_ddd_framework/observability/context.py +43 -0
- python_ddd_framework/observability/export.py +73 -0
- python_ddd_framework/observability/formatting.py +89 -0
- python_ddd_framework/observability/logging.py +118 -0
- python_ddd_framework/observability/options.py +45 -0
- python_ddd_framework/observability/tracing.py +80 -0
- python_ddd_framework/options/__init__.py +19 -0
- python_ddd_framework/options/aliases.py +331 -0
- python_ddd_framework/options/contribution.py +62 -0
- python_ddd_framework/options/immutability.py +48 -0
- python_ddd_framework/options/input_keys.py +214 -0
- python_ddd_framework/options/issues.py +335 -0
- python_ddd_framework/options/models.py +114 -0
- python_ddd_framework/options/registry.py +280 -0
- python_ddd_framework/options/schema.py +488 -0
- python_ddd_framework/options/validation.py +120 -0
- python_ddd_framework/py.typed +0 -0
- python_ddd_framework/realtime/__init__.py +12 -0
- python_ddd_framework/realtime/contracts.py +28 -0
- python_ddd_framework/realtime/diagnostics.py +37 -0
- python_ddd_framework/realtime/messages.py +113 -0
- python_ddd_framework/redis/__init__.py +6 -0
- python_ddd_framework/redis/distributed_lock.py +212 -0
- python_ddd_framework/redis/lease_lock.py +36 -0
- python_ddd_framework/redis/module.py +50 -0
- python_ddd_framework/redis/notification_runtime.py +182 -0
- python_ddd_framework/redis/notifications.py +25 -0
- python_ddd_framework/redis/options.py +35 -0
- python_ddd_framework/redis/runtime.py +116 -0
- python_ddd_framework/services/__init__.py +41 -0
- python_ddd_framework/services/application_bindings.py +221 -0
- python_ddd_framework/services/arbitration.py +381 -0
- python_ddd_framework/services/binding.py +102 -0
- python_ddd_framework/services/contribution.py +438 -0
- python_ddd_framework/services/convention.py +301 -0
- python_ddd_framework/services/convention_contracts.py +101 -0
- python_ddd_framework/services/exposure.py +23 -0
- python_ddd_framework/services/fixed_lifetime.py +52 -0
- python_ddd_framework/services/framework_provider.py +178 -0
- python_ddd_framework/services/native_graph.py +201 -0
- python_ddd_framework/services/provider.py +204 -0
- python_ddd_framework/services/registration.py +101 -0
- python_ddd_framework/services/repository.py +38 -0
- python_ddd_framework/services/runtime.py +302 -0
- python_ddd_framework/settings/__init__.py +30 -0
- python_ddd_framework/settings/application.py +95 -0
- python_ddd_framework/settings/binding.py +33 -0
- python_ddd_framework/settings/catalog.py +121 -0
- python_ddd_framework/settings/changes.py +14 -0
- python_ddd_framework/settings/contracts.py +115 -0
- python_ddd_framework/settings/definition_discovery.py +71 -0
- python_ddd_framework/settings/definitions.py +41 -0
- python_ddd_framework/settings/errors.py +22 -0
- python_ddd_framework/settings/handlers.py +43 -0
- python_ddd_framework/settings/management.py +57 -0
- python_ddd_framework/settings/manager.py +89 -0
- python_ddd_framework/settings/module.py +10 -0
- python_ddd_framework/settings/notifications.py +39 -0
- python_ddd_framework/settings/permissions.py +14 -0
- python_ddd_framework/settings/provider.py +74 -0
- python_ddd_framework/settings/refresh.py +140 -0
- python_ddd_framework/settings/refresh_module.py +45 -0
- python_ddd_framework/settings/sqlalchemy/__init__.py +5 -0
- python_ddd_framework/settings/sqlalchemy/migrations/0001_settings.py +31 -0
- python_ddd_framework/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
- python_ddd_framework/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
- python_ddd_framework/settings/sqlalchemy/migrations/__init__.py +1 -0
- python_ddd_framework/settings/sqlalchemy/models.py +36 -0
- python_ddd_framework/settings/sqlalchemy/module.py +36 -0
- python_ddd_framework/settings/sqlalchemy/store.py +86 -0
- python_ddd_framework/settings/store.py +16 -0
- python_ddd_framework/settings/values.py +39 -0
- python_ddd_framework/sqlalchemy/__init__.py +54 -0
- python_ddd_framework/sqlalchemy/alembic_runtime/__init__.py +1 -0
- python_ddd_framework/sqlalchemy/alembic_runtime/env.py +33 -0
- python_ddd_framework/sqlalchemy/alembic_runtime/script.py.mako +14 -0
- python_ddd_framework/sqlalchemy/auditing.py +48 -0
- python_ddd_framework/sqlalchemy/errors.py +85 -0
- python_ddd_framework/sqlalchemy/metadata.py +576 -0
- python_ddd_framework/sqlalchemy/migration.py +378 -0
- python_ddd_framework/sqlalchemy/migration_options.py +56 -0
- python_ddd_framework/sqlalchemy/module.py +51 -0
- python_ddd_framework/sqlalchemy/module_migration.py +151 -0
- python_ddd_framework/sqlalchemy/options.py +66 -0
- python_ddd_framework/sqlalchemy/repository.py +33 -0
- python_ddd_framework/sqlalchemy/runtime.py +85 -0
- python_ddd_framework/sqlalchemy/session_provider.py +45 -0
- python_ddd_framework/sqlalchemy/unit_of_work.py +97 -0
- python_ddd_framework/testing/__init__.py +5 -0
- python_ddd_framework/testing/runtime.py +112 -0
- python_ddd_framework/unit_of_work/__init__.py +14 -0
- python_ddd_framework/unit_of_work/contracts.py +223 -0
- python_ddd_framework/unit_of_work/errors.py +27 -0
- python_ddd_framework/unit_of_work/manager.py +210 -0
- python_ddd_framework/unit_of_work/options.py +92 -0
- python_ddd_framework-0.3.1.dist-info/METADATA +379 -0
- python_ddd_framework-0.3.1.dist-info/RECORD +355 -0
- python_ddd_framework-0.3.1.dist-info/WHEEL +4 -0
- python_ddd_framework-0.3.1.dist-info/entry_points.txt +9 -0
- python_ddd_framework-0.3.1.dist-info/licenses/LICENSE +7 -0
- python_ddd_framework-0.3.1.dist-info/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""当前 Host 组合快照的只读展示;不解析服务实例或启动运行时。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Mapping
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from pydantic import ConfigDict, TypeAdapter
|
|
9
|
+
|
|
10
|
+
from ..application import Application
|
|
11
|
+
from ..configuration import ConfigurationRoot
|
|
12
|
+
from .errors import CommandError
|
|
13
|
+
from .project import Project
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def inspect_application(environment: str) -> None:
|
|
17
|
+
project = Project.current()
|
|
18
|
+
application = project.host("application").load()(environment=environment)
|
|
19
|
+
if not isinstance(application, Application):
|
|
20
|
+
raise CommandError("The application entry point must return Application")
|
|
21
|
+
try:
|
|
22
|
+
report = _report(application)
|
|
23
|
+
output = TypeAdapter(
|
|
24
|
+
dict[str, object], config=ConfigDict(ser_json_bytes="base64")
|
|
25
|
+
).dump_json(report, indent=2, fallback=_serialize_mapping)
|
|
26
|
+
finally:
|
|
27
|
+
# COMPOSED 也拥有 DI container;读取或序列化失败时同样交还给正式关闭流程。
|
|
28
|
+
await application.stop()
|
|
29
|
+
print(output.decode("utf-8"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _report(application: Application) -> dict[str, object]:
|
|
33
|
+
graph = application.modules.snapshot
|
|
34
|
+
return {
|
|
35
|
+
"modules": {
|
|
36
|
+
"startup_order": [str(key) for key in graph.dependency_order],
|
|
37
|
+
"dependencies": [
|
|
38
|
+
{
|
|
39
|
+
"module": str(module.key),
|
|
40
|
+
"dependencies": [str(key) for key in module.dependencies],
|
|
41
|
+
"source": module.declaration_source,
|
|
42
|
+
}
|
|
43
|
+
for module in application.modules.dependency_order
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
"services": [
|
|
47
|
+
{
|
|
48
|
+
"service": str(row.key.type_hint),
|
|
49
|
+
"component": row.key.component,
|
|
50
|
+
"scope": row.scope.name,
|
|
51
|
+
"owner": str(row.owner),
|
|
52
|
+
"source": row.source,
|
|
53
|
+
"kind": row.kind.value,
|
|
54
|
+
"decision": row.decision.value,
|
|
55
|
+
"reason": row.decision_reason,
|
|
56
|
+
}
|
|
57
|
+
for row in application.service_catalog.registrations
|
|
58
|
+
],
|
|
59
|
+
"configuration": {
|
|
60
|
+
"sources": application.configuration.sources,
|
|
61
|
+
"values": [
|
|
62
|
+
row
|
|
63
|
+
for section in application.configuration
|
|
64
|
+
for row in _configuration_rows(
|
|
65
|
+
application.configuration,
|
|
66
|
+
(section,),
|
|
67
|
+
application.configuration.diagnostic(section).value,
|
|
68
|
+
)
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _configuration_rows(
|
|
75
|
+
configuration: ConfigurationRoot, path: tuple[str, ...], value: object
|
|
76
|
+
) -> Iterator[dict[str, object]]:
|
|
77
|
+
# 只遍历正式 diagnostic 已脱敏的值;被整体隐藏的父节点不得再展开原值。
|
|
78
|
+
if isinstance(value, Mapping) and value:
|
|
79
|
+
for key, item in cast(Mapping[str, object], value).items():
|
|
80
|
+
yield from _configuration_rows(configuration, (*path, key), item)
|
|
81
|
+
return
|
|
82
|
+
# Mapping 数据键允许大小写和点号,查询和输出均保留 segment 身份。
|
|
83
|
+
provenance = configuration._provenance_at(path)
|
|
84
|
+
yield {
|
|
85
|
+
"path": path,
|
|
86
|
+
"value": value,
|
|
87
|
+
"sources": provenance.chain,
|
|
88
|
+
"effective_source": provenance.effective_source,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _serialize_mapping(value: object) -> object:
|
|
93
|
+
# Pydantic 原生 JSON serializer 负责日期、枚举、集合和 bytes;仅补不可变 Mapping。
|
|
94
|
+
if isinstance(value, Mapping):
|
|
95
|
+
return dict(value)
|
|
96
|
+
raise TypeError("Unsupported diagnostic value")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""当前项目的标准 pyproject/distribution/entry points,禁止第二份 Host 清单。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.metadata as metadata
|
|
6
|
+
import tomllib
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..application import Application
|
|
12
|
+
from ..modularity import AppModule
|
|
13
|
+
from .errors import CommandError
|
|
14
|
+
|
|
15
|
+
HOST_ENTRY_POINT_GROUP = "python_ddd_framework.hosts"
|
|
16
|
+
MODULE_ENTRY_POINT_GROUP = "python_ddd_framework.modules"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_project(path: Path) -> dict[str, Any]:
|
|
20
|
+
try:
|
|
21
|
+
with path.open("rb") as stream:
|
|
22
|
+
return tomllib.load(stream)
|
|
23
|
+
except (OSError, ValueError) as error:
|
|
24
|
+
raise CommandError(
|
|
25
|
+
f"Cannot read project metadata at {path}: {type(error).__name__}"
|
|
26
|
+
) from None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class Project:
|
|
31
|
+
root: Path
|
|
32
|
+
name: str
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def current(cls) -> Project:
|
|
36
|
+
root = Path.cwd().resolve()
|
|
37
|
+
project = read_project(root / "pyproject.toml").get("project", {})
|
|
38
|
+
name = project.get("name") if isinstance(project, dict) else None
|
|
39
|
+
if not isinstance(name, str) or not name:
|
|
40
|
+
raise CommandError("Current pyproject.toml must declare project.name")
|
|
41
|
+
return cls(root, name)
|
|
42
|
+
|
|
43
|
+
def host(self, name: str) -> metadata.EntryPoint:
|
|
44
|
+
try:
|
|
45
|
+
distribution = metadata.distribution(self.name)
|
|
46
|
+
except metadata.PackageNotFoundError:
|
|
47
|
+
raise CommandError(
|
|
48
|
+
"Install the current project with uv sync, then use uv run pddd"
|
|
49
|
+
) from None
|
|
50
|
+
matches = tuple(
|
|
51
|
+
entry
|
|
52
|
+
for entry in distribution.entry_points
|
|
53
|
+
if entry.group == HOST_ENTRY_POINT_GROUP and entry.name == name
|
|
54
|
+
)
|
|
55
|
+
if len(matches) != 1:
|
|
56
|
+
raise CommandError(f"Project must declare exactly one {HOST_ENTRY_POINT_GROUP}:{name}")
|
|
57
|
+
return matches[0]
|
|
58
|
+
|
|
59
|
+
def module(self, name: str, application: Application) -> type[AppModule]:
|
|
60
|
+
# entry point 只命名已在当前图中的正式 Module;不导入未启用的 optional provider。
|
|
61
|
+
live = {str(item.key): item.module_type for item in application.modules.dependency_order}
|
|
62
|
+
matches = {
|
|
63
|
+
entry.value
|
|
64
|
+
for entry in metadata.entry_points(group=MODULE_ENTRY_POINT_GROUP)
|
|
65
|
+
if entry.name == name and entry.value in live
|
|
66
|
+
}
|
|
67
|
+
if len(matches) != 1:
|
|
68
|
+
raise CommandError(f"Module alias {name!r} must select exactly one live Module")
|
|
69
|
+
return live[matches.pop()]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""项目运行命令只编排正式 Host、迁移与 seed,不拥有业务初始化规则。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from . import ENVIRONMENT_VARIABLE
|
|
8
|
+
from .errors import CommandError
|
|
9
|
+
from .project import Project
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def database(operation: str, environment: str, module: str) -> None:
|
|
13
|
+
from ..application import Application
|
|
14
|
+
from ..application_services.seeding import DataSeeder
|
|
15
|
+
from ..sqlalchemy import SqlAlchemyMigrator
|
|
16
|
+
|
|
17
|
+
project = Project.current()
|
|
18
|
+
factory = project.host("application").load()
|
|
19
|
+
application = factory(environment=environment)
|
|
20
|
+
if not isinstance(application, Application):
|
|
21
|
+
raise CommandError("The application entry point must return Application")
|
|
22
|
+
try:
|
|
23
|
+
selected = project.module(module, application)
|
|
24
|
+
migrator = SqlAlchemyMigrator(application)
|
|
25
|
+
if operation == "revision":
|
|
26
|
+
await migrator.revision(selected)
|
|
27
|
+
elif operation == "upgrade":
|
|
28
|
+
await migrator.upgrade_heads(module=selected)
|
|
29
|
+
elif operation == "status":
|
|
30
|
+
await migrator.check_current_heads(module=selected)
|
|
31
|
+
elif operation == "seed":
|
|
32
|
+
await migrator.check_current_heads(module=selected)
|
|
33
|
+
await application._start(run_services=False)
|
|
34
|
+
await DataSeeder(application).seed_all(module=selected)
|
|
35
|
+
else:
|
|
36
|
+
raise CommandError(f"Unknown database operation: {operation}")
|
|
37
|
+
finally:
|
|
38
|
+
await application.stop()
|
|
39
|
+
print(f"Database {operation} completed")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def development_server(host: str, port: int) -> None:
|
|
43
|
+
from ..fastapi.server import run_host
|
|
44
|
+
|
|
45
|
+
project = Project.current()
|
|
46
|
+
entry = project.host("web")
|
|
47
|
+
os.environ[ENVIRONMENT_VARIABLE] = "development"
|
|
48
|
+
# 原生 access handler 不传播到 Host 的安全 formatter,不能记录含凭据的 query。
|
|
49
|
+
run_host(
|
|
50
|
+
entry.value,
|
|
51
|
+
host=host,
|
|
52
|
+
port=port,
|
|
53
|
+
reload=True,
|
|
54
|
+
project=project.root,
|
|
55
|
+
environment="development",
|
|
56
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Configuration public contracts and Application-internal composition owners."""
|
|
2
|
+
|
|
3
|
+
from .contracts import (
|
|
4
|
+
ConfigurationContribution,
|
|
5
|
+
ConfigurationProvenance,
|
|
6
|
+
ConfigurationSource,
|
|
7
|
+
ConfigurationSourceDiagnostic,
|
|
8
|
+
ConfigurationSourceStatus,
|
|
9
|
+
ConfigurationValueDiagnostic,
|
|
10
|
+
)
|
|
11
|
+
from .root import ConfigurationRoot
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
"ConfigurationContribution",
|
|
15
|
+
"ConfigurationProvenance",
|
|
16
|
+
"ConfigurationRoot",
|
|
17
|
+
"ConfigurationSource",
|
|
18
|
+
"ConfigurationSourceDiagnostic",
|
|
19
|
+
"ConfigurationSourceStatus",
|
|
20
|
+
"ConfigurationValueDiagnostic",
|
|
21
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Configuration source loading 与 merge 的 Application composition owner。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from .contracts import _ConfigurationDefaultContribution
|
|
11
|
+
from .merge import _ConfigurationMerger
|
|
12
|
+
from .root import ConfigurationRoot
|
|
13
|
+
from .sources import _ConfigurationSourceLoader
|
|
14
|
+
from .values import freeze_value, normalize_mapping
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _ConfigurationComposer:
|
|
18
|
+
"""组合已验证参数,不暴露 source 或 mutable merge state 给 Application。"""
|
|
19
|
+
|
|
20
|
+
__slots__ = ("_loader",)
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
environment: str,
|
|
26
|
+
base_path: Path,
|
|
27
|
+
env_prefix: str,
|
|
28
|
+
dotenv_path: Path | None,
|
|
29
|
+
explicit_overrides: Mapping[str, object],
|
|
30
|
+
cli_args: tuple[str, ...],
|
|
31
|
+
) -> None:
|
|
32
|
+
self._loader = _ConfigurationSourceLoader(
|
|
33
|
+
environment=environment,
|
|
34
|
+
base_path=base_path,
|
|
35
|
+
env_prefix=env_prefix,
|
|
36
|
+
dotenv_path=dotenv_path,
|
|
37
|
+
explicit_overrides=explicit_overrides,
|
|
38
|
+
cli_args=cli_args,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def build(
|
|
42
|
+
self,
|
|
43
|
+
defaults: tuple[_ConfigurationDefaultContribution, ...],
|
|
44
|
+
) -> ConfigurationRoot:
|
|
45
|
+
payloads, diagnostics = self._loader.load(defaults)
|
|
46
|
+
frozen_payloads = tuple(
|
|
47
|
+
replace(payload, values=cast(Mapping[str, object], freeze_value(payload.values)))
|
|
48
|
+
for payload in payloads
|
|
49
|
+
)
|
|
50
|
+
initial = _ConfigurationMerger().merge(
|
|
51
|
+
payloads=tuple(
|
|
52
|
+
replace(payload, values=normalize_mapping(payload.values, pending=True))
|
|
53
|
+
for payload in frozen_payloads
|
|
54
|
+
),
|
|
55
|
+
diagnostics=diagnostics,
|
|
56
|
+
secret_paths=frozenset(key for payload in payloads for key in payload.values),
|
|
57
|
+
)
|
|
58
|
+
# 临时配置视图用于 composition;类型明确后只重解释此快照,绝不重读来源。
|
|
59
|
+
return ConfigurationRoot(
|
|
60
|
+
tree=initial,
|
|
61
|
+
provenance=initial._provenance,
|
|
62
|
+
secret_paths=frozenset(initial),
|
|
63
|
+
sources=initial.sources,
|
|
64
|
+
payloads=frozen_payloads,
|
|
65
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Configuration source、provenance 与 diagnostic value contracts。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from ..diagnostics import SourceLocation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConfigurationSource(str, Enum):
|
|
13
|
+
MODULE_DEFAULTS = "module-defaults"
|
|
14
|
+
YAML_BASE = "yaml-base"
|
|
15
|
+
YAML_ENVIRONMENT = "yaml-environment"
|
|
16
|
+
DOTENV = "dotenv"
|
|
17
|
+
OS_ENVIRONMENT = "os-environment"
|
|
18
|
+
CLI = "cli"
|
|
19
|
+
EXPLICIT = "explicit"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigurationSourceStatus(str, Enum):
|
|
23
|
+
LOADED = "loaded"
|
|
24
|
+
SKIPPED = "skipped"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class ConfigurationSourceDiagnostic:
|
|
29
|
+
source: ConfigurationSource
|
|
30
|
+
status: ConfigurationSourceStatus
|
|
31
|
+
location: SourceLocation
|
|
32
|
+
reason: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class ConfigurationContribution:
|
|
37
|
+
source_id: ConfigurationSource
|
|
38
|
+
source_location: SourceLocation
|
|
39
|
+
raw_kind: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class ConfigurationProvenance:
|
|
44
|
+
path: str
|
|
45
|
+
chain: tuple[ConfigurationContribution, ...]
|
|
46
|
+
effective_source: ConfigurationContribution
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class ConfigurationValueDiagnostic:
|
|
51
|
+
path: str
|
|
52
|
+
value: object
|
|
53
|
+
redacted: bool
|
|
54
|
+
provenance: ConfigurationProvenance
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True, slots=True)
|
|
58
|
+
class _ConfigurationDefaultContribution:
|
|
59
|
+
source_location: SourceLocation
|
|
60
|
+
values: Mapping[str, object]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class _SourcePayload:
|
|
65
|
+
source: ConfigurationSource
|
|
66
|
+
location: SourceLocation
|
|
67
|
+
values: Mapping[str, object]
|
|
68
|
+
input_depth: int = 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""dotenv 文件单次读取;语法预检与原生 value 解码消费同一份文本。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from dotenv import dotenv_values
|
|
10
|
+
from dotenv.parser import parse_stream
|
|
11
|
+
from pydantic_settings import DotEnvSettingsSource
|
|
12
|
+
from pydantic_settings.sources.utils import parse_env_vars
|
|
13
|
+
|
|
14
|
+
from ..diagnostics import SourceLocation
|
|
15
|
+
from ..errors import ConfigurationSourceError
|
|
16
|
+
from .contracts import ConfigurationSource
|
|
17
|
+
from .environment_source import _LayeredEnvSource
|
|
18
|
+
from .input_shape import _environment_input_name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _SnapshotDotEnvSource(DotEnvSettingsSource, _LayeredEnvSource):
|
|
22
|
+
def _read_env_file(self, file_path: Path) -> Mapping[str, str | None]:
|
|
23
|
+
text = file_path.read_text(encoding=self.env_file_encoding or "utf-8")
|
|
24
|
+
seen: set[str] = set()
|
|
25
|
+
for binding in parse_stream(StringIO(text)):
|
|
26
|
+
if binding.error:
|
|
27
|
+
raise ConfigurationSourceError(
|
|
28
|
+
source_id=ConfigurationSource.DOTENV.value,
|
|
29
|
+
location=SourceLocation(
|
|
30
|
+
owner=ConfigurationSource.DOTENV.value,
|
|
31
|
+
file=str(file_path),
|
|
32
|
+
line=binding.original.line,
|
|
33
|
+
symbol=None,
|
|
34
|
+
),
|
|
35
|
+
original_error=ValueError("invalid dotenv syntax"),
|
|
36
|
+
)
|
|
37
|
+
key = binding.key
|
|
38
|
+
if key is not None and key.casefold().startswith(self.env_prefix.casefold()):
|
|
39
|
+
parts = _environment_input_name(key, self.env_prefix)
|
|
40
|
+
if parts in seen:
|
|
41
|
+
raise ValueError("duplicate dotenv path")
|
|
42
|
+
seen.add(parts)
|
|
43
|
+
return parse_env_vars(
|
|
44
|
+
{
|
|
45
|
+
_environment_input_name(key, self.env_prefix): value
|
|
46
|
+
for key, value in dotenv_values(stream=StringIO(text)).items()
|
|
47
|
+
},
|
|
48
|
+
True,
|
|
49
|
+
self.env_ignore_empty,
|
|
50
|
+
self.env_parse_none_str,
|
|
51
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""原生环境 source 的读取期分层;保留 JSON 与嵌套变量的覆盖身份。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic.fields import FieldInfo
|
|
8
|
+
from pydantic_settings import EnvSettingsSource
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _LayeredEnvSource(EnvSettingsSource):
|
|
12
|
+
def __call__(self) -> dict[str, Any]:
|
|
13
|
+
self.input_layers: dict[int, dict[str, object]] = {}
|
|
14
|
+
return super().__call__()
|
|
15
|
+
|
|
16
|
+
def prepare_field_value(
|
|
17
|
+
self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool
|
|
18
|
+
) -> Any:
|
|
19
|
+
snapshot = self.env_vars
|
|
20
|
+
delimiter = self.env_nested_delimiter
|
|
21
|
+
assert delimiter is not None
|
|
22
|
+
depths = sorted({name.count(delimiter) for name in snapshot})
|
|
23
|
+
prepared: object = None
|
|
24
|
+
try:
|
|
25
|
+
for depth in depths:
|
|
26
|
+
self.env_vars = {
|
|
27
|
+
name: raw for name, raw in snapshot.items() if name.count(delimiter) == depth
|
|
28
|
+
}
|
|
29
|
+
layer_value, _, complex_value = self.get_field_value(field, field_name)
|
|
30
|
+
decoded = super().prepare_field_value(field_name, field, layer_value, complex_value)
|
|
31
|
+
if decoded is not None:
|
|
32
|
+
self.input_layers.setdefault(depth, {})[field_name] = decoded
|
|
33
|
+
prepared = decoded
|
|
34
|
+
finally:
|
|
35
|
+
self.env_vars = snapshot
|
|
36
|
+
# __call__ 仍只执行一次;最终合并消费 input_layers,不消费这个原生返回视图。
|
|
37
|
+
# 这里不校验 Options,也不重读 OS 或 dotenv。
|
|
38
|
+
return prepared
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Configuration value 所有的 typed snapshot 规则,供 Options 与 Settings 复用。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping, MutableMapping, MutableSequence, MutableSet
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Annotated, Any, Literal, cast, get_args, get_origin
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Secret, SecretBytes, SecretStr
|
|
12
|
+
|
|
13
|
+
from .values import freeze_value, is_frozen_secret, is_supported_scalar
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_model_shape(model_type: type[BaseModel]) -> None:
|
|
17
|
+
# frozen 只保护字段赋值,不能保护 extra 和 PrivateAttr。
|
|
18
|
+
if model_type.model_config.get("frozen") is not True:
|
|
19
|
+
raise TypeError("snapshot models must be frozen")
|
|
20
|
+
if model_type.model_config.get("extra") == "allow":
|
|
21
|
+
raise TypeError("snapshot models must not allow mutable extra fields")
|
|
22
|
+
if model_type.__private_attributes__:
|
|
23
|
+
raise TypeError("snapshot models must not declare private attributes")
|
|
24
|
+
if any(
|
|
25
|
+
isinstance(member, cached_property)
|
|
26
|
+
for base in model_type.__mro__
|
|
27
|
+
for member in vars(base).values()
|
|
28
|
+
):
|
|
29
|
+
# cached_property 可绕过 frozen 在读取时向 __dict__ 写入缓存,不能发布为共享默认值。
|
|
30
|
+
raise TypeError("snapshot models must not declare cached properties")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_immutable_annotation(
|
|
34
|
+
annotation: object, seen: frozenset[object] = frozenset()
|
|
35
|
+
) -> None:
|
|
36
|
+
if annotation in seen:
|
|
37
|
+
return
|
|
38
|
+
origin = get_origin(annotation)
|
|
39
|
+
if annotation in (Any, object):
|
|
40
|
+
raise TypeError("snapshot annotations must not use Any/object")
|
|
41
|
+
mutable = (list, dict, set, bytearray, MutableMapping, MutableSequence, MutableSet)
|
|
42
|
+
if annotation in mutable or origin in mutable:
|
|
43
|
+
raise TypeError("snapshot annotations must not use mutable containers")
|
|
44
|
+
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
45
|
+
validate_model_shape(annotation)
|
|
46
|
+
for field in annotation.model_fields.values():
|
|
47
|
+
validate_immutable_annotation(field.annotation, seen | {annotation})
|
|
48
|
+
elif isinstance(annotation, type) and issubclass(annotation, Enum):
|
|
49
|
+
if any(not is_supported_scalar(member) for member in annotation):
|
|
50
|
+
raise TypeError("snapshot enum members must be deeply immutable")
|
|
51
|
+
elif origin is Annotated:
|
|
52
|
+
validate_immutable_annotation(get_args(annotation)[0], seen)
|
|
53
|
+
elif origin is not Literal:
|
|
54
|
+
_validate_annotation_arguments(get_args(annotation), seen)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _validate_annotation_arguments(arguments: tuple[object, ...], seen: frozenset[object]) -> None:
|
|
58
|
+
for argument in arguments:
|
|
59
|
+
if argument is not Ellipsis:
|
|
60
|
+
validate_immutable_annotation(argument, seen)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def freeze_typed_value(value: object, *, allow_secrets: bool = True) -> object:
|
|
64
|
+
if isinstance(value, (Secret, SecretStr, SecretBytes)):
|
|
65
|
+
if not allow_secrets:
|
|
66
|
+
raise TypeError("runtime settings cannot contain secrets")
|
|
67
|
+
return freeze_value(value)
|
|
68
|
+
if isinstance(value, BaseModel):
|
|
69
|
+
return _freeze_model(value, allow_secrets=allow_secrets)
|
|
70
|
+
if isinstance(value, Mapping):
|
|
71
|
+
return MappingProxyType(
|
|
72
|
+
{
|
|
73
|
+
freeze_typed_value(key, allow_secrets=allow_secrets): freeze_typed_value(
|
|
74
|
+
item, allow_secrets=allow_secrets
|
|
75
|
+
)
|
|
76
|
+
for key, item in value.items()
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
if isinstance(value, tuple):
|
|
80
|
+
items = tuple(freeze_typed_value(item, allow_secrets=allow_secrets) for item in value)
|
|
81
|
+
if type(value) is tuple:
|
|
82
|
+
return items
|
|
83
|
+
fields = getattr(type(value), "_fields", None)
|
|
84
|
+
if isinstance(fields, tuple) and not hasattr(value, "__dict__"):
|
|
85
|
+
return cast(Callable[..., tuple[object, ...]], type(value))(*items)
|
|
86
|
+
raise TypeError("unsupported tuple subtype")
|
|
87
|
+
if isinstance(value, frozenset):
|
|
88
|
+
return frozenset(freeze_typed_value(item, allow_secrets=allow_secrets) for item in value)
|
|
89
|
+
if is_supported_scalar(value) or (allow_secrets and is_frozen_secret(value)):
|
|
90
|
+
return value
|
|
91
|
+
raise TypeError(f"validated object graph contains unsupported {type(value).__name__}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _freeze_model(value: BaseModel, *, allow_secrets: bool) -> BaseModel:
|
|
95
|
+
validate_model_shape(type(value))
|
|
96
|
+
# model_copy 会保留 __dict__ 中的额外状态,因此复制前必须验证实例边界。
|
|
97
|
+
if set(vars(value)) - type(value).model_fields.keys():
|
|
98
|
+
raise TypeError("snapshot models must not contain undeclared instance state")
|
|
99
|
+
updates = {
|
|
100
|
+
name: freeze_typed_value(getattr(value, name), allow_secrets=allow_secrets)
|
|
101
|
+
for name in type(value).model_fields
|
|
102
|
+
}
|
|
103
|
+
return value.model_copy(update=updates)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _serialization_value(value: object) -> object:
|
|
107
|
+
"""仅在原生 serializer 边界复制冻结容器,不发布这个可变副本。"""
|
|
108
|
+
if isinstance(value, BaseModel):
|
|
109
|
+
return value.model_copy(
|
|
110
|
+
update={
|
|
111
|
+
name: _serialization_value(getattr(value, name))
|
|
112
|
+
for name in type(value).model_fields
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
if isinstance(value, Mapping):
|
|
116
|
+
return {key: _serialization_value(item) for key, item in value.items()}
|
|
117
|
+
if isinstance(value, tuple):
|
|
118
|
+
items = tuple(_serialization_value(item) for item in value)
|
|
119
|
+
if type(value) is tuple:
|
|
120
|
+
return items
|
|
121
|
+
return cast(Callable[..., tuple[object, ...]], type(value))(*items)
|
|
122
|
+
if isinstance(value, frozenset):
|
|
123
|
+
return frozenset(_serialization_value(item) for item in value)
|
|
124
|
+
return value
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""从实际输入路径构造原生 source 的读取形状,不持有 Options schema。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic_settings import BaseSettings
|
|
10
|
+
|
|
11
|
+
from .values import canonical_segment
|
|
12
|
+
|
|
13
|
+
_PathTree: TypeAlias = dict[str, "_PathTree"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _environment_input_name(name: str, prefix: str) -> str:
|
|
17
|
+
if not name.casefold().startswith(prefix.casefold()):
|
|
18
|
+
return name
|
|
19
|
+
first, separator, tail = name[len(prefix) :].partition("__")
|
|
20
|
+
# section 是已知字段;后续路径的类型尚未声明,不能提前折叠 Mapping 的数据键。
|
|
21
|
+
return prefix.casefold() + canonical_segment(first) + separator + tail
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _input_sections(names: Iterable[str], prefix: str) -> frozenset[str]:
|
|
25
|
+
return frozenset(
|
|
26
|
+
canonical_segment(name[len(prefix) :].split("__")[0])
|
|
27
|
+
for name in names
|
|
28
|
+
if name.casefold().startswith(prefix.casefold())
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _cli_settings_type(args: tuple[str, ...]) -> type[BaseSettings]:
|
|
33
|
+
paths: _PathTree = {}
|
|
34
|
+
seen: set[tuple[str, ...]] = set()
|
|
35
|
+
for argument in args:
|
|
36
|
+
if argument in ("-h", "--help"):
|
|
37
|
+
# Builder 只接收应用配置参数;Host 处理 help,原生 argparse 不得退出宿主进程。
|
|
38
|
+
raise ValueError("help flags belong to the Host")
|
|
39
|
+
if not argument.startswith("--"):
|
|
40
|
+
continue
|
|
41
|
+
parts = tuple(canonical_segment(part) for part in argument[2:].split("=", 1)[0].split("."))
|
|
42
|
+
# 原生 CLI source 会合并对象或覆盖叶子;平台在解码前拒绝同一路径的重复声明。
|
|
43
|
+
if parts in seen:
|
|
44
|
+
raise ValueError("duplicate canonical CLI path")
|
|
45
|
+
seen.add(parts)
|
|
46
|
+
node = paths
|
|
47
|
+
for part in parts:
|
|
48
|
+
node = node.setdefault(part, {})
|
|
49
|
+
annotations = {key: _shape_annotation(value) for key, value in paths.items()}
|
|
50
|
+
return type("_ApplicationCliSettings", (BaseSettings,), {"__annotations__": annotations})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _shape_annotation(value: object) -> object:
|
|
54
|
+
if isinstance(value, dict) and value:
|
|
55
|
+
annotations = {key: _shape_annotation(child) for key, child in value.items()}
|
|
56
|
+
return type("_CliSection", (BaseModel,), {"__annotations__": annotations})
|
|
57
|
+
# Union 保留普通字符串,同时让原生 source 解码 JSON mapping/sequence。
|
|
58
|
+
return dict[str, object] | list[object] | str
|