python-ddd-framework 0.3.2__py3-none-any.whl → 0.5.1__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 +120 -288
- 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 +197 -15
- 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/local.py +20 -1
- python_ddd_framework/background_execution/module.py +107 -1
- python_ddd_framework/background_execution/processes.py +11 -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 +47 -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 +42 -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/hosted_services/order_integration_service.py.jinja +71 -0
- 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 +23 -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 +42 -11
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +144 -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 +95 -17
- python_ddd_framework/hosted_services/catalog.py +6 -16
- python_ddd_framework/hosted_services/contracts.py +9 -1
- 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 +101 -18
- 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 +37 -0
- python_ddd_framework/messaging/__init__.py +25 -0
- python_ddd_framework/messaging/channel.py +232 -0
- python_ddd_framework/messaging/contracts.py +78 -0
- python_ddd_framework/messaging/module.py +114 -0
- python_ddd_framework/messaging/options.py +13 -0
- python_ddd_framework/messaging/receipt.py +27 -0
- python_ddd_framework/messaging/runtime.py +139 -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.5.1.dist-info}/METADATA +147 -44
- python_ddd_framework-0.5.1.dist-info/RECORD +488 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.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/integration.py.jinja +0 -41
- 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.5.1.dist-info}/entry_points.txt +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.5.1.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(
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import logging
|
|
7
|
+
import threading
|
|
7
8
|
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
|
|
8
9
|
from contextlib import asynccontextmanager, contextmanager
|
|
9
10
|
from contextvars import ContextVar, Token
|
|
@@ -16,9 +17,9 @@ from dishka import AsyncContainer, Scope
|
|
|
16
17
|
from ..authorization import CurrentUser
|
|
17
18
|
from ..invocation import ApplicationInvocationContext
|
|
18
19
|
from ..invocation.contracts import _is_safe_correlation_id
|
|
19
|
-
from ..lifecycle import ApplicationState
|
|
20
|
+
from ..lifecycle import ApplicationState, RuntimeParticipant, ShutdownReason
|
|
20
21
|
from ..unit_of_work import UnitOfWork, UnitOfWorkManager
|
|
21
|
-
from ..unit_of_work.options import UnitOfWorkDeclaration
|
|
22
|
+
from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME, UnitOfWorkDeclaration
|
|
22
23
|
from .catalog import (
|
|
23
24
|
ApplicationServiceCatalog,
|
|
24
25
|
)
|
|
@@ -26,6 +27,7 @@ from .errors import (
|
|
|
26
27
|
ApplicationInvocationCleanupError,
|
|
27
28
|
ApplicationInvocationRejectedError,
|
|
28
29
|
CrossTaskInvocationError,
|
|
30
|
+
NestedConnectionError,
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -61,20 +63,40 @@ class _CallScope:
|
|
|
61
63
|
owner_task: asyncio.Task[object]
|
|
62
64
|
|
|
63
65
|
|
|
66
|
+
@dataclass(eq=False, slots=True)
|
|
67
|
+
class _InvocationReservation:
|
|
68
|
+
"""只携带准入事实,不携带 caller、DI scope 或事务;由接受工作的 owner 收口。"""
|
|
69
|
+
|
|
70
|
+
runtime: _InvocationRuntime
|
|
71
|
+
executing: bool = False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(eq=False, frozen=True, slots=True)
|
|
75
|
+
class _StoppingPermission:
|
|
76
|
+
"""单 Application、服务 owner 与单次收尾钩子的准入身份,不携带业务 scope。"""
|
|
77
|
+
|
|
78
|
+
runtime: _InvocationRuntime
|
|
79
|
+
owner: type[object]
|
|
80
|
+
|
|
81
|
+
|
|
64
82
|
class _InvocationRuntime:
|
|
65
83
|
"""每个 Application 独占 admission 与 ContextVar,禁止进程级 current app。"""
|
|
66
84
|
|
|
67
85
|
__slots__ = (
|
|
68
86
|
"_active",
|
|
87
|
+
"_admission_lock",
|
|
69
88
|
"_ambient",
|
|
70
89
|
"_application_id",
|
|
71
90
|
"_authenticating",
|
|
72
91
|
"_call_scope",
|
|
73
92
|
"_caller",
|
|
74
|
-
"
|
|
93
|
+
"_changed",
|
|
75
94
|
"_http_method",
|
|
76
95
|
"_is_accepting",
|
|
77
96
|
"_lease",
|
|
97
|
+
"_loop",
|
|
98
|
+
"_permissions",
|
|
99
|
+
"_reservations",
|
|
78
100
|
"_state_getter",
|
|
79
101
|
"tracer",
|
|
80
102
|
)
|
|
@@ -106,9 +128,13 @@ class _InvocationRuntime:
|
|
|
106
128
|
f"python_ddd_framework_invocation_lease_{application_id}",
|
|
107
129
|
default=None,
|
|
108
130
|
)
|
|
109
|
-
self.
|
|
131
|
+
self._admission_lock = threading.RLock()
|
|
132
|
+
self._changed = asyncio.Event()
|
|
133
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
134
|
+
self._permissions: set[_StoppingPermission] = set()
|
|
110
135
|
self._is_accepting = False
|
|
111
136
|
self._active = 0
|
|
137
|
+
self._reservations: set[_InvocationReservation] = set()
|
|
112
138
|
self._call_scope: ContextVar[_CallScope | None] = ContextVar(
|
|
113
139
|
f"python_ddd_framework_call_scope_{application_id}",
|
|
114
140
|
default=None,
|
|
@@ -155,12 +181,115 @@ class _InvocationRuntime:
|
|
|
155
181
|
return self._application_id
|
|
156
182
|
|
|
157
183
|
def start_accepting(self) -> None:
|
|
184
|
+
self._loop = asyncio.get_running_loop()
|
|
158
185
|
self._is_accepting = True
|
|
159
186
|
|
|
160
|
-
|
|
161
|
-
async
|
|
187
|
+
def runtime_participant(self) -> RuntimeParticipant:
|
|
188
|
+
async def start() -> None:
|
|
189
|
+
self.start_accepting()
|
|
190
|
+
|
|
191
|
+
async def stop(reason: ShutdownReason) -> tuple[BaseException, ...]:
|
|
192
|
+
self.close_admission()
|
|
193
|
+
return ()
|
|
194
|
+
|
|
195
|
+
return RuntimeParticipant(
|
|
196
|
+
start=start,
|
|
197
|
+
stop=stop,
|
|
198
|
+
quiesce=self.quiesce,
|
|
199
|
+
close_admission=self.close_admission,
|
|
200
|
+
drain=self.stop_accepting_and_wait,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def quiesce(self) -> None:
|
|
204
|
+
with self._admission_lock:
|
|
205
|
+
self._is_accepting = False
|
|
206
|
+
|
|
207
|
+
def close_admission(self) -> None:
|
|
208
|
+
with self._admission_lock:
|
|
162
209
|
self._is_accepting = False
|
|
163
|
-
|
|
210
|
+
self._permissions.clear()
|
|
211
|
+
|
|
212
|
+
def grant_stopping(self, owner: type[object]) -> _StoppingPermission:
|
|
213
|
+
with self._admission_lock:
|
|
214
|
+
if self._state_getter() not in (
|
|
215
|
+
ApplicationState.STOPPING,
|
|
216
|
+
ApplicationState.ROLLING_BACK,
|
|
217
|
+
):
|
|
218
|
+
raise ApplicationInvocationRejectedError(reason="Not in the stopping phase")
|
|
219
|
+
permission = _StoppingPermission(self, owner)
|
|
220
|
+
self._permissions.add(permission)
|
|
221
|
+
return permission
|
|
222
|
+
|
|
223
|
+
def revoke_stopping(self, permission: _StoppingPermission) -> None:
|
|
224
|
+
with self._admission_lock:
|
|
225
|
+
self._permissions.discard(permission)
|
|
226
|
+
|
|
227
|
+
def require_stopping(self, permission: _StoppingPermission) -> None:
|
|
228
|
+
with self._admission_lock:
|
|
229
|
+
if permission.runtime is not self or permission not in self._permissions:
|
|
230
|
+
raise ApplicationInvocationRejectedError(reason="Invalid stopping permission")
|
|
231
|
+
|
|
232
|
+
async def stop_accepting_and_wait(self) -> None:
|
|
233
|
+
self.close_admission()
|
|
234
|
+
while True:
|
|
235
|
+
self._changed.clear()
|
|
236
|
+
with self._admission_lock:
|
|
237
|
+
if self._active == 0 and not self._reservations:
|
|
238
|
+
return
|
|
239
|
+
await self._changed.wait()
|
|
240
|
+
|
|
241
|
+
def reserve(self, permission: _StoppingPermission | None = None) -> _InvocationReservation:
|
|
242
|
+
# SDK thread 与权限撤销在同一锁下线性化;已接受许可独立于钩子权限的寿命。
|
|
243
|
+
with self._admission_lock:
|
|
244
|
+
if permission is None:
|
|
245
|
+
self.require_accepting_work()
|
|
246
|
+
else:
|
|
247
|
+
self.require_stopping(permission)
|
|
248
|
+
reservation = _InvocationReservation(self)
|
|
249
|
+
self._reservations.add(reservation)
|
|
250
|
+
return reservation
|
|
251
|
+
|
|
252
|
+
def require_accepting_work(self) -> None:
|
|
253
|
+
if not self._is_accepting or self._state_getter() not in (
|
|
254
|
+
ApplicationState.STARTING,
|
|
255
|
+
ApplicationState.RUNNING,
|
|
256
|
+
):
|
|
257
|
+
raise ApplicationInvocationRejectedError(reason="Application is not accepting work")
|
|
258
|
+
|
|
259
|
+
async def discard(self, reservation: _InvocationReservation) -> None:
|
|
260
|
+
self.release_reservation(reservation)
|
|
261
|
+
|
|
262
|
+
def release_reservation(self, reservation: _InvocationReservation) -> None:
|
|
263
|
+
with self._admission_lock:
|
|
264
|
+
if reservation.runtime is not self or reservation not in self._reservations:
|
|
265
|
+
raise ApplicationInvocationRejectedError(reason="Invalid or released reservation")
|
|
266
|
+
self._reservations.remove(reservation)
|
|
267
|
+
if self._loop is not None and not self._loop.is_closed():
|
|
268
|
+
try:
|
|
269
|
+
self._loop.call_soon_threadsafe(self._changed.set)
|
|
270
|
+
except RuntimeError:
|
|
271
|
+
# 外部 submit 可能在 loop 关闭竞争中退回许可;此时已无 loop 排空等待者。
|
|
272
|
+
if not self._loop.is_closed():
|
|
273
|
+
raise
|
|
274
|
+
|
|
275
|
+
@asynccontextmanager
|
|
276
|
+
async def reserved_lease(self, reservation: _InvocationReservation) -> AsyncIterator[None]:
|
|
277
|
+
if (
|
|
278
|
+
reservation.runtime is not self
|
|
279
|
+
or reservation not in self._reservations
|
|
280
|
+
or reservation.executing
|
|
281
|
+
or self._lease.get() is not None
|
|
282
|
+
or self.current() is not None
|
|
283
|
+
or self.current_call_scope() is not None
|
|
284
|
+
):
|
|
285
|
+
raise ApplicationInvocationRejectedError(reason="Invalid reservation execution owner")
|
|
286
|
+
reservation.executing = True
|
|
287
|
+
token = self._lease.set(_OuterInvocationLease(_current_task(), True))
|
|
288
|
+
try:
|
|
289
|
+
yield
|
|
290
|
+
finally:
|
|
291
|
+
self._lease.reset(token)
|
|
292
|
+
await self.discard(reservation)
|
|
164
293
|
|
|
165
294
|
@asynccontextmanager
|
|
166
295
|
async def outer_lease(self, *, allow_composed_request: bool = False) -> AsyncIterator[None]:
|
|
@@ -180,7 +309,7 @@ class _InvocationRuntime:
|
|
|
180
309
|
yield
|
|
181
310
|
return
|
|
182
311
|
|
|
183
|
-
|
|
312
|
+
with self._admission_lock:
|
|
184
313
|
state = self._state_getter()
|
|
185
314
|
# HostedService.start 可调用业务入口,但全部后台启动成功前 readiness 仍为 false。
|
|
186
315
|
allows_invocation = self._is_accepting and state in (
|
|
@@ -200,11 +329,11 @@ class _InvocationRuntime:
|
|
|
200
329
|
yield
|
|
201
330
|
finally:
|
|
202
331
|
self._lease.reset(token)
|
|
203
|
-
|
|
332
|
+
with self._admission_lock:
|
|
204
333
|
assert self._active > 0
|
|
205
334
|
self._active -= 1
|
|
206
335
|
if self._active == 0:
|
|
207
|
-
self.
|
|
336
|
+
self._changed.set()
|
|
208
337
|
|
|
209
338
|
def current(self) -> _InvocationState | None:
|
|
210
339
|
return self._ambient.get()
|
|
@@ -286,6 +415,30 @@ class _InvocationRuntime:
|
|
|
286
415
|
self._caller.reset(caller_token)
|
|
287
416
|
self._lease.reset(lease_token)
|
|
288
417
|
|
|
418
|
+
def context_for_action(self, container: AsyncContainer) -> ApplicationInvocationContext:
|
|
419
|
+
"""由原生 ACTION factory 缓存上下文,DI 与调用管线共享同一身份。"""
|
|
420
|
+
# Dishka 1.10 的公开 context 属性已弃用;这里只读取原生 scope 的输入,
|
|
421
|
+
# 不写 context/cache,也不复制容器的解析或缓存。第三方字段访问集中于此。
|
|
422
|
+
native_context = container._context
|
|
423
|
+
if native_context is None or ApplicationInvocationContext not in native_context:
|
|
424
|
+
return self._new_context(_DEFAULT_CONNECTION_NAME)
|
|
425
|
+
supplied: object = native_context[ApplicationInvocationContext]
|
|
426
|
+
if not isinstance(supplied, ApplicationInvocationContext):
|
|
427
|
+
raise ApplicationInvocationRejectedError(reason="invalid ACTION invocation context")
|
|
428
|
+
if supplied.application_id != self.application_id:
|
|
429
|
+
raise ApplicationInvocationRejectedError(
|
|
430
|
+
reason="ACTION invocation context belongs to another Application"
|
|
431
|
+
)
|
|
432
|
+
return supplied
|
|
433
|
+
|
|
434
|
+
def _new_context(self, connection_name: str) -> ApplicationInvocationContext:
|
|
435
|
+
return ApplicationInvocationContext(
|
|
436
|
+
application_id=self.application_id,
|
|
437
|
+
invocation_id=uuid4(),
|
|
438
|
+
parent_invocation_id=None,
|
|
439
|
+
connection_name=connection_name,
|
|
440
|
+
)
|
|
441
|
+
|
|
289
442
|
async def run_action(
|
|
290
443
|
self,
|
|
291
444
|
container: AsyncContainer,
|
|
@@ -294,14 +447,16 @@ class _InvocationRuntime:
|
|
|
294
447
|
callback: Callable[[AsyncContainer], Awaitable[object]],
|
|
295
448
|
declaration: UnitOfWorkDeclaration,
|
|
296
449
|
method_name: str = "execute",
|
|
450
|
+
unit_of_work_enabled: bool = True,
|
|
297
451
|
) -> object:
|
|
298
452
|
"""执行 scope 独立于数据库 UoW;自动边界只完成自己创建或加入的工作。"""
|
|
299
|
-
context =
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
connection_name=connection_name,
|
|
453
|
+
context = (
|
|
454
|
+
await container.get(ApplicationInvocationContext)
|
|
455
|
+
if container.scope is Scope.ACTION
|
|
456
|
+
else self._new_context(connection_name)
|
|
304
457
|
)
|
|
458
|
+
if context.connection_name != connection_name:
|
|
459
|
+
raise NestedConnectionError(outer=context.connection_name, requested=connection_name)
|
|
305
460
|
work: UnitOfWork | None = None
|
|
306
461
|
async with _managed_action(
|
|
307
462
|
container, context, committed=lambda: work is not None and work.committed
|
|
@@ -309,6 +464,8 @@ class _InvocationRuntime:
|
|
|
309
464
|
state = _InvocationState(context, _current_task(), action)
|
|
310
465
|
token = self.bind(state)
|
|
311
466
|
try:
|
|
467
|
+
if not unit_of_work_enabled:
|
|
468
|
+
return await callback(action)
|
|
312
469
|
manager = await action.get(UnitOfWorkManager)
|
|
313
470
|
if declaration.disabled:
|
|
314
471
|
manager._check_current(connection_name)
|
|
@@ -322,6 +479,27 @@ class _InvocationRuntime:
|
|
|
322
479
|
self.reset(token)
|
|
323
480
|
|
|
324
481
|
|
|
482
|
+
@asynccontextmanager
|
|
483
|
+
async def _managed_request(parent: AsyncContainer) -> AsyncIterator[AsyncContainer]:
|
|
484
|
+
"""排空入口自己创建的 REQUEST;不推断业务事务是否已经提交。"""
|
|
485
|
+
request = parent(scope=Scope.REQUEST)
|
|
486
|
+
primary: BaseException | None = None
|
|
487
|
+
try:
|
|
488
|
+
yield request
|
|
489
|
+
except BaseException as error:
|
|
490
|
+
primary = error
|
|
491
|
+
primary, failures = await _close_action(request, primary)
|
|
492
|
+
if failures:
|
|
493
|
+
# REQUEST 无事务 owner;保留业务原异常及已有 cause,不伪造 committed=False。
|
|
494
|
+
inherited = (primary.__cause__,) if primary is not None and primary.__cause__ else ()
|
|
495
|
+
cleanup = BaseExceptionGroup("REQUEST cleanup failed", (*inherited, *failures))
|
|
496
|
+
if primary is not None:
|
|
497
|
+
raise primary from cleanup
|
|
498
|
+
raise cleanup
|
|
499
|
+
if primary is not None:
|
|
500
|
+
raise primary
|
|
501
|
+
|
|
502
|
+
|
|
325
503
|
@asynccontextmanager
|
|
326
504
|
async def _managed_action(
|
|
327
505
|
parent: AsyncContainer,
|
|
@@ -329,6 +507,10 @@ async def _managed_action(
|
|
|
329
507
|
*,
|
|
330
508
|
committed: Callable[[], bool],
|
|
331
509
|
) -> AsyncIterator[AsyncContainer]:
|
|
510
|
+
if parent.scope is Scope.ACTION:
|
|
511
|
+
# 普通服务可能已经由调用方的 ACTION 解析;资源仍由该 scope 的 owner 关闭。
|
|
512
|
+
yield parent
|
|
513
|
+
return
|
|
332
514
|
action = parent(context={ApplicationInvocationContext: context}, scope=Scope.ACTION)
|
|
333
515
|
primary: BaseException | None = None
|
|
334
516
|
try:
|