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
|
@@ -3,54 +3,26 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
import logging
|
|
7
|
-
import time
|
|
8
6
|
from collections.abc import Awaitable, Callable
|
|
9
7
|
from contextvars import ContextVar
|
|
10
8
|
from dataclasses import dataclass
|
|
11
|
-
from
|
|
12
|
-
from typing import TYPE_CHECKING, cast
|
|
13
|
-
from uuid import uuid4
|
|
9
|
+
from typing import cast
|
|
14
10
|
|
|
15
11
|
from dishka import AsyncContainer
|
|
16
|
-
from dishka.exceptions import NoFactoryError
|
|
17
12
|
|
|
18
|
-
from ..
|
|
19
|
-
from ..
|
|
20
|
-
from ..
|
|
21
|
-
from ..authorization.contracts import _AuthorizationRequirement
|
|
22
|
-
from ..authorization.errors import (
|
|
23
|
-
PermissionCheckerUnavailableError,
|
|
24
|
-
PermissionDeniedError,
|
|
25
|
-
UnauthenticatedError,
|
|
26
|
-
)
|
|
27
|
-
from ..observability.context import invocation_span
|
|
28
|
-
from ..options import Options
|
|
13
|
+
from ..invocation.dispatcher import _MethodDispatcher
|
|
14
|
+
from ..invocation.execution import _InterceptorPipeline
|
|
15
|
+
from ..invocation.methods import _CompiledMethod
|
|
29
16
|
from ..settings.provider import SettingProvider
|
|
30
|
-
from ..unit_of_work import UnitOfWorkManager
|
|
31
17
|
from .catalog import (
|
|
32
18
|
_APPLICATION_SERVICE_COMPONENT,
|
|
33
|
-
ApplicationServiceCatalog,
|
|
34
|
-
ApplicationServiceRegistration,
|
|
35
19
|
_CompiledApplicationService,
|
|
36
|
-
_CompiledMethod,
|
|
37
20
|
)
|
|
38
21
|
from .contracts import ApplicationService
|
|
39
22
|
from .errors import (
|
|
40
|
-
ApplicationServiceValidationError,
|
|
41
|
-
ApplicationServiceValidationIssue,
|
|
42
23
|
CrossTaskInvocationError,
|
|
43
|
-
NestedConnectionError,
|
|
44
24
|
)
|
|
45
|
-
from .execution import _InterceptorPipeline
|
|
46
|
-
from .interceptors import InterceptionStage
|
|
47
25
|
from .invocation import _current_task, _InvocationRuntime
|
|
48
|
-
from .validation import _validate_arguments
|
|
49
|
-
|
|
50
|
-
_LOGGER = logging.getLogger(__name__)
|
|
51
|
-
|
|
52
|
-
if TYPE_CHECKING:
|
|
53
|
-
pass
|
|
54
26
|
|
|
55
27
|
|
|
56
28
|
@dataclass(slots=True)
|
|
@@ -61,7 +33,6 @@ class _HttpInvocationState:
|
|
|
61
33
|
pipeline: _InterceptorPipeline
|
|
62
34
|
consumed: bool = False
|
|
63
35
|
is_short_circuited: Callable[[], bool] = lambda: False
|
|
64
|
-
defer_audit: Callable[[Callable[[BaseException | None], Awaitable[None]]], None] | None = None
|
|
65
36
|
|
|
66
37
|
|
|
67
38
|
class _ApplicationServiceProxy:
|
|
@@ -88,20 +59,17 @@ class _ApplicationServiceProxy:
|
|
|
88
59
|
return invoke
|
|
89
60
|
|
|
90
61
|
|
|
91
|
-
class _ApplicationServiceDispatcher:
|
|
92
|
-
"""
|
|
62
|
+
class _ApplicationServiceDispatcher(_MethodDispatcher):
|
|
63
|
+
"""应用服务的 HTTP 许可与 REQUEST 到 ACTION 实例桥接。"""
|
|
93
64
|
|
|
94
|
-
__slots__ = ("
|
|
65
|
+
__slots__ = ("_http_boundary",)
|
|
95
66
|
|
|
96
67
|
def __init__(
|
|
97
68
|
self,
|
|
98
69
|
container: AsyncContainer,
|
|
99
70
|
runtime: _InvocationRuntime,
|
|
100
|
-
catalog: ApplicationServiceCatalog,
|
|
101
71
|
) -> None:
|
|
102
|
-
|
|
103
|
-
self._runtime = runtime
|
|
104
|
-
self._catalog = catalog
|
|
72
|
+
super().__init__(container, runtime)
|
|
105
73
|
self._http_boundary: ContextVar[_HttpInvocationState | None] = ContextVar(
|
|
106
74
|
f"python_ddd_framework_http_invocation_{runtime.application_id}_{id(self)}",
|
|
107
75
|
default=None,
|
|
@@ -117,62 +85,11 @@ class _ApplicationServiceDispatcher:
|
|
|
117
85
|
pipeline = _InterceptorPipeline(method.descriptor, method.interceptor_steps, args, kwargs)
|
|
118
86
|
async with self._runtime.outer_lease():
|
|
119
87
|
return await self._invoke_entry(
|
|
120
|
-
service,
|
|
121
88
|
method,
|
|
122
89
|
pipeline,
|
|
123
90
|
lambda: self._invoke_after_authorization(service, method, pipeline),
|
|
124
91
|
)
|
|
125
92
|
|
|
126
|
-
async def _invoke_entry(
|
|
127
|
-
self,
|
|
128
|
-
service: _CompiledApplicationService,
|
|
129
|
-
method: _CompiledMethod,
|
|
130
|
-
pipeline: _InterceptorPipeline,
|
|
131
|
-
callback: Callable[[], Awaitable[object]],
|
|
132
|
-
) -> object:
|
|
133
|
-
async def authorize() -> object:
|
|
134
|
-
if not service.registration.is_integration_service:
|
|
135
|
-
await self._authorize(method.authorization)
|
|
136
|
-
return await callback()
|
|
137
|
-
|
|
138
|
-
async def audit() -> object:
|
|
139
|
-
occurred_at = datetime.now(UTC)
|
|
140
|
-
started_ns = time.perf_counter_ns()
|
|
141
|
-
failure: BaseException | None = None
|
|
142
|
-
try:
|
|
143
|
-
return await pipeline.run(
|
|
144
|
-
InterceptionStage.AUTHORIZATION, self._container, authorize
|
|
145
|
-
)
|
|
146
|
-
except BaseException as error:
|
|
147
|
-
failure = error
|
|
148
|
-
raise
|
|
149
|
-
finally:
|
|
150
|
-
|
|
151
|
-
async def write(final_failure: BaseException | None) -> None:
|
|
152
|
-
await self._write_audit(
|
|
153
|
-
service.registration,
|
|
154
|
-
method,
|
|
155
|
-
occurred_at=occurred_at,
|
|
156
|
-
duration_ns=time.perf_counter_ns() - started_ns,
|
|
157
|
-
failure=final_failure,
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
boundary = self._http_boundary.get()
|
|
161
|
-
if (
|
|
162
|
-
boundary is not None
|
|
163
|
-
and boundary.pipeline is pipeline
|
|
164
|
-
and boundary.defer_audit is not None
|
|
165
|
-
):
|
|
166
|
-
boundary.defer_audit(write)
|
|
167
|
-
else:
|
|
168
|
-
await write(failure)
|
|
169
|
-
|
|
170
|
-
async def trace() -> object:
|
|
171
|
-
with invocation_span(self._runtime.tracer, method.contract_method.__qualname__):
|
|
172
|
-
return await pipeline.run(InterceptionStage.AUDITING, self._container, audit)
|
|
173
|
-
|
|
174
|
-
return await pipeline.run(InterceptionStage.TRACING, self._container, trace)
|
|
175
|
-
|
|
176
93
|
async def invoke_http_request(
|
|
177
94
|
self,
|
|
178
95
|
service: _CompiledApplicationService,
|
|
@@ -190,7 +107,6 @@ class _ApplicationServiceDispatcher:
|
|
|
190
107
|
pipeline = _InterceptorPipeline(method.descriptor, method.interceptor_steps, (), {})
|
|
191
108
|
state = _HttpInvocationState(service, method, _current_task(), pipeline)
|
|
192
109
|
state.is_short_circuited = is_short_circuited
|
|
193
|
-
state.defer_audit = defer_audit
|
|
194
110
|
token = self._http_boundary.set(state)
|
|
195
111
|
|
|
196
112
|
async def invoke_endpoint() -> object:
|
|
@@ -201,7 +117,9 @@ class _ApplicationServiceDispatcher:
|
|
|
201
117
|
return result
|
|
202
118
|
|
|
203
119
|
try:
|
|
204
|
-
return await self._invoke_entry(
|
|
120
|
+
return await self._invoke_entry(
|
|
121
|
+
method, pipeline, invoke_endpoint, defer_audit=defer_audit
|
|
122
|
+
)
|
|
205
123
|
finally:
|
|
206
124
|
self._http_boundary.reset(token)
|
|
207
125
|
|
|
@@ -230,188 +148,20 @@ class _ApplicationServiceDispatcher:
|
|
|
230
148
|
method: _CompiledMethod,
|
|
231
149
|
pipeline: _InterceptorPipeline,
|
|
232
150
|
) -> object:
|
|
233
|
-
async def
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
InterceptionStage.UNIT_OF_WORK,
|
|
237
|
-
self._container,
|
|
238
|
-
lambda: self._invoke_work(service.registration, method, pipeline),
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
return await pipeline.run(InterceptionStage.VALIDATION, self._container, validate)
|
|
242
|
-
|
|
243
|
-
@staticmethod
|
|
244
|
-
async def _validate_pipeline(
|
|
245
|
-
registration: ApplicationServiceRegistration,
|
|
246
|
-
method: _CompiledMethod,
|
|
247
|
-
pipeline: _InterceptorPipeline,
|
|
248
|
-
) -> None:
|
|
249
|
-
arguments = pipeline.arguments
|
|
250
|
-
if not arguments.needs_validation:
|
|
251
|
-
return
|
|
252
|
-
if method.descriptor.validation_enabled:
|
|
253
|
-
arguments.args, arguments.kwargs = await _validate_arguments(
|
|
254
|
-
method, registration, arguments.args, arguments.kwargs
|
|
255
|
-
)
|
|
256
|
-
else:
|
|
257
|
-
# 可选类型验证关闭后,必需参数与参数名仍属于公开调用契约。
|
|
258
|
-
try:
|
|
259
|
-
method.signature.bind(None, *arguments.args, **arguments.kwargs)
|
|
260
|
-
except TypeError:
|
|
261
|
-
raise ApplicationServiceValidationError(
|
|
262
|
-
service=registration.contract,
|
|
263
|
-
method_name=method.name,
|
|
264
|
-
issues=(
|
|
265
|
-
ApplicationServiceValidationIssue(
|
|
266
|
-
location=(), error_type="arguments", message="Invalid method arguments"
|
|
267
|
-
),
|
|
268
|
-
),
|
|
269
|
-
) from None
|
|
270
|
-
arguments.mark_validated()
|
|
271
|
-
|
|
272
|
-
async def _invoke_work(
|
|
273
|
-
self,
|
|
274
|
-
registration: ApplicationServiceRegistration,
|
|
275
|
-
method: _CompiledMethod,
|
|
276
|
-
pipeline: _InterceptorPipeline,
|
|
277
|
-
) -> object:
|
|
278
|
-
ambient = self._runtime.current()
|
|
279
|
-
if ambient is not None:
|
|
280
|
-
if _current_task() is not ambient.owner_task:
|
|
281
|
-
raise CrossTaskInvocationError
|
|
282
|
-
if registration.connection_name != ambient.context.connection_name:
|
|
283
|
-
raise NestedConnectionError(
|
|
284
|
-
outer=ambient.context.connection_name, requested=registration.connection_name
|
|
285
|
-
)
|
|
286
|
-
manager = await self._container.get(UnitOfWorkManager)
|
|
287
|
-
|
|
288
|
-
async def nested() -> object:
|
|
289
|
-
return await self._invoke_target(
|
|
290
|
-
ambient.action_container, registration, method, pipeline
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
if method.descriptor.unit_of_work.disabled:
|
|
294
|
-
manager._check_current(registration.connection_name)
|
|
295
|
-
return await nested()
|
|
296
|
-
options = manager._resolve_options(
|
|
297
|
-
registration.connection_name, method.descriptor.unit_of_work, method.name
|
|
298
|
-
)
|
|
299
|
-
async with manager._begin(options) as work:
|
|
300
|
-
result = await nested()
|
|
301
|
-
await work.complete()
|
|
302
|
-
return result
|
|
303
|
-
|
|
304
|
-
async def invoke(action: AsyncContainer) -> object:
|
|
305
|
-
return await self._invoke_target(action, registration, method, pipeline)
|
|
306
|
-
|
|
307
|
-
return await self._runtime.run_action(
|
|
308
|
-
self._container,
|
|
309
|
-
connection_name=registration.connection_name,
|
|
310
|
-
callback=invoke,
|
|
311
|
-
declaration=method.descriptor.unit_of_work,
|
|
312
|
-
method_name=method.name,
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
async def _invoke_target(
|
|
316
|
-
self,
|
|
317
|
-
container: AsyncContainer,
|
|
318
|
-
registration: ApplicationServiceRegistration,
|
|
319
|
-
method: _CompiledMethod,
|
|
320
|
-
pipeline: _InterceptorPipeline,
|
|
321
|
-
) -> object:
|
|
322
|
-
async def invoke() -> object:
|
|
323
|
-
# 位于验证之后的扩展可以改参数;目标调用不能使用修改前的验证结果。
|
|
324
|
-
await self._validate_pipeline(registration, method, pipeline)
|
|
151
|
+
async def target(
|
|
152
|
+
container: AsyncContainer, args: tuple[object, ...], kwargs: dict[str, object]
|
|
153
|
+
) -> object:
|
|
325
154
|
implementation = await container.get(
|
|
326
|
-
registration.contract, component=_APPLICATION_SERVICE_COMPONENT
|
|
155
|
+
service.registration.contract, component=_APPLICATION_SERVICE_COMPONENT
|
|
327
156
|
)
|
|
328
|
-
# 实例已由当前 ACTION 容器取得,绑定只属于本 Application 的读取服务。
|
|
329
157
|
cast(ApplicationService, implementation)._setting_provider = await container.get(
|
|
330
158
|
SettingProvider
|
|
331
159
|
)
|
|
332
|
-
return await method.implementation_method(
|
|
333
|
-
implementation, *pipeline.arguments.args, **pipeline.arguments.kwargs
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
return await pipeline.run(InterceptionStage.METHOD, container, invoke)
|
|
337
|
-
|
|
338
|
-
async def _write_audit(
|
|
339
|
-
self,
|
|
340
|
-
registration: ApplicationServiceRegistration,
|
|
341
|
-
method: _CompiledMethod,
|
|
342
|
-
*,
|
|
343
|
-
occurred_at: datetime,
|
|
344
|
-
duration_ns: int,
|
|
345
|
-
failure: BaseException | None,
|
|
346
|
-
) -> None:
|
|
347
|
-
options = (await self._container.get(Options[AuditingOptions])).value
|
|
348
|
-
control = await self._container.get(AuditingControl)
|
|
349
|
-
if (
|
|
350
|
-
not method.descriptor.auditing_enabled
|
|
351
|
-
or not options.is_enabled
|
|
352
|
-
or not control.is_enabled
|
|
353
|
-
):
|
|
354
|
-
return
|
|
355
|
-
try:
|
|
356
|
-
sink = await self._container.get(AuditSink)
|
|
357
|
-
except NoFactoryError:
|
|
358
|
-
return
|
|
359
|
-
except Exception as audit_error:
|
|
360
|
-
_LOGGER.error(
|
|
361
|
-
"application service audit write failed",
|
|
362
|
-
exc_info=audit_error,
|
|
363
|
-
extra={
|
|
364
|
-
"audit_exception_type": type(audit_error).__name__,
|
|
365
|
-
"audit_contract": registration.contract.__qualname__,
|
|
366
|
-
"audit_method": method.name,
|
|
367
|
-
},
|
|
368
|
-
)
|
|
369
|
-
return
|
|
370
|
-
try:
|
|
371
|
-
current_user = self._runtime.current_user()
|
|
372
|
-
correlation_id = self._runtime.correlation_id() or str(uuid4())
|
|
373
|
-
await sink.write_application_service(
|
|
374
|
-
ApplicationServiceAuditRecord(
|
|
375
|
-
occurred_at=occurred_at,
|
|
376
|
-
user_id=current_user.user_id,
|
|
377
|
-
session_id=current_user.session_id,
|
|
378
|
-
correlation_id=correlation_id,
|
|
379
|
-
contract=(
|
|
380
|
-
f"{registration.contract.__module__}.{registration.contract.__qualname__}"
|
|
381
|
-
),
|
|
382
|
-
method=method.name,
|
|
383
|
-
succeeded=failure is None,
|
|
384
|
-
duration_ns=duration_ns,
|
|
385
|
-
exception_type=None if failure is None else type(failure).__name__,
|
|
386
|
-
)
|
|
387
|
-
)
|
|
388
|
-
except Exception as audit_error:
|
|
389
|
-
# sink resolution/context/write 都属于独立观测边界;失败不能覆盖业务结果。
|
|
390
|
-
_LOGGER.error(
|
|
391
|
-
"application service audit write failed",
|
|
392
|
-
exc_info=audit_error,
|
|
393
|
-
extra={
|
|
394
|
-
"audit_exception_type": type(audit_error).__name__,
|
|
395
|
-
"audit_contract": registration.contract.__qualname__,
|
|
396
|
-
"audit_method": method.name,
|
|
397
|
-
},
|
|
398
|
-
)
|
|
160
|
+
return await method.implementation_method(implementation, *args, **kwargs)
|
|
399
161
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
current_user = self._runtime.current_user()
|
|
404
|
-
if not current_user.is_authenticated:
|
|
405
|
-
raise UnauthenticatedError
|
|
406
|
-
if not requirement.permissions:
|
|
407
|
-
return
|
|
408
|
-
try:
|
|
409
|
-
checker = await self._container.get(PermissionChecker)
|
|
410
|
-
except NoFactoryError:
|
|
411
|
-
raise PermissionCheckerUnavailableError from None
|
|
412
|
-
for permission in requirement.permissions:
|
|
413
|
-
if not await checker.is_granted(current_user, permission):
|
|
414
|
-
raise PermissionDeniedError(permission=str(permission))
|
|
162
|
+
return await self._invoke_validated(
|
|
163
|
+
method, pipeline, target, service.registration.connection_name
|
|
164
|
+
)
|
|
415
165
|
|
|
416
166
|
|
|
417
167
|
def _proxy_factory(
|
|
@@ -16,9 +16,9 @@ from dishka import AsyncContainer, Scope
|
|
|
16
16
|
from ..authorization import CurrentUser
|
|
17
17
|
from ..invocation import ApplicationInvocationContext
|
|
18
18
|
from ..invocation.contracts import _is_safe_correlation_id
|
|
19
|
-
from ..lifecycle import ApplicationState
|
|
19
|
+
from ..lifecycle import ApplicationState, RuntimeParticipant, ShutdownReason
|
|
20
20
|
from ..unit_of_work import UnitOfWork, UnitOfWorkManager
|
|
21
|
-
from ..unit_of_work.options import UnitOfWorkDeclaration
|
|
21
|
+
from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME, UnitOfWorkDeclaration
|
|
22
22
|
from .catalog import (
|
|
23
23
|
ApplicationServiceCatalog,
|
|
24
24
|
)
|
|
@@ -26,6 +26,7 @@ from .errors import (
|
|
|
26
26
|
ApplicationInvocationCleanupError,
|
|
27
27
|
ApplicationInvocationRejectedError,
|
|
28
28
|
CrossTaskInvocationError,
|
|
29
|
+
NestedConnectionError,
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -61,6 +62,14 @@ class _CallScope:
|
|
|
61
62
|
owner_task: asyncio.Task[object]
|
|
62
63
|
|
|
63
64
|
|
|
65
|
+
@dataclass(eq=False, slots=True)
|
|
66
|
+
class _InvocationReservation:
|
|
67
|
+
"""只携带准入事实,不携带 caller、DI scope 或事务;由接受工作的 owner 收口。"""
|
|
68
|
+
|
|
69
|
+
runtime: _InvocationRuntime
|
|
70
|
+
executing: bool = False
|
|
71
|
+
|
|
72
|
+
|
|
64
73
|
class _InvocationRuntime:
|
|
65
74
|
"""每个 Application 独占 admission 与 ContextVar,禁止进程级 current app。"""
|
|
66
75
|
|
|
@@ -75,6 +84,7 @@ class _InvocationRuntime:
|
|
|
75
84
|
"_http_method",
|
|
76
85
|
"_is_accepting",
|
|
77
86
|
"_lease",
|
|
87
|
+
"_reservations",
|
|
78
88
|
"_state_getter",
|
|
79
89
|
"tracer",
|
|
80
90
|
)
|
|
@@ -109,6 +119,7 @@ class _InvocationRuntime:
|
|
|
109
119
|
self._condition = asyncio.Condition()
|
|
110
120
|
self._is_accepting = False
|
|
111
121
|
self._active = 0
|
|
122
|
+
self._reservations: set[_InvocationReservation] = set()
|
|
112
123
|
self._call_scope: ContextVar[_CallScope | None] = ContextVar(
|
|
113
124
|
f"python_ddd_framework_call_scope_{application_id}",
|
|
114
125
|
default=None,
|
|
@@ -157,11 +168,66 @@ class _InvocationRuntime:
|
|
|
157
168
|
def start_accepting(self) -> None:
|
|
158
169
|
self._is_accepting = True
|
|
159
170
|
|
|
171
|
+
def runtime_participant(self) -> RuntimeParticipant:
|
|
172
|
+
async def start() -> None:
|
|
173
|
+
self.start_accepting()
|
|
174
|
+
|
|
175
|
+
async def stop(reason: ShutdownReason) -> tuple[BaseException, ...]:
|
|
176
|
+
self._is_accepting = False
|
|
177
|
+
return ()
|
|
178
|
+
|
|
179
|
+
# 未 start 的 COMPOSED HTTP 也可持有许可;所有消费者停止后、容器关闭前统一排空。
|
|
180
|
+
return RuntimeParticipant(start=start, stop=stop, drain=self.stop_accepting_and_wait)
|
|
181
|
+
|
|
160
182
|
async def stop_accepting_and_wait(self) -> None:
|
|
161
183
|
async with self._condition:
|
|
162
184
|
self._is_accepting = False
|
|
163
185
|
await self._condition.wait_for(lambda: self._active == 0)
|
|
164
186
|
|
|
187
|
+
def reserve(self) -> _InvocationReservation:
|
|
188
|
+
# 与 loop 上的接受/关闭无 await 地线性化;等待执行不打开 REQUEST/ACTION。
|
|
189
|
+
self.require_accepting_work()
|
|
190
|
+
reservation = _InvocationReservation(self)
|
|
191
|
+
self._reservations.add(reservation)
|
|
192
|
+
self._active += 1
|
|
193
|
+
return reservation
|
|
194
|
+
|
|
195
|
+
def require_accepting_work(self) -> None:
|
|
196
|
+
_current_task()
|
|
197
|
+
if not self._is_accepting or self._state_getter() not in (
|
|
198
|
+
ApplicationState.STARTING,
|
|
199
|
+
ApplicationState.RUNNING,
|
|
200
|
+
):
|
|
201
|
+
raise ApplicationInvocationRejectedError(reason="Application is not accepting work")
|
|
202
|
+
|
|
203
|
+
async def discard(self, reservation: _InvocationReservation) -> None:
|
|
204
|
+
if reservation.runtime is not self or reservation not in self._reservations:
|
|
205
|
+
raise ApplicationInvocationRejectedError(reason="Invalid or released reservation")
|
|
206
|
+
async with self._condition:
|
|
207
|
+
self._reservations.remove(reservation)
|
|
208
|
+
self._active -= 1
|
|
209
|
+
if self._active == 0:
|
|
210
|
+
self._condition.notify_all()
|
|
211
|
+
|
|
212
|
+
@asynccontextmanager
|
|
213
|
+
async def reserved_lease(self, reservation: _InvocationReservation) -> AsyncIterator[None]:
|
|
214
|
+
if (
|
|
215
|
+
reservation.runtime is not self
|
|
216
|
+
or reservation not in self._reservations
|
|
217
|
+
or reservation.executing
|
|
218
|
+
or self._lease.get() is not None
|
|
219
|
+
or self.current() is not None
|
|
220
|
+
or self.current_call_scope() is not None
|
|
221
|
+
):
|
|
222
|
+
raise ApplicationInvocationRejectedError(reason="Invalid reservation execution owner")
|
|
223
|
+
reservation.executing = True
|
|
224
|
+
token = self._lease.set(_OuterInvocationLease(_current_task(), True))
|
|
225
|
+
try:
|
|
226
|
+
yield
|
|
227
|
+
finally:
|
|
228
|
+
self._lease.reset(token)
|
|
229
|
+
await self.discard(reservation)
|
|
230
|
+
|
|
165
231
|
@asynccontextmanager
|
|
166
232
|
async def outer_lease(self, *, allow_composed_request: bool = False) -> AsyncIterator[None]:
|
|
167
233
|
owner_task = _current_task()
|
|
@@ -286,6 +352,30 @@ class _InvocationRuntime:
|
|
|
286
352
|
self._caller.reset(caller_token)
|
|
287
353
|
self._lease.reset(lease_token)
|
|
288
354
|
|
|
355
|
+
def context_for_action(self, container: AsyncContainer) -> ApplicationInvocationContext:
|
|
356
|
+
"""由原生 ACTION factory 缓存上下文,DI 与调用管线共享同一身份。"""
|
|
357
|
+
# Dishka 1.10 的公开 context 属性已弃用;这里只读取原生 scope 的输入,
|
|
358
|
+
# 不写 context/cache,也不复制容器的解析或缓存。第三方字段访问集中于此。
|
|
359
|
+
native_context = container._context
|
|
360
|
+
if native_context is None or ApplicationInvocationContext not in native_context:
|
|
361
|
+
return self._new_context(_DEFAULT_CONNECTION_NAME)
|
|
362
|
+
supplied: object = native_context[ApplicationInvocationContext]
|
|
363
|
+
if not isinstance(supplied, ApplicationInvocationContext):
|
|
364
|
+
raise ApplicationInvocationRejectedError(reason="invalid ACTION invocation context")
|
|
365
|
+
if supplied.application_id != self.application_id:
|
|
366
|
+
raise ApplicationInvocationRejectedError(
|
|
367
|
+
reason="ACTION invocation context belongs to another Application"
|
|
368
|
+
)
|
|
369
|
+
return supplied
|
|
370
|
+
|
|
371
|
+
def _new_context(self, connection_name: str) -> ApplicationInvocationContext:
|
|
372
|
+
return ApplicationInvocationContext(
|
|
373
|
+
application_id=self.application_id,
|
|
374
|
+
invocation_id=uuid4(),
|
|
375
|
+
parent_invocation_id=None,
|
|
376
|
+
connection_name=connection_name,
|
|
377
|
+
)
|
|
378
|
+
|
|
289
379
|
async def run_action(
|
|
290
380
|
self,
|
|
291
381
|
container: AsyncContainer,
|
|
@@ -294,14 +384,16 @@ class _InvocationRuntime:
|
|
|
294
384
|
callback: Callable[[AsyncContainer], Awaitable[object]],
|
|
295
385
|
declaration: UnitOfWorkDeclaration,
|
|
296
386
|
method_name: str = "execute",
|
|
387
|
+
unit_of_work_enabled: bool = True,
|
|
297
388
|
) -> object:
|
|
298
389
|
"""执行 scope 独立于数据库 UoW;自动边界只完成自己创建或加入的工作。"""
|
|
299
|
-
context =
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
connection_name=connection_name,
|
|
390
|
+
context = (
|
|
391
|
+
await container.get(ApplicationInvocationContext)
|
|
392
|
+
if container.scope is Scope.ACTION
|
|
393
|
+
else self._new_context(connection_name)
|
|
304
394
|
)
|
|
395
|
+
if context.connection_name != connection_name:
|
|
396
|
+
raise NestedConnectionError(outer=context.connection_name, requested=connection_name)
|
|
305
397
|
work: UnitOfWork | None = None
|
|
306
398
|
async with _managed_action(
|
|
307
399
|
container, context, committed=lambda: work is not None and work.committed
|
|
@@ -309,6 +401,8 @@ class _InvocationRuntime:
|
|
|
309
401
|
state = _InvocationState(context, _current_task(), action)
|
|
310
402
|
token = self.bind(state)
|
|
311
403
|
try:
|
|
404
|
+
if not unit_of_work_enabled:
|
|
405
|
+
return await callback(action)
|
|
312
406
|
manager = await action.get(UnitOfWorkManager)
|
|
313
407
|
if declaration.disabled:
|
|
314
408
|
manager._check_current(connection_name)
|
|
@@ -322,6 +416,27 @@ class _InvocationRuntime:
|
|
|
322
416
|
self.reset(token)
|
|
323
417
|
|
|
324
418
|
|
|
419
|
+
@asynccontextmanager
|
|
420
|
+
async def _managed_request(parent: AsyncContainer) -> AsyncIterator[AsyncContainer]:
|
|
421
|
+
"""排空入口自己创建的 REQUEST;不推断业务事务是否已经提交。"""
|
|
422
|
+
request = parent(scope=Scope.REQUEST)
|
|
423
|
+
primary: BaseException | None = None
|
|
424
|
+
try:
|
|
425
|
+
yield request
|
|
426
|
+
except BaseException as error:
|
|
427
|
+
primary = error
|
|
428
|
+
primary, failures = await _close_action(request, primary)
|
|
429
|
+
if failures:
|
|
430
|
+
# REQUEST 无事务 owner;保留业务原异常及已有 cause,不伪造 committed=False。
|
|
431
|
+
inherited = (primary.__cause__,) if primary is not None and primary.__cause__ else ()
|
|
432
|
+
cleanup = BaseExceptionGroup("REQUEST cleanup failed", (*inherited, *failures))
|
|
433
|
+
if primary is not None:
|
|
434
|
+
raise primary from cleanup
|
|
435
|
+
raise cleanup
|
|
436
|
+
if primary is not None:
|
|
437
|
+
raise primary
|
|
438
|
+
|
|
439
|
+
|
|
325
440
|
@asynccontextmanager
|
|
326
441
|
async def _managed_action(
|
|
327
442
|
parent: AsyncContainer,
|
|
@@ -329,6 +444,10 @@ async def _managed_action(
|
|
|
329
444
|
*,
|
|
330
445
|
committed: Callable[[], bool],
|
|
331
446
|
) -> AsyncIterator[AsyncContainer]:
|
|
447
|
+
if parent.scope is Scope.ACTION:
|
|
448
|
+
# 普通服务可能已经由调用方的 ACTION 解析;资源仍由该 scope 的 owner 关闭。
|
|
449
|
+
yield parent
|
|
450
|
+
return
|
|
332
451
|
action = parent(context={ApplicationInvocationContext: context}, scope=Scope.ACTION)
|
|
333
452
|
primary: BaseException | None = None
|
|
334
453
|
try:
|