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
|
@@ -12,11 +12,12 @@ from alembic.config import Config
|
|
|
12
12
|
from alembic.migration import MigrationContext
|
|
13
13
|
from alembic.operations.ops import MigrationScript
|
|
14
14
|
from alembic.script import ScriptDirectory
|
|
15
|
-
from sqlalchemy import Connection
|
|
15
|
+
from sqlalchemy import Column, Connection, Index
|
|
16
16
|
|
|
17
17
|
from ..modularity import AppModule, ModuleKey
|
|
18
18
|
from .errors import SqlAlchemyModelRegistrationError
|
|
19
19
|
from .metadata import SqlAlchemyModelCatalog
|
|
20
|
+
from .migration_operations import _MigrationOperations
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class _ModuleMigration:
|
|
@@ -35,6 +36,31 @@ class _ModuleMigration:
|
|
|
35
36
|
for item in models.for_connection(connection_name)
|
|
36
37
|
if item.owner == self.owner
|
|
37
38
|
)
|
|
39
|
+
self.extension = self.sources[0].extension if len(self.sources) == 1 else None
|
|
40
|
+
self._columns = tuple(
|
|
41
|
+
column
|
|
42
|
+
for model in models.extension_models
|
|
43
|
+
for column in model.columns
|
|
44
|
+
if any(
|
|
45
|
+
column.column.table.metadata is item.registration.metadata
|
|
46
|
+
for item in models.for_connection(connection_name)
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
if self.extension is not None:
|
|
50
|
+
tables = {column.column.table for column in self._columns if column.owner == self.owner}
|
|
51
|
+
self.metadata = tuple(
|
|
52
|
+
item.registration.metadata
|
|
53
|
+
for item in models.for_connection(connection_name)
|
|
54
|
+
if any(table in item.registration.metadata.tables.values() for table in tables)
|
|
55
|
+
)
|
|
56
|
+
self._operations = _MigrationOperations(
|
|
57
|
+
(
|
|
58
|
+
item.column
|
|
59
|
+
for item in self._columns
|
|
60
|
+
if self.extension is None or item.owner == self.owner
|
|
61
|
+
),
|
|
62
|
+
extension=self.extension is not None,
|
|
63
|
+
)
|
|
38
64
|
self.script = ScriptDirectory.from_config(config)
|
|
39
65
|
self._version_table_options = config.attributes["version_table_options"]
|
|
40
66
|
self.revisions = tuple(self.script.walk_revisions())
|
|
@@ -111,12 +137,19 @@ class _ModuleMigration:
|
|
|
111
137
|
item.revision
|
|
112
138
|
for item in self.script.iterate_revisions(heads, "base", implicit_base=True)
|
|
113
139
|
)
|
|
140
|
+
dependencies = self.extension.depends_on if self.extension is not None else ()
|
|
141
|
+
if dependencies:
|
|
142
|
+
required |= frozenset(
|
|
143
|
+
item.revision
|
|
144
|
+
for item in self.script.iterate_revisions(dependencies, "base", implicit_base=True)
|
|
145
|
+
)
|
|
114
146
|
# Schema 依赖只来自原生 revision;Module 的运行时依赖不推断为数据库前置关系。
|
|
115
147
|
self._require(required, self._applied(connection))
|
|
116
148
|
config.attributes.update(
|
|
117
149
|
connection=connection,
|
|
118
150
|
target_metadata=self.metadata,
|
|
119
151
|
include_name=self._include_name,
|
|
152
|
+
include_object=self._include_object,
|
|
120
153
|
)
|
|
121
154
|
config.set_main_option("revision_environment", "true")
|
|
122
155
|
# command.revision 的整库 autogenerate 检查会误阻塞无关未升级分支;改用其公开 hook
|
|
@@ -131,6 +164,14 @@ class _ModuleMigration:
|
|
|
131
164
|
)
|
|
132
165
|
|
|
133
166
|
def _include_name(self, name: str | None, type_: str, parents: dict[str, Any]) -> bool:
|
|
167
|
+
if self.extension is not None:
|
|
168
|
+
tables = {column.column.table for column in self._columns if column.owner == self.owner}
|
|
169
|
+
if type_ == "schema":
|
|
170
|
+
return name in {table.schema for table in tables if table is not None}
|
|
171
|
+
if type_ == "table":
|
|
172
|
+
return parents["schema_qualified_table_name"] in {
|
|
173
|
+
table.key for table in tables if table is not None
|
|
174
|
+
}
|
|
134
175
|
if type_ == "schema":
|
|
135
176
|
return name in {
|
|
136
177
|
table.schema for meta in self.metadata for table in meta.tables.values()
|
|
@@ -141,11 +182,38 @@ class _ModuleMigration:
|
|
|
141
182
|
)
|
|
142
183
|
return True
|
|
143
184
|
|
|
185
|
+
def _include_object(
|
|
186
|
+
self,
|
|
187
|
+
obj: Any,
|
|
188
|
+
name: str | None,
|
|
189
|
+
type_: str,
|
|
190
|
+
reflected: bool,
|
|
191
|
+
compare_to: Any,
|
|
192
|
+
) -> bool:
|
|
193
|
+
if type_ == "table" and self.extension is not None:
|
|
194
|
+
if not self._include_name(name, type_, {"schema_qualified_table_name": obj.key}):
|
|
195
|
+
return False
|
|
196
|
+
if compare_to is None:
|
|
197
|
+
raise ValueError("extension revisions cannot create or remove a base table")
|
|
198
|
+
return True
|
|
199
|
+
table = getattr(obj, "table", None)
|
|
200
|
+
if isinstance(obj, Column) and table is not None:
|
|
201
|
+
return self._operations.column(table.schema, table.name, name)
|
|
202
|
+
if isinstance(obj, Index) and table is not None:
|
|
203
|
+
# 同列上反射到的其他索引仍归其原 owner,不能被本扩展自动删除。
|
|
204
|
+
return self._operations.index(table.schema, table.name, name)
|
|
205
|
+
return self.extension is None
|
|
206
|
+
|
|
144
207
|
def _generate(
|
|
145
208
|
self, context: MigrationContext, revision: Any, directives: list[MigrationScript]
|
|
146
209
|
) -> None:
|
|
147
210
|
# 一个 migration source 对应一份已登记 MetaData;不复制表形成影子 metadata。
|
|
148
|
-
|
|
211
|
+
# Alembic 1.19.1 AutogenContext.sorted_tables 原生支持 metadata 序列,入口注解滞后。
|
|
212
|
+
generated = produce_migrations(context, self.metadata) # type: ignore[arg-type]
|
|
149
213
|
assert generated.upgrade_ops is not None and generated.downgrade_ops is not None
|
|
214
|
+
self._operations.restrict(generated.upgrade_ops)
|
|
150
215
|
directives[0].upgrade_ops = generated.upgrade_ops
|
|
151
|
-
|
|
216
|
+
# 限定后的 upgrade 由 Alembic 原生 reverse 生成 downgrade,所有权范围保持对称。
|
|
217
|
+
directives[0].downgrade_ops = generated.upgrade_ops.reverse()
|
|
218
|
+
if self.extension is not None:
|
|
219
|
+
directives[0].depends_on = self.extension.depends_on
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""原生 ORM 仓储;业务筛选和领域映射由具体仓储拥有。"""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import Any, Generic, TypeVar, cast
|
|
6
7
|
|
|
8
|
+
from sqlalchemy import Select, func, inspect, select
|
|
9
|
+
from sqlalchemy.orm import LoaderCriteriaOption, Mapper
|
|
10
|
+
from sqlalchemy.orm.exc import StaleDataError
|
|
11
|
+
from sqlalchemy.orm.interfaces import ORMOption
|
|
12
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
13
|
+
|
|
14
|
+
from ..domain import OptimisticConcurrencyError
|
|
15
|
+
from ..errors import ResourceNotFoundError
|
|
16
|
+
from ..events.entities import EntityCreatedEvent, EntityDeletedEvent, EntityUpdatedEvent
|
|
7
17
|
from .session_provider import SqlAlchemySessionProvider
|
|
8
18
|
|
|
9
19
|
_TModel = TypeVar("_TModel")
|
|
@@ -11,23 +21,217 @@ _TId = TypeVar("_TId")
|
|
|
11
21
|
|
|
12
22
|
|
|
13
23
|
class SqlAlchemyRepository(Generic[_TModel, _TId]):
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
provider: SqlAlchemySessionProvider,
|
|
27
|
+
model_type: type[_TModel],
|
|
28
|
+
*,
|
|
29
|
+
detail_options: tuple[ORMOption, ...] = (),
|
|
30
|
+
) -> None:
|
|
17
31
|
self._provider = provider
|
|
18
32
|
self._model_type = model_type
|
|
33
|
+
self._detail_options = detail_options
|
|
34
|
+
|
|
35
|
+
def _query(self) -> Select[tuple[_TModel]]:
|
|
36
|
+
"""业务仓储覆写此原生查询时,list/count/page 共享同一筛选。"""
|
|
37
|
+
return select(self._model_type)
|
|
38
|
+
|
|
39
|
+
def _default_sorting(self) -> str | None:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def _entity_query(self) -> Select[tuple[_TModel]]:
|
|
43
|
+
mapper = inspect(self._model_type)
|
|
44
|
+
assert isinstance(mapper, Mapper)
|
|
45
|
+
query = self._query()
|
|
46
|
+
source = query.order_by(None).subquery()
|
|
47
|
+
predicates = []
|
|
48
|
+
for primary_key in mapper.primary_key:
|
|
49
|
+
selected = source.c.corresponding_column(primary_key)
|
|
50
|
+
if selected is None:
|
|
51
|
+
raise ValueError("_query must select the mapped entity primary key")
|
|
52
|
+
predicates.append(primary_key == selected)
|
|
53
|
+
# 关联筛选只决定实体是否入选;EXISTS 在 SQL 分页前消除关联行的倍增。
|
|
54
|
+
# 只比较真实主键(含复合键),不要求 JSON 等实体字段支持 DISTINCT。
|
|
55
|
+
# SQLAlchemy 2.0 的 ORM options 只由顶层语句解释;子查询不会自行携带它们。
|
|
56
|
+
# 原生语句没有公开的 options 读取器,仅在此边界转交已声明元数据。
|
|
57
|
+
return (
|
|
58
|
+
select(self._model_type)
|
|
59
|
+
.where(select(1).select_from(source).where(*predicates).exists())
|
|
60
|
+
.options(*query._with_options)
|
|
61
|
+
.execution_options(**query.get_execution_options())
|
|
62
|
+
)
|
|
19
63
|
|
|
20
|
-
|
|
64
|
+
def _order_by(self, sorting: str | None) -> tuple[ColumnElement[Any], ...]:
|
|
65
|
+
mapper = inspect(self._model_type)
|
|
66
|
+
assert isinstance(mapper, Mapper)
|
|
67
|
+
attributes = {item.key: item for item in mapper.column_attrs}
|
|
68
|
+
result: list[ColumnElement[Any]] = []
|
|
69
|
+
used: set[str] = set()
|
|
70
|
+
for term in (sorting or self._default_sorting() or "").split(","):
|
|
71
|
+
if not term.strip():
|
|
72
|
+
continue
|
|
73
|
+
parts = term.split()
|
|
74
|
+
if (
|
|
75
|
+
len(parts) > 2
|
|
76
|
+
or parts[0] not in attributes
|
|
77
|
+
or (len(parts) == 2 and parts[1].lower() not in ("asc", "desc"))
|
|
78
|
+
):
|
|
79
|
+
raise ValueError("sorting must use mapped column names and asc/desc")
|
|
80
|
+
name = parts[0]
|
|
81
|
+
column = attributes[name].columns[0]
|
|
82
|
+
result.append(
|
|
83
|
+
column.desc() if len(parts) == 2 and parts[1].lower() == "desc" else column.asc()
|
|
84
|
+
)
|
|
85
|
+
used.add(name)
|
|
86
|
+
# 唯一补序来自 ORM 的真实主键,不维护第二份排序字段名单。
|
|
87
|
+
for primary_key in mapper.primary_key:
|
|
88
|
+
if mapper.get_property_by_column(primary_key).key not in used:
|
|
89
|
+
result.append(primary_key.asc())
|
|
90
|
+
return tuple(result)
|
|
91
|
+
|
|
92
|
+
async def find(self, entity_id: _TId, include_details: bool = True) -> _TModel | None:
|
|
21
93
|
async with self._provider.operation("get"):
|
|
22
94
|
session = await self._provider.get_session()
|
|
23
|
-
|
|
95
|
+
if include_details and self._detail_options:
|
|
96
|
+
mapper = inspect(self._model_type)
|
|
97
|
+
assert isinstance(mapper, Mapper)
|
|
98
|
+
values = (
|
|
99
|
+
tuple(
|
|
100
|
+
entity_id[mapper.get_property_by_column(column).key]
|
|
101
|
+
for column in mapper.primary_key
|
|
102
|
+
)
|
|
103
|
+
if isinstance(entity_id, dict)
|
|
104
|
+
else entity_id
|
|
105
|
+
if isinstance(entity_id, tuple)
|
|
106
|
+
else (entity_id,)
|
|
107
|
+
)
|
|
108
|
+
if len(values) != len(mapper.primary_key):
|
|
109
|
+
raise ValueError("identity must match the mapped primary key")
|
|
110
|
+
# Session.get 的 identity-map 命中不会执行 loader options;显式 SELECT
|
|
111
|
+
# 补加载关系而不 populate_existing,避免覆盖同事务尚未 flush 的修改。
|
|
112
|
+
result = await session.scalars(
|
|
113
|
+
select(self._model_type)
|
|
114
|
+
.where(
|
|
115
|
+
*(
|
|
116
|
+
column == value
|
|
117
|
+
for column, value in zip(mapper.primary_key, values, strict=True)
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
.options(*self._detail_options)
|
|
121
|
+
)
|
|
122
|
+
return result.unique().one_or_none()
|
|
123
|
+
return cast(
|
|
124
|
+
_TModel | None,
|
|
125
|
+
await session.get(
|
|
126
|
+
self._model_type,
|
|
127
|
+
entity_id,
|
|
128
|
+
options=self._detail_options if include_details else (),
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async def get(self, entity_id: _TId, include_details: bool = True) -> _TModel:
|
|
133
|
+
entity = await self.find(entity_id, include_details)
|
|
134
|
+
if entity is None:
|
|
135
|
+
raise ResourceNotFoundError(f"{self._model_type.__name__} was not found")
|
|
136
|
+
return entity
|
|
137
|
+
|
|
138
|
+
async def get_list(self, include_details: bool = False) -> list[_TModel]:
|
|
139
|
+
async with self._provider.operation("get_list"):
|
|
140
|
+
session = await self._provider.get_session()
|
|
141
|
+
query = self._entity_query().order_by(*self._order_by(None))
|
|
142
|
+
if include_details:
|
|
143
|
+
query = query.options(*self._detail_options)
|
|
144
|
+
return list((await session.scalars(query)).unique().all())
|
|
24
145
|
|
|
25
|
-
async def
|
|
26
|
-
async with self._provider.operation("
|
|
146
|
+
async def get_count(self) -> int:
|
|
147
|
+
async with self._provider.operation("get_count"):
|
|
27
148
|
session = await self._provider.get_session()
|
|
28
|
-
|
|
149
|
+
query = self._entity_query()
|
|
150
|
+
count = await session.scalar(
|
|
151
|
+
select(func.count())
|
|
152
|
+
.select_from(query.subquery())
|
|
153
|
+
# 计数保留 ORM 筛选,但不对表达式结果应用关系加载选项。
|
|
154
|
+
.options(
|
|
155
|
+
*(
|
|
156
|
+
option
|
|
157
|
+
for option in query._with_options
|
|
158
|
+
if isinstance(option, LoaderCriteriaOption)
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
assert isinstance(count, int)
|
|
163
|
+
return count
|
|
29
164
|
|
|
30
|
-
async def
|
|
31
|
-
|
|
165
|
+
async def get_paged_list(
|
|
166
|
+
self,
|
|
167
|
+
skip_count: int,
|
|
168
|
+
max_result_count: int,
|
|
169
|
+
sorting: str | None = None,
|
|
170
|
+
include_details: bool = False,
|
|
171
|
+
) -> list[_TModel]:
|
|
172
|
+
if skip_count < 0 or max_result_count <= 0:
|
|
173
|
+
raise ValueError("skip_count must be nonnegative and max_result_count positive")
|
|
174
|
+
async with self._provider.operation("get_paged_list"):
|
|
32
175
|
session = await self._provider.get_session()
|
|
33
|
-
|
|
176
|
+
query = self._entity_query().order_by(*self._order_by(sorting))
|
|
177
|
+
if include_details:
|
|
178
|
+
query = query.options(*self._detail_options)
|
|
179
|
+
return list(
|
|
180
|
+
(await session.scalars(query.offset(skip_count).limit(max_result_count)))
|
|
181
|
+
.unique()
|
|
182
|
+
.all()
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
async def insert(self, entity: _TModel, auto_save: bool = False) -> _TModel:
|
|
186
|
+
async with self._provider.operation(
|
|
187
|
+
"insert", entity_event=EntityCreatedEvent(entity), auto_save=auto_save
|
|
188
|
+
):
|
|
189
|
+
(await self._provider.get_session()).add(entity)
|
|
190
|
+
return entity
|
|
191
|
+
|
|
192
|
+
async def update(self, entity: _TModel, auto_save: bool = False) -> _TModel:
|
|
193
|
+
async with self._provider.operation("update", write=True, auto_save=auto_save):
|
|
194
|
+
session = await self._provider.get_session()
|
|
195
|
+
# 原生 merge 保留 version_id_col 的竞争检查,不使用 bulk DML 绕过事件/审计。
|
|
196
|
+
try:
|
|
197
|
+
entity = await session.merge(entity)
|
|
198
|
+
except StaleDataError:
|
|
199
|
+
raise OptimisticConcurrencyError from None
|
|
200
|
+
if session.is_modified(entity):
|
|
201
|
+
async with self._provider.operation(
|
|
202
|
+
"update", entity_event=EntityUpdatedEvent(entity)
|
|
203
|
+
):
|
|
204
|
+
pass
|
|
205
|
+
return entity
|
|
206
|
+
|
|
207
|
+
async def delete(self, entity: _TModel, auto_save: bool = False) -> None:
|
|
208
|
+
async with self._provider.operation(
|
|
209
|
+
"delete", entity_event=EntityDeletedEvent(entity), auto_save=auto_save
|
|
210
|
+
):
|
|
211
|
+
await (await self._provider.get_session()).delete(entity)
|
|
212
|
+
|
|
213
|
+
async def delete_by_id(self, entity_id: _TId, auto_save: bool = False) -> None:
|
|
214
|
+
async with self._provider.operation("delete_by_id", write=True, auto_save=auto_save):
|
|
215
|
+
entity = await self.find(entity_id)
|
|
216
|
+
if entity is not None:
|
|
217
|
+
await self.delete(entity)
|
|
218
|
+
|
|
219
|
+
async def insert_many(self, entities: Iterable[_TModel], auto_save: bool = False) -> None:
|
|
220
|
+
async with self._provider.operation("insert_many", write=True, auto_save=auto_save):
|
|
221
|
+
for entity in entities:
|
|
222
|
+
await self.insert(entity)
|
|
223
|
+
|
|
224
|
+
async def update_many(self, entities: Iterable[_TModel], auto_save: bool = False) -> None:
|
|
225
|
+
async with self._provider.operation("update_many", write=True, auto_save=auto_save):
|
|
226
|
+
for entity in entities:
|
|
227
|
+
await self.update(entity)
|
|
228
|
+
|
|
229
|
+
async def delete_many(self, entities: Iterable[_TModel], auto_save: bool = False) -> None:
|
|
230
|
+
async with self._provider.operation("delete_many", write=True, auto_save=auto_save):
|
|
231
|
+
for entity in entities:
|
|
232
|
+
await self.delete(entity)
|
|
233
|
+
|
|
234
|
+
async def delete_many_by_ids(self, entity_ids: Iterable[_TId], auto_save: bool = False) -> None:
|
|
235
|
+
async with self._provider.operation("delete_many_by_ids", write=True, auto_save=auto_save):
|
|
236
|
+
for entity_id in entity_ids:
|
|
237
|
+
await self.delete_by_id(entity_id)
|
|
@@ -8,6 +8,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
8
8
|
|
|
9
9
|
from ..domain import AggregateRoot
|
|
10
10
|
from ..events.aggregate import _collect_aggregate_events
|
|
11
|
+
from ..events.entities import EntityChangedEvent
|
|
12
|
+
from ..events.unit_of_work import _local_event_queue
|
|
11
13
|
from ..unit_of_work.manager import UnitOfWorkManager
|
|
12
14
|
from ..unit_of_work.options import UnitOfWorkDeclaration
|
|
13
15
|
from .unit_of_work import _SqlAlchemyUnitOfWork
|
|
@@ -29,17 +31,28 @@ class SqlAlchemySessionProvider:
|
|
|
29
31
|
method_name: str,
|
|
30
32
|
*,
|
|
31
33
|
aggregate: AggregateRoot[Any] | None = None,
|
|
34
|
+
entity_event: EntityChangedEvent | None = None,
|
|
35
|
+
auto_save: bool = False,
|
|
36
|
+
write: bool = False,
|
|
32
37
|
) -> AsyncIterator[None]:
|
|
33
38
|
"""仓储暂存边界;成功后收集聚合事件,Session 仍由 get_session 懒创建。"""
|
|
34
39
|
if aggregate is not None and not isinstance(aggregate, AggregateRoot):
|
|
35
40
|
raise TypeError("aggregate must be AggregateRoot")
|
|
36
41
|
options = self._manager._resolve_options(None, UnitOfWorkDeclaration(), method_name)
|
|
37
42
|
async with self._manager._begin(options) as work:
|
|
38
|
-
|
|
43
|
+
# 自动事件的资格检查先于写入、版本暂存和 flush,已保存批次不受影响。
|
|
44
|
+
owner = self._manager.require_current(
|
|
45
|
+
transactional=write or aggregate is not None or entity_event is not None
|
|
46
|
+
)
|
|
39
47
|
yield
|
|
40
48
|
if aggregate is not None:
|
|
41
49
|
# 参与者不拥有提交权;成功暂存后必须仍在进入时的同一有效事务中。
|
|
42
50
|
if self._manager.require_current(transactional=True) is not owner:
|
|
43
51
|
raise RuntimeError("aggregate operation changed its UnitOfWork owner")
|
|
44
52
|
_collect_aggregate_events(owner, aggregate)
|
|
53
|
+
if entity_event is not None:
|
|
54
|
+
queue = _local_event_queue(owner)
|
|
55
|
+
queue.enqueue(entity_event)
|
|
56
|
+
if auto_save:
|
|
57
|
+
await owner.flush()
|
|
45
58
|
await work.complete()
|
|
@@ -9,6 +9,7 @@ from typing import Concatenate, ParamSpec, TypeVar
|
|
|
9
9
|
from ..application import Application, ApplicationBuilder
|
|
10
10
|
from ..application_services import ApplicationServiceContract
|
|
11
11
|
from ..authorization import CurrentUser
|
|
12
|
+
from ..invocation import entrypoints
|
|
12
13
|
|
|
13
14
|
_P = ParamSpec("_P")
|
|
14
15
|
_R = TypeVar("_R")
|
|
@@ -80,7 +81,7 @@ class TestApplication:
|
|
|
80
81
|
*args: object,
|
|
81
82
|
**kwargs: object,
|
|
82
83
|
) -> _R:
|
|
83
|
-
return await self.application
|
|
84
|
+
return await entrypoints.call(self.application, function, *args, **kwargs)
|
|
84
85
|
|
|
85
86
|
async def call_as(
|
|
86
87
|
self,
|
|
@@ -90,7 +91,7 @@ class TestApplication:
|
|
|
90
91
|
*args: object,
|
|
91
92
|
**kwargs: object,
|
|
92
93
|
) -> _R:
|
|
93
|
-
return await self.application
|
|
94
|
+
return await entrypoints.call_as(self.application, current_user, function, *args, **kwargs)
|
|
94
95
|
|
|
95
96
|
async def invoke(
|
|
96
97
|
self,
|
|
@@ -99,7 +100,7 @@ class TestApplication:
|
|
|
99
100
|
*args: _P.args,
|
|
100
101
|
**kwargs: _P.kwargs,
|
|
101
102
|
) -> _R:
|
|
102
|
-
return await self.application
|
|
103
|
+
return await entrypoints.invoke(self.application, method, *args, **kwargs)
|
|
103
104
|
|
|
104
105
|
async def invoke_as(
|
|
105
106
|
self,
|
|
@@ -109,4 +110,4 @@ class TestApplication:
|
|
|
109
110
|
*args: _P.args,
|
|
110
111
|
**kwargs: _P.kwargs,
|
|
111
112
|
) -> _R:
|
|
112
|
-
return await self.application
|
|
113
|
+
return await entrypoints.invoke_as(self.application, current_user, method, *args, **kwargs)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from .contracts import UnitOfWork, UnitOfWorkState
|
|
2
|
-
from .errors import UnitOfWorkStateError
|
|
2
|
+
from .errors import UnitOfWorkCompletionError, UnitOfWorkStateError
|
|
3
3
|
from .manager import UnitOfWorkManager
|
|
4
4
|
from .options import UnitOfWorkOptions, UnitOfWorkTransactionBehavior, unit_of_work
|
|
5
5
|
|
|
6
6
|
__all__ = (
|
|
7
7
|
"UnitOfWork",
|
|
8
|
+
"UnitOfWorkCompletionError",
|
|
8
9
|
"UnitOfWorkManager",
|
|
9
10
|
"UnitOfWorkOptions",
|
|
10
11
|
"UnitOfWorkState",
|
|
@@ -5,14 +5,11 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
from collections.abc import Awaitable, Callable
|
|
7
7
|
from enum import Enum
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
9
8
|
|
|
10
9
|
from .errors import UnitOfWorkStateError
|
|
10
|
+
from .lifecycle import UnitOfWorkLifecycle
|
|
11
11
|
from .options import _ResolvedUnitOfWorkOptions
|
|
12
12
|
|
|
13
|
-
if TYPE_CHECKING:
|
|
14
|
-
from ..events.runtime import _LocalEventQueue
|
|
15
|
-
|
|
16
13
|
|
|
17
14
|
class UnitOfWorkState(str, Enum):
|
|
18
15
|
ACTIVE = "active"
|
|
@@ -34,7 +31,7 @@ class UnitOfWork:
|
|
|
34
31
|
self._saved_batches = 0
|
|
35
32
|
self._cleanup_failures: tuple[BaseException, ...] = ()
|
|
36
33
|
self._options: _ResolvedUnitOfWorkOptions | None = None
|
|
37
|
-
self.
|
|
34
|
+
self._lifecycles: tuple[UnitOfWorkLifecycle, ...] = ()
|
|
38
35
|
self._after_commit: Callable[[], Awaitable[None]] | None = None
|
|
39
36
|
|
|
40
37
|
@property
|
|
@@ -101,8 +98,10 @@ class UnitOfWork:
|
|
|
101
98
|
self._require_active("complete")
|
|
102
99
|
self._completing = True
|
|
103
100
|
try:
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
# 能力在提交前完成自己的工作;任何失败都必须阻止后续 flush/commit。
|
|
102
|
+
# UoW 只编排生命周期,不认识事件队列或其他能力的运行状态。
|
|
103
|
+
for lifecycle in self._lifecycles:
|
|
104
|
+
await lifecycle.before_commit()
|
|
106
105
|
await self.flush()
|
|
107
106
|
await self._save_batch()
|
|
108
107
|
self._committed = True
|
|
@@ -25,3 +25,14 @@ class UnitOfWorkStateError(FrameworkError):
|
|
|
25
25
|
super().__init__(
|
|
26
26
|
f"UnitOfWork.{operation} requires state in ({allowed_values}), got {current.value}"
|
|
27
27
|
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UnitOfWorkCompletionError(FrameworkError):
|
|
31
|
+
"""提交后参与者失败;数据库提交事实不随缓存/通知失败撤销。"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, failures: tuple[Exception, ...]) -> None:
|
|
34
|
+
self.committed = True
|
|
35
|
+
self.failures = failures
|
|
36
|
+
super().__init__(
|
|
37
|
+
f"UnitOfWork committed, but {len(failures)} completion participant(s) failed"
|
|
38
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""事务完成的能力扩展点;不依赖事件、缓存或具体持久化实现。"""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from dishka import AsyncContainer
|
|
6
|
+
|
|
7
|
+
from ..invocation import ApplicationInvocationContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UnitOfWorkLifecycle(Protocol):
|
|
11
|
+
async def before_commit(self) -> None: ...
|
|
12
|
+
|
|
13
|
+
async def after_commit(self) -> None: ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UnitOfWorkLifecycleFactory(Protocol):
|
|
17
|
+
def create(
|
|
18
|
+
self, container: AsyncContainer, context: ApplicationInvocationContext
|
|
19
|
+
) -> UnitOfWorkLifecycle: ...
|