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,45 @@
|
|
|
1
|
+
# {{ cookiecutter.class_prefix }}
|
|
2
|
+
|
|
3
|
+
This basic module demonstrates an ordinary in-process DI service. Text conversion is an example to replace with your own capability, not a required business model.
|
|
4
|
+
|
|
5
|
+
| File | Owner |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `module.py` | `{{ cookiecutter.class_prefix }}Module`: dependencies, scanning, and lifecycle |
|
|
8
|
+
| `contracts/conversion_service.py` | The public `ConversionService` Protocol |
|
|
9
|
+
| `services/default_conversion_service.py` | The transient default implementation |
|
|
10
|
+
| `__init__.py` files | Package markers only |
|
|
11
|
+
|
|
12
|
+
The CLI adds `{{ cookiecutter.class_prefix }}Module` to the Host dependencies and publishes the `{{ cookiecutter.module_name }}` alias through the project's standard module entry points. This template does not require database or Redis providers. Other modules selected by your Host may require them.
|
|
13
|
+
|
|
14
|
+
## Consume the interface
|
|
15
|
+
|
|
16
|
+
Declare a dependency on `{{ cookiecutter.class_prefix }}Module` in the consuming Module and inject `ConversionService` through a constructor. Keep the consuming service in that Module's `scan_packages`:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from python_ddd_framework import AppModule, TransientDependency
|
|
20
|
+
from modules.{{ cookiecutter.module_name }}.module import {{ cookiecutter.class_prefix }}Module
|
|
21
|
+
from modules.{{ cookiecutter.module_name }}.contracts.conversion_service import ConversionService
|
|
22
|
+
|
|
23
|
+
class LabelService(TransientDependency):
|
|
24
|
+
def __init__(self, conversions: ConversionService) -> None:
|
|
25
|
+
self.conversions = conversions
|
|
26
|
+
|
|
27
|
+
def label(self, text: str) -> str:
|
|
28
|
+
return self.conversions.to_upper(text)
|
|
29
|
+
|
|
30
|
+
class LabelsModule(AppModule):
|
|
31
|
+
dependencies = ({{ cookiecutter.class_prefix }}Module,)
|
|
32
|
+
scan_packages = (__package__,)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Add `LabelsModule` to the Host dependencies. Each resolution inside a REQUEST creates a fresh transient instance; dependencies still obey their own scope constraints. No custom Provider, factory, or service locator is required.
|
|
36
|
+
|
|
37
|
+
## Lifecycle and replacement
|
|
38
|
+
|
|
39
|
+
All seven lifecycle hooks are explicit. Empty hooks use `pass`; composition is synchronous and does not start I/O, while initialization and shutdown are asynchronous. Resources belong to the Module that creates them and must be released before shutdown returns.
|
|
40
|
+
|
|
41
|
+
To replace the example, subclass `DefaultConversionService` in a consuming Module that directly depends on `{{ cookiecutter.class_prefix }}Module`. In its `post_configure`, use the framework's `context.services.add_transient(ConversionService, YourConversionService, override=True, reason="...")`. The default is transient and the replacement should preserve that contract. Do not register a parallel lookup path or modify the original contract to point at the replacement.
|
|
42
|
+
|
|
43
|
+
The synchronous `to_upper` method is unmarked and returns `text.upper()`. It is not an ApplicationService, has no interception or HTTP exposure, and does not open a transaction. To use authorization, validation, or UoW interception later, adopt the supported asynchronous instance-method contract and explicitly declare its capability dependencies and policies.
|
|
44
|
+
|
|
45
|
+
This template generates no Domain, persistence, migration, seed, HTTP, background task, or empty layer. Use `pddd add module <name> --template ddd` for the complete six-layer example instead. Project commands can run from the application root or any child directory; Python files and metadata belong under `backend/`.
|
python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/__init__.py.jinja
ADDED
|
File without changes
|
|
File without changes
|
python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/module.py.jinja
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Declare this capability and its scan boundary; service logic belongs in services."""
|
|
2
|
+
|
|
3
|
+
from python_ddd_framework import (
|
|
4
|
+
AppModule,
|
|
5
|
+
ConfigureContext,
|
|
6
|
+
InitializeContext,
|
|
7
|
+
PostConfigureContext,
|
|
8
|
+
PostInitializeContext,
|
|
9
|
+
PreConfigureContext,
|
|
10
|
+
PreInitializeContext,
|
|
11
|
+
ShutdownContext,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class {{ cookiecutter.class_prefix }}Module(AppModule):
|
|
16
|
+
dependencies = ()
|
|
17
|
+
scan_packages = (__package__,)
|
|
18
|
+
|
|
19
|
+
def pre_configure(self, context: PreConfigureContext) -> None:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def post_configure(self, context: PostConfigureContext) -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
async def pre_initialize(self, context: PreInitializeContext) -> None:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
async def initialize(self, context: InitializeContext) -> None:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
async def post_initialize(self, context: PostInitializeContext) -> None:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
async def shutdown(self, context: ShutdownContext) -> None:
|
|
38
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""A minimal synchronous service discovered through the normal DI conventions."""
|
|
2
|
+
|
|
3
|
+
from python_ddd_framework import TransientDependency
|
|
4
|
+
|
|
5
|
+
from ..contracts.conversion_service import ConversionService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DefaultConversionService(ConversionService, TransientDependency):
|
|
9
|
+
def to_upper(self, text: str) -> str:
|
|
10
|
+
return text.upper()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"module_name": "orders", "class_prefix": "Orders"}
|
|
1
|
+
{"module_name": "orders", "class_prefix": "Orders", "http_service_path": "/orders", "http_operation_prefix": "orders_"}
|
|
@@ -1,70 +1,62 @@
|
|
|
1
|
-
# {{ cookiecutter.
|
|
1
|
+
# {{ cookiecutter.class_prefix }} module
|
|
2
2
|
|
|
3
|
-
This module
|
|
3
|
+
This module owns the generated order example. Adapt its business rules from confirmed requirements. The Host explicitly selects its Application, SQLAlchemy, and HttpApi Modules and infrastructure providers.
|
|
4
4
|
|
|
5
|
-
[
|
|
5
|
+
[Project setup](../../../../README.md) · [Working rules](../../../../AGENTS.md) · [Architecture](../../../../docs/architecture.md) · [Directory and coding rules](../../../../docs/development.md#module-layout-and-coding-rules)
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Owners and entry points
|
|
8
8
|
|
|
9
|
-
|
|
|
9
|
+
| Capability | Entry |
|
|
10
10
|
| --- | --- |
|
|
11
|
-
| Shared
|
|
12
|
-
| Aggregate
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
11
|
+
| Shared constraints, permissions, errors, messages | [Shared layer](domain_shared/module.py), [constants](domain_shared/constants/order_constants.py), [permissions](domain_shared/permissions/order_permission_provider.py) |
|
|
12
|
+
| Aggregate, value, domain policy | [Order](domain/entities/order.py), [title](domain/value_objects/order_title.py), [approval policy](domain/services/order_approval_service.py) |
|
|
13
|
+
| Shared immutable value example | [Money](domain_shared/value_objects/money.py), reusable by Domain and public DTOs; no new sample HTTP fields |
|
|
14
|
+
| Repository contract and settings | [Repository](domain/repositories/order_repository.py), [approval setting](domain/settings/approval_settings.py) |
|
|
15
|
+
| User contracts and use cases | [Query contract](application_contracts/services/order_query_service.py), [management](application/services/order_management_service.py), [approval](application/services/order_approval_service.py) |
|
|
16
|
+
| Internal reporting | [Contract](application_contracts/integration_services/order_reporting_service.py), [implementation](application/integration_services/order_reporting_service.py) |
|
|
17
|
+
| Background and lifecycle | [Approval Job](application/background_jobs/order_approval/handler.py), [statistics schedule](application/background_jobs/order_statistics/schedule.py), [maintenance Worker](application/background_workers/order_maintenance_worker.py), [HostedService](application/hosted_services/order_integration_service.py) |
|
|
18
|
+
| Events, interception, settings refresh | [After-commit handler](application/event_handlers/order_changed_handler.py), [interceptor](application/interceptors/order_timing_interceptor.py), [observer](application/setting_handlers/approval_setting_observer.py) |
|
|
19
|
+
| Persistence | [Model](sqlalchemy/models/order_model.py), [repository](sqlalchemy/repositories/order_repository.py), [registration](sqlalchemy/module.py) |
|
|
20
|
+
| Transport | [HTTP exposure](http_api/module.py), [file router](http_api/routers/order_files.py), [WebSocket](http_api/websockets/order_socket.py) |
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
## Behavior and enablement
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
- Query, management, and approval use separate `{{ cookiecutter.class_prefix }}QueryApplicationService`, `{{ cookiecutter.class_prefix }}ManagementApplicationService`, and `{{ cookiecutter.class_prefix }}ApprovalApplicationService` contracts. The exposure decorator contributes the ContractsModule dependency; overrides retain the sample's deliberate paths and operation IDs. Other conventional methods are automatic, with plain GET Pydantic DTOs bound as query parameters unless explicitly declared otherwise. Reporting uses `OrderReportingService` internally without end-user permissions or HTTP exposure.
|
|
25
|
+
- The aggregate owns pending-to-approved transitions. The domain service reads the approval setting; the caller owns version checks, transaction, and save. Repository writes collect events; AFTER_COMMIT removes the cache before online notification. Notification failure cannot undo a committed write.
|
|
26
|
+
- The approval Job participates in transactional enqueue and ignores missing/already-approved orders. The statistics schedule refers to its Job type and typed payload; the Job definition owns persisted name/version/schema. Generated Workers are code-disabled; HostedService and periodic schedule require explicit Module registration as described in the project development guide. The hosted owner waits for its thread to exit. Settings refresh and timing interception reuse framework extensions.
|
|
27
|
+
- File upload/download/stream and `/ws/{{ cookiecutter.module_name }}` call public contracts. Typed messages in [order_messages](domain_shared/messages/order_messages.py) own their versioned names and payload schemas; handlers and the socket explicitly map business values. Sending reports local queue acceptance, with no offline replay, cross-process backplane or client acknowledgment.
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
- The public `{{ cookiecutter.class_prefix }}ApplicationService` contract supports create, get, list, approve, and queued approval. Approval accepts an expected version; repository and database checks protect competing writes.
|
|
26
|
-
- Read and write permissions are defined by the module. Approval also reads the module's runtime setting; queued approval additionally uses its startup Options. Keep these choices separate when changing policy.
|
|
27
|
-
- Writes stage aggregate events through the repository. AFTER_COMMIT handlers invalidate the typed cache and send a notification to the initiating user when that identity is available. A notification failure does not undo the database commit.
|
|
28
|
-
- `ApprovalJob` carries an `order_id` and safely ignores orders that are missing or already approved. Both periodic example workers are code-disabled by default. The hosted integration example is opt-in and is not registered initially.
|
|
29
|
-
- The authenticated WebSocket at `/ws/{{ cookiecutter.module_name }}` sends a snapshot and accepts `{}` to refresh. Online notifications have no cross-process backplane or offline replay. File upload/download/stream examples share the public service boundary.
|
|
29
|
+
Startup [Options](application/options/order_options.py) bind to the `{{ cookiecutter.module_name }}` YAML section; `allow_background_approval` defaults to true. Runtime approval settings and seed contributors belong to Domain. Its Module declares `SettingsModule` and `DataSeedingModule`; contributors are independent DI services, not ApplicationServices. The Domain [OrderApprovalService](domain/services/order_approval_service.py) opts into `ValidationEnabled` through the transitive InvocationModule dependency, retains its transient lifetime, and leaves saving and transaction completion to its caller. Capability dependencies and typed declarations are owned by each Module, while Host selects providers. Change declarations at their owners rather than copying names into another registry.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
## Optional in-process observations
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
The external integration example includes a pure-value [snapshot](domain_shared/messages/order_observation.py)
|
|
34
|
+
and a [Handler](application/hosted_services/order_observation_handler.py). It is not enabled by
|
|
35
|
+
default and does not replace existing callbacks. Follow the project's development guide to add
|
|
36
|
+
MessagingModule, declare the typed channel and register the ACTION Handler. Only pending
|
|
37
|
+
snapshots may coalesce; acceptance, completion, commit and external acknowledgement differ.
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
## Persistence and verification
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
uv run pddd db revision --module {{ cookiecutter.module_name }}
|
|
39
|
-
```
|
|
41
|
+
This module's ordinary models do not automatically become extension targets. To publish a reusable extension point, declare `ExtensionPoints` with explicit DTO/entity targets and use `ExtensibleModel` for those DTOs. Customer modules contribute typed `ExtensionProperties`; SQL column mappings reference the same fields and belong to their persistence Module. The base module keeps table/Schema/core ownership, while customer extension revisions declare native Alembic `depends_on` and include only their declared columns/indexes. Effective DTO and mapper state belong to each Application.
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
From the application root, generate and review a revision, then apply and seed explicitly:
|
|
42
44
|
|
|
43
45
|
```sh
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Startup options are declared in [application/options.py](application/options.py) and bound to the `{{ cookiecutter.module_name }}` YAML section. For example:
|
|
52
|
-
|
|
53
|
-
```yaml
|
|
54
|
-
{{ cookiecutter.module_name }}:
|
|
55
|
-
allow_background_approval: false
|
|
46
|
+
pddd db revision --module {{ cookiecutter.module_name }}
|
|
47
|
+
pddd db upgrade --module {{ cookiecutter.module_name }}
|
|
48
|
+
pddd db status --module {{ cookiecutter.module_name }}
|
|
49
|
+
pddd db seed --module {{ cookiecutter.module_name }}
|
|
50
|
+
cd backend
|
|
51
|
+
uv run pytest src/modules/{{ cookiecutter.module_name }}/tests
|
|
56
52
|
```
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
## Verification and maintenance
|
|
54
|
+
Keep applied migrations immutable. The domain test covers repeat-approval rejection; it does not prove infrastructure, HTTP, background, or deployment behavior. Host tests cover generic health and identity. Update the owning contracts, implementation, documentation, and existing verification when adapting the example. Every Module supplies all seven lifecycle hooks; keep empty stages as `pass`.
|
|
61
55
|
|
|
62
|
-
|
|
56
|
+
## Data and events
|
|
63
57
|
|
|
64
|
-
|
|
65
|
-
uv run pytest src/modules/{{ cookiecutter.module_name }}/tests
|
|
66
|
-
```
|
|
58
|
+
`OrderRepository.find` returns an optional aggregate; `get` requires one. List/count/page methods keep explicit ORM mapping and stable ordering. Business `save` participates in the caller's UoW and records entity and aggregate events once. Nontransactional UoWs reject event-producing writes before staging. The framework repository's `auto_save` flushes without committing an outer UoW.
|
|
67
59
|
|
|
68
|
-
|
|
60
|
+
Inject `DistributedCache[OrderCacheItem]` (or the statistics item), using keys directly. Item classes in `application/caching/` own stable names. Global defaults and explicit entry options are complete alternatives. Use `consider_uow=True` for writes/deletions that should apply after a successful commit. `await events.publish(message)` dispatches immediately without a UoW and follows DOMAIN/AFTER_COMMIT stages inside a transaction; post-commit errors do not undo saved data.
|
|
69
61
|
|
|
70
|
-
|
|
62
|
+
This is the `ddd` template (the default), with all six layers and supported examples. Use `--template basic` for an ordinary in-process service. This module lives under `backend/src/modules/`; native uv/test/build commands run from `backend/`, while installed `pddd` project commands locate the owning backend from any application directory.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""所属能力的实现或契约。"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""所属能力的实现或契约。"""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
from modules.{{ cookiecutter.module_name }}.application.background_jobs.order_approval.payload import (
|
|
4
|
+
ApprovalPayload,
|
|
5
|
+
)
|
|
6
|
+
from modules.{{ cookiecutter.module_name }}.application.options.order_options import {{ cookiecutter.class_prefix }}Options
|
|
7
|
+
from modules.{{ cookiecutter.module_name }}.domain.repositories.order_repository import OrderRepository
|
|
8
|
+
from modules.{{ cookiecutter.module_name }}.domain.services.order_approval_service import OrderApprovalService
|
|
9
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
10
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.enums.order_status import OrderStatus
|
|
11
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.errors.order_errors import APPROVAL_DISABLED
|
|
12
|
+
|
|
13
|
+
from python_ddd_framework import Options
|
|
14
|
+
from python_ddd_framework.background_jobs import BackgroundJobContext, BackgroundJobHandler
|
|
15
|
+
from python_ddd_framework.errors import BusinessError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ApprovalJob(BackgroundJobHandler[ApprovalPayload]):
|
|
19
|
+
name = f"{MODULE_NAME}.approve"
|
|
20
|
+
version = 1
|
|
21
|
+
current = True
|
|
22
|
+
timeout = timedelta(seconds=30)
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
repository: OrderRepository,
|
|
27
|
+
approval: OrderApprovalService,
|
|
28
|
+
options: Options[{{ cookiecutter.class_prefix }}Options],
|
|
29
|
+
) -> None:
|
|
30
|
+
self._repository, self._approval, self._options = repository, approval, options
|
|
31
|
+
|
|
32
|
+
async def execute(self, payload: ApprovalPayload, context: BackgroundJobContext) -> None:
|
|
33
|
+
# 业务扩展:这里执行可重试的后台用例;任务可能重复投递,先核对当前业务状态再产生副作用。
|
|
34
|
+
# 把审批替换为自己的业务,并同步调整载荷;不要删除幂等判断或吞掉应触发重试的异常。
|
|
35
|
+
if self._options.value.allow_background_approval:
|
|
36
|
+
order = await self._repository.find(payload.order_id)
|
|
37
|
+
if order is not None and order.status is OrderStatus.PENDING:
|
|
38
|
+
try:
|
|
39
|
+
await self._approval.approve(order)
|
|
40
|
+
except BusinessError as error:
|
|
41
|
+
# 保留后台业务的既有语义:设置禁止时跳过;其他失败仍交给任务重试。
|
|
42
|
+
if error.definition == APPROVAL_DISABLED:
|
|
43
|
+
return
|
|
44
|
+
raise
|
|
45
|
+
await self._repository.save(order)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""所属能力的实现或契约。"""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import UTC, datetime, timedelta
|
|
3
|
+
|
|
4
|
+
from modules.{{ cookiecutter.module_name }}.application.background_jobs.order_statistics.payload import (
|
|
5
|
+
StatisticsPayload,
|
|
6
|
+
)
|
|
7
|
+
from modules.{{ cookiecutter.module_name }}.application.caching.statistics_cache import OrderStatisticsCacheItem
|
|
8
|
+
from modules.{{ cookiecutter.module_name }}.application_contracts.integration_services.order_reporting_service import (
|
|
9
|
+
OrderReportingService,
|
|
10
|
+
)
|
|
11
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
12
|
+
|
|
13
|
+
from python_ddd_framework.background_jobs import BackgroundJobContext, BackgroundJobHandler
|
|
14
|
+
from python_ddd_framework.caching import DistributedCache
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OrderStatisticsJob(BackgroundJobHandler[StatisticsPayload]):
|
|
18
|
+
name = f"{MODULE_NAME}.scheduled-statistics"
|
|
19
|
+
version = 1
|
|
20
|
+
current = True
|
|
21
|
+
timeout = timedelta(seconds=10)
|
|
22
|
+
|
|
23
|
+
def __init__(self, reporting: OrderReportingService, cache: DistributedCache[OrderStatisticsCacheItem]) -> None:
|
|
24
|
+
self._reporting, self._cache = reporting, cache
|
|
25
|
+
|
|
26
|
+
async def execute(self, payload: StatisticsPayload, context: BackgroundJobContext) -> None:
|
|
27
|
+
count = await self._reporting.get_count()
|
|
28
|
+
await self._cache.set(
|
|
29
|
+
"current",
|
|
30
|
+
OrderStatisticsCacheItem(
|
|
31
|
+
count=count,
|
|
32
|
+
captured_at=datetime.now(UTC),
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
logging.getLogger(__name__).info("Scheduled order statistics: %s", count)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""周期入队声明;Module 显式引用后才启用,处理体由持久 Job 拥有。"""
|
|
2
|
+
|
|
3
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
4
|
+
|
|
5
|
+
from python_ddd_framework.background_jobs import BackgroundJobSchedule
|
|
6
|
+
|
|
7
|
+
from .handler import OrderStatisticsJob
|
|
8
|
+
from .payload import StatisticsPayload
|
|
9
|
+
|
|
10
|
+
STATISTICS_SCHEDULE = BackgroundJobSchedule(
|
|
11
|
+
name=f"{MODULE_NAME}.statistics.every-ten-seconds",
|
|
12
|
+
cron="* * * * * */10",
|
|
13
|
+
job=OrderStatisticsJob,
|
|
14
|
+
payload=StatisticsPayload(),
|
|
15
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""所属能力的实现或契约。"""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
|
|
4
|
+
from modules.{{ cookiecutter.module_name }}.domain.repositories.order_repository import OrderRepository
|
|
5
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
6
|
+
|
|
7
|
+
from python_ddd_framework import BackgroundWorker, BackgroundWorkerContext
|
|
8
|
+
from python_ddd_framework.distributed_lock import DistributedLock
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OrderMaintenanceWorker(BackgroundWorker):
|
|
14
|
+
name = f"{MODULE_NAME}.maintenance"
|
|
15
|
+
enabled = False
|
|
16
|
+
interval = timedelta(seconds=30)
|
|
17
|
+
iteration_timeout = timedelta(seconds=10)
|
|
18
|
+
|
|
19
|
+
def __init__(self, repository: OrderRepository, lock: DistributedLock) -> None:
|
|
20
|
+
self._repository, self._lock = repository, lock
|
|
21
|
+
|
|
22
|
+
async def run_iteration(self, context: BackgroundWorkerContext) -> None:
|
|
23
|
+
# 租约失效由框架请求取消并等待本轮清理;业务不吞 CancelledError。
|
|
24
|
+
async with self._lock.acquire(f"{MODULE_NAME}:maintenance") as acquired:
|
|
25
|
+
if acquired:
|
|
26
|
+
# 业务扩展:需要互斥的维护逻辑写在成功持锁的分支内;当前读取并计数只是演示。
|
|
27
|
+
orders = await self._repository.get_list()
|
|
28
|
+
logger.info("Order maintenance completed: %s", len(orders))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
|
|
4
|
+
from modules.{{ cookiecutter.module_name }}.application_contracts.integration_services.order_reporting_service import (
|
|
5
|
+
OrderReportingService,
|
|
6
|
+
)
|
|
7
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
8
|
+
|
|
9
|
+
from python_ddd_framework import BackgroundWorker, BackgroundWorkerContext
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OrderStatisticsWorker(BackgroundWorker):
|
|
15
|
+
name = f"{MODULE_NAME}.statistics"
|
|
16
|
+
enabled = False
|
|
17
|
+
interval = timedelta(minutes=5)
|
|
18
|
+
iteration_timeout = timedelta(seconds=10)
|
|
19
|
+
|
|
20
|
+
def __init__(self, reporting: OrderReportingService) -> None:
|
|
21
|
+
self._reporting = reporting
|
|
22
|
+
|
|
23
|
+
async def run_iteration(self, context: BackgroundWorkerContext) -> None:
|
|
24
|
+
# 业务扩展:这里是一轮周期统计的处理体;替换统计/汇总逻辑,不自行启动 while 循环或线程。
|
|
25
|
+
# 启用这个默认关闭的示例 Worker 前,先确定自己的执行周期和失败处理要求。
|
|
26
|
+
logger.info("Order count: %s", await self._reporting.get_count())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""所属能力的实现或契约。"""
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from modules.{{ cookiecutter.module_name }}.application_contracts.views.order_view import OrderView
|
|
2
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
3
|
+
|
|
4
|
+
from python_ddd_framework.caching import cache_name
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@cache_name(f"{MODULE_NAME}.view")
|
|
8
|
+
class OrderCacheItem(OrderView):
|
|
9
|
+
pass
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from modules.{{ cookiecutter.module_name }}.application_contracts.views.order_statistics_snapshot import OrderStatisticsSnapshot
|
|
2
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.constants.order_constants import MODULE_NAME
|
|
3
|
+
|
|
4
|
+
from python_ddd_framework.caching import cache_name
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@cache_name(f"{MODULE_NAME}.statistics")
|
|
8
|
+
class OrderStatisticsCacheItem(OrderStatisticsSnapshot):
|
|
9
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""所属能力的实现或契约。"""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from modules.{{ cookiecutter.module_name }}.application.caching.order_cache import OrderCacheItem
|
|
2
|
+
from modules.{{ cookiecutter.module_name }}.domain.events.order_changed import OrderChanged
|
|
3
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.messages.order_messages import OrderChangedMessage
|
|
4
|
+
|
|
5
|
+
from python_ddd_framework import LocalEventPhase, local_event_handler
|
|
6
|
+
from python_ddd_framework.caching import DistributedCache
|
|
7
|
+
from python_ddd_framework.realtime import RealtimePublisher
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@local_event_handler(phase=LocalEventPhase.AFTER_COMMIT)
|
|
11
|
+
class OrderChangedHandler:
|
|
12
|
+
def __init__(self, cache: DistributedCache[OrderCacheItem], realtime: RealtimePublisher) -> None:
|
|
13
|
+
self._cache, self._realtime = cache, realtime
|
|
14
|
+
|
|
15
|
+
async def handle(self, event: OrderChanged) -> None:
|
|
16
|
+
# 业务扩展:这里处理提交后的缓存失效/在线通知,替换自己的事件、缓存定义和消息载荷。
|
|
17
|
+
# 必须随业务一起回滚的规则应放在聚合或 DOMAIN 阶段;这里失败不会撤销已经提交的数据。
|
|
18
|
+
await self._cache.remove(str(event.order_id))
|
|
19
|
+
if event.user_id is not None:
|
|
20
|
+
await self._realtime.send_to_user(
|
|
21
|
+
event.user_id,
|
|
22
|
+
OrderChangedMessage(id=event.order_id, status=event.status),
|
|
23
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""所属能力的实现或契约。"""
|
|
@@ -4,6 +4,10 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
from threading import Event, Thread
|
|
6
6
|
|
|
7
|
+
from modules.{{ cookiecutter.module_name }}.application_contracts.integration_services.order_reporting_service import (
|
|
8
|
+
OrderReportingService,
|
|
9
|
+
)
|
|
10
|
+
|
|
7
11
|
from python_ddd_framework import (
|
|
8
12
|
HostedService,
|
|
9
13
|
HostedServiceContext,
|
|
@@ -12,8 +16,9 @@ from python_ddd_framework import (
|
|
|
12
16
|
)
|
|
13
17
|
|
|
14
18
|
|
|
15
|
-
async def observe_integration() -> None:
|
|
16
|
-
|
|
19
|
+
async def observe_integration(reporting: OrderReportingService) -> None:
|
|
20
|
+
count = await reporting.get_count()
|
|
21
|
+
logging.getLogger(__name__).info("External integration callback observed %s orders", count)
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
class OrderIntegrationService(HostedService):
|
|
@@ -39,3 +44,4 @@ class OrderIntegrationService(HostedService):
|
|
|
39
44
|
if self._thread is not None:
|
|
40
45
|
await asyncio.to_thread(self._thread.join)
|
|
41
46
|
self._thread = None
|
|
47
|
+
logging.getLogger(__name__).info("External integration thread stopped")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""可选线程集成的快照消费者;通过消息能力进入一次受管理调用。"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.messages.order_observation import (
|
|
6
|
+
OrderObservation,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from python_ddd_framework import MessageHandler, unit_of_work
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OrderObservationHandler(MessageHandler[OrderObservation]):
|
|
13
|
+
@unit_of_work(disabled=True)
|
|
14
|
+
async def handle(self, message: OrderObservation) -> None:
|
|
15
|
+
# 替换为快照应用逻辑;若需要保存数据,显式选择事务而非把接收等待放入 UoW。
|
|
16
|
+
logging.getLogger(__name__).info("Observed order count: %s", message.count)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""所属能力的实现或契约。"""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""内部统计使用正式 IntegrationService 调用链,不开放 HTTP。"""
|
|
2
|
+
|
|
3
|
+
from modules.{{ cookiecutter.module_name }}.application_contracts.integration_services.order_reporting_service import (
|
|
4
|
+
OrderReportingService,
|
|
5
|
+
)
|
|
6
|
+
from modules.{{ cookiecutter.module_name }}.domain.repositories.order_repository import OrderRepository
|
|
7
|
+
|
|
8
|
+
from python_ddd_framework import IntegrationService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _OrderReportingService(IntegrationService, OrderReportingService):
|
|
12
|
+
def __init__(self, repository: OrderRepository) -> None:
|
|
13
|
+
self._repository = repository
|
|
14
|
+
|
|
15
|
+
async def get_count(self) -> int:
|
|
16
|
+
return await self._repository.get_count()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""所属能力的实现或契约。"""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Record invocation timing through the framework's interceptor extension."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from time import monotonic
|
|
5
|
+
|
|
6
|
+
from python_ddd_framework import Interceptor, MethodInvocation
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OrderTimingInterceptor(Interceptor):
|
|
10
|
+
async def intercept(self, invocation: MethodInvocation) -> object:
|
|
11
|
+
started = monotonic()
|
|
12
|
+
try:
|
|
13
|
+
return await invocation.proceed()
|
|
14
|
+
finally:
|
|
15
|
+
logging.getLogger(__name__).info(
|
|
16
|
+
"Order invocation completed in %.4fs", monotonic() - started
|
|
17
|
+
)
|