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,205 @@
|
|
|
1
|
+
"""单通道的接受、顺序消费与待执行快照 owner;原生 stream 持有顺序。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import concurrent.futures
|
|
7
|
+
from collections.abc import Callable, Hashable
|
|
8
|
+
from contextvars import Context
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
11
|
+
|
|
12
|
+
from dishka import AsyncContainer
|
|
13
|
+
|
|
14
|
+
from ..application_services import ApplicationServiceCatalog
|
|
15
|
+
from ..application_services.errors import ApplicationInvocationRejectedError
|
|
16
|
+
from ..application_services.invocation import _InvocationReservation, _InvocationRuntime
|
|
17
|
+
from ..hosted_services.bridge import HostedServiceContext, _HostedServiceBridge
|
|
18
|
+
from ..invocation.interception import _EntryInterceptionCatalog
|
|
19
|
+
from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME
|
|
20
|
+
from .contracts import MessageChannelDefinition, MessageRejectedError, MessageState
|
|
21
|
+
from .receipt import MessageReceipt
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(slots=True)
|
|
30
|
+
class _Delivery(Generic[T]):
|
|
31
|
+
message: T
|
|
32
|
+
receipt: MessageReceipt
|
|
33
|
+
reservation: _InvocationReservation
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MessageChannel(Generic[T]):
|
|
37
|
+
"""APP 注入的类型化发送入口;容量包含正在执行的消息,满载明确拒绝。"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, definition: MessageChannelDefinition[T]) -> None:
|
|
40
|
+
self._definition = definition
|
|
41
|
+
self._pending: dict[Hashable, _Delivery[T]] = {}
|
|
42
|
+
self._active: _Delivery[T] | None = None
|
|
43
|
+
self._accepting = False
|
|
44
|
+
self._failure: BaseException | None = None
|
|
45
|
+
self._context: HostedServiceContext | None = None
|
|
46
|
+
self._bridge: _HostedServiceBridge | None = None
|
|
47
|
+
self._runtime: _InvocationRuntime | None = None
|
|
48
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
49
|
+
self._send: MemoryObjectSendStream[Hashable] | None = None
|
|
50
|
+
self._receive: MemoryObjectReceiveStream[Hashable] | None = None
|
|
51
|
+
|
|
52
|
+
async def send(self, message: T) -> MessageReceipt:
|
|
53
|
+
"""返回只表示接受;本入口无等待发送者、无后台发送 task。"""
|
|
54
|
+
self._require_loop()
|
|
55
|
+
if self._failure is not None:
|
|
56
|
+
raise MessageRejectedError("consumer_failed")
|
|
57
|
+
if not self._accepting:
|
|
58
|
+
raise MessageRejectedError("closed")
|
|
59
|
+
assert self._runtime is not None
|
|
60
|
+
try:
|
|
61
|
+
self._runtime.require_accepting_work()
|
|
62
|
+
except ApplicationInvocationRejectedError:
|
|
63
|
+
raise MessageRejectedError("closed") from None
|
|
64
|
+
if not isinstance(message, self._definition.message_type):
|
|
65
|
+
raise TypeError("Message does not match the declared channel type")
|
|
66
|
+
key = self._definition.snapshot_key(message) if self._definition.snapshot_key else object()
|
|
67
|
+
hash(key)
|
|
68
|
+
receipt = MessageReceipt()
|
|
69
|
+
if key in self._pending:
|
|
70
|
+
delivery = self._pending[key]
|
|
71
|
+
delivery.receipt._finish(MessageState.SUPERSEDED)
|
|
72
|
+
delivery.message = message
|
|
73
|
+
delivery.receipt = receipt
|
|
74
|
+
return receipt
|
|
75
|
+
if len(self._pending) + (self._active is not None) >= self._definition.capacity:
|
|
76
|
+
raise MessageRejectedError("capacity_exceeded")
|
|
77
|
+
assert self._runtime is not None and self._send is not None
|
|
78
|
+
# 接受与许可预留在同一 loop turn 完成;slot 的许可随最终快照转交,不携带 scope。
|
|
79
|
+
reservation = self._runtime.reserve()
|
|
80
|
+
self._pending[key] = _Delivery(message, receipt, reservation)
|
|
81
|
+
self._send.send_nowait(key)
|
|
82
|
+
return receipt
|
|
83
|
+
|
|
84
|
+
def submit(self, message: T) -> concurrent.futures.Future[MessageReceipt]:
|
|
85
|
+
"""外部线程入口;bridge pending 上限等于通道容量,Future 结果才表示接受。"""
|
|
86
|
+
context = self._context
|
|
87
|
+
if context is None:
|
|
88
|
+
raise MessageRejectedError("closed")
|
|
89
|
+
# to_thread 可能复制生产者的 ContextVar;线程交接只传消息值,不传调用 scope。
|
|
90
|
+
return Context().run(context.submit_call, self.send, message)
|
|
91
|
+
|
|
92
|
+
def _start(
|
|
93
|
+
self,
|
|
94
|
+
container: AsyncContainer,
|
|
95
|
+
catalog: ApplicationServiceCatalog,
|
|
96
|
+
runtime: _InvocationRuntime,
|
|
97
|
+
) -> None:
|
|
98
|
+
import anyio
|
|
99
|
+
|
|
100
|
+
if self._loop is not None:
|
|
101
|
+
raise RuntimeError("Channel instances cannot be restarted; build a new Application")
|
|
102
|
+
self._loop = asyncio.get_running_loop()
|
|
103
|
+
self._runtime = runtime
|
|
104
|
+
self._send, self._receive = anyio.create_memory_object_stream[Hashable](
|
|
105
|
+
self._definition.capacity
|
|
106
|
+
)
|
|
107
|
+
self._bridge = _HostedServiceBridge(
|
|
108
|
+
name=f"message:{self._definition.message_type.__qualname__}",
|
|
109
|
+
loop=self._loop,
|
|
110
|
+
container=container,
|
|
111
|
+
catalog=catalog,
|
|
112
|
+
limit=self._definition.capacity,
|
|
113
|
+
)
|
|
114
|
+
self._context = HostedServiceContext(
|
|
115
|
+
self._definition.message_type.__qualname__, self._bridge
|
|
116
|
+
)
|
|
117
|
+
self._bridge.open()
|
|
118
|
+
self._accepting = True
|
|
119
|
+
|
|
120
|
+
def _close(self) -> None:
|
|
121
|
+
self._accepting = False
|
|
122
|
+
if self._bridge is not None:
|
|
123
|
+
self._bridge.close()
|
|
124
|
+
if self._send is not None:
|
|
125
|
+
# 发送端仅由此 owner 持有,不向生产者暴露 clone;因此关闭即关闭全部发送端。
|
|
126
|
+
self._send.close()
|
|
127
|
+
|
|
128
|
+
async def _dispatch(self, message: T, container: AsyncContainer) -> None:
|
|
129
|
+
runtime = await container.get(_InvocationRuntime)
|
|
130
|
+
entries = await container.get(_EntryInterceptionCatalog)
|
|
131
|
+
await entries.invoke(
|
|
132
|
+
runtime,
|
|
133
|
+
container,
|
|
134
|
+
self._definition.handler,
|
|
135
|
+
"handle",
|
|
136
|
+
(message,),
|
|
137
|
+
connection_name=_DEFAULT_CONNECTION_NAME,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def _execute(self, delivery: _Delivery[T]) -> None:
|
|
141
|
+
import anyio
|
|
142
|
+
|
|
143
|
+
assert self._bridge is not None and self._runtime is not None
|
|
144
|
+
# bridge 执行任务不复制消费者/生产者的 ACTION、UoW 或 AnyIO 取消上下文。
|
|
145
|
+
task = asyncio.create_task(
|
|
146
|
+
self._bridge.invoke_reserved(delivery.reservation, self._dispatch, delivery.message),
|
|
147
|
+
context=Context(),
|
|
148
|
+
)
|
|
149
|
+
try:
|
|
150
|
+
await asyncio.shield(task)
|
|
151
|
+
except asyncio.CancelledError:
|
|
152
|
+
task.cancel()
|
|
153
|
+
# CancelScope 只请求取消;task 的真实退出才是事务和 finalizer 完成依据。
|
|
154
|
+
with anyio.CancelScope(shield=True):
|
|
155
|
+
await task
|
|
156
|
+
# 处理器处理取消后若正常返回,应保留真实完成事实;下一次 receive 仍会取消。
|
|
157
|
+
finally:
|
|
158
|
+
if not delivery.reservation.executing:
|
|
159
|
+
with anyio.CancelScope(shield=True):
|
|
160
|
+
await self._runtime.discard(delivery.reservation)
|
|
161
|
+
|
|
162
|
+
async def _consume(self, report: Callable[[BaseException], None]) -> None:
|
|
163
|
+
import anyio
|
|
164
|
+
|
|
165
|
+
assert self._receive is not None and self._runtime is not None
|
|
166
|
+
try:
|
|
167
|
+
async with self._receive:
|
|
168
|
+
async for key in self._receive:
|
|
169
|
+
delivery = self._pending.pop(key)
|
|
170
|
+
self._active = delivery
|
|
171
|
+
delivery.receipt._state = MessageState.PROCESSING
|
|
172
|
+
try:
|
|
173
|
+
await self._execute(delivery)
|
|
174
|
+
except BaseException as error:
|
|
175
|
+
state = (
|
|
176
|
+
MessageState.CANCELLED
|
|
177
|
+
if isinstance(error, asyncio.CancelledError)
|
|
178
|
+
else MessageState.FAILED
|
|
179
|
+
)
|
|
180
|
+
delivery.receipt._finish(state, error)
|
|
181
|
+
raise
|
|
182
|
+
else:
|
|
183
|
+
delivery.receipt._finish(MessageState.COMPLETED)
|
|
184
|
+
finally:
|
|
185
|
+
self._active = None
|
|
186
|
+
except BaseException as error:
|
|
187
|
+
self._failure = error
|
|
188
|
+
report(error)
|
|
189
|
+
finally:
|
|
190
|
+
self._close()
|
|
191
|
+
with anyio.CancelScope(shield=True):
|
|
192
|
+
for delivery in self._pending.values():
|
|
193
|
+
await self._runtime.discard(delivery.reservation)
|
|
194
|
+
delivery.receipt._finish(MessageState.ABORTED, self._failure)
|
|
195
|
+
self._pending.clear()
|
|
196
|
+
|
|
197
|
+
async def _drain_bridge(self, timeout: float) -> None:
|
|
198
|
+
if self._bridge is not None:
|
|
199
|
+
await self._bridge.drain(timeout)
|
|
200
|
+
|
|
201
|
+
def _require_loop(self) -> None:
|
|
202
|
+
if self._loop is None:
|
|
203
|
+
raise MessageRejectedError("closed")
|
|
204
|
+
if asyncio.get_running_loop() is not self._loop:
|
|
205
|
+
raise MessageRejectedError("wrong_event_loop")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""进程内消息声明与处理结果;不提供持久化、事务提交或外部确认契约。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Callable, Hashable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Generic, TypeVar
|
|
11
|
+
|
|
12
|
+
from ..modularity import ModuleDeclaration, ModuleDependency, ModuleRef
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MessageHandler(ABC, Generic[T]):
|
|
18
|
+
@abstractmethod
|
|
19
|
+
async def handle(self, message: T) -> None: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class MessageChannelDefinition(ModuleDeclaration, Generic[T]):
|
|
24
|
+
message_type: type[T]
|
|
25
|
+
handler: type[MessageHandler[T]]
|
|
26
|
+
capacity: int = 64
|
|
27
|
+
snapshot_key: Callable[[T], Hashable] | None = None
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
if not isinstance(self.message_type, type):
|
|
31
|
+
raise TypeError("message_type must be a concrete type")
|
|
32
|
+
if (
|
|
33
|
+
not isinstance(self.handler, type)
|
|
34
|
+
or not issubclass(self.handler, MessageHandler)
|
|
35
|
+
or inspect.isabstract(self.handler)
|
|
36
|
+
):
|
|
37
|
+
raise TypeError("handler must be a concrete MessageHandler")
|
|
38
|
+
if type(self.capacity) is not int or self.capacity < 1:
|
|
39
|
+
raise ValueError("capacity must be a positive integer")
|
|
40
|
+
if self.snapshot_key is not None and (
|
|
41
|
+
not callable(self.snapshot_key) or inspect.iscoroutinefunction(self.snapshot_key)
|
|
42
|
+
):
|
|
43
|
+
raise TypeError("snapshot_key must be a synchronous business identity function")
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def required_module(self) -> ModuleDependency:
|
|
47
|
+
return ModuleRef("python_ddd_framework.messaging.module:MessagingModule")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MessageState(str, Enum):
|
|
51
|
+
PENDING = "pending"
|
|
52
|
+
PROCESSING = "processing"
|
|
53
|
+
COMPLETED = "completed"
|
|
54
|
+
SUPERSEDED = "superseded"
|
|
55
|
+
FAILED = "failed"
|
|
56
|
+
CANCELLED = "cancelled"
|
|
57
|
+
ABORTED = "aborted"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class MessageOutcome:
|
|
62
|
+
state: MessageState
|
|
63
|
+
error: BaseException | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MessageRejectedError(RuntimeError):
|
|
67
|
+
def __init__(self, reason: str) -> None:
|
|
68
|
+
self.reason = reason
|
|
69
|
+
super().__init__(f"Message not accepted: {reason}")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""消息声明、类型化 DI 绑定与消费者生命周期的唯一能力 owner。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import cast, get_type_hints
|
|
5
|
+
|
|
6
|
+
from dishka import DEFAULT_COMPONENT, DependencyKey, Provider, Scope
|
|
7
|
+
|
|
8
|
+
from ..hosted_services.module import HostedServicesModule
|
|
9
|
+
from ..invocation.interception import InvocationEntry
|
|
10
|
+
from ..invocation.module import InvocationModule
|
|
11
|
+
from ..lifecycle import ConfigureContext
|
|
12
|
+
from ..modularity import AppModule
|
|
13
|
+
from ..modularity.discovery import _require_type_dependencies
|
|
14
|
+
from ..services.composition import (
|
|
15
|
+
ServiceBindingContext,
|
|
16
|
+
ServiceComposition,
|
|
17
|
+
ServiceValidationContext,
|
|
18
|
+
)
|
|
19
|
+
from .channel import MessageChannel
|
|
20
|
+
from .contracts import MessageChannelDefinition, MessageHandler
|
|
21
|
+
from .options import MessagingOptions
|
|
22
|
+
from .runtime import _MessagingRuntime
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MessagingModule(AppModule):
|
|
26
|
+
dependencies = (HostedServicesModule,)
|
|
27
|
+
_runtime: _MessagingRuntime
|
|
28
|
+
|
|
29
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
30
|
+
context.configure.default(MessagingOptions, section="messaging")
|
|
31
|
+
context.services.add_composition(
|
|
32
|
+
ServiceComposition(bind=self._bind, validate=self._validate)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def _bind(self, context: ServiceBindingContext) -> None:
|
|
36
|
+
invocation = context.modules.instance(InvocationModule)
|
|
37
|
+
definitions = context.modules.declarations(MessageChannelDefinition)
|
|
38
|
+
channels: dict[type[object], MessageChannel[object]] = {}
|
|
39
|
+
provider = Provider()
|
|
40
|
+
for owner, raw_definition in definitions:
|
|
41
|
+
definition = cast(MessageChannelDefinition[object], raw_definition)
|
|
42
|
+
if definition.message_type in channels:
|
|
43
|
+
raise ValueError("A message type must have exactly one channel owner")
|
|
44
|
+
factory = context.arbitration.single_factory(DependencyKey(definition.handler, ""))
|
|
45
|
+
if (
|
|
46
|
+
factory is None
|
|
47
|
+
or factory.scope is not Scope.ACTION
|
|
48
|
+
or not factory.cache
|
|
49
|
+
or not isinstance(factory.source, type)
|
|
50
|
+
or not issubclass(factory.source, MessageHandler)
|
|
51
|
+
):
|
|
52
|
+
raise TypeError("MessageHandler requires a cached ACTION single class binding")
|
|
53
|
+
hints = get_type_hints(factory.source.handle)
|
|
54
|
+
if hints.get("message") is not definition.message_type or hints.get(
|
|
55
|
+
"return"
|
|
56
|
+
) is not type(None):
|
|
57
|
+
raise TypeError("MessageHandler.handle must declare (message: ChannelType) -> None")
|
|
58
|
+
contribution = next(
|
|
59
|
+
item.contribution for item in context.arbitration.drafts if item.factory is factory
|
|
60
|
+
)
|
|
61
|
+
_require_type_dependencies(
|
|
62
|
+
context.modules, contribution.owner, factory.source, source=contribution.source
|
|
63
|
+
)
|
|
64
|
+
invocation.entries.append(
|
|
65
|
+
InvocationEntry(
|
|
66
|
+
definition.handler,
|
|
67
|
+
"handle",
|
|
68
|
+
owner.key,
|
|
69
|
+
1,
|
|
70
|
+
managed_method=True,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
channel = MessageChannel(definition)
|
|
74
|
+
channels[definition.message_type] = channel
|
|
75
|
+
# 闭合泛型 key 来自唯一声明;原生 Provider 唯一拥有 APP 缓存。
|
|
76
|
+
provider.provide(
|
|
77
|
+
_channel_factory(channel),
|
|
78
|
+
# DI key 来自已验证的运行时类型,静态 checker 无法表达该值。
|
|
79
|
+
provides=MessageChannel[definition.message_type], # type: ignore[name-defined]
|
|
80
|
+
scope=Scope.APP,
|
|
81
|
+
)
|
|
82
|
+
self._runtime = _MessagingRuntime(
|
|
83
|
+
tuple(channels.values()), context.options.get(MessagingOptions)
|
|
84
|
+
)
|
|
85
|
+
self.runtime = self._runtime.participant()
|
|
86
|
+
context.add_provider(provider)
|
|
87
|
+
|
|
88
|
+
def _validate(self, context: ServiceValidationContext) -> None:
|
|
89
|
+
for _, definition in context.binding.modules.declarations(MessageChannelDefinition):
|
|
90
|
+
key = DependencyKey(definition.handler, DEFAULT_COMPONENT)
|
|
91
|
+
row = next((row for row in context.native_graph if row.key == key), None)
|
|
92
|
+
if row is None:
|
|
93
|
+
raise TypeError("MessageHandler has no final single binding")
|
|
94
|
+
factory = context.native_graph.effective_factory(row)
|
|
95
|
+
if factory.scope is not Scope.ACTION or not factory.cache:
|
|
96
|
+
raise TypeError("Final MessageHandler binding must be cached ACTION")
|
|
97
|
+
self._runtime.container = context.container
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _channel_factory(channel: MessageChannel[object]) -> Callable[[], MessageChannel[object]]:
|
|
101
|
+
return lambda: channel
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Host 配置消息消费者的退出期限。"""
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
from pydantic import ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
from ..options import BaseOptions
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MessagingOptions(BaseOptions):
|
|
11
|
+
model_config = ConfigDict(extra="forbid")
|
|
12
|
+
|
|
13
|
+
shutdown_timeout: timedelta = Field(default=timedelta(seconds=30), gt=timedelta(0))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""单次接受回执;观察者取消不撤销已经接受的消息。"""
|
|
2
|
+
|
|
3
|
+
from .contracts import MessageOutcome, MessageState
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MessageReceipt:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
import anyio
|
|
9
|
+
|
|
10
|
+
self._state = MessageState.PENDING
|
|
11
|
+
self._outcome: MessageOutcome | None = None
|
|
12
|
+
self._done = anyio.Event()
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def state(self) -> MessageState:
|
|
16
|
+
return self._state
|
|
17
|
+
|
|
18
|
+
async def wait(self) -> MessageOutcome:
|
|
19
|
+
await self._done.wait()
|
|
20
|
+
assert self._outcome is not None
|
|
21
|
+
return self._outcome
|
|
22
|
+
|
|
23
|
+
def _finish(self, state: MessageState, error: BaseException | None = None) -> None:
|
|
24
|
+
assert self._outcome is None
|
|
25
|
+
self._state = state
|
|
26
|
+
self._outcome = MessageOutcome(state, error)
|
|
27
|
+
self._done.set()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""消息能力的 Application-local 生命周期;内核只看通用参与者。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Awaitable
|
|
7
|
+
from contextvars import Context
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from dishka import AsyncContainer
|
|
11
|
+
|
|
12
|
+
from ..application_services import ApplicationServiceCatalog
|
|
13
|
+
from ..application_services.invocation import _InvocationRuntime
|
|
14
|
+
from ..errors import ApplicationRuntimeError
|
|
15
|
+
from ..lifecycle import RuntimeParticipant, ShutdownReason
|
|
16
|
+
from .channel import MessageChannel
|
|
17
|
+
from .options import MessagingOptions
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
import anyio
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _MessagingRuntime:
|
|
24
|
+
def __init__(
|
|
25
|
+
self, channels: tuple[MessageChannel[object], ...], options: MessagingOptions
|
|
26
|
+
) -> None:
|
|
27
|
+
self.channels = channels
|
|
28
|
+
self.options = options
|
|
29
|
+
self.container: AsyncContainer | None = None
|
|
30
|
+
self._task: asyncio.Task[None] | None = None
|
|
31
|
+
self._scope: anyio.CancelScope | None = None
|
|
32
|
+
self._started = asyncio.Event()
|
|
33
|
+
self._failed = asyncio.Event()
|
|
34
|
+
self._failures: list[ApplicationRuntimeError] = []
|
|
35
|
+
self._stopping = False
|
|
36
|
+
self._enabled = True
|
|
37
|
+
|
|
38
|
+
def participant(self) -> RuntimeParticipant:
|
|
39
|
+
return RuntimeParticipant(
|
|
40
|
+
prepare=self._prepare,
|
|
41
|
+
start=self.start,
|
|
42
|
+
stop=self.stop,
|
|
43
|
+
close_admission=self.close,
|
|
44
|
+
ready=self.ready,
|
|
45
|
+
failure=self.failure,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def _prepare(self, enabled: bool) -> None:
|
|
49
|
+
self._enabled = enabled
|
|
50
|
+
|
|
51
|
+
def ready(self) -> bool:
|
|
52
|
+
return not self._enabled or (
|
|
53
|
+
self._started.is_set() and not self._failures and not self._stopping
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def start(self) -> None:
|
|
57
|
+
if not self._enabled:
|
|
58
|
+
return
|
|
59
|
+
self._task = asyncio.create_task(self._run(), context=Context())
|
|
60
|
+
await self._started.wait()
|
|
61
|
+
if self._failures:
|
|
62
|
+
raise self._failures[0]
|
|
63
|
+
|
|
64
|
+
async def _run(self) -> None:
|
|
65
|
+
import anyio
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
assert self.container is not None
|
|
69
|
+
runtime = await self.container.get(_InvocationRuntime)
|
|
70
|
+
catalog = await self.container.get(ApplicationServiceCatalog)
|
|
71
|
+
if self._stopping:
|
|
72
|
+
return
|
|
73
|
+
async with anyio.create_task_group() as group:
|
|
74
|
+
self._scope = group.cancel_scope
|
|
75
|
+
for channel in self.channels:
|
|
76
|
+
channel._start(self.container, catalog, runtime)
|
|
77
|
+
group.start_soon(channel._consume, self._report)
|
|
78
|
+
self._started.set()
|
|
79
|
+
except BaseException as error:
|
|
80
|
+
self._report(error)
|
|
81
|
+
finally:
|
|
82
|
+
self._started.set()
|
|
83
|
+
|
|
84
|
+
def _report(self, error: BaseException) -> None:
|
|
85
|
+
if isinstance(error, asyncio.CancelledError) and self._stopping:
|
|
86
|
+
if error.__cause__ is None:
|
|
87
|
+
return
|
|
88
|
+
# 取消本身是正常的停止请求,但其 cause 中的清理失败仍必须报告。
|
|
89
|
+
error = error.__cause__
|
|
90
|
+
self._failures.append(
|
|
91
|
+
ApplicationRuntimeError(component="messaging", error_type=type(error).__name__)
|
|
92
|
+
)
|
|
93
|
+
self._failed.set()
|
|
94
|
+
self.close()
|
|
95
|
+
|
|
96
|
+
def close(self) -> None:
|
|
97
|
+
for channel in self.channels:
|
|
98
|
+
channel._close()
|
|
99
|
+
|
|
100
|
+
def failure(self) -> Awaitable[ApplicationRuntimeError] | None:
|
|
101
|
+
return self._wait_failure() if self._enabled else None
|
|
102
|
+
|
|
103
|
+
async def _wait_failure(self) -> ApplicationRuntimeError:
|
|
104
|
+
await self._failed.wait()
|
|
105
|
+
return self._failures[0]
|
|
106
|
+
|
|
107
|
+
async def stop(self, reason: ShutdownReason) -> tuple[BaseException, ...]:
|
|
108
|
+
import anyio
|
|
109
|
+
|
|
110
|
+
self._stopping = True
|
|
111
|
+
self.close()
|
|
112
|
+
failures: list[BaseException] = []
|
|
113
|
+
timeout = self.options.shutdown_timeout.total_seconds()
|
|
114
|
+
if self._task is not None:
|
|
115
|
+
with anyio.move_on_after(timeout) as deadline:
|
|
116
|
+
await asyncio.shield(self._task)
|
|
117
|
+
if deadline.cancel_called:
|
|
118
|
+
failures.append(TimeoutError("Message drain deadline exceeded"))
|
|
119
|
+
if self._scope is not None:
|
|
120
|
+
self._scope.cancel()
|
|
121
|
+
await self._task
|
|
122
|
+
for channel in self.channels:
|
|
123
|
+
await channel._drain_bridge(timeout)
|
|
124
|
+
return (*failures, *self._failures)
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
"""Module contracts、graph builder 与 immutable registry capability。"""
|
|
2
2
|
|
|
3
|
-
from .contracts import
|
|
3
|
+
from .contracts import (
|
|
4
|
+
AppModule,
|
|
5
|
+
ModuleDeclaration,
|
|
6
|
+
ModuleDependency,
|
|
7
|
+
ModuleDependencyEdge,
|
|
8
|
+
ModuleKey,
|
|
9
|
+
ModuleRef,
|
|
10
|
+
ModuleRequirement,
|
|
11
|
+
)
|
|
4
12
|
from .registry import ModuleDescriptor, ModuleGraphSnapshot, ModuleRegistry
|
|
5
13
|
|
|
6
14
|
__all__ = (
|
|
7
15
|
"AppModule",
|
|
16
|
+
"ModuleDeclaration",
|
|
8
17
|
"ModuleDependency",
|
|
9
18
|
"ModuleDependencyEdge",
|
|
10
19
|
"ModuleDescriptor",
|
|
@@ -12,4 +21,5 @@ __all__ = (
|
|
|
12
21
|
"ModuleKey",
|
|
13
22
|
"ModuleRef",
|
|
14
23
|
"ModuleRegistry",
|
|
24
|
+
"ModuleRequirement",
|
|
15
25
|
)
|
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
5
6
|
from collections.abc import Mapping
|
|
6
7
|
from dataclasses import dataclass
|
|
7
|
-
from types import MappingProxyType
|
|
8
|
-
from typing import TYPE_CHECKING,
|
|
8
|
+
from types import FunctionType, MappingProxyType
|
|
9
|
+
from typing import TYPE_CHECKING, ClassVar, TypeAlias
|
|
9
10
|
|
|
10
11
|
from ..diagnostics import SourceLocation
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
|
-
from ..background_jobs import BackgroundJobDefinition, BackgroundJobSchedule
|
|
14
|
-
from ..background_workers import BackgroundWorker, BackgroundWorkerDefinition
|
|
15
|
-
from ..caching import CacheDefinition
|
|
16
|
-
from ..hosted_services import HostedService
|
|
17
14
|
from ..lifecycle import (
|
|
18
15
|
ConfigureContext,
|
|
19
16
|
InitializeContext,
|
|
@@ -21,10 +18,9 @@ if TYPE_CHECKING:
|
|
|
21
18
|
PostInitializeContext,
|
|
22
19
|
PreConfigureContext,
|
|
23
20
|
PreInitializeContext,
|
|
21
|
+
RuntimeParticipant,
|
|
24
22
|
ShutdownContext,
|
|
25
23
|
)
|
|
26
|
-
from ..notifications import NotificationDefinition
|
|
27
|
-
from ..settings.handlers import SettingHandlerDefinition
|
|
28
24
|
|
|
29
25
|
|
|
30
26
|
@dataclass(frozen=True, slots=True)
|
|
@@ -51,20 +47,55 @@ class ModuleDependencyEdge:
|
|
|
51
47
|
source: SourceLocation
|
|
52
48
|
|
|
53
49
|
|
|
50
|
+
class ModuleDeclaration(ABC):
|
|
51
|
+
"""能力拥有的不可变声明;内核只认识其依赖,不解释声明内容。"""
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def required_module(self) -> ModuleDependency:
|
|
56
|
+
"""解释此声明的能力 owner;声明不自动启用该模块。"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class ModuleRequirement(ModuleDeclaration):
|
|
61
|
+
"""供扫描角色或装饰器携带的依赖事实;ModuleRef 允许契约延迟引用实现模块。"""
|
|
62
|
+
|
|
63
|
+
module: ModuleDependency
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def required_module(self) -> ModuleDependency:
|
|
67
|
+
return self.module
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _module_declarations(candidate: type[object]) -> dict[str, ModuleDeclaration]:
|
|
71
|
+
attributes = {
|
|
72
|
+
name: value for base in reversed(candidate.__mro__) for name, value in vars(base).items()
|
|
73
|
+
}
|
|
74
|
+
declarations = {
|
|
75
|
+
name: value for name, value in attributes.items() if isinstance(value, ModuleDeclaration)
|
|
76
|
+
}
|
|
77
|
+
# 方法声明与类声明使用同一依赖权威;内核不识别具体策略或能力名称。
|
|
78
|
+
for name, value in attributes.items():
|
|
79
|
+
targets = (
|
|
80
|
+
(value, value.__func__) if isinstance(value, (staticmethod, classmethod)) else (value,)
|
|
81
|
+
)
|
|
82
|
+
for target in targets:
|
|
83
|
+
if isinstance(target, (FunctionType, staticmethod, classmethod)):
|
|
84
|
+
declarations.update(
|
|
85
|
+
(f"{name}.{attribute}", declaration)
|
|
86
|
+
for attribute, declaration in vars(target).items()
|
|
87
|
+
if isinstance(declaration, ModuleDeclaration)
|
|
88
|
+
)
|
|
89
|
+
return declarations
|
|
90
|
+
|
|
91
|
+
|
|
54
92
|
class AppModule:
|
|
55
93
|
"""模块 Template Method;每次 Application build 都创建独立实例。"""
|
|
56
94
|
|
|
57
95
|
dependencies: ClassVar[tuple[type[AppModule] | ModuleRef, ...]] = ()
|
|
58
96
|
scan_packages: ClassVar[tuple[str, ...]] = ()
|
|
59
97
|
configuration_defaults: ClassVar[Mapping[str, object]] = MappingProxyType({})
|
|
60
|
-
|
|
61
|
-
setting_handler_definitions: ClassVar[tuple[SettingHandlerDefinition[Any], ...]] = ()
|
|
62
|
-
notification_definitions: ClassVar[tuple[NotificationDefinition[Any], ...]] = ()
|
|
63
|
-
background_worker_definitions: ClassVar[tuple[BackgroundWorkerDefinition, ...]] = ()
|
|
64
|
-
background_workers: ClassVar[tuple[type[BackgroundWorker], ...]] = ()
|
|
65
|
-
background_job_definitions: ClassVar[tuple[BackgroundJobDefinition[Any], ...]] = ()
|
|
66
|
-
background_job_schedules: ClassVar[tuple[BackgroundJobSchedule, ...]] = ()
|
|
67
|
-
hosted_services: ClassVar[tuple[type[HostedService], ...]] = ()
|
|
98
|
+
runtime: RuntimeParticipant | None = None
|
|
68
99
|
|
|
69
100
|
def pre_configure(self, context: PreConfigureContext) -> None:
|
|
70
101
|
pass
|