python-ddd-framework 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_ddd_framework/__init__.py +139 -21
- python_ddd_framework/application/builder.py +23 -78
- python_ddd_framework/application/composition.py +4 -22
- python_ddd_framework/application/runtime.py +96 -291
- python_ddd_framework/application/state.py +7 -75
- python_ddd_framework/application_services/__init__.py +5 -21
- python_ddd_framework/{services/application_bindings.py → application_services/bindings.py} +52 -9
- python_ddd_framework/application_services/catalog.py +36 -288
- python_ddd_framework/application_services/contracts.py +14 -13
- python_ddd_framework/application_services/dispatcher.py +19 -269
- python_ddd_framework/application_services/invocation.py +126 -7
- python_ddd_framework/application_services/module.py +168 -0
- python_ddd_framework/application_services/pagination.py +21 -0
- python_ddd_framework/auditing/control.py +1 -1
- python_ddd_framework/auditing/module.py +15 -0
- python_ddd_framework/authorization/__init__.py +2 -0
- python_ddd_framework/authorization/contracts.py +7 -2
- python_ddd_framework/authorization/definitions.py +5 -0
- python_ddd_framework/authorization/module.py +40 -0
- python_ddd_framework/authorization/options.py +7 -0
- python_ddd_framework/background_execution/child.py +13 -3
- python_ddd_framework/background_execution/declarations.py +45 -0
- python_ddd_framework/background_execution/lifecycle.py +12 -0
- python_ddd_framework/background_execution/module.py +107 -1
- python_ddd_framework/background_jobs/catalog.py +59 -42
- python_ddd_framework/background_jobs/contracts.py +16 -19
- python_ddd_framework/background_jobs/pgqueuer/enqueue.py +4 -0
- python_ddd_framework/background_jobs/pgqueuer/module.py +2 -1
- python_ddd_framework/background_jobs/pgqueuer/runtime.py +7 -7
- python_ddd_framework/background_workers/catalog.py +23 -36
- python_ddd_framework/background_workers/contracts.py +3 -0
- python_ddd_framework/background_workers/execution.py +1 -1
- python_ddd_framework/background_workers/runtime.py +38 -1
- python_ddd_framework/caching/__init__.py +7 -6
- python_ddd_framework/caching/contracts.py +65 -67
- python_ddd_framework/caching/errors.py +0 -7
- python_ddd_framework/caching/module.py +18 -0
- python_ddd_framework/caching/naming.py +29 -0
- python_ddd_framework/caching/options.py +38 -0
- python_ddd_framework/caching/unit_of_work.py +46 -0
- python_ddd_framework/cli/__init__.py +31 -6
- python_ddd_framework/cli/project.py +68 -6
- python_ddd_framework/cli/runtime.py +1 -1
- python_ddd_framework/developer_kit/generation.py +75 -22
- python_ddd_framework/developer_kit/project_metadata.py +2 -4
- python_ddd_framework/developer_kit/templates/basic/cookiecutter.json +4 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/README.md +45 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/__init__.py.jinja +0 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/__init__.py.jinja +0 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/conversion_service.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/module.py.jinja +38 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/__init__.py.jinja +0 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/default_conversion_service.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +41 -49
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +45 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +8 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +35 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +5 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +15 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +28 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +26 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +9 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +9 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +23 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{integration.py.jinja → hosted_services/order_integration_service.py.jinja} +8 -2
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +16 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +16 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +17 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +105 -10
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{options.py.jinja → options/order_options.py.jinja} +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +65 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +35 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +41 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +17 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +6 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +39 -2
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +13 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +12 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +9 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +12 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{orders.py.jinja → entities/order.py.jinja} +10 -18
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +13 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +42 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +17 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +22 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +23 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{settings.py.jinja → settings/approval_settings.py.jinja} +3 -2
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +3 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +6 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +11 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +26 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +13 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +40 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/{permissions.py.jinja → permissions/order_permission_provider.py.jinja} +6 -2
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +18 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +15 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +5 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +85 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/{files.py.jinja → routers/order_files.py.jinja} +23 -20
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +44 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +1 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/base.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/{orders.py.jinja → order_model.py.jinja} +7 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +42 -5
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +79 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +4 -3
- python_ddd_framework/developer_kit/templates/project/cookiecutter.json +1 -1
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +16 -3
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +29 -22
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{app.development.yaml.jinja → backend/app.development.yaml.jinja} +2 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{pyproject.toml.jinja → backend/pyproject.toml.jinja} +1 -1
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/main.py.jinja +2 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/module.py.jinja +40 -3
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +40 -10
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +134 -23
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/scripts/.gitkeep +0 -0
- python_ddd_framework/distributed_lock/contracts.py +4 -1
- python_ddd_framework/domain/__init__.py +2 -0
- python_ddd_framework/domain/services.py +11 -0
- python_ddd_framework/errors/business.py +10 -1
- python_ddd_framework/errors/lifecycle.py +3 -31
- python_ddd_framework/events/__init__.py +5 -2
- python_ddd_framework/events/aggregate.py +3 -3
- python_ddd_framework/events/catalog.py +12 -11
- python_ddd_framework/events/contracts.py +6 -1
- python_ddd_framework/events/contribution.py +1 -52
- python_ddd_framework/events/entities.py +23 -0
- python_ddd_framework/events/module.py +97 -0
- python_ddd_framework/events/runtime.py +80 -29
- python_ddd_framework/events/subscription.py +36 -0
- python_ddd_framework/events/unit_of_work.py +25 -0
- python_ddd_framework/extensions/__init__.py +22 -0
- python_ddd_framework/extensions/catalog.py +198 -0
- python_ddd_framework/extensions/contracts.py +85 -0
- python_ddd_framework/extensions/models.py +142 -0
- python_ddd_framework/extensions/module.py +21 -0
- python_ddd_framework/fastapi/__init__.py +2 -0
- python_ddd_framework/fastapi/action.py +1 -2
- python_ddd_framework/fastapi/adapter.py +16 -0
- python_ddd_framework/fastapi/application_services.py +43 -25
- python_ddd_framework/fastapi/background.py +2 -1
- python_ddd_framework/fastapi/contracts.py +26 -8
- python_ddd_framework/fastapi/health.py +1 -1
- python_ddd_framework/fastapi/module.py +14 -0
- python_ddd_framework/fastapi/parameters.py +3 -18
- python_ddd_framework/fastapi/realtime/connection.py +10 -7
- python_ddd_framework/fastapi/realtime/module.py +3 -0
- python_ddd_framework/fastapi/realtime/runtime.py +10 -24
- python_ddd_framework/fastapi/request_context.py +2 -1
- python_ddd_framework/fastapi/routing.py +2 -1
- python_ddd_framework/fastapi/server.py +8 -0
- python_ddd_framework/fastapi/settings.py +2 -1
- python_ddd_framework/hosted_services/bridge.py +20 -1
- python_ddd_framework/hosted_services/catalog.py +6 -16
- python_ddd_framework/hosted_services/contracts.py +4 -0
- python_ddd_framework/hosted_services/declarations.py +15 -0
- python_ddd_framework/hosted_services/module.py +69 -0
- python_ddd_framework/hosted_services/runtime.py +30 -1
- python_ddd_framework/identity/__init__.py +24 -0
- python_ddd_framework/identity/application.py +38 -90
- python_ddd_framework/identity/contracts.py +181 -16
- python_ddd_framework/identity/http_api.py +79 -2
- python_ddd_framework/identity/management.py +166 -0
- python_ddd_framework/identity/module.py +12 -4
- python_ddd_framework/identity/services.py +29 -16
- python_ddd_framework/identity/sqlalchemy/migrations/0003_extra_properties.py +26 -0
- python_ddd_framework/identity/sqlalchemy/migrations/0004_concurrency_version.py +29 -0
- python_ddd_framework/identity/sqlalchemy/models.py +21 -2
- python_ddd_framework/identity/sqlalchemy/module.py +11 -2
- python_ddd_framework/identity/sqlalchemy/repository.py +458 -0
- python_ddd_framework/identity/sqlalchemy/session_security.py +88 -0
- python_ddd_framework/identity/sqlalchemy/stores.py +69 -290
- python_ddd_framework/identity/tokens.py +2 -5
- python_ddd_framework/{application_services/validation.py → invocation/arguments.py} +8 -6
- python_ddd_framework/invocation/contribution.py +61 -0
- python_ddd_framework/invocation/dispatcher.py +291 -0
- python_ddd_framework/invocation/entries.py +2 -25
- python_ddd_framework/invocation/entrypoints.py +126 -0
- python_ddd_framework/{application_services → invocation}/execution.py +8 -5
- python_ddd_framework/invocation/function_runtime.py +3 -1
- python_ddd_framework/invocation/interception.py +113 -42
- python_ddd_framework/{application_services → invocation}/interceptors.py +2 -2
- python_ddd_framework/invocation/managed_proxy.py +116 -0
- python_ddd_framework/invocation/managed_services.py +250 -0
- python_ddd_framework/invocation/methods.py +345 -0
- python_ddd_framework/invocation/module.py +113 -0
- python_ddd_framework/{application_services → invocation}/policies.py +8 -1
- python_ddd_framework/invocation/validation.py +25 -0
- python_ddd_framework/lifecycle/__init__.py +2 -0
- python_ddd_framework/lifecycle/composition.py +5 -2
- python_ddd_framework/lifecycle/participants.py +33 -0
- python_ddd_framework/messaging/__init__.py +25 -0
- python_ddd_framework/messaging/channel.py +205 -0
- python_ddd_framework/messaging/contracts.py +69 -0
- python_ddd_framework/messaging/module.py +101 -0
- python_ddd_framework/messaging/options.py +13 -0
- python_ddd_framework/messaging/receipt.py +27 -0
- python_ddd_framework/messaging/runtime.py +124 -0
- python_ddd_framework/modularity/__init__.py +11 -1
- python_ddd_framework/modularity/contracts.py +47 -16
- python_ddd_framework/modularity/discovery.py +21 -2
- python_ddd_framework/modularity/graph.py +45 -1
- python_ddd_framework/modularity/registry.py +83 -3
- python_ddd_framework/notifications/__init__.py +2 -0
- python_ddd_framework/notifications/catalog.py +3 -2
- python_ddd_framework/notifications/declarations.py +18 -0
- python_ddd_framework/notifications/module.py +21 -0
- python_ddd_framework/observability/logging.py +3 -0
- python_ddd_framework/observability/tracing.py +3 -0
- python_ddd_framework/options/contribution.py +12 -0
- python_ddd_framework/options/registry.py +44 -11
- python_ddd_framework/realtime/__init__.py +2 -1
- python_ddd_framework/realtime/contracts.py +3 -7
- python_ddd_framework/realtime/messages.py +26 -1
- python_ddd_framework/redis/cache.py +243 -0
- python_ddd_framework/redis/cache_scripts.py +40 -0
- python_ddd_framework/redis/distributed_lock.py +10 -2
- python_ddd_framework/redis/module.py +13 -4
- python_ddd_framework/redis/notifications.py +5 -2
- python_ddd_framework/redis/runtime.py +1 -79
- python_ddd_framework/seeding/__init__.py +13 -0
- python_ddd_framework/seeding/contracts.py +45 -0
- python_ddd_framework/seeding/module.py +73 -0
- python_ddd_framework/seeding/runtime.py +63 -0
- python_ddd_framework/services/arbitration.py +23 -0
- python_ddd_framework/services/binding.py +2 -3
- python_ddd_framework/services/composition.py +108 -0
- python_ddd_framework/services/contribution.py +44 -54
- python_ddd_framework/services/convention.py +53 -181
- python_ddd_framework/services/fixed_lifetime.py +12 -15
- python_ddd_framework/services/framework_provider.py +3 -158
- python_ddd_framework/services/native_graph.py +35 -60
- python_ddd_framework/services/provider.py +59 -4
- python_ddd_framework/services/repository.py +6 -0
- python_ddd_framework/services/runtime.py +129 -215
- python_ddd_framework/settings/catalog.py +5 -4
- python_ddd_framework/settings/declarations.py +18 -0
- python_ddd_framework/settings/definitions.py +5 -0
- python_ddd_framework/settings/manager.py +1 -1
- python_ddd_framework/settings/module.py +50 -1
- python_ddd_framework/settings/notifications.py +4 -2
- python_ddd_framework/settings/refresh_module.py +9 -2
- python_ddd_framework/settings/sqlalchemy/module.py +2 -1
- python_ddd_framework/sqlalchemy/__init__.py +12 -0
- python_ddd_framework/sqlalchemy/alembic_runtime/env.py +1 -0
- python_ddd_framework/sqlalchemy/auditing.py +7 -1
- python_ddd_framework/sqlalchemy/extensions.py +186 -0
- python_ddd_framework/sqlalchemy/metadata.py +110 -6
- python_ddd_framework/sqlalchemy/migration.py +61 -8
- python_ddd_framework/sqlalchemy/migration_operations.py +86 -0
- python_ddd_framework/sqlalchemy/module.py +14 -11
- python_ddd_framework/sqlalchemy/module_migration.py +71 -3
- python_ddd_framework/sqlalchemy/repository.py +217 -13
- python_ddd_framework/sqlalchemy/session_provider.py +14 -1
- python_ddd_framework/testing/runtime.py +5 -4
- python_ddd_framework/unit_of_work/__init__.py +2 -1
- python_ddd_framework/unit_of_work/contracts.py +6 -7
- python_ddd_framework/unit_of_work/errors.py +11 -0
- python_ddd_framework/unit_of_work/lifecycle.py +19 -0
- python_ddd_framework/unit_of_work/manager.py +86 -32
- python_ddd_framework/unit_of_work/module.py +61 -0
- python_ddd_framework/unit_of_work/options.py +7 -1
- python_ddd_framework/validation/__init__.py +1 -0
- python_ddd_framework/validation/models.py +37 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/METADATA +139 -44
- python_ddd_framework-0.4.0.dist-info/RECORD +488 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/WHEEL +1 -1
- python_ddd_framework/application_services/seeding.py +0 -72
- python_ddd_framework/caching/catalog.py +0 -73
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +0 -10
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +0 -24
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +0 -83
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +0 -84
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +0 -35
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +0 -14
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +0 -19
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +0 -24
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +0 -36
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +0 -49
- python_ddd_framework-0.3.2.dist-info/RECORD +0 -355
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.dockerignore → backend/.dockerignore} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.gitignore → backend/.gitignore} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.python-version → backend/.python-version} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{Dockerfile → backend/Dockerfile} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.dev.yaml.jinja → backend/compose.dev.yaml.jinja} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.production.yaml.jinja → backend/compose.production.yaml.jinja} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/__init__.py.jinja +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/conftest.py.jinja +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/host/test_http.py.jinja +0 -0
- /python_ddd_framework/{application_services → invocation}/signature.py +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/entry_points.txt +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
|
@@ -23,6 +23,7 @@ context.configure(
|
|
|
23
23
|
target_metadata=target_metadata,
|
|
24
24
|
include_schemas=True,
|
|
25
25
|
include_name=config.attributes.get("include_name"),
|
|
26
|
+
include_object=config.attributes.get("include_object"),
|
|
26
27
|
**version_table_options,
|
|
27
28
|
)
|
|
28
29
|
with context.begin_transaction():
|
|
@@ -39,7 +39,13 @@ def _apply_audit_fields(session: Session, flush_context: object, instances: obje
|
|
|
39
39
|
entity.created_at = now
|
|
40
40
|
entity.created_by = user_id
|
|
41
41
|
for entity in tuple(session.dirty):
|
|
42
|
-
|
|
42
|
+
# merge 或相同值赋值也会标记 dirty;审计不能把无净变化的实体变成一次更新。
|
|
43
|
+
# 保留集合变化,和仓储更新事件使用相同的原生变更判定。
|
|
44
|
+
if (
|
|
45
|
+
isinstance(entity, AuditedEntityMixin)
|
|
46
|
+
and entity not in session.new
|
|
47
|
+
and session.is_modified(entity)
|
|
48
|
+
):
|
|
43
49
|
entity.updated_at = now
|
|
44
50
|
entity.updated_by = user_id
|
|
45
51
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""扩展字段的原生列映射、应用私有 mapper 及持久化边界。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Generic, TypeVar, get_args
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from pydantic.fields import FieldInfo
|
|
11
|
+
from pydantic_core import to_json
|
|
12
|
+
from sqlalchemy import Column, MetaData, Table, inspect
|
|
13
|
+
from sqlalchemy.orm import registry
|
|
14
|
+
from sqlalchemy.schema import SchemaItem
|
|
15
|
+
|
|
16
|
+
from ..extensions import ExtensionCatalog, ExtensionProperties, ExtensionTarget, ModelExtension
|
|
17
|
+
from ..modularity import ModuleDeclaration, ModuleKey, ModuleRef
|
|
18
|
+
|
|
19
|
+
_T = TypeVar("_T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class ExtensionColumn:
|
|
24
|
+
"""引用契约的原生字段身份;列工厂留在消费者 persistence Module。"""
|
|
25
|
+
|
|
26
|
+
field: FieldInfo
|
|
27
|
+
factory: Callable[[], SchemaItem]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class SqlAlchemyModelExtension(ModelExtension):
|
|
32
|
+
columns: tuple[ExtensionColumn, ...] = ()
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def required_module(self) -> ModuleRef:
|
|
36
|
+
return ModuleRef("python_ddd_framework.sqlalchemy.module:SqlAlchemyPersistenceModule")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class SqlAlchemyExtensionMapping:
|
|
41
|
+
target: ExtensionTarget
|
|
42
|
+
table: Table
|
|
43
|
+
model_base: type[object]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class SqlAlchemyExtensionMigrations(ModuleDeclaration):
|
|
48
|
+
migrations_package: str
|
|
49
|
+
branch_label: str
|
|
50
|
+
depends_on: tuple[str, ...]
|
|
51
|
+
connection_name: str = "default"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def required_module(self) -> ModuleRef:
|
|
55
|
+
return ModuleRef("python_ddd_framework.sqlalchemy.module:SqlAlchemyPersistenceModule")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class _ExtensionColumn:
|
|
60
|
+
owner: ModuleKey
|
|
61
|
+
field_name: str
|
|
62
|
+
column: Column[object]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SqlAlchemyExtensionModel(Generic[_T]):
|
|
66
|
+
"""一处有效 row 映射;模型、读写和迁移范围引用相同列对象。"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
model: type[_T],
|
|
71
|
+
target: ExtensionTarget,
|
|
72
|
+
catalog: ExtensionCatalog,
|
|
73
|
+
columns: tuple[_ExtensionColumn, ...],
|
|
74
|
+
) -> None:
|
|
75
|
+
self.model = model
|
|
76
|
+
self.target = target
|
|
77
|
+
self.columns = columns
|
|
78
|
+
self._catalog = catalog
|
|
79
|
+
|
|
80
|
+
def read(self, row: _T) -> ExtensionProperties:
|
|
81
|
+
if not isinstance(row, self.model):
|
|
82
|
+
raise TypeError("row belongs to a different Application's mapper")
|
|
83
|
+
stored = getattr(row, "extra_properties") # noqa: B009 - 原生 mapper 的动态属性边界
|
|
84
|
+
if not isinstance(stored, dict):
|
|
85
|
+
raise TypeError("extra_properties must be a JSON object")
|
|
86
|
+
values = dict(stored)
|
|
87
|
+
for mapping in self.columns:
|
|
88
|
+
if mapping.field_name in values:
|
|
89
|
+
raise ValueError("mapped extension field cannot also be stored in JSON")
|
|
90
|
+
values[mapping.field_name] = getattr(row, mapping.column.key)
|
|
91
|
+
return self._catalog.property_model(self.target).model_validate_json(to_json(values))
|
|
92
|
+
|
|
93
|
+
def write(self, row: _T, properties: ExtensionProperties) -> None:
|
|
94
|
+
if not isinstance(row, self.model):
|
|
95
|
+
raise TypeError("row belongs to a different Application's mapper")
|
|
96
|
+
effective = self._catalog.properties(self.target, properties)
|
|
97
|
+
values = effective.model_dump(mode="json")
|
|
98
|
+
for mapping in self.columns:
|
|
99
|
+
setattr(row, mapping.column.key, getattr(effective, mapping.field_name))
|
|
100
|
+
del values[mapping.field_name]
|
|
101
|
+
setattr(row, "extra_properties", values) # noqa: B010 - 原生 mapper 的动态属性边界
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _map_extensions(
|
|
105
|
+
metadata: MetaData,
|
|
106
|
+
mappings: tuple[SqlAlchemyExtensionMapping, ...],
|
|
107
|
+
catalog: ExtensionCatalog,
|
|
108
|
+
) -> tuple[MetaData, tuple[SqlAlchemyExtensionModel[object], ...]]:
|
|
109
|
+
# 这份 metadata 就是运行时和迁移共同消费的有效模型;不为差异过滤另复制表。
|
|
110
|
+
mapper_registry = registry(metadata=MetaData(naming_convention=metadata.naming_convention))
|
|
111
|
+
native_base = mapper_registry.generate_base()
|
|
112
|
+
for table in metadata.sorted_tables:
|
|
113
|
+
table.to_metadata(mapper_registry.metadata)
|
|
114
|
+
models: list[SqlAlchemyExtensionModel[object]] = []
|
|
115
|
+
for mapping in mappings:
|
|
116
|
+
catalog.property_model(mapping.target)
|
|
117
|
+
if any(model.target is mapping.target for model in models):
|
|
118
|
+
raise ValueError("an extension target has only one ORM mapping")
|
|
119
|
+
if mapping.table.metadata is not metadata or inspect(mapping.model_base, raiseerr=False):
|
|
120
|
+
raise ValueError("extension mapping requires an owned table and an unmapped model base")
|
|
121
|
+
table = mapper_registry.metadata.tables[mapping.table.key]
|
|
122
|
+
if "extra_properties" not in table.c:
|
|
123
|
+
raise ValueError("the base table must own its extra_properties JSON container")
|
|
124
|
+
columns = _columns_for_target(mapping.target, table, catalog)
|
|
125
|
+
model = type(
|
|
126
|
+
mapping.model_base.__name__, (mapping.model_base, native_base), {"__table__": table}
|
|
127
|
+
)
|
|
128
|
+
assert issubclass(model, mapping.model_base)
|
|
129
|
+
models.append(SqlAlchemyExtensionModel(model, mapping.target, catalog, columns))
|
|
130
|
+
return mapper_registry.metadata, tuple(models)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _columns_for_target(
|
|
134
|
+
target: ExtensionTarget,
|
|
135
|
+
table: Table,
|
|
136
|
+
catalog: ExtensionCatalog,
|
|
137
|
+
) -> tuple[_ExtensionColumn, ...]:
|
|
138
|
+
columns: list[_ExtensionColumn] = []
|
|
139
|
+
for contribution in catalog.contributions:
|
|
140
|
+
if contribution.declaration.target is not target:
|
|
141
|
+
continue
|
|
142
|
+
declaration = contribution.declaration
|
|
143
|
+
if not isinstance(declaration, SqlAlchemyModelExtension):
|
|
144
|
+
continue
|
|
145
|
+
fields = {id(field): name for name, field in declaration.properties.model_fields.items()}
|
|
146
|
+
mapped: set[str] = set()
|
|
147
|
+
for storage in declaration.columns:
|
|
148
|
+
name = fields.get(id(storage.field))
|
|
149
|
+
if name is None or name in mapped:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
"column mapping must reference a unique field of its property schema"
|
|
152
|
+
)
|
|
153
|
+
mapped.add(name)
|
|
154
|
+
column = _validate_column(storage.factory(), name, storage.field.annotation, table)
|
|
155
|
+
table.append_column(column)
|
|
156
|
+
columns.append(_ExtensionColumn(contribution.owner, name, column))
|
|
157
|
+
return tuple(columns)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _validate_column(column: object, name: str, annotation: object, table: Table) -> Column[object]:
|
|
161
|
+
if not isinstance(column, Column) or getattr(column, "table", None) is not None:
|
|
162
|
+
raise TypeError("extension column factory must return a fresh unattached Column")
|
|
163
|
+
if (
|
|
164
|
+
column.primary_key
|
|
165
|
+
or column.foreign_keys
|
|
166
|
+
or (column.unique and not column.index)
|
|
167
|
+
or column.constraints
|
|
168
|
+
):
|
|
169
|
+
raise ValueError("extension mapping only owns standalone columns and indexes")
|
|
170
|
+
column_name = column.name or name
|
|
171
|
+
if not column_name.isidentifier() or column_name in table.c:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
"extension column conflicts with an existing column or has an invalid name"
|
|
174
|
+
)
|
|
175
|
+
if column.key and column.key != column_name:
|
|
176
|
+
raise ValueError("extension column key must match its name")
|
|
177
|
+
candidates = get_args(annotation) or (annotation,)
|
|
178
|
+
if column.type.python_type not in candidates:
|
|
179
|
+
raise TypeError("extension column Python type does not match the property schema")
|
|
180
|
+
if column.nullable and type(None) not in candidates:
|
|
181
|
+
raise ValueError("nullable extension columns require an optional property type")
|
|
182
|
+
if any(isinstance(item, type) and issubclass(item, BaseModel) for item in candidates):
|
|
183
|
+
raise TypeError("nested Pydantic properties use JSON storage")
|
|
184
|
+
column.name = column_name
|
|
185
|
+
column.key = column_name
|
|
186
|
+
return column
|
|
@@ -4,18 +4,29 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import importlib.util
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
-
from dataclasses import dataclass, field
|
|
7
|
+
from dataclasses import dataclass, field, replace
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import TypeVar
|
|
9
|
+
from typing import TypeVar, cast
|
|
10
10
|
|
|
11
11
|
from sqlalchemy import Constraint, ForeignKey, MetaData, Table
|
|
12
12
|
|
|
13
|
+
from ..extensions import ExtensionCatalog, ExtensionTarget
|
|
14
|
+
from ..extensions.module import ExtensionsModule
|
|
13
15
|
from ..modularity import AppModule, ModuleDescriptor, ModuleKey, ModuleRegistry
|
|
14
16
|
from ..modularity.discovery import _import_package_modules
|
|
15
17
|
from ..options import OptionsSnapshot
|
|
16
18
|
from .errors import SqlAlchemyMetadataChangedError, SqlAlchemyModelRegistrationError
|
|
19
|
+
from .extensions import (
|
|
20
|
+
SqlAlchemyExtensionMapping,
|
|
21
|
+
SqlAlchemyExtensionMigrations,
|
|
22
|
+
SqlAlchemyExtensionModel,
|
|
23
|
+
SqlAlchemyModelExtension,
|
|
24
|
+
_map_extensions,
|
|
25
|
+
)
|
|
17
26
|
from .options import _validate_connection_name
|
|
18
27
|
|
|
28
|
+
_TRow = TypeVar("_TRow")
|
|
29
|
+
|
|
19
30
|
_MODEL_REGISTRATIONS_ATTRIBUTE = "__python_ddd_framework_sqlalchemy_model_registrations__"
|
|
20
31
|
_MIGRATION_REGISTRATIONS_ATTRIBUTE = "__python_ddd_framework_sqlalchemy_migration_registrations__"
|
|
21
32
|
_TModule = TypeVar("_TModule", bound=type[AppModule])
|
|
@@ -27,6 +38,7 @@ class SqlAlchemyModelRegistration:
|
|
|
27
38
|
migrations_package: str
|
|
28
39
|
branch_label: str
|
|
29
40
|
connection_name: str = "default"
|
|
41
|
+
extension_mappings: tuple[SqlAlchemyExtensionMapping, ...] = ()
|
|
30
42
|
_declared_fingerprint: tuple[object, ...] = field(init=False, repr=False, compare=False)
|
|
31
43
|
|
|
32
44
|
@classmethod
|
|
@@ -38,6 +50,7 @@ class SqlAlchemyModelRegistration:
|
|
|
38
50
|
migrations_package: str,
|
|
39
51
|
branch_label: str,
|
|
40
52
|
connection_name: str = "default",
|
|
53
|
+
extension_mappings: tuple[SqlAlchemyExtensionMapping, ...] = (),
|
|
41
54
|
) -> SqlAlchemyModelRegistration:
|
|
42
55
|
if not isinstance(models_package, str) or not models_package:
|
|
43
56
|
raise ValueError("models_package must be a non-empty import path")
|
|
@@ -50,7 +63,7 @@ class SqlAlchemyModelRegistration:
|
|
|
50
63
|
raise ValueError("models_package must identify a package")
|
|
51
64
|
# Declarative class 导入会写入原生 MetaData,必须早于构造时的指纹冻结。
|
|
52
65
|
_import_package_modules(models_package)
|
|
53
|
-
return cls(metadata, migrations_package, branch_label, connection_name)
|
|
66
|
+
return cls(metadata, migrations_package, branch_label, connection_name, extension_mappings)
|
|
54
67
|
|
|
55
68
|
def __post_init__(self) -> None:
|
|
56
69
|
if not isinstance(self.metadata, MetaData):
|
|
@@ -123,10 +136,17 @@ class _SqlAlchemyMigrationSource:
|
|
|
123
136
|
migrations_package: str
|
|
124
137
|
branch_label: str
|
|
125
138
|
migration_path: Path
|
|
139
|
+
extension: SqlAlchemyExtensionMigrations | None = None
|
|
126
140
|
|
|
127
141
|
|
|
128
142
|
class SqlAlchemyModelCatalog:
|
|
129
|
-
__slots__ = (
|
|
143
|
+
__slots__ = (
|
|
144
|
+
"_contributions",
|
|
145
|
+
"_migration_contributions",
|
|
146
|
+
"_migration_declarations",
|
|
147
|
+
"extension_models",
|
|
148
|
+
"extension_sources",
|
|
149
|
+
)
|
|
130
150
|
|
|
131
151
|
def __init__(
|
|
132
152
|
self,
|
|
@@ -139,6 +159,16 @@ class SqlAlchemyModelCatalog:
|
|
|
139
159
|
self._contributions = contributions
|
|
140
160
|
self._migration_contributions = migration_contributions
|
|
141
161
|
self._migration_declarations = migration_declarations
|
|
162
|
+
self.extension_models: tuple[SqlAlchemyExtensionModel[object], ...] = ()
|
|
163
|
+
self.extension_sources: tuple[_SqlAlchemyMigrationSource, ...] = ()
|
|
164
|
+
|
|
165
|
+
def extension_model(
|
|
166
|
+
self, target: ExtensionTarget, model_base: type[_TRow]
|
|
167
|
+
) -> SqlAlchemyExtensionModel[_TRow]:
|
|
168
|
+
for model in self.extension_models:
|
|
169
|
+
if model.target is target and issubclass(model.model, model_base):
|
|
170
|
+
return cast(SqlAlchemyExtensionModel[_TRow], model)
|
|
171
|
+
raise ValueError("extension persistence mapping is not registered")
|
|
142
172
|
|
|
143
173
|
@property
|
|
144
174
|
def contributions(self) -> tuple[SqlAlchemyModelContribution, ...]:
|
|
@@ -152,6 +182,9 @@ class SqlAlchemyModelCatalog:
|
|
|
152
182
|
def connection_names(self) -> tuple[str, ...]:
|
|
153
183
|
names = {item.registration.connection_name for item in self._contributions}
|
|
154
184
|
names.update(item.registration.connection_name for item in self._migration_contributions)
|
|
185
|
+
names.update(
|
|
186
|
+
item.extension.connection_name for item in self.extension_sources if item.extension
|
|
187
|
+
)
|
|
155
188
|
return tuple(name for name in ("default", *sorted(names - {"default"})) if name in names)
|
|
156
189
|
|
|
157
190
|
def for_connection(
|
|
@@ -196,7 +229,15 @@ class SqlAlchemyModelCatalog:
|
|
|
196
229
|
)
|
|
197
230
|
for item in self.migrations_for_connection(connection_name)
|
|
198
231
|
)
|
|
199
|
-
return (
|
|
232
|
+
return (
|
|
233
|
+
*model_sources,
|
|
234
|
+
*external_sources,
|
|
235
|
+
*(
|
|
236
|
+
item
|
|
237
|
+
for item in self.extension_sources
|
|
238
|
+
if item.extension and item.extension.connection_name == connection_name
|
|
239
|
+
),
|
|
240
|
+
)
|
|
200
241
|
|
|
201
242
|
def _assert_unchanged(self) -> None:
|
|
202
243
|
for contribution in self._contributions:
|
|
@@ -257,7 +298,70 @@ def _build_model_catalog(
|
|
|
257
298
|
builder = _SqlAlchemyModelCatalogBuilder(options)
|
|
258
299
|
for descriptor in registry.dependency_order:
|
|
259
300
|
builder.add_module(descriptor)
|
|
260
|
-
|
|
301
|
+
catalog = builder.build()
|
|
302
|
+
effective: list[SqlAlchemyModelContribution] = []
|
|
303
|
+
extension_models: list[SqlAlchemyExtensionModel[object]] = []
|
|
304
|
+
for contribution in catalog.contributions:
|
|
305
|
+
registration = contribution.registration
|
|
306
|
+
if registration.extension_mappings:
|
|
307
|
+
metadata, models = _map_extensions(
|
|
308
|
+
registration.metadata,
|
|
309
|
+
registration.extension_mappings,
|
|
310
|
+
registry.instance(ExtensionsModule).catalog,
|
|
311
|
+
)
|
|
312
|
+
extension_models.extend(models)
|
|
313
|
+
registration = replace(registration, metadata=metadata)
|
|
314
|
+
effective.append(replace(contribution, registration=registration))
|
|
315
|
+
catalog._contributions = tuple(effective)
|
|
316
|
+
catalog.extension_models = tuple(extension_models)
|
|
317
|
+
sources: list[_SqlAlchemyMigrationSource] = []
|
|
318
|
+
for owner, declaration in registry.declarations(SqlAlchemyExtensionMigrations):
|
|
319
|
+
_validate_connection_name(declaration.connection_name)
|
|
320
|
+
if not declaration.depends_on or not all(
|
|
321
|
+
isinstance(item, str) and item.strip() == item and item
|
|
322
|
+
for item in declaration.depends_on
|
|
323
|
+
):
|
|
324
|
+
raise ValueError("extension migrations require explicit base revision dependencies")
|
|
325
|
+
builder._claim_branch(declaration.branch_label, owner.key)
|
|
326
|
+
sources.append(
|
|
327
|
+
_SqlAlchemyMigrationSource(
|
|
328
|
+
owner.key,
|
|
329
|
+
declaration.migrations_package,
|
|
330
|
+
declaration.branch_label,
|
|
331
|
+
_migration_package_path(declaration.migrations_package, owner=owner.key),
|
|
332
|
+
declaration,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
catalog.extension_sources = tuple(sources)
|
|
336
|
+
_validate_extension_models(catalog, registry.instance(ExtensionsModule).catalog)
|
|
337
|
+
return catalog
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _validate_extension_models(
|
|
341
|
+
catalog: SqlAlchemyModelCatalog, extensions: ExtensionCatalog
|
|
342
|
+
) -> None:
|
|
343
|
+
if len({model.target for model in catalog.extension_models}) != len(catalog.extension_models):
|
|
344
|
+
raise ValueError("an extension target has only one ORM mapping")
|
|
345
|
+
for item in extensions.contributions:
|
|
346
|
+
if isinstance(item.declaration, SqlAlchemyModelExtension) and not any(
|
|
347
|
+
model.target is item.declaration.target for model in catalog.extension_models
|
|
348
|
+
):
|
|
349
|
+
raise ValueError("SQL extension requires exactly one registered ORM target")
|
|
350
|
+
for model in catalog.extension_models:
|
|
351
|
+
for column in model.columns:
|
|
352
|
+
source = next(
|
|
353
|
+
(item for item in catalog.extension_sources if item.owner == column.owner), None
|
|
354
|
+
)
|
|
355
|
+
if source is None:
|
|
356
|
+
raise ValueError("mapped extension columns require an owning migration declaration")
|
|
357
|
+
base = next(
|
|
358
|
+
item
|
|
359
|
+
for item in catalog.contributions
|
|
360
|
+
if column.column.table in item.registration.metadata.tables.values()
|
|
361
|
+
)
|
|
362
|
+
assert source.extension is not None
|
|
363
|
+
if source.extension.connection_name != base.registration.connection_name:
|
|
364
|
+
raise ValueError("extension migration and base table must share a connection")
|
|
261
365
|
|
|
262
366
|
|
|
263
367
|
class _SqlAlchemyModelCatalogBuilder:
|
|
@@ -28,7 +28,7 @@ from .errors import (
|
|
|
28
28
|
SqlAlchemyMigrationError,
|
|
29
29
|
SqlAlchemyModelRegistrationError,
|
|
30
30
|
)
|
|
31
|
-
from .metadata import SqlAlchemyModelCatalog,
|
|
31
|
+
from .metadata import SqlAlchemyModelCatalog, _SqlAlchemyMigrationSource
|
|
32
32
|
from .migration_options import SqlAlchemyMigrationOptions
|
|
33
33
|
from .module_migration import _ModuleMigration
|
|
34
34
|
from .options import ConnectionStringsOptions
|
|
@@ -51,7 +51,9 @@ class SqlAlchemyMigrator:
|
|
|
51
51
|
"SqlAlchemyMigrator requires composed Application, "
|
|
52
52
|
f"got {self._application.state.value}"
|
|
53
53
|
)
|
|
54
|
-
|
|
54
|
+
from .module import SqlAlchemyPersistenceModule
|
|
55
|
+
|
|
56
|
+
models = self._application.modules.instance(SqlAlchemyPersistenceModule).catalog
|
|
55
57
|
models._assert_unchanged()
|
|
56
58
|
return models
|
|
57
59
|
|
|
@@ -88,8 +90,10 @@ class SqlAlchemyMigrator:
|
|
|
88
90
|
) -> tuple[str, ...]:
|
|
89
91
|
models = self.model_catalog
|
|
90
92
|
owner = ModuleKey(module.__module__, module.__qualname__) if module is not None else None
|
|
91
|
-
if
|
|
92
|
-
|
|
93
|
+
if (
|
|
94
|
+
module_operation is _ModuleMigration.revision
|
|
95
|
+
and not any(item.owner == owner for item in models.contributions)
|
|
96
|
+
and not any(item.owner == owner for item in models.extension_sources)
|
|
93
97
|
):
|
|
94
98
|
raise SqlAlchemyModelRegistrationError(
|
|
95
99
|
owner=str(owner), reason="revision requires a Module with registered ORM models"
|
|
@@ -111,9 +115,11 @@ class SqlAlchemyMigrator:
|
|
|
111
115
|
completed.append(connection_name)
|
|
112
116
|
continue
|
|
113
117
|
config = _alembic_config(models, connection_name, self._application.options)
|
|
114
|
-
|
|
118
|
+
# 未选择后台能力就没有执行资源;不能为了迁移强制引入整个后台模块。
|
|
115
119
|
config.attributes["background_execution_disabled"] = (
|
|
116
|
-
|
|
120
|
+
BackgroundExecutionOptions not in self._application.options.values
|
|
121
|
+
or self._application.options.get(BackgroundExecutionOptions).profile
|
|
122
|
+
is BackgroundExecutionProfile.DISABLED
|
|
117
123
|
)
|
|
118
124
|
_validate_revision_graph(
|
|
119
125
|
models,
|
|
@@ -323,8 +329,11 @@ def _validate_revision_graph(
|
|
|
323
329
|
) -> None:
|
|
324
330
|
migration_sources = models._migration_sources_for_connection(connection_name)
|
|
325
331
|
# walk_revisions 强制 Alembic 在任何 engine 创建前加载并校验组合 graph/depends_on。
|
|
326
|
-
|
|
332
|
+
script = ScriptDirectory.from_config(config)
|
|
333
|
+
revisions = tuple(script.walk_revisions())
|
|
327
334
|
for source in migration_sources:
|
|
335
|
+
if source.extension is not None:
|
|
336
|
+
_validate_extension_prerequisites(models, connection_name, source, script)
|
|
328
337
|
owned_revisions = tuple(
|
|
329
338
|
revision
|
|
330
339
|
for revision in revisions
|
|
@@ -352,6 +361,50 @@ def _validate_revision_graph(
|
|
|
352
361
|
f"{actual_labels!r}, expected {(source.branch_label,)!r}"
|
|
353
362
|
),
|
|
354
363
|
)
|
|
364
|
+
if source.extension is not None and not (allow_empty and source.owner == owner):
|
|
365
|
+
# 声明约束当前分支的每个 head;历史 revision 保留当时的 depends_on。
|
|
366
|
+
# revision 命令允许在已应用新前置后,为所选 owner 追加满足新声明的 revision。
|
|
367
|
+
for head in script.get_revisions(f"{source.branch_label}@heads"):
|
|
368
|
+
ancestry = {
|
|
369
|
+
item.revision
|
|
370
|
+
for item in script.iterate_revisions(head.revision, "base", implicit_base=True)
|
|
371
|
+
}
|
|
372
|
+
if not set(source.extension.depends_on) <= ancestry:
|
|
373
|
+
raise ValueError("extension revision is missing required Alembic depends_on")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _validate_extension_prerequisites(
|
|
377
|
+
models: SqlAlchemyModelCatalog,
|
|
378
|
+
connection_name: str,
|
|
379
|
+
source: _SqlAlchemyMigrationSource,
|
|
380
|
+
script: ScriptDirectory,
|
|
381
|
+
) -> None:
|
|
382
|
+
assert source.extension is not None
|
|
383
|
+
prerequisites = tuple(script.get_revision(item) for item in source.extension.depends_on)
|
|
384
|
+
if any(
|
|
385
|
+
item is None or item.revision != requested
|
|
386
|
+
for item, requested in zip(prerequisites, source.extension.depends_on, strict=True)
|
|
387
|
+
):
|
|
388
|
+
raise ValueError("extension depends_on must name exact existing base revisions")
|
|
389
|
+
# 必须覆盖被扩展表的真实基础迁移 owner;运行时 Module 图不参与判断。
|
|
390
|
+
base_paths = {
|
|
391
|
+
contribution.migration_path
|
|
392
|
+
for contribution in models.for_connection(connection_name)
|
|
393
|
+
if any(
|
|
394
|
+
column.owner == source.owner
|
|
395
|
+
and column.column.table in contribution.registration.metadata.tables.values()
|
|
396
|
+
for model in models.extension_models
|
|
397
|
+
for column in model.columns
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
ancestor_paths = {
|
|
401
|
+
Path(item.path).resolve().parent
|
|
402
|
+
for item in script.iterate_revisions(
|
|
403
|
+
source.extension.depends_on, "base", implicit_base=True
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
if not base_paths <= ancestor_paths:
|
|
407
|
+
raise ValueError("extension depends_on does not cover its base table migrations")
|
|
355
408
|
|
|
356
409
|
|
|
357
410
|
def _upgrade_heads(connection: Connection, config: Config, connection_name: str) -> None:
|
|
@@ -369,7 +422,7 @@ def _check_current_heads(connection: Connection, config: Config, connection_name
|
|
|
369
422
|
).get_current_heads()
|
|
370
423
|
)
|
|
371
424
|
)
|
|
372
|
-
expected = tuple(sorted(script.get_heads()))
|
|
425
|
+
expected = tuple(sorted(script.get_heads(consider_depends_on=True)))
|
|
373
426
|
if current != expected:
|
|
374
427
|
raise MigrationHeadsMismatchError(
|
|
375
428
|
connection_name=connection_name,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""从正式扩展 metadata 推导 Alembic 操作的列/索引所有权。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from alembic.operations import ops
|
|
9
|
+
from sqlalchemy import Column
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _MigrationOperations:
|
|
13
|
+
def __init__(self, columns: Iterable[Column[Any]], *, extension: bool) -> None:
|
|
14
|
+
columns = tuple(columns)
|
|
15
|
+
self._columns = {
|
|
16
|
+
(column.table.schema, column.table.name, column.name)
|
|
17
|
+
for column in columns
|
|
18
|
+
if column.table is not None
|
|
19
|
+
}
|
|
20
|
+
self._indexes = {
|
|
21
|
+
(column.table.schema, column.table.name, index.name)
|
|
22
|
+
for column in columns
|
|
23
|
+
if column.table is not None
|
|
24
|
+
for index in column.table.indexes
|
|
25
|
+
if index.columns
|
|
26
|
+
and all(
|
|
27
|
+
(column.table.schema, column.table.name, item.name) in self._columns
|
|
28
|
+
for item in index.columns
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
self._extension = extension
|
|
32
|
+
|
|
33
|
+
def column(self, schema: str | None, table: str, name: str | None) -> bool:
|
|
34
|
+
selected = (schema, table, name) in self._columns
|
|
35
|
+
return selected if self._extension else not selected
|
|
36
|
+
|
|
37
|
+
def index(self, schema: str | None, table: str, name: str | None) -> bool:
|
|
38
|
+
selected = (schema, table, name) in self._indexes
|
|
39
|
+
return selected if self._extension else not selected
|
|
40
|
+
|
|
41
|
+
def restrict(self, container: ops.OpContainer) -> None:
|
|
42
|
+
retained: list[ops.MigrateOperation] = []
|
|
43
|
+
for operation in container.ops:
|
|
44
|
+
if isinstance(operation, ops.OpContainer):
|
|
45
|
+
self.restrict(operation)
|
|
46
|
+
if operation.ops:
|
|
47
|
+
retained.append(operation)
|
|
48
|
+
else:
|
|
49
|
+
selected = self._operation(operation)
|
|
50
|
+
if selected is not None:
|
|
51
|
+
retained.append(selected)
|
|
52
|
+
container.ops = retained
|
|
53
|
+
|
|
54
|
+
def _operation(self, operation: ops.MigrateOperation) -> ops.MigrateOperation | None:
|
|
55
|
+
if isinstance(operation, (ops.CreateTableOp, ops.DropTableOp)):
|
|
56
|
+
if self._extension:
|
|
57
|
+
raise ValueError("extension revisions cannot create or remove a base table")
|
|
58
|
+
return self._base_table(operation)
|
|
59
|
+
if isinstance(operation, (ops.AddColumnOp, ops.AlterColumnOp, ops.DropColumnOp)):
|
|
60
|
+
name = (
|
|
61
|
+
operation.column.name
|
|
62
|
+
if isinstance(operation, ops.AddColumnOp)
|
|
63
|
+
else operation.column_name
|
|
64
|
+
)
|
|
65
|
+
return operation if self.column(operation.schema, operation.table_name, name) else None
|
|
66
|
+
if isinstance(operation, (ops.CreateIndexOp, ops.DropIndexOp)):
|
|
67
|
+
return (
|
|
68
|
+
operation
|
|
69
|
+
if operation.table_name is not None
|
|
70
|
+
and self.index(operation.schema, operation.table_name, operation.index_name)
|
|
71
|
+
else None
|
|
72
|
+
)
|
|
73
|
+
# 表注释等 table comparator 不经过 include_object;扩展只有列和索引的权利。
|
|
74
|
+
return None if self._extension else operation
|
|
75
|
+
|
|
76
|
+
def _base_table(self, operation: ops.CreateTableOp | ops.DropTableOp) -> ops.MigrateOperation:
|
|
77
|
+
created = operation.reverse() if isinstance(operation, ops.DropTableOp) else operation
|
|
78
|
+
# 原生 CreateTableOp 包含内联列,不会逐列执行 include_object。只重写 operation,
|
|
79
|
+
# 不复制/改写作为迁移权威的 Table 或 MetaData。
|
|
80
|
+
created.columns = [
|
|
81
|
+
item
|
|
82
|
+
for item in created.columns
|
|
83
|
+
if not isinstance(item, Column)
|
|
84
|
+
or self.column(created.schema, created.table_name, item.name)
|
|
85
|
+
]
|
|
86
|
+
return created.reverse() if isinstance(operation, ops.DropTableOp) else created
|
|
@@ -4,9 +4,11 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from dishka import Provider, Scope
|
|
6
6
|
|
|
7
|
+
from ..events.module import LocalEventsModule
|
|
8
|
+
from ..extensions.module import ExtensionsModule
|
|
7
9
|
from ..lifecycle import ConfigureContext, InitializeContext
|
|
8
|
-
from ..modularity import AppModule
|
|
9
|
-
from ..
|
|
10
|
+
from ..modularity import AppModule
|
|
11
|
+
from ..services.composition import ServiceBindingContext, ServiceComposition
|
|
10
12
|
from ..unit_of_work.contracts import _UnitOfWorkFactory
|
|
11
13
|
from .metadata import SqlAlchemyModelCatalog, _build_model_catalog
|
|
12
14
|
from .migration_options import SqlAlchemyMigrationOptions
|
|
@@ -20,6 +22,9 @@ from .unit_of_work import _SqlAlchemyUnitOfWorkFactory
|
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class SqlAlchemyPersistenceModule(AppModule):
|
|
25
|
+
dependencies = (LocalEventsModule, ExtensionsModule)
|
|
26
|
+
catalog: SqlAlchemyModelCatalog
|
|
27
|
+
|
|
23
28
|
def configure(self, context: ConfigureContext) -> None:
|
|
24
29
|
context.configure(
|
|
25
30
|
ConnectionStringsOptions,
|
|
@@ -28,11 +33,7 @@ class SqlAlchemyPersistenceModule(AppModule):
|
|
|
28
33
|
)
|
|
29
34
|
context.configure(SqlAlchemyMigrationOptions, section="sqlalchemy_migrations")
|
|
30
35
|
provider = Provider()
|
|
31
|
-
|
|
32
|
-
_model_catalog,
|
|
33
|
-
provides=SqlAlchemyModelCatalog,
|
|
34
|
-
scope=Scope.APP,
|
|
35
|
-
)
|
|
36
|
+
context.services.add_composition(ServiceComposition(bind=self._bind))
|
|
36
37
|
provider.provide(
|
|
37
38
|
_engine_registry,
|
|
38
39
|
provides=_SqlAlchemyEngineRegistry,
|
|
@@ -42,10 +43,12 @@ class SqlAlchemyPersistenceModule(AppModule):
|
|
|
42
43
|
provider.provide(SqlAlchemySessionProvider, scope=Scope.APP)
|
|
43
44
|
context.services.contribute(provider, reason="SQLAlchemy PostgreSQL persistence boundary")
|
|
44
45
|
|
|
46
|
+
def _bind(self, context: ServiceBindingContext) -> None:
|
|
47
|
+
self.catalog = _build_model_catalog(context.modules, context.options)
|
|
48
|
+
provider = Provider()
|
|
49
|
+
provider.provide(lambda: self.catalog, provides=SqlAlchemyModelCatalog, scope=Scope.APP)
|
|
50
|
+
context.add_provider(provider)
|
|
51
|
+
|
|
45
52
|
async def initialize(self, context: InitializeContext) -> None:
|
|
46
53
|
engines = await context.container.get(_SqlAlchemyEngineRegistry)
|
|
47
54
|
await engines.ping_all()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _model_catalog(registry: ModuleRegistry, options: OptionsSnapshot) -> SqlAlchemyModelCatalog:
|
|
51
|
-
return _build_model_catalog(registry, options)
|