python-ddd-framework 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_ddd_framework/__init__.py +139 -21
- python_ddd_framework/application/builder.py +23 -78
- python_ddd_framework/application/composition.py +4 -22
- python_ddd_framework/application/runtime.py +96 -291
- python_ddd_framework/application/state.py +7 -75
- python_ddd_framework/application_services/__init__.py +5 -21
- python_ddd_framework/{services/application_bindings.py → application_services/bindings.py} +52 -9
- python_ddd_framework/application_services/catalog.py +36 -288
- python_ddd_framework/application_services/contracts.py +14 -13
- python_ddd_framework/application_services/dispatcher.py +19 -269
- python_ddd_framework/application_services/invocation.py +126 -7
- python_ddd_framework/application_services/module.py +168 -0
- python_ddd_framework/application_services/pagination.py +21 -0
- python_ddd_framework/auditing/control.py +1 -1
- python_ddd_framework/auditing/module.py +15 -0
- python_ddd_framework/authorization/__init__.py +2 -0
- python_ddd_framework/authorization/contracts.py +7 -2
- python_ddd_framework/authorization/definitions.py +5 -0
- python_ddd_framework/authorization/module.py +40 -0
- python_ddd_framework/authorization/options.py +7 -0
- python_ddd_framework/background_execution/child.py +13 -3
- python_ddd_framework/background_execution/declarations.py +45 -0
- python_ddd_framework/background_execution/lifecycle.py +12 -0
- python_ddd_framework/background_execution/module.py +107 -1
- python_ddd_framework/background_jobs/catalog.py +59 -42
- python_ddd_framework/background_jobs/contracts.py +16 -19
- python_ddd_framework/background_jobs/pgqueuer/enqueue.py +4 -0
- python_ddd_framework/background_jobs/pgqueuer/module.py +2 -1
- python_ddd_framework/background_jobs/pgqueuer/runtime.py +7 -7
- python_ddd_framework/background_workers/catalog.py +23 -36
- python_ddd_framework/background_workers/contracts.py +3 -0
- python_ddd_framework/background_workers/execution.py +1 -1
- python_ddd_framework/background_workers/runtime.py +38 -1
- python_ddd_framework/caching/__init__.py +7 -6
- python_ddd_framework/caching/contracts.py +65 -67
- python_ddd_framework/caching/errors.py +0 -7
- python_ddd_framework/caching/module.py +18 -0
- python_ddd_framework/caching/naming.py +29 -0
- python_ddd_framework/caching/options.py +38 -0
- python_ddd_framework/caching/unit_of_work.py +46 -0
- python_ddd_framework/cli/__init__.py +31 -6
- python_ddd_framework/cli/project.py +68 -6
- python_ddd_framework/cli/runtime.py +1 -1
- python_ddd_framework/developer_kit/generation.py +75 -22
- python_ddd_framework/developer_kit/project_metadata.py +2 -4
- python_ddd_framework/developer_kit/templates/basic/cookiecutter.json +4 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/README.md +45 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/__init__.py.jinja +0 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/__init__.py.jinja +0 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/contracts/conversion_service.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/module.py.jinja +38 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/__init__.py.jinja +0 -0
- python_ddd_framework/developer_kit/templates/basic/{{cookiecutter.module_name}}/services/default_conversion_service.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +41 -49
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/handler.py.jinja +45 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_approval/payload.py.jinja +8 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/handler.py.jinja +35 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/payload.py.jinja +5 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_jobs/order_statistics/schedule.py.jinja +15 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_maintenance_worker.py.jinja +28 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/background_workers/order_statistics_worker.py.jinja +26 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/order_cache.py.jinja +9 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/caching/statistics_cache.py.jinja +9 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/event_handlers/order_changed_handler.py.jinja +23 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{integration.py.jinja → hosted_services/order_integration_service.py.jinja} +8 -2
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +16 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration_services/order_reporting_service.py.jinja +16 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/interceptors/order_timing_interceptor.py.jinja +17 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +105 -10
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/{options.py.jinja → options/order_options.py.jinja} +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_approval_service.py.jinja +65 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_management_service.py.jinja +35 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/services/order_query_service.py.jinja +41 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/setting_handlers/approval_setting_observer.py.jinja +17 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/approve_order.py.jinja +6 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/inputs/create_order.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/integration_services/order_reporting_service.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +39 -2
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_approval_service.py.jinja +13 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_management_service.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/services/order_query_service.py.jinja +12 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_statistics_snapshot.py.jinja +9 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/views/order_view.py.jinja +12 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/entities/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{orders.py.jinja → entities/order.py.jinja} +10 -18
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/events/order_changed.py.jinja +13 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +42 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repositories/order_repository.py.jinja +17 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding/order_seed_contributor.py.jinja +22 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/services/order_approval_service.py.jinja +23 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/{settings.py.jinja → settings/approval_settings.py.jinja} +3 -2
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/value_objects/order_title.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/constants/order_constants.py.jinja +3 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/enums/order_status.py.jinja +6 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/errors/order_errors.py.jinja +11 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_messages.py.jinja +26 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/messages/order_observation.py.jinja +13 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +40 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/{permissions.py.jinja → permissions/order_permission_provider.py.jinja} +6 -2
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions/order_permissions.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/value_objects/money.py.jinja +18 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/filters/export_filter.py.jinja +15 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/models/refresh_orders.py.jinja +5 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +85 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/routers/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/{files.py.jinja → routers/order_files.py.jinja} +23 -20
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/websockets/order_socket.py.jinja +44 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -1
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +1 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/base.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/{orders.py.jinja → order_model.py.jinja} +7 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +42 -5
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +79 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +4 -3
- python_ddd_framework/developer_kit/templates/project/cookiecutter.json +1 -1
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +16 -3
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +29 -22
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{app.development.yaml.jinja → backend/app.development.yaml.jinja} +2 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{pyproject.toml.jinja → backend/pyproject.toml.jinja} +1 -1
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/main.py.jinja +2 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/module.py.jinja +40 -3
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +40 -10
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +134 -23
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/scripts/.gitkeep +0 -0
- python_ddd_framework/distributed_lock/contracts.py +4 -1
- python_ddd_framework/domain/__init__.py +2 -0
- python_ddd_framework/domain/services.py +11 -0
- python_ddd_framework/errors/business.py +10 -1
- python_ddd_framework/errors/lifecycle.py +3 -31
- python_ddd_framework/events/__init__.py +5 -2
- python_ddd_framework/events/aggregate.py +3 -3
- python_ddd_framework/events/catalog.py +12 -11
- python_ddd_framework/events/contracts.py +6 -1
- python_ddd_framework/events/contribution.py +1 -52
- python_ddd_framework/events/entities.py +23 -0
- python_ddd_framework/events/module.py +97 -0
- python_ddd_framework/events/runtime.py +80 -29
- python_ddd_framework/events/subscription.py +36 -0
- python_ddd_framework/events/unit_of_work.py +25 -0
- python_ddd_framework/extensions/__init__.py +22 -0
- python_ddd_framework/extensions/catalog.py +198 -0
- python_ddd_framework/extensions/contracts.py +85 -0
- python_ddd_framework/extensions/models.py +142 -0
- python_ddd_framework/extensions/module.py +21 -0
- python_ddd_framework/fastapi/__init__.py +2 -0
- python_ddd_framework/fastapi/action.py +1 -2
- python_ddd_framework/fastapi/adapter.py +16 -0
- python_ddd_framework/fastapi/application_services.py +43 -25
- python_ddd_framework/fastapi/background.py +2 -1
- python_ddd_framework/fastapi/contracts.py +26 -8
- python_ddd_framework/fastapi/health.py +1 -1
- python_ddd_framework/fastapi/module.py +14 -0
- python_ddd_framework/fastapi/parameters.py +3 -18
- python_ddd_framework/fastapi/realtime/connection.py +10 -7
- python_ddd_framework/fastapi/realtime/module.py +3 -0
- python_ddd_framework/fastapi/realtime/runtime.py +10 -24
- python_ddd_framework/fastapi/request_context.py +2 -1
- python_ddd_framework/fastapi/routing.py +2 -1
- python_ddd_framework/fastapi/server.py +8 -0
- python_ddd_framework/fastapi/settings.py +2 -1
- python_ddd_framework/hosted_services/bridge.py +20 -1
- python_ddd_framework/hosted_services/catalog.py +6 -16
- python_ddd_framework/hosted_services/contracts.py +4 -0
- python_ddd_framework/hosted_services/declarations.py +15 -0
- python_ddd_framework/hosted_services/module.py +69 -0
- python_ddd_framework/hosted_services/runtime.py +30 -1
- python_ddd_framework/identity/__init__.py +24 -0
- python_ddd_framework/identity/application.py +38 -90
- python_ddd_framework/identity/contracts.py +181 -16
- python_ddd_framework/identity/http_api.py +79 -2
- python_ddd_framework/identity/management.py +166 -0
- python_ddd_framework/identity/module.py +12 -4
- python_ddd_framework/identity/services.py +29 -16
- python_ddd_framework/identity/sqlalchemy/migrations/0003_extra_properties.py +26 -0
- python_ddd_framework/identity/sqlalchemy/migrations/0004_concurrency_version.py +29 -0
- python_ddd_framework/identity/sqlalchemy/models.py +21 -2
- python_ddd_framework/identity/sqlalchemy/module.py +11 -2
- python_ddd_framework/identity/sqlalchemy/repository.py +458 -0
- python_ddd_framework/identity/sqlalchemy/session_security.py +88 -0
- python_ddd_framework/identity/sqlalchemy/stores.py +69 -290
- python_ddd_framework/identity/tokens.py +2 -5
- python_ddd_framework/{application_services/validation.py → invocation/arguments.py} +8 -6
- python_ddd_framework/invocation/contribution.py +61 -0
- python_ddd_framework/invocation/dispatcher.py +291 -0
- python_ddd_framework/invocation/entries.py +2 -25
- python_ddd_framework/invocation/entrypoints.py +126 -0
- python_ddd_framework/{application_services → invocation}/execution.py +8 -5
- python_ddd_framework/invocation/function_runtime.py +3 -1
- python_ddd_framework/invocation/interception.py +113 -42
- python_ddd_framework/{application_services → invocation}/interceptors.py +2 -2
- python_ddd_framework/invocation/managed_proxy.py +116 -0
- python_ddd_framework/invocation/managed_services.py +250 -0
- python_ddd_framework/invocation/methods.py +345 -0
- python_ddd_framework/invocation/module.py +113 -0
- python_ddd_framework/{application_services → invocation}/policies.py +8 -1
- python_ddd_framework/invocation/validation.py +25 -0
- python_ddd_framework/lifecycle/__init__.py +2 -0
- python_ddd_framework/lifecycle/composition.py +5 -2
- python_ddd_framework/lifecycle/participants.py +33 -0
- python_ddd_framework/messaging/__init__.py +25 -0
- python_ddd_framework/messaging/channel.py +205 -0
- python_ddd_framework/messaging/contracts.py +69 -0
- python_ddd_framework/messaging/module.py +101 -0
- python_ddd_framework/messaging/options.py +13 -0
- python_ddd_framework/messaging/receipt.py +27 -0
- python_ddd_framework/messaging/runtime.py +124 -0
- python_ddd_framework/modularity/__init__.py +11 -1
- python_ddd_framework/modularity/contracts.py +47 -16
- python_ddd_framework/modularity/discovery.py +21 -2
- python_ddd_framework/modularity/graph.py +45 -1
- python_ddd_framework/modularity/registry.py +83 -3
- python_ddd_framework/notifications/__init__.py +2 -0
- python_ddd_framework/notifications/catalog.py +3 -2
- python_ddd_framework/notifications/declarations.py +18 -0
- python_ddd_framework/notifications/module.py +21 -0
- python_ddd_framework/observability/logging.py +3 -0
- python_ddd_framework/observability/tracing.py +3 -0
- python_ddd_framework/options/contribution.py +12 -0
- python_ddd_framework/options/registry.py +44 -11
- python_ddd_framework/realtime/__init__.py +2 -1
- python_ddd_framework/realtime/contracts.py +3 -7
- python_ddd_framework/realtime/messages.py +26 -1
- python_ddd_framework/redis/cache.py +243 -0
- python_ddd_framework/redis/cache_scripts.py +40 -0
- python_ddd_framework/redis/distributed_lock.py +10 -2
- python_ddd_framework/redis/module.py +13 -4
- python_ddd_framework/redis/notifications.py +5 -2
- python_ddd_framework/redis/runtime.py +1 -79
- python_ddd_framework/seeding/__init__.py +13 -0
- python_ddd_framework/seeding/contracts.py +45 -0
- python_ddd_framework/seeding/module.py +73 -0
- python_ddd_framework/seeding/runtime.py +63 -0
- python_ddd_framework/services/arbitration.py +23 -0
- python_ddd_framework/services/binding.py +2 -3
- python_ddd_framework/services/composition.py +108 -0
- python_ddd_framework/services/contribution.py +44 -54
- python_ddd_framework/services/convention.py +53 -181
- python_ddd_framework/services/fixed_lifetime.py +12 -15
- python_ddd_framework/services/framework_provider.py +3 -158
- python_ddd_framework/services/native_graph.py +35 -60
- python_ddd_framework/services/provider.py +59 -4
- python_ddd_framework/services/repository.py +6 -0
- python_ddd_framework/services/runtime.py +129 -215
- python_ddd_framework/settings/catalog.py +5 -4
- python_ddd_framework/settings/declarations.py +18 -0
- python_ddd_framework/settings/definitions.py +5 -0
- python_ddd_framework/settings/manager.py +1 -1
- python_ddd_framework/settings/module.py +50 -1
- python_ddd_framework/settings/notifications.py +4 -2
- python_ddd_framework/settings/refresh_module.py +9 -2
- python_ddd_framework/settings/sqlalchemy/module.py +2 -1
- python_ddd_framework/sqlalchemy/__init__.py +12 -0
- python_ddd_framework/sqlalchemy/alembic_runtime/env.py +1 -0
- python_ddd_framework/sqlalchemy/auditing.py +7 -1
- python_ddd_framework/sqlalchemy/extensions.py +186 -0
- python_ddd_framework/sqlalchemy/metadata.py +110 -6
- python_ddd_framework/sqlalchemy/migration.py +61 -8
- python_ddd_framework/sqlalchemy/migration_operations.py +86 -0
- python_ddd_framework/sqlalchemy/module.py +14 -11
- python_ddd_framework/sqlalchemy/module_migration.py +71 -3
- python_ddd_framework/sqlalchemy/repository.py +217 -13
- python_ddd_framework/sqlalchemy/session_provider.py +14 -1
- python_ddd_framework/testing/runtime.py +5 -4
- python_ddd_framework/unit_of_work/__init__.py +2 -1
- python_ddd_framework/unit_of_work/contracts.py +6 -7
- python_ddd_framework/unit_of_work/errors.py +11 -0
- python_ddd_framework/unit_of_work/lifecycle.py +19 -0
- python_ddd_framework/unit_of_work/manager.py +86 -32
- python_ddd_framework/unit_of_work/module.py +61 -0
- python_ddd_framework/unit_of_work/options.py +7 -1
- python_ddd_framework/validation/__init__.py +1 -0
- python_ddd_framework/validation/models.py +37 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/METADATA +139 -44
- python_ddd_framework-0.4.0.dist-info/RECORD +488 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/WHEEL +1 -1
- python_ddd_framework/application_services/seeding.py +0 -72
- python_ddd_framework/caching/catalog.py +0 -73
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +0 -10
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +0 -24
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +0 -83
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +0 -84
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +0 -35
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +0 -14
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +0 -19
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +0 -24
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +0 -36
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +0 -49
- python_ddd_framework-0.3.2.dist-info/RECORD +0 -355
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.dockerignore → backend/.dockerignore} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.gitignore → backend/.gitignore} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{.python-version → backend/.python-version} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{Dockerfile → backend/Dockerfile} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.dev.yaml.jinja → backend/compose.dev.yaml.jinja} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{compose.production.yaml.jinja → backend/compose.production.yaml.jinja} +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{src → backend/src}/host/__init__.py.jinja +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/conftest.py.jinja +0 -0
- /python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/{tests → backend/tests}/host/test_http.py.jinja +0 -0
- /python_ddd_framework/{application_services → invocation}/signature.py +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/entry_points.txt +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {python_ddd_framework-0.3.2.dist-info → python_ddd_framework-0.4.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""类型化 Redis Provider;序列化错误与业务 factory 不进入故障隐藏边界。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Awaitable, Callable, Iterable
|
|
7
|
+
from functools import partial
|
|
8
|
+
from typing import Generic, TypeVar
|
|
9
|
+
|
|
10
|
+
from pydantic import TypeAdapter
|
|
11
|
+
from redis.exceptions import RedisError
|
|
12
|
+
|
|
13
|
+
from ..caching import DistributedCacheEntryOptions, DistributedCacheError, DistributedCacheOptions
|
|
14
|
+
from ..caching.naming import _cache_name
|
|
15
|
+
from ..caching.unit_of_work import _CachePending, _pending
|
|
16
|
+
from ..options import Options
|
|
17
|
+
from ..unit_of_work import UnitOfWorkManager
|
|
18
|
+
from .cache_scripts import _READ, _WRITE
|
|
19
|
+
from .options import RedisOptions
|
|
20
|
+
from .runtime import _RedisClientOwner
|
|
21
|
+
|
|
22
|
+
_T = TypeVar("_T")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RedisDistributedCache(Generic[_T]):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
value_type: type[_T],
|
|
29
|
+
client: _RedisClientOwner,
|
|
30
|
+
options: Options[RedisOptions],
|
|
31
|
+
cache_options: Options[DistributedCacheOptions],
|
|
32
|
+
manager: UnitOfWorkManager,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._client = client.client
|
|
35
|
+
self._name = _cache_name(value_type)
|
|
36
|
+
self._prefix = options.value.key_prefix
|
|
37
|
+
self._options = cache_options.value
|
|
38
|
+
self._manager = manager
|
|
39
|
+
self._adapter: TypeAdapter[_T] = TypeAdapter(value_type)
|
|
40
|
+
|
|
41
|
+
def _key(self, key: str) -> str:
|
|
42
|
+
if not isinstance(key, str) or not key or key != key.strip() or len(key) > 512:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"cache key must be a nonempty trimmed string of at most 512 characters"
|
|
45
|
+
)
|
|
46
|
+
# 长度前缀明确隔开 Host/type/caller 三个片段,调用 key 可包含冒号而不串缓存。
|
|
47
|
+
return f"{len(self._prefix)}:{self._prefix}:{len(self._name)}:{self._name}:{key}"
|
|
48
|
+
|
|
49
|
+
def _state(self, consider_uow: bool) -> _CachePending | None:
|
|
50
|
+
if not consider_uow:
|
|
51
|
+
return None
|
|
52
|
+
work = self._manager.current
|
|
53
|
+
return _pending(work) if work is not None else None
|
|
54
|
+
|
|
55
|
+
async def _access(
|
|
56
|
+
self,
|
|
57
|
+
operation: str,
|
|
58
|
+
hide_errors: bool | None,
|
|
59
|
+
call: Callable[[], Awaitable[object]],
|
|
60
|
+
) -> object:
|
|
61
|
+
try:
|
|
62
|
+
return await call()
|
|
63
|
+
except RedisError as error:
|
|
64
|
+
if self._options.hide_errors if hide_errors is None else hide_errors:
|
|
65
|
+
return None
|
|
66
|
+
raise DistributedCacheError(
|
|
67
|
+
operation=operation, cache=self._name, error_type=type(error).__name__
|
|
68
|
+
) from None
|
|
69
|
+
|
|
70
|
+
async def _read(self, redis_key: str, hide_errors: bool | None) -> bytes | None:
|
|
71
|
+
payload = await self._access(
|
|
72
|
+
"get", hide_errors, partial(self._client.eval, _READ, 1, redis_key)
|
|
73
|
+
)
|
|
74
|
+
if payload is not None and not isinstance(payload, bytes):
|
|
75
|
+
raise TypeError("Redis cache returned a non-bytes payload")
|
|
76
|
+
return payload
|
|
77
|
+
|
|
78
|
+
async def get(
|
|
79
|
+
self, key: str, *, hide_errors: bool | None = None, consider_uow: bool = False
|
|
80
|
+
) -> _T | None:
|
|
81
|
+
redis_key = self._key(key)
|
|
82
|
+
state = self._state(consider_uow)
|
|
83
|
+
payload = (
|
|
84
|
+
state.writes[redis_key][0]
|
|
85
|
+
if state is not None and redis_key in state.writes
|
|
86
|
+
else await self._read(redis_key, hide_errors)
|
|
87
|
+
)
|
|
88
|
+
return None if payload is None else self._adapter.validate_json(payload, strict=True)
|
|
89
|
+
|
|
90
|
+
async def get_many(
|
|
91
|
+
self, keys: Iterable[str], *, hide_errors: bool | None = None, consider_uow: bool = False
|
|
92
|
+
) -> list[tuple[str, _T | None]]:
|
|
93
|
+
keys = list(keys)
|
|
94
|
+
for key in keys:
|
|
95
|
+
self._key(key)
|
|
96
|
+
return [
|
|
97
|
+
(key, await self.get(key, hide_errors=hide_errors, consider_uow=consider_uow))
|
|
98
|
+
for key in keys
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
async def get_or_add(
|
|
102
|
+
self,
|
|
103
|
+
key: str,
|
|
104
|
+
factory: Callable[[], Awaitable[_T | None]],
|
|
105
|
+
*,
|
|
106
|
+
options_factory: Callable[[], DistributedCacheEntryOptions] | None = None,
|
|
107
|
+
hide_errors: bool | None = None,
|
|
108
|
+
consider_uow: bool = False,
|
|
109
|
+
) -> _T | None:
|
|
110
|
+
value = await self.get(key, hide_errors=hide_errors, consider_uow=consider_uow)
|
|
111
|
+
if value is not None:
|
|
112
|
+
return value
|
|
113
|
+
value = await factory()
|
|
114
|
+
if value is not None:
|
|
115
|
+
await self.set(
|
|
116
|
+
key,
|
|
117
|
+
value,
|
|
118
|
+
options=options_factory() if options_factory else None,
|
|
119
|
+
hide_errors=hide_errors,
|
|
120
|
+
consider_uow=consider_uow,
|
|
121
|
+
)
|
|
122
|
+
return value
|
|
123
|
+
|
|
124
|
+
async def get_or_add_many(
|
|
125
|
+
self,
|
|
126
|
+
keys: Iterable[str],
|
|
127
|
+
factory: Callable[[list[str]], Awaitable[Iterable[tuple[str, _T]]]],
|
|
128
|
+
*,
|
|
129
|
+
options_factory: Callable[[], DistributedCacheEntryOptions] | None = None,
|
|
130
|
+
hide_errors: bool | None = None,
|
|
131
|
+
consider_uow: bool = False,
|
|
132
|
+
) -> list[tuple[str, _T | None]]:
|
|
133
|
+
found = await self.get_many(keys, hide_errors=hide_errors, consider_uow=consider_uow)
|
|
134
|
+
missing = list(dict.fromkeys(key for key, value in found if value is None))
|
|
135
|
+
if not missing:
|
|
136
|
+
return found
|
|
137
|
+
values: dict[str, _T] = {}
|
|
138
|
+
for key, value in await factory(missing):
|
|
139
|
+
if key not in missing or key in values:
|
|
140
|
+
raise ValueError("factory must return unique keys from the requested missing keys")
|
|
141
|
+
values[key] = value
|
|
142
|
+
await self.set_many(
|
|
143
|
+
values.items(),
|
|
144
|
+
options=options_factory() if options_factory else None,
|
|
145
|
+
hide_errors=hide_errors,
|
|
146
|
+
consider_uow=consider_uow,
|
|
147
|
+
)
|
|
148
|
+
return [(key, values.get(key, value)) for key, value in found]
|
|
149
|
+
|
|
150
|
+
def _expiration(self, options: DistributedCacheEntryOptions | None) -> tuple[int, int, int]:
|
|
151
|
+
selected = self._options.default_entry_options if options is None else options
|
|
152
|
+
if not isinstance(selected, DistributedCacheEntryOptions):
|
|
153
|
+
raise TypeError("options must be DistributedCacheEntryOptions")
|
|
154
|
+
return (
|
|
155
|
+
math.ceil(selected.absolute_expiration.timestamp() * 1000)
|
|
156
|
+
if selected.absolute_expiration is not None
|
|
157
|
+
else 0,
|
|
158
|
+
math.ceil(selected.absolute_expiration_relative_to_now.total_seconds() * 1000)
|
|
159
|
+
if selected.absolute_expiration_relative_to_now is not None
|
|
160
|
+
else 0,
|
|
161
|
+
math.ceil(selected.sliding_expiration.total_seconds() * 1000)
|
|
162
|
+
if selected.sliding_expiration is not None
|
|
163
|
+
else 0,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
async def _write(
|
|
167
|
+
self,
|
|
168
|
+
redis_key: str,
|
|
169
|
+
payload: bytes,
|
|
170
|
+
expiration: tuple[int, int, int],
|
|
171
|
+
hide_errors: bool | None,
|
|
172
|
+
) -> None:
|
|
173
|
+
await self._access(
|
|
174
|
+
"set",
|
|
175
|
+
hide_errors,
|
|
176
|
+
partial(self._client.eval, _WRITE, 1, redis_key, payload, *expiration),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async def set(
|
|
180
|
+
self,
|
|
181
|
+
key: str,
|
|
182
|
+
value: _T,
|
|
183
|
+
*,
|
|
184
|
+
options: DistributedCacheEntryOptions | None = None,
|
|
185
|
+
hide_errors: bool | None = None,
|
|
186
|
+
consider_uow: bool = False,
|
|
187
|
+
) -> None:
|
|
188
|
+
await self.set_many(
|
|
189
|
+
[(key, value)], options=options, hide_errors=hide_errors, consider_uow=consider_uow
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
async def set_many(
|
|
193
|
+
self,
|
|
194
|
+
items: Iterable[tuple[str, _T]],
|
|
195
|
+
*,
|
|
196
|
+
options: DistributedCacheEntryOptions | None = None,
|
|
197
|
+
hide_errors: bool | None = None,
|
|
198
|
+
consider_uow: bool = False,
|
|
199
|
+
) -> None:
|
|
200
|
+
expiration = self._expiration(options)
|
|
201
|
+
# 全部输入先校验/序列化,再做写入;mutable value 不泄漏到待提交快照。
|
|
202
|
+
encoded = [
|
|
203
|
+
(
|
|
204
|
+
self._key(key),
|
|
205
|
+
self._adapter.dump_json(self._adapter.validate_python(value, strict=True)),
|
|
206
|
+
)
|
|
207
|
+
for key, value in items
|
|
208
|
+
]
|
|
209
|
+
state = self._state(consider_uow)
|
|
210
|
+
for redis_key, payload in encoded:
|
|
211
|
+
apply = partial(self._write, redis_key, payload, expiration, hide_errors)
|
|
212
|
+
if state is None:
|
|
213
|
+
await apply()
|
|
214
|
+
else:
|
|
215
|
+
state.writes[redis_key] = (payload, apply)
|
|
216
|
+
|
|
217
|
+
async def remove(
|
|
218
|
+
self, key: str, *, hide_errors: bool | None = None, consider_uow: bool = False
|
|
219
|
+
) -> None:
|
|
220
|
+
await self.remove_many([key], hide_errors=hide_errors, consider_uow=consider_uow)
|
|
221
|
+
|
|
222
|
+
async def _remove(self, redis_key: str, hide_errors: bool | None) -> None:
|
|
223
|
+
await self._access("remove", hide_errors, partial(self._client.delete, redis_key))
|
|
224
|
+
|
|
225
|
+
async def remove_many(
|
|
226
|
+
self, keys: Iterable[str], *, hide_errors: bool | None = None, consider_uow: bool = False
|
|
227
|
+
) -> None:
|
|
228
|
+
redis_keys = [self._key(key) for key in keys]
|
|
229
|
+
state = self._state(consider_uow)
|
|
230
|
+
for redis_key in redis_keys:
|
|
231
|
+
apply = partial(self._remove, redis_key, hide_errors)
|
|
232
|
+
if state is None:
|
|
233
|
+
await apply()
|
|
234
|
+
else:
|
|
235
|
+
state.writes[redis_key] = (None, apply)
|
|
236
|
+
|
|
237
|
+
async def refresh(self, key: str, *, hide_errors: bool | None = None) -> None:
|
|
238
|
+
await self._read(self._key(key), hide_errors)
|
|
239
|
+
|
|
240
|
+
async def refresh_many(self, keys: Iterable[str], *, hide_errors: bool | None = None) -> None:
|
|
241
|
+
redis_keys = [self._key(key) for key in keys]
|
|
242
|
+
for redis_key in redis_keys:
|
|
243
|
+
await self._read(redis_key, hide_errors)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""单 key 的值、截止时间与续期在 Redis 内原子处理;不用客户端调度器。"""
|
|
2
|
+
|
|
3
|
+
_WRITE = """
|
|
4
|
+
local time = redis.call('TIME')
|
|
5
|
+
local now = time[1] * 1000 + math.floor(time[2] / 1000)
|
|
6
|
+
local absolute = tonumber(ARGV[2])
|
|
7
|
+
local relative = tonumber(ARGV[3])
|
|
8
|
+
local sliding = tonumber(ARGV[4])
|
|
9
|
+
if relative > 0 and (absolute == 0 or now + relative < absolute) then
|
|
10
|
+
absolute = now + relative
|
|
11
|
+
end
|
|
12
|
+
local deadline = absolute
|
|
13
|
+
if sliding > 0 and (deadline == 0 or now + sliding < deadline) then
|
|
14
|
+
deadline = now + sliding
|
|
15
|
+
end
|
|
16
|
+
redis.call('DEL', KEYS[1])
|
|
17
|
+
if deadline > 0 and deadline <= now then return 0 end
|
|
18
|
+
redis.call('HSET', KEYS[1], 'value', ARGV[1], 'absolute', absolute, 'sliding', sliding)
|
|
19
|
+
if deadline > 0 then redis.call('PEXPIREAT', KEYS[1], deadline) end
|
|
20
|
+
return 1
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_READ = """
|
|
24
|
+
local entry = redis.call('HMGET', KEYS[1], 'value', 'absolute', 'sliding')
|
|
25
|
+
if not entry[1] then return false end
|
|
26
|
+
local time = redis.call('TIME')
|
|
27
|
+
local now = time[1] * 1000 + math.floor(time[2] / 1000)
|
|
28
|
+
local absolute = tonumber(entry[2])
|
|
29
|
+
local sliding = tonumber(entry[3])
|
|
30
|
+
if absolute > 0 and absolute <= now then
|
|
31
|
+
redis.call('DEL', KEYS[1])
|
|
32
|
+
return false
|
|
33
|
+
end
|
|
34
|
+
if sliding > 0 then
|
|
35
|
+
local deadline = now + sliding
|
|
36
|
+
if absolute > 0 and absolute < deadline then deadline = absolute end
|
|
37
|
+
redis.call('PEXPIREAT', KEYS[1], deadline)
|
|
38
|
+
end
|
|
39
|
+
return entry[1]
|
|
40
|
+
"""
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
from collections.abc import AsyncIterator, Awaitable
|
|
7
7
|
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
|
8
|
+
from datetime import timedelta
|
|
8
9
|
from typing import TypeVar, cast
|
|
9
10
|
from uuid import uuid4
|
|
10
11
|
|
|
@@ -33,13 +34,20 @@ class RedisDistributedLock:
|
|
|
33
34
|
self._options = options.value
|
|
34
35
|
self._prefix = redis_options.value.key_prefix
|
|
35
36
|
|
|
36
|
-
def acquire(
|
|
37
|
+
def acquire(
|
|
38
|
+
self, key: str, *, wait_timeout: timedelta | None = None
|
|
39
|
+
) -> AbstractAsyncContextManager[bool]:
|
|
37
40
|
if not isinstance(key, str) or not key or key != key.strip():
|
|
38
41
|
raise ValueError("Distributed lock key must be a nonempty trimmed string")
|
|
42
|
+
if wait_timeout is not None and (
|
|
43
|
+
not isinstance(wait_timeout, timedelta) or wait_timeout < timedelta(0)
|
|
44
|
+
):
|
|
45
|
+
raise ValueError("wait_timeout must be a non-negative timedelta or None")
|
|
46
|
+
wait = self._options.wait_timeout if wait_timeout is None else wait_timeout
|
|
39
47
|
native = self._client.lock(
|
|
40
48
|
f"{self._prefix}:locks:{key}",
|
|
41
49
|
timeout=self._options.lease_timeout.total_seconds(),
|
|
42
|
-
blocking_timeout=
|
|
50
|
+
blocking_timeout=wait.total_seconds(),
|
|
43
51
|
thread_local=False,
|
|
44
52
|
lock_class=_LeaseLock,
|
|
45
53
|
)
|
|
@@ -2,15 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
|
|
5
7
|
from dishka import Provider, Scope
|
|
6
8
|
|
|
7
9
|
from ..caching import DistributedCache
|
|
10
|
+
from ..caching.module import CachingModule
|
|
8
11
|
from ..distributed_lock import DistributedLock, DistributedLockOptions
|
|
9
12
|
from ..lifecycle import ConfigureContext, InitializeContext
|
|
10
13
|
from ..modularity import AppModule
|
|
14
|
+
from .cache import RedisDistributedCache
|
|
11
15
|
from .distributed_lock import RedisDistributedLock
|
|
12
16
|
from .options import RedisOptions
|
|
13
|
-
from .runtime import
|
|
17
|
+
from .runtime import _redis_client, _RedisClientOwner
|
|
18
|
+
|
|
19
|
+
_T = TypeVar("_T")
|
|
20
|
+
# Dishka 的开放泛型契约与实现必须使用同一个类型变量;闭合由原生 registry 完成。
|
|
21
|
+
_CacheImplementation = RedisDistributedCache[_T]
|
|
22
|
+
_CacheContract = DistributedCache[_T]
|
|
14
23
|
|
|
15
24
|
|
|
16
25
|
class RedisConnectionModule(AppModule):
|
|
@@ -24,13 +33,13 @@ class RedisConnectionModule(AppModule):
|
|
|
24
33
|
|
|
25
34
|
|
|
26
35
|
class RedisModule(AppModule):
|
|
27
|
-
dependencies = (RedisConnectionModule,)
|
|
36
|
+
dependencies = (RedisConnectionModule, CachingModule)
|
|
28
37
|
|
|
29
38
|
def configure(self, context: ConfigureContext) -> None:
|
|
30
39
|
provider = Provider()
|
|
31
40
|
provider.provide(
|
|
32
|
-
|
|
33
|
-
provides=
|
|
41
|
+
_CacheImplementation,
|
|
42
|
+
provides=_CacheContract,
|
|
34
43
|
scope=Scope.APP,
|
|
35
44
|
)
|
|
36
45
|
context.services.contribute(provider, reason="Redis typed distributed cache")
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
from dishka import Provider, Scope
|
|
4
4
|
|
|
5
|
+
from ..hosted_services.declarations import HostedServices
|
|
6
|
+
from ..hosted_services.module import HostedServicesModule
|
|
5
7
|
from ..lifecycle import ConfigureContext
|
|
6
8
|
from ..modularity import AppModule
|
|
7
9
|
from ..notifications import Notifications
|
|
10
|
+
from ..notifications.module import NotificationsModule
|
|
8
11
|
from .module import RedisConnectionModule
|
|
9
12
|
from .notification_runtime import NotificationOptions, RedisNotifications
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class RedisNotificationsModule(AppModule):
|
|
13
|
-
dependencies = (RedisConnectionModule,)
|
|
14
|
-
hosted_services = (RedisNotifications,)
|
|
16
|
+
dependencies = (RedisConnectionModule, NotificationsModule, HostedServicesModule)
|
|
17
|
+
hosted_services = HostedServices((RedisNotifications,))
|
|
15
18
|
|
|
16
19
|
def configure(self, context: ConfigureContext) -> None:
|
|
17
20
|
context.configure(NotificationOptions, section="notifications")
|
|
@@ -2,15 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import math
|
|
6
5
|
from collections.abc import AsyncIterator
|
|
7
|
-
from typing import Any
|
|
8
6
|
|
|
9
|
-
from pydantic import ValidationError
|
|
10
7
|
from redis.asyncio import Redis
|
|
11
8
|
from redis.exceptions import RedisError
|
|
12
9
|
|
|
13
|
-
from ..caching import
|
|
10
|
+
from ..caching import DistributedCacheError
|
|
14
11
|
from ..options import Options
|
|
15
12
|
from .options import RedisOptions
|
|
16
13
|
|
|
@@ -39,78 +36,3 @@ async def _redis_client(options: Options[RedisOptions]) -> AsyncIterator[_RedisC
|
|
|
39
36
|
yield owner
|
|
40
37
|
finally:
|
|
41
38
|
await client.aclose()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class RedisDistributedCache:
|
|
45
|
-
__slots__ = ("_catalog", "_client", "_prefix")
|
|
46
|
-
|
|
47
|
-
def __init__(
|
|
48
|
-
self,
|
|
49
|
-
client: _RedisClientOwner,
|
|
50
|
-
catalog: CacheCatalog,
|
|
51
|
-
options: Options[RedisOptions],
|
|
52
|
-
) -> None:
|
|
53
|
-
self._client = client.client
|
|
54
|
-
self._catalog = catalog
|
|
55
|
-
self._prefix = options.value.key_prefix
|
|
56
|
-
|
|
57
|
-
async def get(self, definition: CacheDefinition[Any], key: str) -> Any | None:
|
|
58
|
-
redis_key = self._key(definition, key)
|
|
59
|
-
try:
|
|
60
|
-
payload = await self._client.get(redis_key)
|
|
61
|
-
return None if payload is None else definition._deserialize(payload)
|
|
62
|
-
except (RedisError, TypeError, ValueError, ValidationError) as original_error:
|
|
63
|
-
raise DistributedCacheError(
|
|
64
|
-
operation="get",
|
|
65
|
-
cache=definition.name,
|
|
66
|
-
error_type=type(original_error).__name__,
|
|
67
|
-
) from None
|
|
68
|
-
|
|
69
|
-
async def set(
|
|
70
|
-
self,
|
|
71
|
-
definition: CacheDefinition[Any],
|
|
72
|
-
key: str,
|
|
73
|
-
value: Any,
|
|
74
|
-
) -> None:
|
|
75
|
-
redis_key = self._key(definition, key)
|
|
76
|
-
try:
|
|
77
|
-
payload = definition._serialize(value)
|
|
78
|
-
await self._client.set(redis_key, payload, ex=math.ceil(definition.ttl.total_seconds()))
|
|
79
|
-
except (RedisError, TypeError, ValueError, ValidationError) as original_error:
|
|
80
|
-
raise DistributedCacheError(
|
|
81
|
-
operation="set",
|
|
82
|
-
cache=definition.name,
|
|
83
|
-
error_type=type(original_error).__name__,
|
|
84
|
-
) from None
|
|
85
|
-
|
|
86
|
-
async def remove(self, definition: CacheDefinition[Any], key: str) -> None:
|
|
87
|
-
redis_key = self._key(definition, key)
|
|
88
|
-
try:
|
|
89
|
-
await self._client.delete(redis_key)
|
|
90
|
-
except RedisError as original_error:
|
|
91
|
-
raise DistributedCacheError(
|
|
92
|
-
operation="remove",
|
|
93
|
-
cache=definition.name,
|
|
94
|
-
error_type=type(original_error).__name__,
|
|
95
|
-
) from None
|
|
96
|
-
|
|
97
|
-
def _key(self, definition: CacheDefinition[Any], key: str) -> str:
|
|
98
|
-
if not self._catalog.owns(definition):
|
|
99
|
-
raise DistributedCacheError(
|
|
100
|
-
operation="key",
|
|
101
|
-
cache=definition.name,
|
|
102
|
-
error_type="ForeignCacheDefinition",
|
|
103
|
-
)
|
|
104
|
-
if (
|
|
105
|
-
not isinstance(key, str)
|
|
106
|
-
or not key
|
|
107
|
-
or key != key.strip()
|
|
108
|
-
or any(character.isspace() for character in key)
|
|
109
|
-
or len(key) > 512
|
|
110
|
-
):
|
|
111
|
-
raise DistributedCacheError(
|
|
112
|
-
operation="key",
|
|
113
|
-
cache=definition.name,
|
|
114
|
-
error_type="InvalidCacheKey",
|
|
115
|
-
)
|
|
116
|
-
return f"{self._prefix}:{definition.name}:{key}"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""按正式模块依赖启用的数据初始化能力。"""
|
|
2
|
+
|
|
3
|
+
from .contracts import DataSeedContributor, DataSeedError, DataSeedRegistration
|
|
4
|
+
from .module import DataSeedingModule
|
|
5
|
+
from .runtime import DataSeeder
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
"DataSeedContributor",
|
|
9
|
+
"DataSeedError",
|
|
10
|
+
"DataSeedRegistration",
|
|
11
|
+
"DataSeeder",
|
|
12
|
+
"DataSeedingModule",
|
|
13
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""数据初始化的独立契约;业务幂等性归贡献者,事务和执行顺序归框架。"""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from dishka import Scope
|
|
8
|
+
|
|
9
|
+
from ..errors.base import FrameworkError
|
|
10
|
+
from ..modularity import ModuleKey, ModuleRef, ModuleRequirement
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DataSeedContributor(ABC):
|
|
14
|
+
_fixed_service_scope: ClassVar[Scope] = Scope.ACTION
|
|
15
|
+
__data_seeding_module__ = ModuleRequirement(
|
|
16
|
+
ModuleRef("python_ddd_framework.seeding.module:DataSeedingModule")
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
async def seed(self) -> None:
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class DataSeedRegistration:
|
|
26
|
+
service: type[DataSeedContributor]
|
|
27
|
+
implementation: type[DataSeedContributor]
|
|
28
|
+
owner: ModuleKey
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DataSeedError(FrameworkError):
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
completed: tuple[type[DataSeedContributor], ...],
|
|
36
|
+
failed: type[DataSeedContributor],
|
|
37
|
+
original_error: BaseException,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.completed = completed
|
|
40
|
+
self.failed = failed
|
|
41
|
+
self.original_error = original_error
|
|
42
|
+
super().__init__(
|
|
43
|
+
f"Data seed contributor {failed.__module__}.{failed.__qualname__} failed after "
|
|
44
|
+
f"{len(completed)} completed contributor(s): {type(original_error).__name__}"
|
|
45
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Seed 发现与最终绑定归所属能力;Domain 不必依赖应用服务组合。"""
|
|
2
|
+
|
|
3
|
+
from dishka import DEFAULT_COMPONENT, DependencyKey, Scope
|
|
4
|
+
|
|
5
|
+
from ..errors import InvalidConventionDefinitionError
|
|
6
|
+
from ..invocation.interception import InvocationEntry
|
|
7
|
+
from ..invocation.module import InvocationModule
|
|
8
|
+
from ..lifecycle import ConfigureContext
|
|
9
|
+
from ..modularity import AppModule, ModuleKey
|
|
10
|
+
from ..modularity.discovery import _is_concrete_implementation
|
|
11
|
+
from ..services.composition import (
|
|
12
|
+
ServiceBindingContext,
|
|
13
|
+
ServiceComposition,
|
|
14
|
+
ServiceConvention,
|
|
15
|
+
ServiceDiscoveryContext,
|
|
16
|
+
)
|
|
17
|
+
from ..services.convention_contracts import ConventionDiscoveryKind, _ServiceDefinition
|
|
18
|
+
from ..unit_of_work.module import UnitOfWorkModule
|
|
19
|
+
from .contracts import DataSeedContributor, DataSeedRegistration
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DataSeedingModule(AppModule):
|
|
23
|
+
dependencies = (UnitOfWorkModule,)
|
|
24
|
+
registrations: tuple[DataSeedRegistration, ...]
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self._discoveries: dict[tuple[ModuleKey, type[DataSeedContributor]], None] = {}
|
|
28
|
+
|
|
29
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
30
|
+
context.services.add_composition(
|
|
31
|
+
ServiceComposition(discover=self._discover, bind=self._bind)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def _discover(self, context: ServiceDiscoveryContext) -> None:
|
|
35
|
+
for discovery in context.discoveries:
|
|
36
|
+
candidate = discovery.candidate
|
|
37
|
+
if not issubclass(candidate, DataSeedContributor) or not _is_concrete_implementation(
|
|
38
|
+
candidate
|
|
39
|
+
):
|
|
40
|
+
continue
|
|
41
|
+
self._discoveries[discovery.owner, candidate] = None
|
|
42
|
+
context.conventions[discovery.owner, candidate] = ServiceConvention(
|
|
43
|
+
_ServiceDefinition(provides=candidate, scope=Scope.ACTION),
|
|
44
|
+
ConventionDiscoveryKind.FRAMEWORK_MARKER,
|
|
45
|
+
"DataSeedContributor declaration",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def _bind(self, context: ServiceBindingContext) -> None:
|
|
49
|
+
registrations = []
|
|
50
|
+
invocation = context.modules.instance(InvocationModule)
|
|
51
|
+
for owner, service in self._discoveries:
|
|
52
|
+
factory = context.arbitration.single_factory(DependencyKey(service, DEFAULT_COMPONENT))
|
|
53
|
+
if (
|
|
54
|
+
factory is None
|
|
55
|
+
or factory.scope is not Scope.ACTION
|
|
56
|
+
or not isinstance(factory.source, type)
|
|
57
|
+
or not issubclass(factory.source, DataSeedContributor)
|
|
58
|
+
):
|
|
59
|
+
raise InvalidConventionDefinitionError(
|
|
60
|
+
symbol=service.__qualname__,
|
|
61
|
+
reason="seed requires a single ACTION-scoped DataSeedContributor class binding",
|
|
62
|
+
)
|
|
63
|
+
# 以正式仲裁结果的实现 owner 选择模块闭包,不能继续执行已被替换的扫描类型。
|
|
64
|
+
implementation_owner = next(
|
|
65
|
+
draft.contribution.owner
|
|
66
|
+
for draft in context.arbitration.drafts
|
|
67
|
+
if draft.factory is factory
|
|
68
|
+
)
|
|
69
|
+
registrations.append(
|
|
70
|
+
DataSeedRegistration(service, factory.source, implementation_owner)
|
|
71
|
+
)
|
|
72
|
+
invocation.entries.append(InvocationEntry(service, "seed", owner, 0))
|
|
73
|
+
self.registrations = tuple(registrations)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""显式 Migration Host 的顺序 Seed;每项复用现有调用管线和独立 UoW。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from dishka import Scope
|
|
9
|
+
|
|
10
|
+
from ..authorization import CurrentUser
|
|
11
|
+
from ..invocation.entrypoints import caller_context, invocation_lease
|
|
12
|
+
from ..invocation.module import InvocationModule
|
|
13
|
+
from ..modularity import AppModule
|
|
14
|
+
from ..modularity.selection import _dependency_keys
|
|
15
|
+
from ..unit_of_work.options import _DEFAULT_CONNECTION_NAME
|
|
16
|
+
from .contracts import DataSeedContributor, DataSeedError
|
|
17
|
+
from .module import DataSeedingModule
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..application import Application
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DataSeeder:
|
|
24
|
+
__slots__ = ("_application",)
|
|
25
|
+
|
|
26
|
+
def __init__(self, application: Application) -> None:
|
|
27
|
+
self._application = application
|
|
28
|
+
|
|
29
|
+
async def seed_all(
|
|
30
|
+
self, *, module: type[AppModule] | None = None
|
|
31
|
+
) -> tuple[type[DataSeedContributor], ...]:
|
|
32
|
+
application = self._application
|
|
33
|
+
owners = _dependency_keys(application.modules, module) if module is not None else None
|
|
34
|
+
registrations = application.modules.instance(DataSeedingModule).registrations
|
|
35
|
+
invocation = application.modules.instance(InvocationModule)
|
|
36
|
+
completed: list[type[DataSeedContributor]] = []
|
|
37
|
+
for registration in registrations:
|
|
38
|
+
if owners is not None and registration.owner not in owners:
|
|
39
|
+
continue
|
|
40
|
+
try:
|
|
41
|
+
# 每项从 root 新建 REQUEST/ACTION;前项已提交的事务不被后项失败回滚。
|
|
42
|
+
async with invocation_lease(application):
|
|
43
|
+
async with caller_context(application, CurrentUser.anonymous(), str(uuid4())):
|
|
44
|
+
async with application._container_for_integration()(
|
|
45
|
+
scope=Scope.REQUEST
|
|
46
|
+
) as request:
|
|
47
|
+
await invocation.catalog.invoke(
|
|
48
|
+
invocation.invocation_runtime,
|
|
49
|
+
request,
|
|
50
|
+
registration.service,
|
|
51
|
+
"seed",
|
|
52
|
+
(),
|
|
53
|
+
connection_name=_DEFAULT_CONNECTION_NAME,
|
|
54
|
+
)
|
|
55
|
+
except Exception as original_error:
|
|
56
|
+
# 原生取消不包装;调用管线完成取消/异常清理后,报告实际已完成项。
|
|
57
|
+
raise DataSeedError(
|
|
58
|
+
completed=tuple(completed),
|
|
59
|
+
failed=registration.implementation,
|
|
60
|
+
original_error=original_error,
|
|
61
|
+
) from original_error
|
|
62
|
+
completed.append(registration.implementation)
|
|
63
|
+
return tuple(completed)
|