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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""本地事件发现、订阅目录及 UoW 阶段参与者的模块 owner。"""
|
|
2
|
+
|
|
3
|
+
from dishka import DEFAULT_COMPONENT, DependencyKey, Provider, Scope
|
|
4
|
+
|
|
5
|
+
from ..invocation.interception import InvocationEntry
|
|
6
|
+
from ..invocation.module import InvocationModule
|
|
7
|
+
from ..lifecycle import ConfigureContext
|
|
8
|
+
from ..modularity import AppModule
|
|
9
|
+
from ..services.composition import (
|
|
10
|
+
ServiceBindingContext,
|
|
11
|
+
ServiceComposition,
|
|
12
|
+
ServiceConvention,
|
|
13
|
+
ServiceDiscoveryContext,
|
|
14
|
+
ServiceValidationContext,
|
|
15
|
+
)
|
|
16
|
+
from ..services.convention_contracts import ConventionDiscoveryKind, _ServiceDefinition
|
|
17
|
+
from ..services.native_graph import _accepted_single_service_scopes
|
|
18
|
+
from ..unit_of_work.lifecycle import UnitOfWorkLifecycleFactory
|
|
19
|
+
from ..unit_of_work.module import UnitOfWorkModule
|
|
20
|
+
from .aggregate import AggregateEventCollector
|
|
21
|
+
from .catalog import LocalEventCatalog, _build_event_catalog, _validate_event_scopes
|
|
22
|
+
from .contracts import LocalEventPhase
|
|
23
|
+
from .contribution import EventContributionRegistry
|
|
24
|
+
from .discovery import _discover_event_handlers
|
|
25
|
+
from .runtime import LocalEventBus
|
|
26
|
+
from .unit_of_work import _LocalEventLifecycleFactory
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LocalEventsModule(AppModule):
|
|
30
|
+
dependencies = (UnitOfWorkModule,)
|
|
31
|
+
catalog: LocalEventCatalog
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self.contributions = EventContributionRegistry()
|
|
35
|
+
|
|
36
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
37
|
+
context.services.add_singleton(
|
|
38
|
+
UnitOfWorkLifecycleFactory,
|
|
39
|
+
_LocalEventLifecycleFactory,
|
|
40
|
+
multi=True,
|
|
41
|
+
)
|
|
42
|
+
context.services.add_composition(
|
|
43
|
+
ServiceComposition(
|
|
44
|
+
discover=self._discover,
|
|
45
|
+
bind=self._bind,
|
|
46
|
+
validate=self._validate,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def _discover(self, context: ServiceDiscoveryContext) -> None:
|
|
51
|
+
scopes = _discover_event_handlers(context.modules, context.discoveries, self.contributions)
|
|
52
|
+
for discovery in context.discoveries:
|
|
53
|
+
if (scope := scopes.get(discovery.candidate)) is not None:
|
|
54
|
+
context.conventions[discovery.owner, discovery.candidate] = ServiceConvention(
|
|
55
|
+
_ServiceDefinition(provides=discovery.candidate, scope=scope),
|
|
56
|
+
ConventionDiscoveryKind.FRAMEWORK_MARKER,
|
|
57
|
+
"local_event_handler declaration",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _bind(self, context: ServiceBindingContext) -> None:
|
|
61
|
+
self.catalog = _build_event_catalog(context.modules, self.contributions)
|
|
62
|
+
# 订阅入口必须有独立的最终 single binding;multi 集合不能冒充可直接解析的处理器。
|
|
63
|
+
scopes = {}
|
|
64
|
+
for item in self.catalog.subscriptions:
|
|
65
|
+
if isinstance(item.handler_type, type):
|
|
66
|
+
factory = context.arbitration.single_factory(
|
|
67
|
+
DependencyKey(item.handler_type, DEFAULT_COMPONENT),
|
|
68
|
+
)
|
|
69
|
+
if factory is not None and factory.scope is not None:
|
|
70
|
+
scopes[item.handler_type] = factory.scope
|
|
71
|
+
_validate_event_scopes(self.catalog, scopes)
|
|
72
|
+
invocation = context.modules.instance(InvocationModule)
|
|
73
|
+
for subscription in self.catalog.subscriptions:
|
|
74
|
+
scope = Scope.ACTION if subscription.phase is LocalEventPhase.DOMAIN else Scope.REQUEST
|
|
75
|
+
invocation.entries.append(
|
|
76
|
+
InvocationEntry(
|
|
77
|
+
subscription.handler_type,
|
|
78
|
+
"handle",
|
|
79
|
+
subscription.owner,
|
|
80
|
+
1,
|
|
81
|
+
outer_scope=scope,
|
|
82
|
+
method_scope=scope,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
provider = Provider()
|
|
86
|
+
provider.provide(lambda: self.catalog, provides=LocalEventCatalog, scope=Scope.APP)
|
|
87
|
+
provider.provide(LocalEventBus, scope=Scope.REQUEST)
|
|
88
|
+
provider.provide(AggregateEventCollector, scope=Scope.ACTION)
|
|
89
|
+
context.add_provider(provider)
|
|
90
|
+
|
|
91
|
+
def _validate(self, context: ServiceValidationContext) -> None:
|
|
92
|
+
_validate_event_scopes(
|
|
93
|
+
self.catalog,
|
|
94
|
+
_accepted_single_service_scopes(
|
|
95
|
+
context.binding.arbitration.drafts, context.native_graph
|
|
96
|
+
),
|
|
97
|
+
)
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections import deque
|
|
6
|
-
from
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from dataclasses import dataclass, field
|
|
7
8
|
from typing import Protocol
|
|
8
9
|
from uuid import UUID, uuid4
|
|
9
10
|
|
|
@@ -11,6 +12,7 @@ from dishka import AsyncContainer, Scope
|
|
|
11
12
|
|
|
12
13
|
from ..invocation import ApplicationInvocationContext
|
|
13
14
|
from ..unit_of_work.manager import UnitOfWorkManager
|
|
15
|
+
from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME
|
|
14
16
|
from .catalog import LocalEventCatalog, LocalEventSubscription
|
|
15
17
|
from .contracts import LocalEventPhase
|
|
16
18
|
from .errors import EventCycleError, LocalEventHandlerError, PostCommitEventError
|
|
@@ -24,6 +26,7 @@ class _EventEnvelope:
|
|
|
24
26
|
event: object
|
|
25
27
|
event_type: type[object]
|
|
26
28
|
ancestry: tuple[type[object], ...]
|
|
29
|
+
handled: set[object] = field(default_factory=set)
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
class _EventHandler(Protocol):
|
|
@@ -55,13 +58,17 @@ class _LocalEventQueue:
|
|
|
55
58
|
self._after_commit: deque[_EventEnvelope] = deque()
|
|
56
59
|
self._current: _EventEnvelope | None = None
|
|
57
60
|
|
|
58
|
-
def
|
|
59
|
-
self
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
def enqueue(
|
|
62
|
+
self, event: object, phase: LocalEventPhase | None = None, *, defer_domain: bool = True
|
|
63
|
+
) -> _EventEnvelope:
|
|
64
|
+
envelope = self._envelope(event)
|
|
65
|
+
if phase is not LocalEventPhase.AFTER_COMMIT and defer_domain:
|
|
66
|
+
self._domain.append(envelope)
|
|
67
|
+
if phase is not LocalEventPhase.DOMAIN:
|
|
68
|
+
self._after_commit.append(envelope)
|
|
69
|
+
return envelope
|
|
70
|
+
|
|
71
|
+
async def before_commit(self) -> None:
|
|
65
72
|
request = (
|
|
66
73
|
self._action_container.parent_container
|
|
67
74
|
if self._action_container.scope is Scope.ACTION
|
|
@@ -84,7 +91,7 @@ class _LocalEventQueue:
|
|
|
84
91
|
committed=False,
|
|
85
92
|
)
|
|
86
93
|
|
|
87
|
-
async def
|
|
94
|
+
async def after_commit(self) -> None:
|
|
88
95
|
request_container = (
|
|
89
96
|
self._action_container.parent_container
|
|
90
97
|
if self._action_container.scope is Scope.ACTION
|
|
@@ -111,23 +118,34 @@ class _LocalEventQueue:
|
|
|
111
118
|
if failures:
|
|
112
119
|
raise PostCommitEventError(failures=tuple(failures))
|
|
113
120
|
|
|
114
|
-
def
|
|
121
|
+
def _envelope(self, event: object) -> _EventEnvelope:
|
|
115
122
|
event_type = type(event)
|
|
116
123
|
parent = self._current
|
|
117
124
|
ancestry = parent.ancestry if parent is not None else ()
|
|
118
125
|
if event_type in ancestry:
|
|
119
126
|
raise EventCycleError(event_type=event_type, ancestry=ancestry)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
ancestry=(*ancestry, event_type),
|
|
128
|
-
)
|
|
127
|
+
return _EventEnvelope(
|
|
128
|
+
envelope_id=uuid4(),
|
|
129
|
+
invocation_id=self._context.invocation_id,
|
|
130
|
+
parent_envelope_id=parent.envelope_id if parent is not None else None,
|
|
131
|
+
event=event,
|
|
132
|
+
event_type=event_type,
|
|
133
|
+
ancestry=(*ancestry, event_type),
|
|
129
134
|
)
|
|
130
135
|
|
|
136
|
+
async def dispatch(self, event: object, phase: LocalEventPhase | None) -> None:
|
|
137
|
+
"""立即派发保留 ancestry;没有 UoW 时任何阶段都不声称已提交。"""
|
|
138
|
+
envelope = self._envelope(event)
|
|
139
|
+
action = self._action_container
|
|
140
|
+
request = action.parent_container if action.scope is Scope.ACTION else action
|
|
141
|
+
assert request is not None
|
|
142
|
+
if phase is not LocalEventPhase.AFTER_COMMIT:
|
|
143
|
+
await self._dispatch_envelope(envelope, action, LocalEventPhase.DOMAIN, committed=False)
|
|
144
|
+
if phase is not LocalEventPhase.DOMAIN:
|
|
145
|
+
await self._dispatch_envelope(
|
|
146
|
+
envelope, request, LocalEventPhase.AFTER_COMMIT, committed=False
|
|
147
|
+
)
|
|
148
|
+
|
|
131
149
|
async def _dispatch_envelope(
|
|
132
150
|
self,
|
|
133
151
|
envelope: _EventEnvelope,
|
|
@@ -152,6 +170,9 @@ class _LocalEventQueue:
|
|
|
152
170
|
*,
|
|
153
171
|
committed: bool,
|
|
154
172
|
) -> None:
|
|
173
|
+
if subscription.handler_type in envelope.handled:
|
|
174
|
+
return
|
|
175
|
+
envelope.handled.add(subscription.handler_type)
|
|
155
176
|
previous = self._current
|
|
156
177
|
self._current = envelope
|
|
157
178
|
try:
|
|
@@ -178,15 +199,45 @@ class _LocalEventQueue:
|
|
|
178
199
|
class LocalEventBus:
|
|
179
200
|
"""发布能力解析具体 UoW 的队列;注入本身不创建 UoW 或资源。"""
|
|
180
201
|
|
|
181
|
-
def __init__(self, manager: UnitOfWorkManager) -> None:
|
|
202
|
+
def __init__(self, manager: UnitOfWorkManager, catalog: LocalEventCatalog) -> None:
|
|
182
203
|
self._manager = manager
|
|
204
|
+
self._catalog = catalog
|
|
205
|
+
self._immediate: ContextVar[_LocalEventQueue | None] = ContextVar(
|
|
206
|
+
"local_event_immediate", default=None
|
|
207
|
+
)
|
|
183
208
|
|
|
184
|
-
def
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
209
|
+
async def publish(
|
|
210
|
+
self,
|
|
211
|
+
event: object,
|
|
212
|
+
*,
|
|
213
|
+
phase: LocalEventPhase | None = None,
|
|
214
|
+
on_unit_of_work_complete: bool = True,
|
|
215
|
+
) -> None:
|
|
216
|
+
from .unit_of_work import _local_event_queue
|
|
217
|
+
|
|
218
|
+
if phase is not None and not isinstance(phase, LocalEventPhase):
|
|
219
|
+
raise TypeError("phase must be LocalEventPhase or None")
|
|
220
|
+
work = self._manager.current
|
|
221
|
+
if work is not None:
|
|
222
|
+
self._manager.require_current(transactional=True)
|
|
223
|
+
queue = _local_event_queue(work)
|
|
224
|
+
envelope = queue.enqueue(event, phase, defer_domain=on_unit_of_work_complete)
|
|
225
|
+
if phase is not LocalEventPhase.AFTER_COMMIT and not on_unit_of_work_complete:
|
|
226
|
+
await queue._dispatch_envelope(
|
|
227
|
+
envelope, queue._action_container, LocalEventPhase.DOMAIN, committed=False
|
|
228
|
+
)
|
|
229
|
+
return
|
|
230
|
+
runtime = self._manager._runtime
|
|
231
|
+
async with runtime.outer_lease():
|
|
232
|
+
async with self._manager._execution_scope(_DEFAULT_CONNECTION_NAME) as action:
|
|
233
|
+
current = self._immediate.get()
|
|
234
|
+
if current is not None:
|
|
235
|
+
await current.dispatch(event, phase)
|
|
236
|
+
return
|
|
237
|
+
context = await action.get(ApplicationInvocationContext)
|
|
238
|
+
queue = _LocalEventQueue(action, context, self._catalog)
|
|
239
|
+
token = self._immediate.set(queue)
|
|
240
|
+
try:
|
|
241
|
+
await queue.dispatch(event, phase)
|
|
242
|
+
finally:
|
|
243
|
+
self._immediate.reset(token)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""模块配置阶段的显式事件订阅入口。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
|
|
5
|
+
from ..diagnostics import LifecycleStage
|
|
6
|
+
from ..diagnostics.source import caller_source
|
|
7
|
+
from ..lifecycle import ConfigureContext
|
|
8
|
+
from .contracts import LocalEventPhase
|
|
9
|
+
from .errors import EventContributionError
|
|
10
|
+
from .module import LocalEventsModule
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def subscribe_local_event(
|
|
14
|
+
context: ConfigureContext,
|
|
15
|
+
event_type: type[object],
|
|
16
|
+
handler_type: type[object] | Callable[..., Awaitable[None]],
|
|
17
|
+
phase: LocalEventPhase,
|
|
18
|
+
*,
|
|
19
|
+
reason: str,
|
|
20
|
+
) -> None:
|
|
21
|
+
context.services.require_active()
|
|
22
|
+
if context.stage is not LifecycleStage.CONFIGURE:
|
|
23
|
+
raise EventContributionError(reason="subscriptions require the configure phase")
|
|
24
|
+
context.modules.require_dependency(
|
|
25
|
+
context.descriptor.key,
|
|
26
|
+
LocalEventsModule,
|
|
27
|
+
source=context.descriptor.declaration_source,
|
|
28
|
+
)
|
|
29
|
+
context.modules.instance(LocalEventsModule).contributions.add(
|
|
30
|
+
event_type=event_type,
|
|
31
|
+
handler_type=handler_type,
|
|
32
|
+
phase=phase,
|
|
33
|
+
descriptor=context.descriptor,
|
|
34
|
+
source=caller_source(str(context.descriptor.key), depth=2),
|
|
35
|
+
reason=reason,
|
|
36
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""本地事件向事务生命周期贡献队列;队列仍由单个具体 UoW 持有。"""
|
|
2
|
+
|
|
3
|
+
from dishka import AsyncContainer
|
|
4
|
+
|
|
5
|
+
from ..invocation import ApplicationInvocationContext
|
|
6
|
+
from ..unit_of_work.contracts import UnitOfWork
|
|
7
|
+
from .catalog import LocalEventCatalog
|
|
8
|
+
from .runtime import _LocalEventQueue
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _LocalEventLifecycleFactory:
|
|
12
|
+
def __init__(self, catalog: LocalEventCatalog) -> None:
|
|
13
|
+
self._catalog = catalog
|
|
14
|
+
|
|
15
|
+
def create(
|
|
16
|
+
self, container: AsyncContainer, context: ApplicationInvocationContext
|
|
17
|
+
) -> _LocalEventQueue:
|
|
18
|
+
return _LocalEventQueue(container, context, self._catalog)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _local_event_queue(work: UnitOfWork) -> _LocalEventQueue:
|
|
22
|
+
for lifecycle in work._lifecycles:
|
|
23
|
+
if isinstance(lifecycle, _LocalEventQueue):
|
|
24
|
+
return lifecycle
|
|
25
|
+
raise RuntimeError("Local event lifecycle is not configured for this UnitOfWork")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .catalog import ExtensionCatalog, ExtensionContribution
|
|
2
|
+
from .contracts import (
|
|
3
|
+
ExtensibleModel,
|
|
4
|
+
ExtensionPoints,
|
|
5
|
+
ExtensionProperties,
|
|
6
|
+
ExtensionTarget,
|
|
7
|
+
ModelExtension,
|
|
8
|
+
ModelExtensions,
|
|
9
|
+
)
|
|
10
|
+
from .module import ExtensionsModule
|
|
11
|
+
|
|
12
|
+
__all__ = (
|
|
13
|
+
"ExtensibleModel",
|
|
14
|
+
"ExtensionCatalog",
|
|
15
|
+
"ExtensionContribution",
|
|
16
|
+
"ExtensionPoints",
|
|
17
|
+
"ExtensionProperties",
|
|
18
|
+
"ExtensionTarget",
|
|
19
|
+
"ExtensionsModule",
|
|
20
|
+
"ModelExtension",
|
|
21
|
+
"ModelExtensions",
|
|
22
|
+
)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""单个 Application 的有效 Pydantic 模型与扩展字段归属。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import (
|
|
7
|
+
Annotated,
|
|
8
|
+
Any,
|
|
9
|
+
TypeAliasType,
|
|
10
|
+
TypeVar,
|
|
11
|
+
cast,
|
|
12
|
+
get_args,
|
|
13
|
+
get_origin,
|
|
14
|
+
get_type_hints,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from pydantic import ConfigDict, TypeAdapter, create_model
|
|
18
|
+
|
|
19
|
+
from ..modularity import ModuleKey, ModuleRegistry
|
|
20
|
+
from .contracts import (
|
|
21
|
+
ExtensibleModel,
|
|
22
|
+
ExtensionPoints,
|
|
23
|
+
ExtensionProperties,
|
|
24
|
+
ExtensionTarget,
|
|
25
|
+
ModelExtension,
|
|
26
|
+
ModelExtensions,
|
|
27
|
+
)
|
|
28
|
+
from .models import _EffectiveModels
|
|
29
|
+
|
|
30
|
+
_TModel = TypeVar("_TModel", bound=ExtensibleModel)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class ExtensionContribution:
|
|
35
|
+
owner: ModuleKey
|
|
36
|
+
declaration: ModelExtension
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ExtensionCatalog:
|
|
40
|
+
def __init__(self, modules: ModuleRegistry) -> None:
|
|
41
|
+
targets = {}
|
|
42
|
+
names: set[str] = set()
|
|
43
|
+
self._model_properties: dict[type[ExtensibleModel], type[ExtensionProperties]] = {}
|
|
44
|
+
self._properties: dict[ExtensionTarget, type[ExtensionProperties]] = {}
|
|
45
|
+
contributions: list[ExtensionContribution] = []
|
|
46
|
+
for owner, points in modules.declarations(ExtensionPoints):
|
|
47
|
+
for target in points.targets:
|
|
48
|
+
if not target.name or target.name in names or target in targets:
|
|
49
|
+
raise ValueError("extension target must have a unique non-empty name and owner")
|
|
50
|
+
if not target.models or any(
|
|
51
|
+
not issubclass(model, ExtensibleModel) for model in target.models
|
|
52
|
+
):
|
|
53
|
+
raise TypeError("extension targets require explicit ExtensibleModel DTOs")
|
|
54
|
+
targets[target] = owner
|
|
55
|
+
names.add(target.name)
|
|
56
|
+
for owner, declared in modules.declarations(ModelExtensions):
|
|
57
|
+
for extension in declared.extensions:
|
|
58
|
+
modules.require_dependency(
|
|
59
|
+
owner.key, extension.required_module, source=owner.declaration_source
|
|
60
|
+
)
|
|
61
|
+
if extension.target not in targets:
|
|
62
|
+
raise ValueError("extension target is not explicitly open in this Application")
|
|
63
|
+
modules.require_dependency(
|
|
64
|
+
owner.key,
|
|
65
|
+
targets[extension.target].module_type,
|
|
66
|
+
source=owner.declaration_source,
|
|
67
|
+
)
|
|
68
|
+
contributions.append(ExtensionContribution(owner.key, extension))
|
|
69
|
+
self.contributions = tuple(contributions)
|
|
70
|
+
for target in targets:
|
|
71
|
+
self._build(target)
|
|
72
|
+
self._models = _EffectiveModels(self._model_properties)
|
|
73
|
+
for model in self._model_properties:
|
|
74
|
+
self._models.annotation(model)
|
|
75
|
+
|
|
76
|
+
def _build(self, target: ExtensionTarget) -> None:
|
|
77
|
+
schemas = tuple(
|
|
78
|
+
item.declaration.properties
|
|
79
|
+
for item in self.contributions
|
|
80
|
+
if item.declaration.target is target
|
|
81
|
+
)
|
|
82
|
+
core = {name for model in target.models for name in model.model_fields}
|
|
83
|
+
if target.entity_type is not None:
|
|
84
|
+
core.update(get_type_hints(target.entity_type))
|
|
85
|
+
declared: set[str] = set()
|
|
86
|
+
decorators: set[str] = set()
|
|
87
|
+
for schema in schemas:
|
|
88
|
+
_validate_schema(schema)
|
|
89
|
+
conflicts = schema.model_fields.keys() & (declared | core)
|
|
90
|
+
if conflicts:
|
|
91
|
+
raise ValueError(f"duplicate or core extension field: {sorted(conflicts)}")
|
|
92
|
+
declared.update(schema.model_fields)
|
|
93
|
+
# 原生多继承按方法名覆盖;跨贡献者同名校验/序列化方法不能静默丢失。
|
|
94
|
+
names = {
|
|
95
|
+
name
|
|
96
|
+
for base in schema.__mro__
|
|
97
|
+
if base not in ExtensionProperties.__mro__
|
|
98
|
+
for name, value in vars(base).items()
|
|
99
|
+
if not name.startswith("__") and (callable(value) or isinstance(value, classmethod))
|
|
100
|
+
}
|
|
101
|
+
if names & decorators:
|
|
102
|
+
raise ValueError("extension schemas cannot override each other's methods")
|
|
103
|
+
decorators.update(names)
|
|
104
|
+
effective = create_model(
|
|
105
|
+
target.name.replace(".", "_") + "Properties",
|
|
106
|
+
__base__=schemas or (ExtensionProperties,),
|
|
107
|
+
__config__=ConfigDict(revalidate_instances="always"),
|
|
108
|
+
)
|
|
109
|
+
assert issubclass(effective, ExtensionProperties)
|
|
110
|
+
self._properties[target] = cast(type[ExtensionProperties], effective)
|
|
111
|
+
for model in target.models:
|
|
112
|
+
if model in self._model_properties:
|
|
113
|
+
raise ValueError("a DTO cannot belong to multiple extension targets")
|
|
114
|
+
self._model_properties[model] = cast(type[ExtensionProperties], effective)
|
|
115
|
+
|
|
116
|
+
def model(self, model: type[_TModel]) -> type[_TModel]:
|
|
117
|
+
if model not in self._model_properties:
|
|
118
|
+
raise KeyError(model)
|
|
119
|
+
effective = self._models.annotation(model)
|
|
120
|
+
assert isinstance(effective, type) and issubclass(effective, model)
|
|
121
|
+
return effective
|
|
122
|
+
|
|
123
|
+
def properties(self, target: ExtensionTarget, value: object) -> ExtensionProperties:
|
|
124
|
+
return self._properties[target].model_validate(value)
|
|
125
|
+
|
|
126
|
+
def property_model(self, target: ExtensionTarget) -> type[ExtensionProperties]:
|
|
127
|
+
return self._properties[target]
|
|
128
|
+
|
|
129
|
+
def annotation(self, annotation: object) -> object:
|
|
130
|
+
return self._models.annotation(annotation)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _validate_schema(schema: type[ExtensionProperties]) -> None:
|
|
134
|
+
if not isinstance(schema, type) or not issubclass(schema, ExtensionProperties):
|
|
135
|
+
raise TypeError("extension declaration must use ExtensionProperties")
|
|
136
|
+
if schema.model_config.get("extra") != "forbid":
|
|
137
|
+
raise ValueError("extension properties must reject unknown fields")
|
|
138
|
+
_validate_type_schema(TypeAdapter(schema).core_schema)
|
|
139
|
+
for field in schema.model_fields.values():
|
|
140
|
+
_validate_type_parameters(field.annotation)
|
|
141
|
+
if any(
|
|
142
|
+
value is not None
|
|
143
|
+
for value in (
|
|
144
|
+
field.alias,
|
|
145
|
+
field.validation_alias,
|
|
146
|
+
field.serialization_alias,
|
|
147
|
+
field.exclude,
|
|
148
|
+
)
|
|
149
|
+
):
|
|
150
|
+
raise ValueError("extension field identity cannot use aliases or exclusions")
|
|
151
|
+
# 原生 schema 同时拒绝不可序列化类型及改变字段归属的 serializer/computed field。
|
|
152
|
+
for mode in ("validation", "serialization"):
|
|
153
|
+
if set(schema.model_json_schema(mode=mode).get("properties", {})) != set(
|
|
154
|
+
schema.model_fields
|
|
155
|
+
):
|
|
156
|
+
raise ValueError("extension serialization must preserve declared fields")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _validate_type_parameters(annotation: object) -> None:
|
|
160
|
+
# 有界 TypeVar 也不是闭合公共类型;core schema 会将其化为 bound,须保留这一声明检查。
|
|
161
|
+
if isinstance(annotation, TypeVar) or (
|
|
162
|
+
isinstance(annotation, TypeAliasType) and annotation.__type_params__
|
|
163
|
+
):
|
|
164
|
+
raise TypeError("extension fields require concrete types")
|
|
165
|
+
args = get_args(annotation)
|
|
166
|
+
if get_origin(annotation) is Annotated:
|
|
167
|
+
args = args[:1]
|
|
168
|
+
for item in args:
|
|
169
|
+
_validate_type_parameters(item)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _validate_type_schema(value: Any) -> None:
|
|
173
|
+
# Pydantic 已解析别名、泛型、dataclass 和递归定义;校验同一原生 schema,
|
|
174
|
+
# 不再用 get_args 重建一套会漏掉 TypeAliasType 的类型解释器。
|
|
175
|
+
if isinstance(value, dict):
|
|
176
|
+
kind = value.get("type")
|
|
177
|
+
if isinstance(kind, str):
|
|
178
|
+
_validate_schema_node(value)
|
|
179
|
+
preserved = ("default", "metadata", "config") if isinstance(kind, str) else ()
|
|
180
|
+
for key, item in value.items():
|
|
181
|
+
if key not in preserved:
|
|
182
|
+
_validate_type_schema(item)
|
|
183
|
+
elif isinstance(value, (list, tuple)):
|
|
184
|
+
for item in value:
|
|
185
|
+
_validate_type_schema(item)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _validate_schema_node(schema: dict[str, Any]) -> None:
|
|
189
|
+
kind = schema["type"]
|
|
190
|
+
if kind == "any":
|
|
191
|
+
raise TypeError("extension fields require concrete types")
|
|
192
|
+
if kind == "model":
|
|
193
|
+
for field in schema["cls"].model_fields.values():
|
|
194
|
+
_validate_type_parameters(field.annotation)
|
|
195
|
+
if kind in ("model", "dataclass", "typed-dict"):
|
|
196
|
+
extra = schema.get("extra_behavior", schema.get("config", {}).get("extra_fields_behavior"))
|
|
197
|
+
if extra != "forbid" and not schema.get("root_model", False):
|
|
198
|
+
raise ValueError("nested extension models must reject unknown fields")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""显式扩展点与类型化字段声明;不拥有解析后的应用状态或存储实现。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, SerializeAsAny, model_validator
|
|
9
|
+
|
|
10
|
+
from ..modularity import ModuleDeclaration, ModuleRef
|
|
11
|
+
|
|
12
|
+
_TProperties = TypeVar("_TProperties", bound="ExtensionProperties")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExtensionProperties(BaseModel):
|
|
16
|
+
"""消费者继承此模型声明字段;原生 Pydantic 拥有约束、默认值和序列化。"""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(extra="forbid", frozen=True, validate_default=True)
|
|
19
|
+
|
|
20
|
+
@model_validator(mode="before")
|
|
21
|
+
@classmethod
|
|
22
|
+
def _revalidate_properties(cls, value: object) -> object:
|
|
23
|
+
# 消费者静态属性类型投影为应用有效子类;保留嵌套原生值后再按有效 schema 校验。
|
|
24
|
+
if isinstance(value, ExtensionProperties) and not isinstance(value, cls):
|
|
25
|
+
return {name: getattr(value, name) for name in type(value).model_fields}
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
def read(self, schema: type[_TProperties]) -> _TProperties:
|
|
29
|
+
"""只读取实际有效模型包含的字段;返回消费者声明的静态类型。"""
|
|
30
|
+
if not isinstance(self, schema):
|
|
31
|
+
raise ValueError("property schema is not part of this Application's effective model")
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ExtensibleModel(BaseModel):
|
|
36
|
+
model_config = ConfigDict(
|
|
37
|
+
extra="forbid", frozen=True, validate_default=True, revalidate_instances="always"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
extra_properties: SerializeAsAny[ExtensionProperties] = Field(
|
|
41
|
+
default_factory=ExtensionProperties
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@model_validator(mode="before")
|
|
45
|
+
@classmethod
|
|
46
|
+
def _revalidate_model(cls, value: object) -> object:
|
|
47
|
+
# 公共 DTO 实例进入有效子类型时也经过相同 schema,不能绕过应用隔离。
|
|
48
|
+
if isinstance(value, ExtensibleModel) and not isinstance(value, cls):
|
|
49
|
+
return {name: getattr(value, name) for name in type(value).model_fields}
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True, eq=False)
|
|
54
|
+
class ExtensionTarget:
|
|
55
|
+
name: str
|
|
56
|
+
models: tuple[type[ExtensibleModel], ...]
|
|
57
|
+
entity_type: type[object] | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class ExtensionPoints(ModuleDeclaration):
|
|
62
|
+
targets: tuple[ExtensionTarget, ...]
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def required_module(self) -> ModuleRef:
|
|
66
|
+
return ModuleRef("python_ddd_framework.extensions.module:ExtensionsModule")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True, slots=True)
|
|
70
|
+
class ModelExtension(ModuleDeclaration):
|
|
71
|
+
target: ExtensionTarget
|
|
72
|
+
properties: type[ExtensionProperties]
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def required_module(self) -> ModuleRef:
|
|
76
|
+
return ModuleRef("python_ddd_framework.extensions.module:ExtensionsModule")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class ModelExtensions(ModuleDeclaration):
|
|
81
|
+
extensions: tuple[ModelExtension, ...]
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def required_module(self) -> ModuleRef:
|
|
85
|
+
return ModuleRef("python_ddd_framework.extensions.module:ExtensionsModule")
|