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
|
@@ -5,15 +5,18 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
7
|
from collections.abc import Iterator, Mapping
|
|
8
|
-
from dataclasses import
|
|
8
|
+
from dataclasses import dataclass
|
|
9
9
|
from datetime import UTC, datetime
|
|
10
10
|
from types import MappingProxyType
|
|
11
11
|
from typing import TypeVar, cast
|
|
12
12
|
|
|
13
13
|
from pydantic import BaseModel, ValidationError
|
|
14
|
+
from pydantic_core import PydanticSerializationError
|
|
14
15
|
|
|
15
|
-
from ..
|
|
16
|
+
from ..background_execution.declarations import BackgroundJobs, BackgroundSchedules
|
|
17
|
+
from ..modularity import ModuleDescriptor, ModuleKey, ModuleRegistry
|
|
16
18
|
from ..modularity.discovery import _DiscoveredType, _is_concrete_implementation
|
|
19
|
+
from ..validation.models import _revalidate_model
|
|
17
20
|
from .contracts import BackgroundJobDefinition, BackgroundJobHandler, BackgroundJobSchedule
|
|
18
21
|
from .declaration import _definition_from_type
|
|
19
22
|
from .errors import BackgroundJobDefinitionError, BackgroundJobPayloadError
|
|
@@ -21,10 +24,19 @@ from .errors import BackgroundJobDefinitionError, BackgroundJobPayloadError
|
|
|
21
24
|
_TPayload = TypeVar("_TPayload", bound=BaseModel)
|
|
22
25
|
|
|
23
26
|
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class _CompiledJobSchedule:
|
|
29
|
+
"""单个 Application 解释后的计划;持久任务事实仍引用冻结定义。"""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
cron: str
|
|
33
|
+
definition: BackgroundJobDefinition[BaseModel]
|
|
34
|
+
payload: bytes
|
|
35
|
+
|
|
36
|
+
|
|
24
37
|
class BackgroundJobCatalog:
|
|
25
38
|
__slots__ = (
|
|
26
39
|
"_current",
|
|
27
|
-
"_declarations",
|
|
28
40
|
"_definitions",
|
|
29
41
|
"_fingerprint",
|
|
30
42
|
"_owners",
|
|
@@ -34,9 +46,8 @@ class BackgroundJobCatalog:
|
|
|
34
46
|
def __init__(
|
|
35
47
|
self,
|
|
36
48
|
definitions: tuple[BackgroundJobDefinition[BaseModel], ...],
|
|
37
|
-
schedules: tuple[
|
|
49
|
+
schedules: tuple[_CompiledJobSchedule, ...],
|
|
38
50
|
*,
|
|
39
|
-
declarations: tuple[tuple[type[AppModule], str, tuple[object, ...]], ...],
|
|
40
51
|
owners: Mapping[tuple[str, int], ModuleKey],
|
|
41
52
|
) -> None:
|
|
42
53
|
self._definitions = MappingProxyType(
|
|
@@ -44,7 +55,6 @@ class BackgroundJobCatalog:
|
|
|
44
55
|
)
|
|
45
56
|
self._current = MappingProxyType({item.name: item for item in definitions if item.current})
|
|
46
57
|
self._schedules = MappingProxyType({item.name: item for item in schedules})
|
|
47
|
-
self._declarations = declarations
|
|
48
58
|
self._owners = MappingProxyType(dict(owners))
|
|
49
59
|
encoded = json.dumps(
|
|
50
60
|
{
|
|
@@ -66,8 +76,8 @@ class BackgroundJobCatalog:
|
|
|
66
76
|
{
|
|
67
77
|
"name": item.name,
|
|
68
78
|
"cron": item.cron,
|
|
69
|
-
"job_name": item.
|
|
70
|
-
"job_version": item.
|
|
79
|
+
"job_name": item.definition.name,
|
|
80
|
+
"job_version": item.definition.version,
|
|
71
81
|
"payload": item.payload.decode(),
|
|
72
82
|
}
|
|
73
83
|
for item in schedules
|
|
@@ -83,7 +93,7 @@ class BackgroundJobCatalog:
|
|
|
83
93
|
return tuple(self._definitions.values())
|
|
84
94
|
|
|
85
95
|
@property
|
|
86
|
-
def schedules(self) -> tuple[
|
|
96
|
+
def schedules(self) -> tuple[_CompiledJobSchedule, ...]:
|
|
87
97
|
return tuple(self._schedules.values())
|
|
88
98
|
|
|
89
99
|
@property
|
|
@@ -99,7 +109,7 @@ class BackgroundJobCatalog:
|
|
|
99
109
|
def current(self, name: str) -> BackgroundJobDefinition[BaseModel] | None:
|
|
100
110
|
return self._current.get(name)
|
|
101
111
|
|
|
102
|
-
def get_schedule(self, name: str) ->
|
|
112
|
+
def get_schedule(self, name: str) -> _CompiledJobSchedule | None:
|
|
103
113
|
return self._schedules.get(name)
|
|
104
114
|
|
|
105
115
|
def owns(self, definition: BackgroundJobDefinition[_TPayload]) -> bool:
|
|
@@ -127,20 +137,16 @@ class BackgroundJobCatalog:
|
|
|
127
137
|
raise BackgroundJobDefinitionError(
|
|
128
138
|
job=definition.name, reason="Job class metadata changed after build"
|
|
129
139
|
)
|
|
130
|
-
for module_type, attribute, declared in self._declarations:
|
|
131
|
-
if module_type.__dict__.get(attribute, ()) is not declared:
|
|
132
|
-
raise BackgroundJobDefinitionError(
|
|
133
|
-
job=f"{module_type.__module__}.{module_type.__qualname__}",
|
|
134
|
-
reason=f"Module {attribute} metadata changed after build",
|
|
135
|
-
)
|
|
136
140
|
|
|
137
141
|
|
|
138
142
|
def _build_background_job_catalog(
|
|
139
143
|
registry: ModuleRegistry, discoveries: tuple[_DiscoveredType, ...]
|
|
140
144
|
) -> BackgroundJobCatalog:
|
|
141
145
|
builder = _BackgroundJobCatalogBuilder()
|
|
142
|
-
for descriptor in registry.
|
|
143
|
-
builder.
|
|
146
|
+
for descriptor, declaration in registry.declarations(BackgroundJobs):
|
|
147
|
+
builder.add_definitions(descriptor, declaration.definitions)
|
|
148
|
+
for descriptor, schedules in registry.declarations(BackgroundSchedules):
|
|
149
|
+
builder.add_schedules(descriptor, schedules.schedules)
|
|
144
150
|
seen: set[tuple[ModuleKey, type[object]]] = set()
|
|
145
151
|
for discovery in discoveries:
|
|
146
152
|
candidate = discovery.candidate
|
|
@@ -159,7 +165,6 @@ class _BackgroundJobCatalogBuilder:
|
|
|
159
165
|
"""拥有 catalog build 期间的 mutable arbitration state,产物仍为 immutable。"""
|
|
160
166
|
|
|
161
167
|
__slots__ = (
|
|
162
|
-
"_declarations",
|
|
163
168
|
"_definitions",
|
|
164
169
|
"_name_owners",
|
|
165
170
|
"_owners",
|
|
@@ -169,31 +174,21 @@ class _BackgroundJobCatalogBuilder:
|
|
|
169
174
|
|
|
170
175
|
def __init__(self) -> None:
|
|
171
176
|
self._definitions: list[BackgroundJobDefinition[BaseModel]] = []
|
|
172
|
-
self._raw_schedules: list[BackgroundJobSchedule] = []
|
|
177
|
+
self._raw_schedules: list[BackgroundJobSchedule[BaseModel]] = []
|
|
173
178
|
self._owners: dict[tuple[str, int], ModuleKey] = {}
|
|
174
179
|
self._name_owners: dict[str, ModuleKey] = {}
|
|
175
180
|
self._schedule_owners: dict[str, ModuleKey] = {}
|
|
176
|
-
self._declarations: list[tuple[type[AppModule], str, tuple[object, ...]]] = []
|
|
177
181
|
|
|
178
|
-
def
|
|
179
|
-
|
|
180
|
-
|
|
182
|
+
def add_definitions(
|
|
183
|
+
self,
|
|
184
|
+
descriptor: ModuleDescriptor,
|
|
185
|
+
declared_definitions: tuple[object, ...],
|
|
186
|
+
) -> None:
|
|
181
187
|
_validate_metadata_tuple(
|
|
182
188
|
declared_definitions,
|
|
183
189
|
owner=str(descriptor.key),
|
|
184
190
|
attribute="background_job_definitions",
|
|
185
191
|
)
|
|
186
|
-
_validate_metadata_tuple(
|
|
187
|
-
declared_schedules,
|
|
188
|
-
owner=str(descriptor.key),
|
|
189
|
-
attribute="background_job_schedules",
|
|
190
|
-
)
|
|
191
|
-
self._declarations.extend(
|
|
192
|
-
(
|
|
193
|
-
(descriptor.module_type, "background_job_definitions", declared_definitions),
|
|
194
|
-
(descriptor.module_type, "background_job_schedules", declared_schedules),
|
|
195
|
-
)
|
|
196
|
-
)
|
|
197
192
|
for definition in declared_definitions:
|
|
198
193
|
if isinstance(definition, BackgroundJobDefinition) and isinstance(
|
|
199
194
|
definition.handler_type, type
|
|
@@ -203,6 +198,17 @@ class _BackgroundJobCatalogBuilder:
|
|
|
203
198
|
reason="class Jobs must be discovered through Module scan_packages",
|
|
204
199
|
)
|
|
205
200
|
self._add_definition(definition, descriptor.key)
|
|
201
|
+
|
|
202
|
+
def add_schedules(
|
|
203
|
+
self,
|
|
204
|
+
descriptor: ModuleDescriptor,
|
|
205
|
+
declared_schedules: tuple[object, ...],
|
|
206
|
+
) -> None:
|
|
207
|
+
_validate_metadata_tuple(
|
|
208
|
+
declared_schedules,
|
|
209
|
+
owner=str(descriptor.key),
|
|
210
|
+
attribute="background_job_schedules",
|
|
211
|
+
)
|
|
206
212
|
for schedule in declared_schedules:
|
|
207
213
|
self._add_schedule(schedule, descriptor.key)
|
|
208
214
|
|
|
@@ -226,7 +232,6 @@ class _BackgroundJobCatalogBuilder:
|
|
|
226
232
|
return BackgroundJobCatalog(
|
|
227
233
|
tuple(self._definitions),
|
|
228
234
|
schedules,
|
|
229
|
-
declarations=tuple(self._declarations),
|
|
230
235
|
owners=self._owners,
|
|
231
236
|
)
|
|
232
237
|
|
|
@@ -278,19 +283,31 @@ def _validate_metadata_tuple(value: object, *, owner: str, attribute: str) -> No
|
|
|
278
283
|
|
|
279
284
|
|
|
280
285
|
def _canonicalize_schedule(
|
|
281
|
-
schedule: BackgroundJobSchedule,
|
|
286
|
+
schedule: BackgroundJobSchedule[BaseModel],
|
|
282
287
|
definitions: Mapping[tuple[str, int], BackgroundJobDefinition[BaseModel]],
|
|
283
|
-
) ->
|
|
284
|
-
|
|
288
|
+
) -> _CompiledJobSchedule:
|
|
289
|
+
target = schedule.job
|
|
290
|
+
definition = (
|
|
291
|
+
next((item for item in definitions.values() if item.handler_type is target), None)
|
|
292
|
+
if isinstance(target, type)
|
|
293
|
+
else definitions.get((target.name, target.version))
|
|
294
|
+
)
|
|
285
295
|
if definition is None:
|
|
286
296
|
raise BackgroundJobDefinitionError(
|
|
287
297
|
job=schedule.name,
|
|
288
298
|
reason="schedule target does not exist in the Application catalog",
|
|
289
299
|
)
|
|
300
|
+
if not isinstance(target, type) and definition is not target:
|
|
301
|
+
raise BackgroundJobDefinitionError(
|
|
302
|
+
job=schedule.name, reason="schedule definition is not owned by this Application catalog"
|
|
303
|
+
)
|
|
290
304
|
cron = _normalize_cron(schedule.name, schedule.cron)
|
|
291
305
|
try:
|
|
292
|
-
|
|
293
|
-
|
|
306
|
+
if not isinstance(schedule.payload, definition.payload_type):
|
|
307
|
+
raise TypeError("scheduled payload does not match the job definition")
|
|
308
|
+
# 重验显式对象,不能让 model_construct 或事后修改绕过持久 payload schema。
|
|
309
|
+
payload = _revalidate_model(definition.payload_type, schedule.payload)
|
|
310
|
+
except (TypeError, ValidationError, PydanticSerializationError):
|
|
294
311
|
raise BackgroundJobPayloadError(
|
|
295
312
|
job=definition.name,
|
|
296
313
|
version=definition.version,
|
|
@@ -302,7 +319,7 @@ def _canonicalize_schedule(
|
|
|
302
319
|
separators=(",", ":"),
|
|
303
320
|
ensure_ascii=False,
|
|
304
321
|
).encode()
|
|
305
|
-
return
|
|
322
|
+
return _CompiledJobSchedule(schedule.name, cron, definition, canonical)
|
|
306
323
|
|
|
307
324
|
|
|
308
325
|
def _normalize_cron(schedule_name: str, expression: str) -> str:
|
|
@@ -10,9 +10,11 @@ from dataclasses import dataclass, field
|
|
|
10
10
|
from datetime import datetime, timedelta
|
|
11
11
|
from typing import ClassVar, Generic, Protocol, TypeVar, cast
|
|
12
12
|
|
|
13
|
+
from dishka import Scope
|
|
13
14
|
from pydantic import BaseModel
|
|
14
15
|
|
|
15
16
|
from ..invocation import BackgroundSystemCaller
|
|
17
|
+
from ..modularity import ModuleRef, ModuleRequirement
|
|
16
18
|
from .errors import BackgroundJobDefinitionError
|
|
17
19
|
|
|
18
20
|
_TPayload = TypeVar("_TPayload", bound=BaseModel)
|
|
@@ -35,6 +37,7 @@ class BackgroundJobContext:
|
|
|
35
37
|
|
|
36
38
|
@dataclass(frozen=True, slots=True)
|
|
37
39
|
class BackgroundJobRetryPolicy:
|
|
40
|
+
# 包含首次执行的总次数;1 表示失败后不再重试。
|
|
38
41
|
max_attempts: int = 5
|
|
39
42
|
initial_delay: timedelta = timedelta(seconds=1)
|
|
40
43
|
max_delay: timedelta = timedelta(minutes=5)
|
|
@@ -80,6 +83,11 @@ class BackgroundJobRetryPolicy:
|
|
|
80
83
|
class BackgroundJobHandler(ABC, Generic[_TPayload]):
|
|
81
84
|
"""Module 扫描的任务声明;queue loop/retry 仍由 provider 独占。"""
|
|
82
85
|
|
|
86
|
+
_fixed_service_scope: ClassVar[Scope] = Scope.ACTION
|
|
87
|
+
__background_execution_module__ = ModuleRequirement(
|
|
88
|
+
ModuleRef("python_ddd_framework.background_execution.module:BackgroundExecutionModule")
|
|
89
|
+
)
|
|
90
|
+
|
|
83
91
|
name: ClassVar[str]
|
|
84
92
|
version: ClassVar[int]
|
|
85
93
|
timeout: ClassVar[timedelta]
|
|
@@ -143,42 +151,31 @@ class BackgroundJobDefinition(Generic[_TPayload]):
|
|
|
143
151
|
|
|
144
152
|
|
|
145
153
|
@dataclass(frozen=True, slots=True)
|
|
146
|
-
class BackgroundJobSchedule:
|
|
154
|
+
class BackgroundJobSchedule(Generic[_TPayload]):
|
|
147
155
|
name: str
|
|
148
156
|
cron: str
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
payload: bytes
|
|
157
|
+
job: type[BackgroundJobHandler[_TPayload]] | BackgroundJobDefinition[_TPayload]
|
|
158
|
+
payload: _TPayload
|
|
152
159
|
|
|
153
160
|
def __post_init__(self) -> None:
|
|
154
161
|
_validate_name(self.name)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
not isinstance(self.job_version, int)
|
|
158
|
-
or isinstance(self.job_version, bool)
|
|
159
|
-
or self.job_version < 1
|
|
162
|
+
if not isinstance(self.job, BackgroundJobDefinition) and not (
|
|
163
|
+
isinstance(self.job, type) and issubclass(self.job, BackgroundJobHandler)
|
|
160
164
|
):
|
|
161
165
|
raise BackgroundJobDefinitionError(
|
|
162
166
|
job=self.name,
|
|
163
|
-
reason="
|
|
167
|
+
reason="schedule job must be a Job type or BackgroundJobDefinition",
|
|
164
168
|
)
|
|
165
169
|
if not isinstance(self.cron, str) or not self.cron.strip():
|
|
166
170
|
raise BackgroundJobDefinitionError(
|
|
167
171
|
job=self.name,
|
|
168
172
|
reason="cron must be a non-empty string",
|
|
169
173
|
)
|
|
170
|
-
if not isinstance(self.payload,
|
|
174
|
+
if not isinstance(self.payload, BaseModel):
|
|
171
175
|
raise BackgroundJobDefinitionError(
|
|
172
176
|
job=self.name,
|
|
173
|
-
reason="payload must be
|
|
177
|
+
reason="schedule payload must be a typed Pydantic model",
|
|
174
178
|
)
|
|
175
|
-
try:
|
|
176
|
-
self.payload.decode("utf-8")
|
|
177
|
-
except UnicodeDecodeError:
|
|
178
|
-
raise BackgroundJobDefinitionError(
|
|
179
|
-
job=self.name,
|
|
180
|
-
reason="payload must be UTF-8 JSON bytes",
|
|
181
|
-
) from None
|
|
182
179
|
|
|
183
180
|
|
|
184
181
|
class BackgroundJobEnqueuer(Protocol):
|
|
@@ -69,6 +69,10 @@ class _PgQueuerBackgroundJobEnqueuer:
|
|
|
69
69
|
driver_connection: Any = raw.driver_connection
|
|
70
70
|
if not isinstance(driver_connection, asyncpg.Connection):
|
|
71
71
|
raise TypeError("SQLAlchemy connection does not expose an asyncpg connection")
|
|
72
|
+
# asyncpg 方言直到首次 SQL 才真正 BEGIN;仅取得 Session connection 仍可能未开事务。
|
|
73
|
+
# 经 SQLAlchemy 公开执行路径启动事务,保留 UoW 的提交/回滚 owner,再交给原生队列。
|
|
74
|
+
if not driver_connection.is_in_transaction():
|
|
75
|
+
await connection.exec_driver_sql("SELECT 1")
|
|
72
76
|
queries = _queries_for_connection(
|
|
73
77
|
driver_connection,
|
|
74
78
|
self._options,
|
|
@@ -8,6 +8,7 @@ from typing import Any
|
|
|
8
8
|
import asyncpg # type: ignore[import-untyped]
|
|
9
9
|
from dishka import Provider, Scope
|
|
10
10
|
|
|
11
|
+
from ...background_execution.module import BackgroundExecutionModule
|
|
11
12
|
from ...background_jobs import BackgroundJobCatalog
|
|
12
13
|
from ...background_workers.runtime import _BackgroundCoordinator
|
|
13
14
|
from ...lifecycle import ConfigureContext, InitializeContext, ShutdownContext
|
|
@@ -38,7 +39,7 @@ def _migration_registration(options: OptionsSnapshot) -> SqlAlchemyMigrationRegi
|
|
|
38
39
|
|
|
39
40
|
@contributes_sqlalchemy_migrations(_migration_registration)
|
|
40
41
|
class PgQueuerBackgroundJobsModule(AppModule):
|
|
41
|
-
dependencies = (SqlAlchemyPersistenceModule,)
|
|
42
|
+
dependencies = (SqlAlchemyPersistenceModule, BackgroundExecutionModule)
|
|
42
43
|
|
|
43
44
|
def __init__(self) -> None:
|
|
44
45
|
self._runtimes: list[_PgQueuerRuntime] = []
|
|
@@ -31,8 +31,8 @@ from ...background_execution.lifecycle import (
|
|
|
31
31
|
_TaskExecution,
|
|
32
32
|
)
|
|
33
33
|
from ...options import Options
|
|
34
|
-
from ..catalog import BackgroundJobCatalog
|
|
35
|
-
from ..contracts import BackgroundJobDefinition
|
|
34
|
+
from ..catalog import BackgroundJobCatalog, _CompiledJobSchedule
|
|
35
|
+
from ..contracts import BackgroundJobDefinition
|
|
36
36
|
from ..errors import (
|
|
37
37
|
BackgroundJobDefinitionError,
|
|
38
38
|
BackgroundJobExecutionError,
|
|
@@ -329,10 +329,9 @@ class _PgQueuerRuntime:
|
|
|
329
329
|
if not self._options.scheduler_enabled:
|
|
330
330
|
return
|
|
331
331
|
for schedule in self._catalog.schedules:
|
|
332
|
-
if schedule.
|
|
332
|
+
if schedule.definition.name != self._job_name:
|
|
333
333
|
continue
|
|
334
|
-
definition =
|
|
335
|
-
assert definition is not None
|
|
334
|
+
definition = schedule.definition
|
|
336
335
|
envelope = _encode_scheduled_envelope(definition, schedule.payload)
|
|
337
336
|
target_entrypoint = _entrypoint_name(definition.name, definition.version)
|
|
338
337
|
|
|
@@ -360,7 +359,8 @@ class _PgQueuerRuntime:
|
|
|
360
359
|
def create(parameters: EntrypointExecutorParameters) -> _ShutdownAwareRetryExecutor:
|
|
361
360
|
return _ShutdownAwareRetryExecutor(
|
|
362
361
|
parameters=parameters,
|
|
363
|
-
|
|
362
|
+
# PgQueuer 从 0 计数,原生阈值代表额外重试;公开策略包含首次执行。
|
|
363
|
+
max_attempts=policy.max_attempts - 1,
|
|
364
364
|
initial_delay=policy.initial_delay,
|
|
365
365
|
max_delay=policy.max_delay,
|
|
366
366
|
backoff_multiplier=policy.backoff_multiplier,
|
|
@@ -426,7 +426,7 @@ def _entrypoint_name(name: str, version: int) -> str:
|
|
|
426
426
|
return f"{name}.v{version}"
|
|
427
427
|
|
|
428
428
|
|
|
429
|
-
def _schedule_entrypoint_name(schedule:
|
|
429
|
+
def _schedule_entrypoint_name(schedule: _CompiledJobSchedule) -> str:
|
|
430
430
|
return f"schedule.{schedule.name}"
|
|
431
431
|
|
|
432
432
|
|
|
@@ -7,24 +7,23 @@ import json
|
|
|
7
7
|
from collections.abc import Iterator, Mapping
|
|
8
8
|
from types import MappingProxyType
|
|
9
9
|
|
|
10
|
-
from ..
|
|
10
|
+
from ..background_execution.declarations import BackgroundWorkers
|
|
11
|
+
from ..modularity import ModuleKey, ModuleRegistry
|
|
11
12
|
from .contracts import BackgroundWorker, BackgroundWorkerDefinition, _definition_from_type
|
|
12
13
|
from .errors import BackgroundWorkerDefinitionError
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class BackgroundWorkerCatalog:
|
|
16
|
-
__slots__ = ("
|
|
17
|
+
__slots__ = ("_definitions", "_fingerprint", "_owners")
|
|
17
18
|
|
|
18
19
|
def __init__(
|
|
19
20
|
self,
|
|
20
21
|
definitions: tuple[BackgroundWorkerDefinition, ...],
|
|
21
22
|
*,
|
|
22
23
|
owners: Mapping[str, ModuleKey],
|
|
23
|
-
declarations: tuple[tuple[type[AppModule], str, tuple[object, ...]], ...],
|
|
24
24
|
) -> None:
|
|
25
25
|
self._definitions = MappingProxyType({item.name: item for item in definitions})
|
|
26
26
|
self._owners = MappingProxyType(dict(owners))
|
|
27
|
-
self._declarations = declarations
|
|
28
27
|
encoded = json.dumps(
|
|
29
28
|
[
|
|
30
29
|
{
|
|
@@ -66,53 +65,41 @@ class BackgroundWorkerCatalog:
|
|
|
66
65
|
raise BackgroundWorkerDefinitionError(
|
|
67
66
|
worker=definition.name, reason="Worker class metadata changed after build"
|
|
68
67
|
)
|
|
69
|
-
for module_type, attribute, declared in self._declarations:
|
|
70
|
-
if module_type.__dict__.get(attribute, ()) is not declared:
|
|
71
|
-
raise BackgroundWorkerDefinitionError(
|
|
72
|
-
worker=f"{module_type.__module__}.{module_type.__qualname__}",
|
|
73
|
-
reason="Module background worker metadata changed after build",
|
|
74
|
-
)
|
|
75
68
|
|
|
76
69
|
|
|
77
70
|
def _build_background_worker_catalog(registry: ModuleRegistry) -> BackgroundWorkerCatalog:
|
|
78
71
|
definitions: list[BackgroundWorkerDefinition] = []
|
|
79
72
|
owners: dict[str, ModuleKey] = {}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
73
|
+
for descriptor, declaration in registry.declarations(BackgroundWorkers):
|
|
74
|
+
if not isinstance(declaration.workers, tuple):
|
|
75
|
+
raise BackgroundWorkerDefinitionError(
|
|
76
|
+
worker=str(descriptor.key),
|
|
77
|
+
reason="workers must be an immutable tuple",
|
|
78
|
+
)
|
|
79
|
+
for candidate in declaration.workers:
|
|
80
|
+
definition = _interpret_worker(candidate)
|
|
81
|
+
previous = owners.get(definition.name)
|
|
82
|
+
if previous is not None:
|
|
85
83
|
raise BackgroundWorkerDefinitionError(
|
|
86
|
-
worker=
|
|
87
|
-
reason=f"
|
|
84
|
+
worker=definition.name,
|
|
85
|
+
reason=f"worker is already owned by {previous}",
|
|
88
86
|
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
definition = _interpret_worker(candidate, attribute)
|
|
92
|
-
previous = owners.get(definition.name)
|
|
93
|
-
if previous is not None:
|
|
94
|
-
raise BackgroundWorkerDefinitionError(
|
|
95
|
-
worker=definition.name,
|
|
96
|
-
reason=f"worker is already owned by {previous}",
|
|
97
|
-
)
|
|
98
|
-
owners[definition.name] = descriptor.key
|
|
99
|
-
definitions.append(definition)
|
|
87
|
+
owners[definition.name] = descriptor.key
|
|
88
|
+
definitions.append(definition)
|
|
100
89
|
return BackgroundWorkerCatalog(
|
|
101
90
|
tuple(definitions),
|
|
102
91
|
owners=owners,
|
|
103
|
-
declarations=tuple(declarations),
|
|
104
92
|
)
|
|
105
93
|
|
|
106
94
|
|
|
107
|
-
def _interpret_worker(candidate: object
|
|
108
|
-
if
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
elif isinstance(candidate, BackgroundWorkerDefinition) and not isinstance(
|
|
95
|
+
def _interpret_worker(candidate: object) -> BackgroundWorkerDefinition:
|
|
96
|
+
if isinstance(candidate, type) and issubclass(candidate, BackgroundWorker):
|
|
97
|
+
return _definition_from_type(candidate)
|
|
98
|
+
if isinstance(candidate, BackgroundWorkerDefinition) and not isinstance(
|
|
112
99
|
candidate.worker_type, type
|
|
113
100
|
):
|
|
114
101
|
return candidate
|
|
115
102
|
raise BackgroundWorkerDefinitionError(
|
|
116
|
-
worker=
|
|
117
|
-
reason="
|
|
103
|
+
worker=str(candidate),
|
|
104
|
+
reason="workers requires Worker types or definitions of async functions",
|
|
118
105
|
)
|
|
@@ -11,6 +11,7 @@ from datetime import timedelta
|
|
|
11
11
|
from enum import Enum
|
|
12
12
|
from typing import ClassVar, cast
|
|
13
13
|
|
|
14
|
+
from dishka import Scope
|
|
14
15
|
from pydantic import ConfigDict, Field, field_serializer
|
|
15
16
|
|
|
16
17
|
from ..background_execution import BackgroundJobOptions, BackgroundTaskOptions
|
|
@@ -53,6 +54,8 @@ class BackgroundExecutionOptions(BaseOptions):
|
|
|
53
54
|
class BackgroundWorker(ABC):
|
|
54
55
|
"""一次 iteration 的实现边界;永久 loop 由 Application coordinator 独占。"""
|
|
55
56
|
|
|
57
|
+
_fixed_service_scope: ClassVar[Scope] = Scope.ACTION
|
|
58
|
+
|
|
56
59
|
name: ClassVar[str]
|
|
57
60
|
interval: ClassVar[timedelta]
|
|
58
61
|
iteration_timeout: ClassVar[timedelta]
|
|
@@ -9,12 +9,12 @@ from uuid import uuid4
|
|
|
9
9
|
|
|
10
10
|
from dishka import AsyncContainer, Scope
|
|
11
11
|
|
|
12
|
-
from ..application_services.contracts import _DEFAULT_CONNECTION_NAME
|
|
13
12
|
from ..application_services.errors import ApplicationInvocationRejectedError
|
|
14
13
|
from ..application_services.invocation import _InvocationRuntime
|
|
15
14
|
from ..authorization import CurrentUser
|
|
16
15
|
from ..background_execution.lifecycle import _TaskExecution
|
|
17
16
|
from ..invocation import BackgroundSystemCaller
|
|
17
|
+
from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME
|
|
18
18
|
from .contracts import BackgroundWorkerContext, BackgroundWorkerDefinition
|
|
19
19
|
|
|
20
20
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -16,18 +16,56 @@ from ..background_execution import (
|
|
|
16
16
|
BackgroundTaskOptions,
|
|
17
17
|
BackgroundTaskSnapshot,
|
|
18
18
|
)
|
|
19
|
-
from ..background_execution.lifecycle import
|
|
19
|
+
from ..background_execution.lifecycle import (
|
|
20
|
+
_BackgroundExecutionStartFailure,
|
|
21
|
+
_BackgroundRuntimeFailure,
|
|
22
|
+
_TaskExecution,
|
|
23
|
+
)
|
|
20
24
|
from ..background_execution.local import _LocalTask
|
|
21
25
|
from ..background_execution.locks import _check_previous_owners
|
|
22
26
|
from ..background_execution.processes import _ProcessSlot
|
|
23
27
|
from ..background_jobs import BackgroundJobCatalog
|
|
28
|
+
from ..errors.lifecycle import ApplicationRuntimeError
|
|
24
29
|
from ..invocation import BackgroundSystemCaller
|
|
30
|
+
from ..lifecycle import RuntimeParticipant, ShutdownReason
|
|
25
31
|
from .catalog import BackgroundWorkerCatalog
|
|
26
32
|
from .contracts import BackgroundExecutionOptions, BackgroundExecutionProfile
|
|
27
33
|
from .execution import _WorkerExecution
|
|
28
34
|
|
|
29
35
|
|
|
30
36
|
class _BackgroundCoordinator:
|
|
37
|
+
def runtime_participant(self) -> RuntimeParticipant:
|
|
38
|
+
def prepare(run_services: bool) -> None:
|
|
39
|
+
if not run_services:
|
|
40
|
+
self.disable_for_database()
|
|
41
|
+
|
|
42
|
+
async def start() -> None:
|
|
43
|
+
try:
|
|
44
|
+
await self.start()
|
|
45
|
+
except BaseException as error:
|
|
46
|
+
raise _BackgroundExecutionStartFailure(original_error=error) from error
|
|
47
|
+
|
|
48
|
+
async def stop(reason: ShutdownReason) -> tuple[BaseException, ...]:
|
|
49
|
+
return ()
|
|
50
|
+
|
|
51
|
+
async def wait() -> ApplicationRuntimeError:
|
|
52
|
+
failure = await self.wait_for_runtime_failure()
|
|
53
|
+
error = ApplicationRuntimeError(
|
|
54
|
+
component=failure.component,
|
|
55
|
+
error_type=failure.error_type,
|
|
56
|
+
)
|
|
57
|
+
error._source = failure._source
|
|
58
|
+
return error
|
|
59
|
+
|
|
60
|
+
return RuntimeParticipant(
|
|
61
|
+
prepare=prepare,
|
|
62
|
+
start=start,
|
|
63
|
+
stop=stop,
|
|
64
|
+
quiesce=self.quiesce,
|
|
65
|
+
drain=self.stop,
|
|
66
|
+
failure=lambda: wait() if self.has_runtime_failure_source else None,
|
|
67
|
+
)
|
|
68
|
+
|
|
31
69
|
def __init__(
|
|
32
70
|
self,
|
|
33
71
|
*,
|
|
@@ -205,6 +243,14 @@ class _BackgroundCoordinator:
|
|
|
205
243
|
return slot.request(key, start)
|
|
206
244
|
return self._tasks[key].request(start)
|
|
207
245
|
|
|
246
|
+
def quiesce(self) -> None:
|
|
247
|
+
self._started = False
|
|
248
|
+
for key, task in self._tasks.items():
|
|
249
|
+
if key in self._local:
|
|
250
|
+
task.quiesce()
|
|
251
|
+
for slot in self._slots.values():
|
|
252
|
+
slot.quiesce()
|
|
253
|
+
|
|
208
254
|
async def stop(self) -> None:
|
|
209
255
|
# 即使启动前的预占或 Module 初始化失败,也必须释放已取得的位置。
|
|
210
256
|
operations = [task.shutdown() for key, task in self._tasks.items() if key in self._local]
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from .
|
|
3
|
-
from .
|
|
1
|
+
from .contracts import DistributedCache
|
|
2
|
+
from .errors import DistributedCacheError
|
|
3
|
+
from .naming import cache_name
|
|
4
|
+
from .options import DistributedCacheEntryOptions, DistributedCacheOptions
|
|
4
5
|
|
|
5
6
|
__all__ = (
|
|
6
|
-
"CacheCatalog",
|
|
7
|
-
"CacheDefinition",
|
|
8
|
-
"CacheDefinitionError",
|
|
9
7
|
"DistributedCache",
|
|
8
|
+
"DistributedCacheEntryOptions",
|
|
10
9
|
"DistributedCacheError",
|
|
10
|
+
"DistributedCacheOptions",
|
|
11
|
+
"cache_name",
|
|
11
12
|
)
|