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
|
@@ -9,14 +9,45 @@ Use this guide with the project's [architecture](architecture.md) and [working r
|
|
|
9
9
|
From the application root:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
pddd add module orders --dry-run
|
|
13
|
+
pddd add module orders --template ddd
|
|
14
|
+
pddd add module conversions --template basic
|
|
14
15
|
```
|
|
15
16
|
|
|
16
|
-
The CLI generates `src/modules/orders/`, registers its application/persistence/HTTP modules in the Host, updates standard package metadata, and synchronizes dependencies. It also creates the module's README. Existing module directories are not overwritten. Repeat with another valid lowercase name when a separate business owner is needed.
|
|
17
|
+
The CLI generates `backend/src/modules/orders/`, registers its application/persistence/HTTP modules in the Host, updates standard package metadata, and synchronizes dependencies. It also creates the module's README. Existing module directories are not overwritten. Repeat with another valid lowercase name when a separate business owner is needed.
|
|
18
|
+
|
|
19
|
+
The default is `ddd`. `basic` instead wires one ordinary Module and its alias, with a synchronous `ConversionService` Protocol and `DefaultConversionService(ConversionService, TransientDependency)`. Each REQUEST resolution creates an instance. Its only files are package markers, `module.py`, the contract, implementation, and README; no providers, Domain, database, migration, seed, HTTP, or background resources are generated. A consuming Module declares a dependency and injects the interface. The basic README shows formal replacement through `post_configure`. Invalid template names are rejected before writes; both templates support dry-run and refuse overwrite.
|
|
17
20
|
|
|
18
21
|
Read the generated module README before replacing the order example with your domain. The examples below assume an `orders` module; substitute your actual package and contract names. The application remains usable as a pure Host before adding a module.
|
|
19
22
|
|
|
23
|
+
## Module layout and coding rules
|
|
24
|
+
|
|
25
|
+
For `basic`, keep public interfaces in `contracts/` and implementations in `services/`, with the Module in `module.py`. All seven lifecycle hooks remain explicit; empty hooks use `pass`. Its synchronous conversion is unmarked, without automatic interception or HTTP. For `ddd`, read the module README and choose the owning layer and directory below, and inspect direct callers. Propose a standards change before adding a responsibility category or reversing a dependency.
|
|
26
|
+
|
|
27
|
+
| Layer | Required responsibility directories |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `domain_shared` | `constants`, `enums`, `errors`, `permissions`, `messages`, `value_objects` |
|
|
30
|
+
| `domain` | `entities`, `value_objects`, `services`, `repositories`, `events`, `settings`, `seeding` |
|
|
31
|
+
| `application_contracts` | `services`, `integration_services`, `inputs`, `views` |
|
|
32
|
+
| `application` | `services`, `integration_services`, `background_jobs`, `background_workers`, `hosted_services`, `event_handlers`, `interceptors`, `caching`, `options`, `setting_handlers` |
|
|
33
|
+
| `sqlalchemy` | `models`, `repositories`, `migrations` |
|
|
34
|
+
| `http_api` | `routers`, `filters`, `websockets`, `models` |
|
|
35
|
+
|
|
36
|
+
- Use specific `snake_case` file names such as `order_query_service.py` or `order_changed_handler.py`. Do not combine unrelated responsibilities into `orders.py` or `tasks.py`.
|
|
37
|
+
- Give each independent service, handler, task, and lifecycle owner its own file. Group parts of one capability, such as `background_jobs/order_approval/payload.py` and `handler.py`, in a capability directory. Closely related small immutable values, enums, and exceptions may share a file; line count and class count are not splitting rules.
|
|
38
|
+
- `module.py` declares dependencies, registrations, and lifecycle hooks. `__init__.py` marks a package; it owns no business code, metadata state, or compatibility re-exports. The ORM metadata owner lives in `sqlalchemy/models/base.py`.
|
|
39
|
+
- User query, management, approval, and item operations belong in separate `services`. Internal reporting contracts and implementations go in their respective `integration_services`. Thread/SDK lifecycle owners go in `hosted_services`. Inputs and output projections go in `inputs` and `views`; transport-only models go in `http_api/models`.
|
|
40
|
+
- Shared immutable values such as `Money` belong in `domain_shared/value_objects`; domain-only values such as `OrderTitle` belong in `domain/value_objects`. Both public DTOs and domain objects may reuse a shared value without Contracts depending on Domain. Keep a single definition owner. The generated Money example does not add monetary fields to the sample HTTP contract.
|
|
41
|
+
- Preserve framework typing and native extension points. Use classes for identity, state, lifecycle, or real variation; keep stateless algorithms private to their capability. Avoid `common`, `utils`, `helpers`, aggregate facades, speculative interfaces, and old-path aliases. Explain key invariants and failure paths with concise Chinese source comments.
|
|
42
|
+
|
|
43
|
+
The `ddd` template supplies directories and executable examples for supported standard capabilities even when the business does not use them yet. Migrations remain generated from real models. This exception is limited to CLI templates and generated projects; it does not authorize speculative framework layers.
|
|
44
|
+
|
|
45
|
+
All Host and six-layer Modules explicitly define seven hooks with framework context types. `pre_configure` contributes PreOptions, `configure` registers services and Options, and `post_configure` completes Options or overrides existing registrations. The async `pre_initialize`, `initialize`, and `post_initialize` hooks prepare, initialize, and finish resources. Async `shutdown` waits for cleanup. Empty phases use `pass`; configuration phases do not start I/O, threads, or tasks. Do not manually start framework-managed background components in these hooks.
|
|
46
|
+
|
|
47
|
+
Worker classes default to `enabled=False`; enabling one requires an explicit code decision. To opt into the hosted example, import `OrderIntegrationService` from `application/hosted_services/order_integration_service.py` in `application/module.py` and declare `hosted_services = HostedServices((OrderIntegrationService,))`. To enable the scheduled example, import `STATISTICS_SCHEDULE` from `application/background_jobs/order_statistics/schedule.py` and declare `background_job_schedules = BackgroundSchedules((STATISTICS_SCHEDULE,))`. Choose the background execution profile/modes in Host configuration. A discovered Job handler alone does not enable periodic enqueue.
|
|
48
|
+
|
|
49
|
+
When splitting services, update Python contracts/imports directly and retain existing HTTP paths, verbs, status codes, response shapes, permissions, and operation IDs using `ApplicationServiceRouteOverride`. Preserve commit-before-event handling, cache removal before notification, transactional enqueue, idempotent Job handling, and awaited resource cleanup.
|
|
50
|
+
|
|
20
51
|
## Services and permissions
|
|
21
52
|
|
|
22
53
|
1. Define shared business values and permission names in `domain_shared/`.
|
|
@@ -33,9 +64,11 @@ from uuid import uuid4
|
|
|
33
64
|
from python_ddd_framework.authorization import authorize
|
|
34
65
|
from python_ddd_framework.fastapi import http
|
|
35
66
|
|
|
36
|
-
from
|
|
37
|
-
from
|
|
38
|
-
from
|
|
67
|
+
from ...application_contracts.inputs.create_order import CreateOrder
|
|
68
|
+
from ...application_contracts.views.order_view import OrderView
|
|
69
|
+
from ...domain.entities.order import Order
|
|
70
|
+
from ...domain.value_objects.order_title import OrderTitle
|
|
71
|
+
from ...domain_shared.permissions.order_permissions import ORDERS_WRITE
|
|
39
72
|
|
|
40
73
|
# Method inside the generated ApplicationService implementation:
|
|
41
74
|
@authorize(ORDERS_WRITE)
|
|
@@ -48,10 +81,34 @@ async def create(self, command: CreateOrder) -> OrderView:
|
|
|
48
81
|
|
|
49
82
|
Reuse the existing permission definition provider when adding a permission. A new permission definition does not replace the application's grant policy. Do not grant end-user permissions to background components to repair an incorrectly chosen service boundary.
|
|
50
83
|
|
|
51
|
-
In DI-managed code, inject the public contract and call `await service.method(...)`. Outside DI, use `await
|
|
84
|
+
In DI-managed code, inject the public contract and call `await service.method(...)`. Outside DI, use `await invoke(application, ServiceContract.method, ...)`; it is anonymous by default. Trusted Host/test callers can supply a validated identity through `invoke_as`. Import `invoke`, `invoke_as`, `call`, and `call_as` from `python_ddd_framework`. Use `call(application, function, ...)` for ordinary async callables that need DI. Do not resolve or instantiate the private service implementation yourself.
|
|
52
85
|
|
|
53
86
|
For ordinary services, use the module's existing registration methods or native Dishka providers. Repository, application-service, and hosted-service scopes are fixed by the framework. Do not add a wrapper or custom container around those roles.
|
|
54
87
|
|
|
88
|
+
Ordinary DI services can use `@unit_of_work`, `@authorize`, or inherit `ValidationEnabled` without becoming ApplicationServices. The owning Module must depend directly or transitively on UnitOfWorkModule, AuthorizationModule, or InvocationModule respectively. Preserve native scope/cache registration. Marked methods must be async instance methods; a class-wide declaration cannot cover synchronous, static, class, or async-generator methods. Unmarked synchronous members keep their original signatures and results. Call injected services directly; use `call` / `call_as` to inject them for an external caller. Manual construction and self-calls do not re-enter interception.
|
|
89
|
+
|
|
90
|
+
The generated Domain `OrderApprovalService` explicitly enables validation and remains transient. Its caller still owns saving and transaction completion. Interceptor match functions receive the shared `ServiceMethod`; plain services and ApplicationServices share the same invocation stages, while ApplicationServices retain their REQUEST-to-ACTION proxy and explicit HTTP exposure rules.
|
|
91
|
+
|
|
92
|
+
## Extend reusable modules
|
|
93
|
+
|
|
94
|
+
Use the existing `post_configure` override registration with a reason and a direct dependency on the target owner. Identity offers `DefaultAuthenticationApplicationService` and `DefaultIdentityManagementApplicationService` for specialization. Keep the public contract signature and policies on the final method; calling `super` does not re-enter interception.
|
|
95
|
+
|
|
96
|
+
For Identity fields, define an `ExtensionProperties` Pydantic class in `application_contracts/inputs` or `views`, then contribute `ModelExtensions((ModelExtension(IDENTITY_USER_EXTENSION, YourProperties),))` from a Module depending on `IdentityModule` and `ExtensionsModule`. Defaults, validators, and types belong to that class. HTTP input/output remains `extra_properties: {...}`; undeclared fields are rejected. Read typed values with `view.extra_properties.read(YourProperties)`. Nested DTOs and native generic containers share the effective schema. Property types, including aliases, must be closed; nested models and dataclasses must reject unknown fields.
|
|
97
|
+
|
|
98
|
+
Optional independent columns belong in the consumer persistence Module: contribute `SqlAlchemyModelExtension` instead of `ModelExtension`, and pass `ExtensionColumn(YourProperties.model_fields["field_name"], lambda: Column(...))` for each mapped field. Import these SQL types from `python_ddd_framework.sqlalchemy`; do not import a provider into DTO code. Add `SqlAlchemyExtensionMigrations` with your local migrations package, branch, and exact base revision dependencies. Register the Module's CLI alias in `backend/pyproject.toml`. Upgrade the base owner first, generate and review your extension revision, then upgrade that alias. Identity's JSON container is introduced by `identity_0003`. Required properties on existing data need explicit backfill migrations.
|
|
99
|
+
|
|
100
|
+
## Identity management
|
|
101
|
+
|
|
102
|
+
Inject `IdentityManagementApplicationService` from `python_ddd_framework.identity`. User and role queries take `PagedResultRequestDto` and return `PagedResult` with `items` and `total_count`. The standard HTTP lists are `GET /api/identity/users` and `GET /api/identity/roles`, with `skip_count` and `max_result_count` query parameters. The configured API prefix applies normally; use OpenAPI for the current routes and DTOs.
|
|
103
|
+
|
|
104
|
+
Read the owner's `concurrency_version` and include it in edits, activation, deletion, password commands, and whole-set relationship changes. Relationship updates return the resulting version. A 409 requires rereading and resolving the conflicting edit. Do not substitute a newer version automatically. `permission_version` is internal authorization cache state. User edits preserve extension values by sending `extra_properties`; creation, editing, and paged output use the same extension declaration.
|
|
105
|
+
|
|
106
|
+
`PUT /api/auth/password` requires a real signed-in user, `old_password`, `new_password`, and the version returned by `/api/auth/me`. Administrators use `PUT /api/identity/users/{user_id}/password` with `new_password` and the user's version. Deactivation uses `/active`; user roles and role permissions use `/roles` and `/permissions` for GET/PUT. DELETE takes a JSON body containing `concurrency_version`. Password changes, deactivation, and deletion invalidate the user's existing sessions after commit. Sign in again after changing a password.
|
|
107
|
+
|
|
108
|
+
Apply `pddd db upgrade --module identity` before running code that needs `identity_0004` (user/role edit versions). Do not change published revisions or author migrations in the framework installation. This does not transfer ownership of your extension columns or indexes to Identity.
|
|
109
|
+
|
|
110
|
+
The Host can set `authorization.always_allow: true` to bypass login/permission authorization on managed service calls, including nested calls. It does not create a user identity or accept an invalid token. Input validation, transactions, password checks, and authenticated realtime connections remain active. Keep the permission declarations; setting the option back to false restores authorization. `IdentityOptions` owns access/refresh lifetimes and lockout controls in the `identity` section; duration values accept Pydantic duration strings such as `PT15M`.
|
|
111
|
+
|
|
55
112
|
## Options and settings
|
|
56
113
|
|
|
57
114
|
Startup configuration belongs in a `BaseOptions` type and is bound by the owning module:
|
|
@@ -73,7 +130,7 @@ class ApprovalPolicy:
|
|
|
73
130
|
return self._options.value.allow_background_approval
|
|
74
131
|
```
|
|
75
132
|
|
|
76
|
-
This demonstrates the existing Options declaration and injection pattern; extend the generated types rather than defining a second `OrdersOptions`. Register any new ordinary service in the owning module. Configure the generated option in `app.development.yaml`:
|
|
133
|
+
This demonstrates the existing Options declaration and injection pattern; extend the generated types rather than defining a second `OrdersOptions`. Register any new ordinary service in the owning module. Configure the generated option in `backend/app.development.yaml`:
|
|
77
134
|
|
|
78
135
|
```yaml
|
|
79
136
|
orders:
|
|
@@ -82,18 +139,20 @@ orders:
|
|
|
82
139
|
|
|
83
140
|
The corresponding environment key is `PDDD_ORDERS__ALLOW_BACKGROUND_APPROVAL`. `PDDD_HTTP__API_PREFIX` changes the business API prefix. `{{ cookiecutter.host_environment_variable }}` selects the Host environment and is separate from business configuration. Values that affect composition require the framework's pre-configuration phase; do not read unbound Options during construction.
|
|
84
141
|
|
|
85
|
-
Use runtime settings when a value must be changed while the application is running. Add definitions through a scanned `SettingDefinitionProvider`, read them through `SettingProvider`, and use `SettingManager` or the management API for conditional updates/reset with the queried version token. Application services can use `self.setting_provider` during managed invocation, not during construction. The sample's approval setting is defined in `domain/settings.py`.
|
|
142
|
+
Use runtime settings when a value must be changed while the application is running. Add definitions through a scanned `SettingDefinitionProvider`, read them through `SettingProvider`, and use `SettingManager` or the management API for conditional updates/reset with the queried version token. Application services can use `self.setting_provider` during managed invocation, not during construction. The sample's approval setting is defined in `domain/settings/approval_settings.py`.
|
|
86
143
|
|
|
87
144
|
Inspect configuration sources and service registrations with:
|
|
88
145
|
|
|
89
146
|
```sh
|
|
90
|
-
|
|
147
|
+
pddd inspect --environment development
|
|
91
148
|
```
|
|
92
149
|
|
|
93
150
|
This composes and closes a new Application without starting it. Output contains redacted configuration inputs, not running-process state or the final Options values after defaults and module contributions. Mark secrets through the framework's existing secret types/metadata; do not log configuration objects or credentials.
|
|
94
151
|
|
|
95
152
|
## Persistence and migrations
|
|
96
153
|
|
|
154
|
+
Use `find` for optional entities and `get` for required entities. Standard list/count/page operations share one native SQLAlchemy query and stable primary-key tie breaking; pass explicit loader options for details. `auto_save=True` flushes, while the owning UoW controls commit. Custom `save` methods keep explicit aggregate/row mapping and pass the appropriate entity event to `operation`.
|
|
155
|
+
|
|
97
156
|
The generated repository interface inherits `RepositoryContract, Protocol`. Its SQLAlchemy implementation nominally implements that contract under `sqlalchemy/repositories/`, which the persistence module scans. Keep Sessions and ORM types inside this layer.
|
|
98
157
|
|
|
99
158
|
Map aggregates and rows explicitly within `SqlAlchemySessionProvider.operation`. Pass `aggregate=order` when staging an aggregate write, keep the optimistic version checks, and let that boundary collect the aggregate's events. Avoid collecting the same events again in the service.
|
|
@@ -103,18 +162,18 @@ Place model files in `sqlalchemy/models/` and inherit the package's Base. `SqlAl
|
|
|
103
162
|
After the initial provider setup in the [README](../README.md#initialize-the-database):
|
|
104
163
|
|
|
105
164
|
```sh
|
|
106
|
-
|
|
165
|
+
pddd db revision --module orders
|
|
107
166
|
```
|
|
108
167
|
|
|
109
168
|
Review the revision, especially renames, data backfills, external revision dependencies, and destructive operations. Autogeneration does not decide whole-table deletion or another owner's migration for you. Then run:
|
|
110
169
|
|
|
111
170
|
```sh
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
171
|
+
pddd db upgrade --module orders
|
|
172
|
+
pddd db status --module orders
|
|
173
|
+
pddd db seed --module orders
|
|
115
174
|
```
|
|
116
175
|
|
|
117
|
-
Upgrade prerequisites explicitly. Do not modify already applied revisions or write migrations into the installed framework package. Seed contributors run in independent transactions and should be idempotent; seeding a business module does not seed identity on its behalf.
|
|
176
|
+
Upgrade prerequisites explicitly. Do not modify already applied revisions or write migrations into the installed framework package. Domain Modules with `DataSeedContributor` depend on `DataSeedingModule`; contributors are ordinary DI classes and do not use `@application_service`. Seed contributors run in independent transactions and should be idempotent; seeding a business module does not seed identity on its behalf.
|
|
118
177
|
|
|
119
178
|
Managed application-service calls provide the normal unit-of-work boundary. Open an explicit short unit of work only when the operation requires it, inside a managed invocation, and complete it explicitly. Never hold it across a device call, long-running task, external network wait, or streamed response. A child task or thread cannot reuse the parent's Session.
|
|
120
179
|
|
|
@@ -124,6 +183,8 @@ For shared databases, configure installation-specific Alembic version table loca
|
|
|
124
183
|
|
|
125
184
|
Raise domain events on the aggregate. Put typed handlers in a scanned package and select the required `LocalEventPhase` with `@local_event_handler`. Use DOMAIN for transactional rules and AFTER_COMMIT for work such as the sample's cache invalidation and online notifications. Post-commit failures cannot undo a committed order; local events are not a durable message bus.
|
|
126
185
|
|
|
186
|
+
Inject `DistributedCache[OrderCacheItem]` and call `get`, `set`, `remove`, or their batch variants with keys directly. Cache item classes in `application/caching/` own stable `@cache_name` metadata. Omitted entry options use global defaults (20-minute sliding expiration); an explicit `DistributedCacheEntryOptions` replaces the entire object. `get_or_add` uses an async factory; cache access error hiding does not hide factory/type errors or cancellation. Development configuration explicitly disables `caching.hide_errors`. Use `consider_uow=True` when a cache mutation must wait for successful UoW completion; handle post-commit failure as committed database work.
|
|
187
|
+
|
|
127
188
|
Jobs inherit `BackgroundJobHandler[Payload]`, declare their durable name/version/current/timeout on the class, and are discovered by module scanning. The generated `ApprovalJob` and `ApprovalPayload` show an idempotent business handler. Enqueue by type from a managed invocation, using the same configured database connection for transactional enqueue:
|
|
128
189
|
|
|
129
190
|
```python
|
|
@@ -131,37 +192,87 @@ from uuid import UUID
|
|
|
131
192
|
|
|
132
193
|
from python_ddd_framework.background_jobs import BackgroundJobEnqueuer
|
|
133
194
|
|
|
134
|
-
from modules.orders.application.
|
|
195
|
+
from modules.orders.application.background_jobs.order_approval.handler import ApprovalJob
|
|
196
|
+
from modules.orders.application.background_jobs.order_approval.payload import ApprovalPayload
|
|
135
197
|
|
|
136
198
|
async def queue_approval(jobs: BackgroundJobEnqueuer, order_id: UUID) -> str:
|
|
137
199
|
return await jobs.enqueue(ApprovalJob, ApprovalPayload(order_id=order_id))
|
|
138
200
|
```
|
|
139
201
|
|
|
140
|
-
Keep persisted payload versions until their queued jobs are drained or explicitly migrated. A job's external side effects must tolerate repeat execution.
|
|
202
|
+
Keep persisted payload versions until their queued jobs are drained or explicitly migrated. A job's external side effects must tolerate repeat execution. A schedule takes a Job type (or a registered function's definition), a typed payload and cron: `BackgroundJobSchedule(name="orders.statistics", cron="0 * * * *", job=OrderStatisticsJob, payload=StatisticsPayload())`. The generated statistics schedule shows this declaration. Register it explicitly with `BackgroundSchedules`; task identity and serialization come from the Application's Job catalog, while PgQueuer owns scheduling.
|
|
141
203
|
|
|
142
|
-
Workers implement `BackgroundWorker.run_iteration` and are registered
|
|
204
|
+
Workers implement `BackgroundWorker.run_iteration` and are registered with `background_workers = BackgroundWorkers((WorkerType,))`; their period, timeout, and enabled state live on the class. Both generated workers are disabled. Change a worker's code declaration deliberately before expecting it to run; configuration and management endpoints cannot enable a code-disabled worker.
|
|
143
205
|
|
|
144
206
|
Background execution Options use catalog names to select `host`, `shared`, or `exclusive` placement. Only worker configuration accepts `enabled`; job configuration does not. Management start/stop is asynchronous: query status after a request. Stopping a job processor preserves its queued data.
|
|
145
207
|
|
|
146
|
-
Use the existing `DistributedLock` provider for explicit business leases. If acquisition returns false, skip or report the operation as appropriate. Preserve cancellation and cleanup on lease loss. Use database concurrency and idempotency for their separate guarantees.
|
|
208
|
+
Use the existing `DistributedLock` provider for explicit business leases. `locks.acquire(key, wait_timeout=timedelta(0))` tries without waiting; omit the argument or pass `None` for configured waiting, or pass a positive timedelta for this acquisition only. If acquisition returns false, skip or report the operation as appropriate. Preserve cancellation and cleanup on lease loss. Use database concurrency and idempotency for their separate guarantees.
|
|
147
209
|
|
|
148
|
-
A `HostedService` manages a long-lived SDK or thread through async `start`/`stop`, including actual thread termination. Register it
|
|
210
|
+
A `HostedService` manages a long-lived SDK or thread through async `start`/`stop`, including actual thread termination. Register it with `hosted_services = HostedServices((ServiceType,))`; the generated integration example is not registered by default. External threads submit through the hosted-service context, and the component owns runtime failures and recovery.
|
|
149
211
|
|
|
150
212
|
## HTTP, files, and real-time communication
|
|
151
213
|
|
|
152
|
-
The Host passes `http.api_prefix` to `FastApiAdapter`, defaulting to `/api`. The generated HTTP module exposes its application contracts;
|
|
214
|
+
The Host passes `http.api_prefix` to `FastApiAdapter`, defaulting to `/api`. The generated HTTP module exposes its application contracts; `exposes_application_services` also contributes their formal Module dependency. Conventional methods are exposed automatically, `remote_service(is_enabled=False)` excludes methods, and overrides retain deliberate route identities. A plain Pydantic GET query DTO binds through native FastAPI Query metadata; explicit Query/Body/Header/Depends takes precedence. Declare Body explicitly for a GET body, and use native explicit input design for complex shapes. Check `/docs` after route changes. Keep method paths relative to their service; module overrides do not repeat the global prefix.
|
|
153
215
|
|
|
154
216
|
For explicit business routes, use `HttpRouter(use_api_prefix=True, ...)` and inject public service contracts. Independent endpoints such as WebSocket and health routes retain separate paths. Native FastAPI parameter and response types remain supported.
|
|
155
217
|
|
|
156
218
|
Use `UploadFile`, `File`, and `Form` for uploads and native response types for downloads/streams. Keep byte limits enforced while reading. Release database work before sending a stream, pass detached data to the producer, and protect asynchronous cleanup in its `finally` block; never carry the previous business Session across yields.
|
|
157
219
|
|
|
158
|
-
The example WebSocket accepts authenticated clients, sends an initial snapshot, and handles `{}` as a refresh request. Invocations recheck identity and permissions. Non-loopback deployments require WSS; query-token authentication requires an explicit Origin policy. Sample notifications stay within the Host process and do not promise replay or delivery acknowledgment.
|
|
220
|
+
The example WebSocket accepts authenticated clients, sends an initial snapshot, and handles `{}` as a refresh request. `OrderChangedMessage` and `OrderSnapshotMessage` in `domain_shared/messages/order_messages.py` own versioned `message_type` values and Pydantic payload schemas. Send an instance through `publisher.send_to_user(user_id, message)` or `connection.send(message)`; explicitly map domain events and query results to that schema. The envelope retains its ID, UTC time and correlation, and reports only local queue acceptance. Invocations recheck identity and permissions. Non-loopback deployments require WSS; query-token authentication requires an explicit Origin policy. Sample notifications stay within the Host process and do not promise replay or delivery acknowledgment.
|
|
221
|
+
|
|
222
|
+
`SettingManager.update(definition, value, expected_version=version)` pairs `SettingDefinition[T]` with `value: T`; read the version before update/reset. Reset follows current defaults while retaining a new version token, and saving remains separate from component refresh. A business error may carry `BusinessError(DEFINITION, data=PublicData(...))`, where PublicData is a Pydantic model of intentionally public fields. HTTP omits data when absent; do not put raw exception text, SQL or secrets in the public DTO.
|
|
159
223
|
|
|
160
224
|
Use `logging.getLogger(__name__)` for application logs. Configure logging in the Host's YAML, keep credentials and payloads out of messages, and enable existing tracing export only when its destination is configured. Avoid process-global logging/tracing setup in a business module.
|
|
161
225
|
|
|
226
|
+
## In-process messages
|
|
227
|
+
|
|
228
|
+
Generated modules include `domain_shared/messages/order_observation.py` and
|
|
229
|
+
`application/hosted_services/order_observation_handler.py` as an optional external integration
|
|
230
|
+
example. Existing callbacks remain direct calls. In that module's `application/module.py`,
|
|
231
|
+
import the two types and `observation_identity`, add `MessagingModule` to `dependencies`, then
|
|
232
|
+
add this declaration and binding:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from python_ddd_framework import MessageChannelDefinition, MessagingModule
|
|
236
|
+
|
|
237
|
+
# On the Application Module class:
|
|
238
|
+
observations = MessageChannelDefinition(
|
|
239
|
+
OrderObservation, OrderObservationHandler, capacity=16, snapshot_key=observation_identity
|
|
240
|
+
)
|
|
241
|
+
# In configure(context), using the existing Scope import:
|
|
242
|
+
context.services.add_scoped(OrderObservationHandler, scope=Scope.ACTION)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Inject `MessageChannel[OrderObservation]` into the producer. `await channel.send(value)` returns
|
|
246
|
+
`MessageReceipt`; `await receipt.wait()` returns `MessageOutcome` with a `MessageState`.
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from python_ddd_framework import MessageChannel, MessageState
|
|
250
|
+
|
|
251
|
+
async def observe(channel: MessageChannel[OrderObservation]) -> None:
|
|
252
|
+
receipt = await channel.send(OrderObservation(source="inventory", count=3))
|
|
253
|
+
outcome = await receipt.wait()
|
|
254
|
+
if outcome.state is not MessageState.COMPLETED:
|
|
255
|
+
raise RuntimeError(f"Observation ended as {outcome.state.value}")
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
COMPLETED includes handler and scope cleanup, SUPERSEDED marks a replaced pending snapshot,
|
|
259
|
+
ABORTED means never executed, and CANCELLED does not imply rollback. FAILED retains the
|
|
260
|
+
exception and cleanup chain. Receipt completion does not claim commit or external acknowledgement.
|
|
261
|
+
Cancelling a receipt wait does not retract accepted work.
|
|
262
|
+
|
|
263
|
+
Capacity covers queued and executing messages. Async send rejects immediately when full and
|
|
264
|
+
creates no waiting tasks. An external SDK thread can use `channel.submit(value)` with at most
|
|
265
|
+
another capacity pending bridge submissions; its Future returns the acceptance receipt.
|
|
266
|
+
Future cancellation can race with acceptance. Handle `MessageRejectedError` for full/closed/failed
|
|
267
|
+
channels and `HostedServiceInvocationRejectedError` for bridge rejection. Never pass Session,
|
|
268
|
+
UoW, scope or concurrently mutable payloads between tasks/threads. Host configuration
|
|
269
|
+
`messaging.shutdown_timeout` requests cancellation after the deadline; shutdown still waits for
|
|
270
|
+
resource exit and reports timeout. Restart requires a new Application. Commands, results and
|
|
271
|
+
events requiring individual processing must not declare a coalescing key.
|
|
272
|
+
|
|
162
273
|
## Verification
|
|
163
274
|
|
|
164
|
-
Run commands from the application root. Read fixtures first: Host tests use disposable PostgreSQL and Redis containers, so Docker must be available.
|
|
275
|
+
Run native uv/test/build commands from `backend/`. Installed `pddd` commands locate the backend from the application root or any descendant, including nested foreign Python projects. Read fixtures first: Host tests use disposable PostgreSQL and Redis containers, so Docker must be available.
|
|
165
276
|
|
|
166
277
|
```sh
|
|
167
278
|
# After adding an orders module: its domain tests do not need Docker.
|
python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/scripts/.gitkeep
ADDED
|
File without changes
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"""锁的业务使用与错误边界;provider 不进入稳定契约。"""
|
|
2
2
|
|
|
3
3
|
from contextlib import AbstractAsyncContextManager
|
|
4
|
+
from datetime import timedelta
|
|
4
5
|
from typing import Protocol
|
|
5
6
|
|
|
6
7
|
from ..errors.base import FrameworkError
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class DistributedLock(Protocol):
|
|
10
|
-
def acquire(
|
|
11
|
+
def acquire(
|
|
12
|
+
self, key: str, *, wait_timeout: timedelta | None = None
|
|
13
|
+
) -> AbstractAsyncContextManager[bool]:
|
|
11
14
|
"""进入后返回是否取得锁;持有者必须配合取消并在退出上下文前完成清理。"""
|
|
12
15
|
...
|
|
13
16
|
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
from .aggregates import AggregateRoot
|
|
4
4
|
from .entities import Entity
|
|
5
5
|
from .errors import OptimisticConcurrencyError
|
|
6
|
+
from .services import DomainService
|
|
6
7
|
from .value_objects import ValueObject
|
|
7
8
|
|
|
8
9
|
__all__ = (
|
|
9
10
|
"AggregateRoot",
|
|
11
|
+
"DomainService",
|
|
10
12
|
"Entity",
|
|
11
13
|
"OptimisticConcurrencyError",
|
|
12
14
|
"ValueObject",
|
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
|
|
9
|
+
from ..validation.models import _revalidate_model
|
|
9
10
|
from .base import FrameworkError
|
|
10
11
|
|
|
11
12
|
|
|
@@ -40,7 +41,11 @@ class BusinessErrorField:
|
|
|
40
41
|
|
|
41
42
|
class BusinessError(FrameworkError):
|
|
42
43
|
def __init__(
|
|
43
|
-
self,
|
|
44
|
+
self,
|
|
45
|
+
definition: BusinessErrorDefinition,
|
|
46
|
+
*,
|
|
47
|
+
fields: tuple[BusinessErrorField, ...] = (),
|
|
48
|
+
data: BaseModel | None = None,
|
|
44
49
|
) -> None:
|
|
45
50
|
if not isinstance(definition, BusinessErrorDefinition):
|
|
46
51
|
raise TypeError("business error requires a static definition")
|
|
@@ -50,4 +55,8 @@ class BusinessError(FrameworkError):
|
|
|
50
55
|
raise TypeError("business error fields must be an immutable tuple")
|
|
51
56
|
self.definition = definition
|
|
52
57
|
self.fields = fields
|
|
58
|
+
if data is not None and not isinstance(data, BaseModel):
|
|
59
|
+
raise TypeError("business error data must be a typed Pydantic model")
|
|
60
|
+
# 只接受业务显式选取的公开 DTO;复制并重验,排除构造绕过及调用者后续修改。
|
|
61
|
+
self.data = _revalidate_model(type(data), data) if data is not None else None
|
|
53
62
|
super().__init__(definition.code)
|
|
@@ -9,10 +9,8 @@ from .base import FrameworkError
|
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from ..diagnostics import LifecyclePhase, LifecycleStage, SourceLocation
|
|
12
|
-
from ..hosted_services.errors import HostedServiceLifecycleError
|
|
13
12
|
from ..lifecycle import ApplicationState
|
|
14
13
|
from ..modularity import ModuleKey
|
|
15
|
-
from .services import ContainerCleanupError
|
|
16
14
|
|
|
17
15
|
|
|
18
16
|
class InvalidApplicationStateError(FrameworkError):
|
|
@@ -97,20 +95,6 @@ class LifecycleInvocationError(FrameworkError):
|
|
|
97
95
|
)
|
|
98
96
|
|
|
99
97
|
|
|
100
|
-
class _BackgroundExecutionStartFailure(FrameworkError):
|
|
101
|
-
"""保留原始 background start failure,同时不伪造 Module lifecycle 位置。"""
|
|
102
|
-
|
|
103
|
-
failure_id = None
|
|
104
|
-
stage = None
|
|
105
|
-
dependency_path: tuple[ModuleKey, ...] = ()
|
|
106
|
-
|
|
107
|
-
def __init__(self, *, original_error: BaseException) -> None:
|
|
108
|
-
self.original_error = original_error
|
|
109
|
-
self.__cause__ = original_error
|
|
110
|
-
self.__suppress_context__ = True
|
|
111
|
-
super().__init__(f"Background execution start failed with {type(original_error).__name__}")
|
|
112
|
-
|
|
113
|
-
|
|
114
98
|
class ApplicationRuntimeError(FrameworkError):
|
|
115
99
|
"""运行期基础设施 fatal fact;不携带第三方异常正文或原始 cause。"""
|
|
116
100
|
|
|
@@ -126,27 +110,15 @@ class ApplicationStartError(FrameworkError):
|
|
|
126
110
|
def __init__(
|
|
127
111
|
self,
|
|
128
112
|
*,
|
|
129
|
-
primary:
|
|
130
|
-
|
|
131
|
-
| _BackgroundExecutionStartFailure
|
|
132
|
-
| HostedServiceLifecycleError
|
|
133
|
-
),
|
|
134
|
-
cleanup_failures: tuple[
|
|
135
|
-
LifecycleInvocationError
|
|
136
|
-
| ContainerCleanupError
|
|
137
|
-
| _BackgroundExecutionStartFailure
|
|
138
|
-
| HostedServiceLifecycleError,
|
|
139
|
-
...,
|
|
140
|
-
],
|
|
113
|
+
primary: BaseException,
|
|
114
|
+
cleanup_failures: tuple[BaseException, ...],
|
|
141
115
|
) -> None:
|
|
142
116
|
self.primary = primary
|
|
143
117
|
self.cleanup_failures = cleanup_failures
|
|
144
118
|
if isinstance(primary, LifecycleInvocationError):
|
|
145
119
|
location = f"{primary.module}/{primary.stage.value}"
|
|
146
|
-
elif isinstance(primary, _BackgroundExecutionStartFailure):
|
|
147
|
-
location = "background-execution"
|
|
148
120
|
else:
|
|
149
|
-
location =
|
|
121
|
+
location = type(primary).__qualname__
|
|
150
122
|
super().__init__(
|
|
151
123
|
f"Application start failed at {location}; cleanup failures: {len(cleanup_failures)}"
|
|
152
124
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from .aggregate import AggregateEventCollector
|
|
2
2
|
from .catalog import LocalEventCatalog, LocalEventSubscription
|
|
3
3
|
from .contracts import LocalEventPhase, local_event_handler
|
|
4
|
-
from .
|
|
4
|
+
from .entities import EntityChangedEvent, EntityCreatedEvent, EntityDeletedEvent, EntityUpdatedEvent
|
|
5
5
|
from .errors import (
|
|
6
6
|
EventContributionError,
|
|
7
7
|
EventCycleError,
|
|
@@ -12,8 +12,11 @@ from .runtime import LocalEventBus
|
|
|
12
12
|
|
|
13
13
|
__all__ = (
|
|
14
14
|
"AggregateEventCollector",
|
|
15
|
+
"EntityChangedEvent",
|
|
16
|
+
"EntityCreatedEvent",
|
|
17
|
+
"EntityDeletedEvent",
|
|
18
|
+
"EntityUpdatedEvent",
|
|
15
19
|
"EventContributionError",
|
|
16
|
-
"EventContributor",
|
|
17
20
|
"EventCycleError",
|
|
18
21
|
"LocalEventBus",
|
|
19
22
|
"LocalEventCatalog",
|
|
@@ -7,6 +7,7 @@ from typing import TypeVar
|
|
|
7
7
|
from ..domain import AggregateRoot
|
|
8
8
|
from ..unit_of_work.contracts import UnitOfWork
|
|
9
9
|
from .runtime import LocalEventBus
|
|
10
|
+
from .unit_of_work import _local_event_queue
|
|
10
11
|
|
|
11
12
|
_TId = TypeVar("_TId")
|
|
12
13
|
|
|
@@ -31,9 +32,8 @@ def _collect_aggregate_events(
|
|
|
31
32
|
work: UnitOfWork, aggregate: AggregateRoot[_TId]
|
|
32
33
|
) -> tuple[object, ...]:
|
|
33
34
|
# 两个公开入口先验证事务 owner;收集不触发 flush,也不恢复已释放事件或自动重放。
|
|
34
|
-
|
|
35
|
+
queue = _local_event_queue(work)
|
|
35
36
|
released = aggregate.release_local_events()
|
|
36
37
|
for event in released:
|
|
37
|
-
|
|
38
|
-
work._event_queue.publish_after_commit(event)
|
|
38
|
+
queue.enqueue(event)
|
|
39
39
|
return released
|
|
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|
|
5
5
|
import inspect
|
|
6
6
|
from collections.abc import Awaitable, Callable, Mapping
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from types import MappingProxyType
|
|
9
8
|
from typing import TYPE_CHECKING
|
|
10
9
|
|
|
11
10
|
from dishka import BaseScope, Scope
|
|
@@ -32,18 +31,10 @@ class LocalEventSubscription:
|
|
|
32
31
|
|
|
33
32
|
|
|
34
33
|
class LocalEventCatalog:
|
|
35
|
-
__slots__ = ("
|
|
34
|
+
__slots__ = ("_subscriptions",)
|
|
36
35
|
|
|
37
36
|
def __init__(self, subscriptions: tuple[LocalEventSubscription, ...]) -> None:
|
|
38
|
-
grouped: dict[tuple[LocalEventPhase, type[object]], list[LocalEventSubscription]] = {}
|
|
39
|
-
for subscription in subscriptions:
|
|
40
|
-
grouped.setdefault((subscription.phase, subscription.event_type), []).append(
|
|
41
|
-
subscription
|
|
42
|
-
)
|
|
43
37
|
self._subscriptions = subscriptions
|
|
44
|
-
self._by_phase_and_type: Mapping[
|
|
45
|
-
tuple[LocalEventPhase, type[object]], tuple[LocalEventSubscription, ...]
|
|
46
|
-
] = MappingProxyType({key: tuple(values) for key, values in grouped.items()})
|
|
47
38
|
|
|
48
39
|
@property
|
|
49
40
|
def subscriptions(self) -> tuple[LocalEventSubscription, ...]:
|
|
@@ -54,7 +45,17 @@ class LocalEventCatalog:
|
|
|
54
45
|
event_type: type[object],
|
|
55
46
|
phase: LocalEventPhase,
|
|
56
47
|
) -> tuple[LocalEventSubscription, ...]:
|
|
57
|
-
|
|
48
|
+
matched: list[LocalEventSubscription] = []
|
|
49
|
+
seen: set[object] = set()
|
|
50
|
+
for subscription in self._subscriptions:
|
|
51
|
+
if (
|
|
52
|
+
subscription.phase is phase
|
|
53
|
+
and issubclass(event_type, subscription.event_type)
|
|
54
|
+
and subscription.handler_type not in seen
|
|
55
|
+
):
|
|
56
|
+
matched.append(subscription)
|
|
57
|
+
seen.add(subscription.handler_type)
|
|
58
|
+
return tuple(matched)
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
def _build_event_catalog(
|
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from typing import TypeVar
|
|
8
8
|
|
|
9
|
+
from ..modularity import ModuleDeclaration, ModuleDependency, ModuleRef
|
|
9
10
|
from .errors import EventContributionError
|
|
10
11
|
|
|
11
12
|
|
|
@@ -19,9 +20,13 @@ _THandler = TypeVar("_THandler")
|
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
@dataclass(frozen=True, slots=True)
|
|
22
|
-
class _LocalEventHandlerDefinition:
|
|
23
|
+
class _LocalEventHandlerDefinition(ModuleDeclaration):
|
|
23
24
|
phase: LocalEventPhase
|
|
24
25
|
|
|
26
|
+
@property
|
|
27
|
+
def required_module(self) -> ModuleDependency:
|
|
28
|
+
return ModuleRef("python_ddd_framework.events.module:LocalEventsModule")
|
|
29
|
+
|
|
25
30
|
|
|
26
31
|
def local_event_handler(*, phase: LocalEventPhase) -> Callable[[type[_THandler]], type[_THandler]]:
|
|
27
32
|
"""只保存 class-local phase;事件类型在 Application build 由 handle 注解解释。"""
|
|
@@ -6,8 +6,7 @@ import inspect
|
|
|
6
6
|
from collections.abc import Awaitable, Callable
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
|
|
9
|
-
from ..diagnostics import
|
|
10
|
-
from ..diagnostics.source import caller_source
|
|
9
|
+
from ..diagnostics import SourceLocation
|
|
11
10
|
from ..modularity import ModuleDescriptor, ModuleKey
|
|
12
11
|
from .contracts import LocalEventPhase
|
|
13
12
|
from .errors import EventContributionError
|
|
@@ -25,48 +24,6 @@ class _EventContribution:
|
|
|
25
24
|
automatic: bool = False
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
class EventContributor:
|
|
29
|
-
"""只在当前 Module.configure 有效,避免 runtime 或 global subscription mutation。"""
|
|
30
|
-
|
|
31
|
-
__slots__ = ("_active", "_descriptor", "_registry", "_stage")
|
|
32
|
-
|
|
33
|
-
def __init__(
|
|
34
|
-
self,
|
|
35
|
-
registry: EventContributionRegistry,
|
|
36
|
-
*,
|
|
37
|
-
descriptor: ModuleDescriptor,
|
|
38
|
-
stage: LifecycleStage,
|
|
39
|
-
) -> None:
|
|
40
|
-
self._registry = registry
|
|
41
|
-
self._descriptor = descriptor
|
|
42
|
-
self._stage = stage
|
|
43
|
-
self._active = True
|
|
44
|
-
|
|
45
|
-
def subscribe(
|
|
46
|
-
self,
|
|
47
|
-
event_type: type[object],
|
|
48
|
-
handler_type: type[object] | Callable[..., Awaitable[None]],
|
|
49
|
-
phase: LocalEventPhase,
|
|
50
|
-
*,
|
|
51
|
-
reason: str,
|
|
52
|
-
) -> None:
|
|
53
|
-
if not self._active:
|
|
54
|
-
raise EventContributionError(
|
|
55
|
-
reason=f"event contributor is closed for {self._stage.value}"
|
|
56
|
-
)
|
|
57
|
-
self._registry.add(
|
|
58
|
-
event_type=event_type,
|
|
59
|
-
handler_type=handler_type,
|
|
60
|
-
phase=phase,
|
|
61
|
-
descriptor=self._descriptor,
|
|
62
|
-
source=caller_source(str(self._descriptor.key), depth=2),
|
|
63
|
-
reason=reason,
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
def _close(self) -> None:
|
|
67
|
-
self._active = False
|
|
68
|
-
|
|
69
|
-
|
|
70
27
|
class EventContributionRegistry:
|
|
71
28
|
"""Application-owned append-only subscription state,freeze 后只读。"""
|
|
72
29
|
|
|
@@ -77,14 +34,6 @@ class EventContributionRegistry:
|
|
|
77
34
|
self._counts: dict[ModuleKey, int] = {}
|
|
78
35
|
self._frozen = False
|
|
79
36
|
|
|
80
|
-
def contributor(
|
|
81
|
-
self,
|
|
82
|
-
*,
|
|
83
|
-
descriptor: ModuleDescriptor,
|
|
84
|
-
stage: LifecycleStage,
|
|
85
|
-
) -> EventContributor:
|
|
86
|
-
return EventContributor(self, descriptor=descriptor, stage=stage)
|
|
87
|
-
|
|
88
37
|
def add(
|
|
89
38
|
self,
|
|
90
39
|
*,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""仓储暂存事实与聚合业务事件分离;不推断领域事件或 ORM 关系。"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class EntityChangedEvent:
|
|
8
|
+
entity: object
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class EntityCreatedEvent(EntityChangedEvent):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class EntityUpdatedEvent(EntityChangedEvent):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class EntityDeletedEvent(EntityChangedEvent):
|
|
23
|
+
pass
|