python-ddd-framework 0.3.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 +449 -0
- python_ddd_framework/application/__init__.py +6 -0
- python_ddd_framework/application/build_spec.py +37 -0
- python_ddd_framework/application/builder.py +260 -0
- python_ddd_framework/application/composition.py +219 -0
- python_ddd_framework/application/runtime.py +549 -0
- python_ddd_framework/application/runtime_hooks.py +327 -0
- python_ddd_framework/application/state.py +216 -0
- python_ddd_framework/application_services/__init__.py +54 -0
- python_ddd_framework/application_services/catalog.py +500 -0
- python_ddd_framework/application_services/contracts.py +190 -0
- python_ddd_framework/application_services/dispatcher.py +423 -0
- python_ddd_framework/application_services/errors.py +89 -0
- python_ddd_framework/application_services/execution.py +199 -0
- python_ddd_framework/application_services/interceptors.py +44 -0
- python_ddd_framework/application_services/invocation.py +431 -0
- python_ddd_framework/application_services/policies.py +46 -0
- python_ddd_framework/application_services/seeding.py +72 -0
- python_ddd_framework/application_services/signature.py +49 -0
- python_ddd_framework/application_services/validation.py +84 -0
- python_ddd_framework/auditing/__init__.py +12 -0
- python_ddd_framework/auditing/contracts.py +39 -0
- python_ddd_framework/auditing/control.py +48 -0
- python_ddd_framework/auditing/sqlalchemy/__init__.py +6 -0
- python_ddd_framework/auditing/sqlalchemy/migrations/0001_auditing.py +45 -0
- python_ddd_framework/auditing/sqlalchemy/migrations/__init__.py +1 -0
- python_ddd_framework/auditing/sqlalchemy/models.py +31 -0
- python_ddd_framework/auditing/sqlalchemy/module.py +32 -0
- python_ddd_framework/auditing/sqlalchemy/store.py +63 -0
- python_ddd_framework/authorization/__init__.py +38 -0
- python_ddd_framework/authorization/catalog.py +63 -0
- python_ddd_framework/authorization/contracts.py +241 -0
- python_ddd_framework/authorization/definition_discovery.py +81 -0
- python_ddd_framework/authorization/definitions.py +43 -0
- python_ddd_framework/authorization/errors.py +40 -0
- python_ddd_framework/background_execution/__init__.py +17 -0
- python_ddd_framework/background_execution/application.py +41 -0
- python_ddd_framework/background_execution/child.py +123 -0
- python_ddd_framework/background_execution/contracts.py +48 -0
- python_ddd_framework/background_execution/lifecycle.py +22 -0
- python_ddd_framework/background_execution/local.py +127 -0
- python_ddd_framework/background_execution/locks.py +33 -0
- python_ddd_framework/background_execution/management.py +17 -0
- python_ddd_framework/background_execution/module.py +10 -0
- python_ddd_framework/background_execution/permissions.py +14 -0
- python_ddd_framework/background_execution/processes.py +331 -0
- python_ddd_framework/background_jobs/__init__.py +31 -0
- python_ddd_framework/background_jobs/catalog.py +340 -0
- python_ddd_framework/background_jobs/contracts.py +208 -0
- python_ddd_framework/background_jobs/declaration.py +67 -0
- python_ddd_framework/background_jobs/errors.py +28 -0
- python_ddd_framework/background_jobs/execution.py +203 -0
- python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
- python_ddd_framework/background_jobs/pgqueuer/__init__.py +6 -0
- python_ddd_framework/background_jobs/pgqueuer/enqueue.py +86 -0
- python_ddd_framework/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +95 -0
- python_ddd_framework/background_jobs/pgqueuer/migrations/__init__.py +1 -0
- python_ddd_framework/background_jobs/pgqueuer/module.py +106 -0
- python_ddd_framework/background_jobs/pgqueuer/options.py +82 -0
- python_ddd_framework/background_jobs/pgqueuer/runtime.py +479 -0
- python_ddd_framework/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
- python_ddd_framework/background_jobs/pgqueuer/supervision.py +201 -0
- python_ddd_framework/background_workers/__init__.py +22 -0
- python_ddd_framework/background_workers/catalog.py +118 -0
- python_ddd_framework/background_workers/contracts.py +152 -0
- python_ddd_framework/background_workers/errors.py +20 -0
- python_ddd_framework/background_workers/execution.py +133 -0
- python_ddd_framework/background_workers/runtime.py +219 -0
- python_ddd_framework/caching/__init__.py +11 -0
- python_ddd_framework/caching/catalog.py +73 -0
- python_ddd_framework/caching/contracts.py +73 -0
- python_ddd_framework/caching/errors.py +20 -0
- python_ddd_framework/cli/__init__.py +105 -0
- python_ddd_framework/cli/development.py +85 -0
- python_ddd_framework/cli/errors.py +5 -0
- python_ddd_framework/cli/inspection.py +96 -0
- python_ddd_framework/cli/project.py +69 -0
- python_ddd_framework/cli/runtime.py +56 -0
- python_ddd_framework/configuration/__init__.py +21 -0
- python_ddd_framework/configuration/composition.py +65 -0
- python_ddd_framework/configuration/contracts.py +68 -0
- python_ddd_framework/configuration/dotenv_source.py +51 -0
- python_ddd_framework/configuration/environment_source.py +38 -0
- python_ddd_framework/configuration/immutability.py +124 -0
- python_ddd_framework/configuration/input_shape.py +58 -0
- python_ddd_framework/configuration/merge.py +103 -0
- python_ddd_framework/configuration/root.py +215 -0
- python_ddd_framework/configuration/sources.py +519 -0
- python_ddd_framework/configuration/values.py +427 -0
- python_ddd_framework/configuration/yaml_source.py +51 -0
- python_ddd_framework/developer_kit/__init__.py +1 -0
- python_ddd_framework/developer_kit/generation.py +182 -0
- python_ddd_framework/developer_kit/project_metadata.py +45 -0
- python_ddd_framework/developer_kit/source.py +53 -0
- python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +70 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +41 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +20 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +83 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +84 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +35 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +56 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +14 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +73 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +25 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +36 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +7 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/orders.py.jinja +25 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +26 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +49 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
- python_ddd_framework/developer_kit/templates/project/cookiecutter.json +6 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +9 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +38 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +130 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/app.development.yaml.jinja +30 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +20 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +84 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +185 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +47 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +37 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +57 -0
- python_ddd_framework/developer_kit/wiring.py +162 -0
- python_ddd_framework/diagnostics/__init__.py +23 -0
- python_ddd_framework/diagnostics/journal.py +174 -0
- python_ddd_framework/diagnostics/model.py +83 -0
- python_ddd_framework/diagnostics/source.py +25 -0
- python_ddd_framework/distributed_lock/__init__.py +6 -0
- python_ddd_framework/distributed_lock/contracts.py +20 -0
- python_ddd_framework/distributed_lock/options.py +19 -0
- python_ddd_framework/domain/__init__.py +13 -0
- python_ddd_framework/domain/aggregates.py +64 -0
- python_ddd_framework/domain/entities.py +33 -0
- python_ddd_framework/domain/errors.py +12 -0
- python_ddd_framework/domain/value_objects.py +31 -0
- python_ddd_framework/errors/__init__.py +75 -0
- python_ddd_framework/errors/base.py +13 -0
- python_ddd_framework/errors/business.py +53 -0
- python_ddd_framework/errors/configuration.py +119 -0
- python_ddd_framework/errors/diagnostics.py +12 -0
- python_ddd_framework/errors/lifecycle.py +164 -0
- python_ddd_framework/errors/modularity.py +100 -0
- python_ddd_framework/errors/services.py +100 -0
- python_ddd_framework/events/__init__.py +25 -0
- python_ddd_framework/events/aggregate.py +39 -0
- python_ddd_framework/events/catalog.py +136 -0
- python_ddd_framework/events/contracts.py +39 -0
- python_ddd_framework/events/contribution.py +128 -0
- python_ddd_framework/events/discovery.py +105 -0
- python_ddd_framework/events/errors.py +48 -0
- python_ddd_framework/events/runtime.py +192 -0
- python_ddd_framework/fastapi/__init__.py +49 -0
- python_ddd_framework/fastapi/action.py +175 -0
- python_ddd_framework/fastapi/adapter.py +391 -0
- python_ddd_framework/fastapi/application_services.py +654 -0
- python_ddd_framework/fastapi/background.py +42 -0
- python_ddd_framework/fastapi/contracts.py +275 -0
- python_ddd_framework/fastapi/errors.py +32 -0
- python_ddd_framework/fastapi/filters.py +111 -0
- python_ddd_framework/fastapi/health.py +44 -0
- python_ddd_framework/fastapi/http.py +72 -0
- python_ddd_framework/fastapi/http_router.py +216 -0
- python_ddd_framework/fastapi/manual_action.py +105 -0
- python_ddd_framework/fastapi/middleware.py +93 -0
- python_ddd_framework/fastapi/parameters.py +97 -0
- python_ddd_framework/fastapi/realtime/__init__.py +13 -0
- python_ddd_framework/fastapi/realtime/authentication.py +181 -0
- python_ddd_framework/fastapi/realtime/connection.py +166 -0
- python_ddd_framework/fastapi/realtime/module.py +41 -0
- python_ddd_framework/fastapi/realtime/options.py +50 -0
- python_ddd_framework/fastapi/realtime/runtime.py +423 -0
- python_ddd_framework/fastapi/request_context.py +247 -0
- python_ddd_framework/fastapi/route_integrity.py +88 -0
- python_ddd_framework/fastapi/routing.py +424 -0
- python_ddd_framework/fastapi/server.py +182 -0
- python_ddd_framework/fastapi/settings.py +40 -0
- python_ddd_framework/fastapi/tracing.py +62 -0
- python_ddd_framework/fastapi/transfer.py +128 -0
- python_ddd_framework/fastapi/upload_limits.py +63 -0
- python_ddd_framework/fastapi/uploads.py +94 -0
- python_ddd_framework/hosted_services/__init__.py +29 -0
- python_ddd_framework/hosted_services/bridge.py +420 -0
- python_ddd_framework/hosted_services/catalog.py +265 -0
- python_ddd_framework/hosted_services/contracts.py +55 -0
- python_ddd_framework/hosted_services/errors.py +53 -0
- python_ddd_framework/hosted_services/options.py +17 -0
- python_ddd_framework/hosted_services/runtime.py +267 -0
- python_ddd_framework/hosted_services/state.py +40 -0
- python_ddd_framework/hosting/__init__.py +4 -0
- python_ddd_framework/hosting/instance.py +40 -0
- python_ddd_framework/identity/__init__.py +73 -0
- python_ddd_framework/identity/application.py +287 -0
- python_ddd_framework/identity/contracts.py +283 -0
- python_ddd_framework/identity/errors.py +28 -0
- python_ddd_framework/identity/http_api.py +90 -0
- python_ddd_framework/identity/module.py +49 -0
- python_ddd_framework/identity/passwords.py +34 -0
- python_ddd_framework/identity/permissions.py +14 -0
- python_ddd_framework/identity/services.py +108 -0
- python_ddd_framework/identity/sqlalchemy/__init__.py +5 -0
- python_ddd_framework/identity/sqlalchemy/migrations/0001_identity.py +143 -0
- python_ddd_framework/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
- python_ddd_framework/identity/sqlalchemy/migrations/__init__.py +1 -0
- python_ddd_framework/identity/sqlalchemy/models.py +85 -0
- python_ddd_framework/identity/sqlalchemy/module.py +62 -0
- python_ddd_framework/identity/sqlalchemy/stores.py +526 -0
- python_ddd_framework/identity/tokens.py +106 -0
- python_ddd_framework/invocation/__init__.py +3 -0
- python_ddd_framework/invocation/callables.py +171 -0
- python_ddd_framework/invocation/contracts.py +33 -0
- python_ddd_framework/invocation/entries.py +54 -0
- python_ddd_framework/invocation/function_runtime.py +48 -0
- python_ddd_framework/invocation/interception.py +193 -0
- python_ddd_framework/lifecycle/__init__.py +25 -0
- python_ddd_framework/lifecycle/composition.py +35 -0
- python_ddd_framework/lifecycle/context.py +31 -0
- python_ddd_framework/lifecycle/runtime.py +43 -0
- python_ddd_framework/lifecycle/state.py +20 -0
- python_ddd_framework/modularity/__init__.py +15 -0
- python_ddd_framework/modularity/contracts.py +91 -0
- python_ddd_framework/modularity/discovery.py +157 -0
- python_ddd_framework/modularity/graph.py +334 -0
- python_ddd_framework/modularity/registry.py +100 -0
- python_ddd_framework/modularity/selection.py +16 -0
- python_ddd_framework/notifications/__init__.py +19 -0
- python_ddd_framework/notifications/catalog.py +46 -0
- python_ddd_framework/notifications/contracts.py +107 -0
- python_ddd_framework/observability/__init__.py +5 -0
- python_ddd_framework/observability/context.py +43 -0
- python_ddd_framework/observability/export.py +73 -0
- python_ddd_framework/observability/formatting.py +89 -0
- python_ddd_framework/observability/logging.py +118 -0
- python_ddd_framework/observability/options.py +45 -0
- python_ddd_framework/observability/tracing.py +80 -0
- python_ddd_framework/options/__init__.py +19 -0
- python_ddd_framework/options/aliases.py +331 -0
- python_ddd_framework/options/contribution.py +62 -0
- python_ddd_framework/options/immutability.py +48 -0
- python_ddd_framework/options/input_keys.py +214 -0
- python_ddd_framework/options/issues.py +335 -0
- python_ddd_framework/options/models.py +114 -0
- python_ddd_framework/options/registry.py +280 -0
- python_ddd_framework/options/schema.py +488 -0
- python_ddd_framework/options/validation.py +120 -0
- python_ddd_framework/py.typed +0 -0
- python_ddd_framework/realtime/__init__.py +12 -0
- python_ddd_framework/realtime/contracts.py +28 -0
- python_ddd_framework/realtime/diagnostics.py +37 -0
- python_ddd_framework/realtime/messages.py +113 -0
- python_ddd_framework/redis/__init__.py +6 -0
- python_ddd_framework/redis/distributed_lock.py +212 -0
- python_ddd_framework/redis/lease_lock.py +36 -0
- python_ddd_framework/redis/module.py +50 -0
- python_ddd_framework/redis/notification_runtime.py +182 -0
- python_ddd_framework/redis/notifications.py +25 -0
- python_ddd_framework/redis/options.py +35 -0
- python_ddd_framework/redis/runtime.py +116 -0
- python_ddd_framework/services/__init__.py +41 -0
- python_ddd_framework/services/application_bindings.py +221 -0
- python_ddd_framework/services/arbitration.py +381 -0
- python_ddd_framework/services/binding.py +102 -0
- python_ddd_framework/services/contribution.py +438 -0
- python_ddd_framework/services/convention.py +301 -0
- python_ddd_framework/services/convention_contracts.py +101 -0
- python_ddd_framework/services/exposure.py +23 -0
- python_ddd_framework/services/fixed_lifetime.py +52 -0
- python_ddd_framework/services/framework_provider.py +178 -0
- python_ddd_framework/services/native_graph.py +201 -0
- python_ddd_framework/services/provider.py +204 -0
- python_ddd_framework/services/registration.py +101 -0
- python_ddd_framework/services/repository.py +38 -0
- python_ddd_framework/services/runtime.py +302 -0
- python_ddd_framework/settings/__init__.py +30 -0
- python_ddd_framework/settings/application.py +95 -0
- python_ddd_framework/settings/binding.py +33 -0
- python_ddd_framework/settings/catalog.py +121 -0
- python_ddd_framework/settings/changes.py +14 -0
- python_ddd_framework/settings/contracts.py +115 -0
- python_ddd_framework/settings/definition_discovery.py +71 -0
- python_ddd_framework/settings/definitions.py +41 -0
- python_ddd_framework/settings/errors.py +22 -0
- python_ddd_framework/settings/handlers.py +43 -0
- python_ddd_framework/settings/management.py +57 -0
- python_ddd_framework/settings/manager.py +89 -0
- python_ddd_framework/settings/module.py +10 -0
- python_ddd_framework/settings/notifications.py +39 -0
- python_ddd_framework/settings/permissions.py +14 -0
- python_ddd_framework/settings/provider.py +74 -0
- python_ddd_framework/settings/refresh.py +140 -0
- python_ddd_framework/settings/refresh_module.py +45 -0
- python_ddd_framework/settings/sqlalchemy/__init__.py +5 -0
- python_ddd_framework/settings/sqlalchemy/migrations/0001_settings.py +31 -0
- python_ddd_framework/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
- python_ddd_framework/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
- python_ddd_framework/settings/sqlalchemy/migrations/__init__.py +1 -0
- python_ddd_framework/settings/sqlalchemy/models.py +36 -0
- python_ddd_framework/settings/sqlalchemy/module.py +36 -0
- python_ddd_framework/settings/sqlalchemy/store.py +86 -0
- python_ddd_framework/settings/store.py +16 -0
- python_ddd_framework/settings/values.py +39 -0
- python_ddd_framework/sqlalchemy/__init__.py +54 -0
- python_ddd_framework/sqlalchemy/alembic_runtime/__init__.py +1 -0
- python_ddd_framework/sqlalchemy/alembic_runtime/env.py +33 -0
- python_ddd_framework/sqlalchemy/alembic_runtime/script.py.mako +14 -0
- python_ddd_framework/sqlalchemy/auditing.py +48 -0
- python_ddd_framework/sqlalchemy/errors.py +85 -0
- python_ddd_framework/sqlalchemy/metadata.py +576 -0
- python_ddd_framework/sqlalchemy/migration.py +378 -0
- python_ddd_framework/sqlalchemy/migration_options.py +56 -0
- python_ddd_framework/sqlalchemy/module.py +51 -0
- python_ddd_framework/sqlalchemy/module_migration.py +151 -0
- python_ddd_framework/sqlalchemy/options.py +66 -0
- python_ddd_framework/sqlalchemy/repository.py +33 -0
- python_ddd_framework/sqlalchemy/runtime.py +85 -0
- python_ddd_framework/sqlalchemy/session_provider.py +45 -0
- python_ddd_framework/sqlalchemy/unit_of_work.py +97 -0
- python_ddd_framework/testing/__init__.py +5 -0
- python_ddd_framework/testing/runtime.py +112 -0
- python_ddd_framework/unit_of_work/__init__.py +14 -0
- python_ddd_framework/unit_of_work/contracts.py +223 -0
- python_ddd_framework/unit_of_work/errors.py +27 -0
- python_ddd_framework/unit_of_work/manager.py +210 -0
- python_ddd_framework/unit_of_work/options.py +92 -0
- python_ddd_framework-0.3.1.dist-info/METADATA +379 -0
- python_ddd_framework-0.3.1.dist-info/RECORD +355 -0
- python_ddd_framework-0.3.1.dist-info/WHEEL +4 -0
- python_ddd_framework-0.3.1.dist-info/entry_points.txt +9 -0
- python_ddd_framework-0.3.1.dist-info/licenses/LICENSE +7 -0
- python_ddd_framework-0.3.1.dist-info/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""模块生成时维护标准打包与 entry point 声明,保留其他 TOML 内容。"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import tomllib
|
|
6
|
+
|
|
7
|
+
from ..cli.errors import CommandError
|
|
8
|
+
from ..cli.project import MODULE_ENTRY_POINT_GROUP
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_module_metadata(source: str, package: str, class_prefix: str) -> str:
|
|
12
|
+
project = tomllib.loads(source)
|
|
13
|
+
backend = project.get("tool", {}).get("uv", {}).get("build-backend", {})
|
|
14
|
+
names = backend.get("module-name")
|
|
15
|
+
if backend.get("module-root") != "src" or not isinstance(names, (str, list)):
|
|
16
|
+
raise CommandError("Module generation requires explicit uv module-root=src and module-name")
|
|
17
|
+
packages = [names] if isinstance(names, str) else list(names)
|
|
18
|
+
if "modules" not in packages:
|
|
19
|
+
packages.append("modules")
|
|
20
|
+
section = re.search(r"(?ms)^\[tool\.uv\.build-backend\][ \t]*\r?\n(.*?)(?=^\[|\Z)", source)
|
|
21
|
+
if section is None:
|
|
22
|
+
raise CommandError("Cannot locate uv build-backend table")
|
|
23
|
+
body, count = re.subn(
|
|
24
|
+
r'(?m)^module-name\s*=\s*(?:"[^"\n]*"|\[[^\]\n]*\])',
|
|
25
|
+
"module-name = " + json.dumps(packages),
|
|
26
|
+
section.group(1),
|
|
27
|
+
)
|
|
28
|
+
if count != 1:
|
|
29
|
+
raise CommandError("Keep module-name on one line to allow safe package wiring")
|
|
30
|
+
updated = source[: section.start(1)] + body + source[section.end(1) :]
|
|
31
|
+
entries = project["project"].get("entry-points", {}).get(MODULE_ENTRY_POINT_GROUP, {})
|
|
32
|
+
if package in entries:
|
|
33
|
+
raise CommandError(f"Module alias already exists: {package}")
|
|
34
|
+
header = f'[project.entry-points."{MODULE_ENTRY_POINT_GROUP}"]'
|
|
35
|
+
declaration = (
|
|
36
|
+
f'{package} = "modules.{package}.sqlalchemy.module:{class_prefix}SqlAlchemyModule"\n'
|
|
37
|
+
)
|
|
38
|
+
if MODULE_ENTRY_POINT_GROUP in project["project"].get("entry-points", {}):
|
|
39
|
+
if header not in updated:
|
|
40
|
+
raise CommandError("Module entry point table must use the standard quoted group name")
|
|
41
|
+
updated = updated.replace(header, header + "\n" + declaration, 1)
|
|
42
|
+
else:
|
|
43
|
+
updated += "\n" + header + "\n" + declaration
|
|
44
|
+
tomllib.loads(updated)
|
|
45
|
+
return updated
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""生成项目固定 CLI 版本;只有显式本地 wheel 才覆盖标准索引来源。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.metadata as metadata
|
|
6
|
+
import json
|
|
7
|
+
import zipfile
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from email.parser import Parser
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..cli.errors import CommandError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class FrameworkSource:
|
|
17
|
+
version: str
|
|
18
|
+
uv_source: str
|
|
19
|
+
wheel: Path | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def framework_source(wheel: Path | None) -> FrameworkSource:
|
|
23
|
+
distribution = metadata.distribution("python-ddd-framework")
|
|
24
|
+
if wheel is not None:
|
|
25
|
+
source = _wheel_source(wheel)
|
|
26
|
+
if source.version != distribution.version:
|
|
27
|
+
raise CommandError(
|
|
28
|
+
"Framework wheel version must match the CLI and its bundled templates"
|
|
29
|
+
)
|
|
30
|
+
return source
|
|
31
|
+
# 安装来源不拥有生成项目的依赖契约;Git/editable CLI 也必须显式选择本地 wheel。
|
|
32
|
+
return FrameworkSource(distribution.version, "")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _wheel_source(path: Path) -> FrameworkSource:
|
|
36
|
+
wheel = path.resolve()
|
|
37
|
+
if not wheel.is_file() or wheel.suffix != ".whl":
|
|
38
|
+
raise CommandError("--framework-wheel must identify an existing wheel")
|
|
39
|
+
try:
|
|
40
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
41
|
+
members = [name for name in archive.namelist() if name.endswith(".dist-info/METADATA")]
|
|
42
|
+
if len(members) != 1:
|
|
43
|
+
raise CommandError(
|
|
44
|
+
"Framework wheel must have exactly one distribution metadata file"
|
|
45
|
+
)
|
|
46
|
+
document = Parser().parsestr(archive.read(members[0]).decode("utf-8"))
|
|
47
|
+
except (OSError, ValueError, zipfile.BadZipFile):
|
|
48
|
+
raise CommandError("Cannot read framework wheel metadata") from None
|
|
49
|
+
if document["Name"] != "python-ddd-framework" or not document["Version"]:
|
|
50
|
+
raise CommandError("Wheel must contain the python-ddd-framework distribution")
|
|
51
|
+
return FrameworkSource(
|
|
52
|
+
document["Version"], f"{{ path = {json.dumps('vendor/' + wheel.name)} }}", wheel
|
|
53
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"module_name": "orders", "class_prefix": "Orders"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# {{ cookiecutter.module_name }} module
|
|
2
|
+
|
|
3
|
+
This module was generated by `pddd add module`. It contains an order-management example to demonstrate the framework's boundaries. The module name does not establish business requirements: replace the example with the application's confirmed domain and keep this document current.
|
|
4
|
+
|
|
5
|
+
[Application setup](../../../README.md) · [Architecture](../../../docs/architecture.md) · [Development guide](../../../docs/development.md) · [Working rules](../../../AGENTS.md)
|
|
6
|
+
|
|
7
|
+
## Ownership and entry points
|
|
8
|
+
|
|
9
|
+
| Concern | Source |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Shared names, errors, permissions, and message types | [Shared definitions](domain_shared/definitions.py) and [permission definitions](domain_shared/permissions.py) |
|
|
12
|
+
| Aggregate rules and events | [Order aggregate](domain/orders.py) |
|
|
13
|
+
| Repository contract and runtime setting | [Repository](domain/repository.py) and [settings](domain/settings.py) |
|
|
14
|
+
| DTOs and public service contract | [Application contracts](application_contracts/orders.py) |
|
|
15
|
+
| Use cases and policies | [Application service](application/orders.py) |
|
|
16
|
+
| Events, jobs, workers, and hosted integration example | [Event handlers](application/events.py), [tasks](application/tasks.py), and [integration](application/integration.py) |
|
|
17
|
+
| ORM, repositories, and migration registration | [Models](sqlalchemy/models/orders.py), [repository implementation](sqlalchemy/repositories/orders.py), and [persistence module](sqlalchemy/module.py) |
|
|
18
|
+
| HTTP exposure, file transfer, and WebSocket | [HTTP module](http_api/module.py), [files](http_api/files.py), and [real-time endpoint](http_api/realtime.py) |
|
|
19
|
+
|
|
20
|
+
The Host selects this module's application, SQLAlchemy, and HTTP modules. Other modules consume its public contract with an explicit dependency; they do not import its concrete repository or query its tables. Source declarations own the actual Module graph, table names, permission names, and routes.
|
|
21
|
+
|
|
22
|
+
## Current example behavior
|
|
23
|
+
|
|
24
|
+
- An order has a validated title and moves from pending to approved. Repeated direct approval is rejected. Shared constraints and errors are declared in `domain_shared/definitions.py`.
|
|
25
|
+
- The public `{{ cookiecutter.class_prefix }}ApplicationService` contract supports create, get, list, approve, and queued approval. Approval accepts an expected version; repository and database checks protect competing writes.
|
|
26
|
+
- Read and write permissions are defined by the module. Approval also reads the module's runtime setting; queued approval additionally uses its startup Options. Keep these choices separate when changing policy.
|
|
27
|
+
- Writes stage aggregate events through the repository. AFTER_COMMIT handlers invalidate the typed cache and send a notification to the initiating user when that identity is available. A notification failure does not undo the database commit.
|
|
28
|
+
- `ApprovalJob` carries an `order_id` and safely ignores orders that are missing or already approved. Both periodic example workers are code-disabled by default. The hosted integration example is opt-in and is not registered initially.
|
|
29
|
+
- The authenticated WebSocket at `/ws/{{ cookiecutter.module_name }}` sends a snapshot and accepts `{}` to refresh. Online notifications have no cross-process backplane or offline replay. File upload/download/stream examples share the public service boundary.
|
|
30
|
+
|
|
31
|
+
Use the generated application's `/docs` for the current HTTP paths and payloads. Change business rules only from confirmed requirements; update their aggregate, service policy, persistence, and verification owners together.
|
|
32
|
+
|
|
33
|
+
## Database and configuration
|
|
34
|
+
|
|
35
|
+
After initializing the framework providers as described in the application README, run these commands from the application root:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
uv run pddd db revision --module {{ cookiecutter.module_name }}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Review the generated revision before applying it, then run:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
uv run pddd db upgrade --module {{ cookiecutter.module_name }}
|
|
45
|
+
uv run pddd db status --module {{ cookiecutter.module_name }}
|
|
46
|
+
uv run pddd db seed --module {{ cookiecutter.module_name }}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The module owns its models and migration package. Keep applied revisions immutable and upgrade external prerequisites explicitly. Seeding this module does not seed identity or migrate the schema on its behalf.
|
|
50
|
+
|
|
51
|
+
Startup options are declared in [application/options.py](application/options.py) and bound to the `{{ cookiecutter.module_name }}` YAML section. For example:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
{{ cookiecutter.module_name }}:
|
|
55
|
+
allow_background_approval: false
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Runtime setting definitions remain in `domain/settings.py`; query the current version token before a conditional update or reset. Do not copy permission, setting, queue, or database identities into a second registry.
|
|
59
|
+
|
|
60
|
+
## Verification and maintenance
|
|
61
|
+
|
|
62
|
+
Run the existing domain test from the application root:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
uv run pytest src/modules/{{ cookiecutter.module_name }}/tests
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This test covers the sample aggregate's approval behavior. It does not prove HTTP authorization, database concurrency, jobs, Redis, WebSocket delivery, or deployment behavior. Add focused coverage as the application implements those requirements; root Host tests cover the generic Host boundary.
|
|
69
|
+
|
|
70
|
+
When changing this module, update the sections affected by its responsibilities, public contract, business invariants, persistence, background work, or validation scope. Add a more specific `AGENTS.md` only if the module needs distinct working rules; shared development instructions belong at the application root.
|
python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""{{ cookiecutter.class_prefix }} 业务模块;各层通过正式 Module 组合。"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""应用编排、提交后处理及后台业务,不依赖具体数据库或 Host。"""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""缓存契约只声明一次;提交后处理负责失效。"""
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
from python_ddd_framework.caching import CacheDefinition
|
|
6
|
+
|
|
7
|
+
from ..application_contracts.orders import OrderView
|
|
8
|
+
from ..domain_shared.definitions import MODULE_NAME
|
|
9
|
+
|
|
10
|
+
ORDER_CACHE = CacheDefinition(f"{MODULE_NAME}.view", OrderView, ttl=timedelta(minutes=1))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""提交后失效缓存与通知;失败不伪装成数据库未提交,不承诺跨进程事件投递。"""
|
|
2
|
+
|
|
3
|
+
from python_ddd_framework import LocalEventPhase, local_event_handler
|
|
4
|
+
from python_ddd_framework.caching import DistributedCache
|
|
5
|
+
from python_ddd_framework.realtime import RealtimePublisher
|
|
6
|
+
|
|
7
|
+
from ..domain.orders import OrderChanged
|
|
8
|
+
from ..domain_shared.definitions import ORDER_CHANGED_MESSAGE
|
|
9
|
+
from .cache import ORDER_CACHE
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@local_event_handler(phase=LocalEventPhase.AFTER_COMMIT)
|
|
13
|
+
class OrderChangedHandler:
|
|
14
|
+
def __init__(self, cache: DistributedCache, realtime: RealtimePublisher) -> None:
|
|
15
|
+
self._cache, self._realtime = cache, realtime
|
|
16
|
+
|
|
17
|
+
async def handle(self, event: OrderChanged) -> None:
|
|
18
|
+
await self._cache.remove(ORDER_CACHE, str(event.order_id))
|
|
19
|
+
if event.user_id is not None:
|
|
20
|
+
await self._realtime.send_to_user(
|
|
21
|
+
event.user_id,
|
|
22
|
+
message_type=ORDER_CHANGED_MESSAGE,
|
|
23
|
+
payload={"id": str(event.order_id), "status": event.status.value},
|
|
24
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""可选外部线程集成示例;替换线程生产逻辑即可接入 SDK,不另建容器或任务执行器。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from threading import Event, Thread
|
|
6
|
+
|
|
7
|
+
from python_ddd_framework import (
|
|
8
|
+
HostedService,
|
|
9
|
+
HostedServiceContext,
|
|
10
|
+
HostedServiceInvocationRejectedError,
|
|
11
|
+
ShutdownReason,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def observe_integration() -> None:
|
|
16
|
+
logging.getLogger(__name__).info("External integration callback received")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OrderIntegrationService(HostedService):
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._stop = Event()
|
|
22
|
+
self._thread: Thread | None = None
|
|
23
|
+
|
|
24
|
+
async def start(self, context: HostedServiceContext) -> None:
|
|
25
|
+
await context.call(observe_integration)
|
|
26
|
+
self._thread = Thread(target=self._receive, args=(context,), name=__name__)
|
|
27
|
+
self._thread.start()
|
|
28
|
+
|
|
29
|
+
def _receive(self, context: HostedServiceContext) -> None:
|
|
30
|
+
# submit_call 只能来自外部线程;Future.result 等待同一正式 invocation 完成。
|
|
31
|
+
try:
|
|
32
|
+
context.submit_call(observe_integration).result()
|
|
33
|
+
except HostedServiceInvocationRejectedError:
|
|
34
|
+
return # Application 关闭已撤销调用许可。
|
|
35
|
+
self._stop.wait()
|
|
36
|
+
|
|
37
|
+
async def stop(self, reason: ShutdownReason) -> None:
|
|
38
|
+
self._stop.set()
|
|
39
|
+
if self._thread is not None:
|
|
40
|
+
await asyncio.to_thread(self._thread.join)
|
|
41
|
+
self._thread = None
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""模块贡献自己的服务与任务;provider 选择留给 Host。"""
|
|
2
|
+
|
|
3
|
+
from python_ddd_framework import AppModule, ConfigureContext
|
|
4
|
+
|
|
5
|
+
from ..application_contracts.module import {{ cookiecutter.class_prefix }}ApplicationContractsModule
|
|
6
|
+
from ..domain.module import {{ cookiecutter.class_prefix }}DomainModule
|
|
7
|
+
from ..domain_shared.definitions import MODULE_NAME
|
|
8
|
+
from .cache import ORDER_CACHE
|
|
9
|
+
from .options import {{ cookiecutter.class_prefix }}Options
|
|
10
|
+
from .tasks import OrderMaintenanceWorker, OrderStatisticsWorker
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class {{ cookiecutter.class_prefix }}ApplicationModule(AppModule):
|
|
14
|
+
dependencies = ({{ cookiecutter.class_prefix }}DomainModule, {{ cookiecutter.class_prefix }}ApplicationContractsModule)
|
|
15
|
+
scan_packages = (__package__,)
|
|
16
|
+
cache_definitions = (ORDER_CACHE,)
|
|
17
|
+
background_workers = (OrderStatisticsWorker, OrderMaintenanceWorker)
|
|
18
|
+
|
|
19
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
20
|
+
context.configure({{ cookiecutter.class_prefix }}Options, section=MODULE_NAME)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""正式应用服务共用验证、授权、审计和 UoW;业务层不接触框架执行器。"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from uuid import UUID, uuid4
|
|
5
|
+
|
|
6
|
+
from python_ddd_framework import ApplicationService, CurrentUser
|
|
7
|
+
from python_ddd_framework.authorization import authorize
|
|
8
|
+
from python_ddd_framework.background_jobs import BackgroundJobEnqueuer
|
|
9
|
+
from python_ddd_framework.caching import DistributedCache
|
|
10
|
+
from python_ddd_framework.errors import BusinessError
|
|
11
|
+
from python_ddd_framework.errors.base import ResourceConflictError, ResourceNotFoundError
|
|
12
|
+
from python_ddd_framework.fastapi import http
|
|
13
|
+
|
|
14
|
+
from ..application_contracts.orders import (
|
|
15
|
+
{%- for contract in ["ApproveOrder", "CreateOrder", "OrderView", cookiecutter.class_prefix ~ "ApplicationService"] | sort %}
|
|
16
|
+
{{ contract }},
|
|
17
|
+
{%- endfor %}
|
|
18
|
+
)
|
|
19
|
+
from ..domain.orders import Order, OrderTitle
|
|
20
|
+
from ..domain.repository import OrderRepository
|
|
21
|
+
from ..domain.settings import APPROVAL_ENABLED
|
|
22
|
+
from ..domain_shared.definitions import APPROVAL_DISABLED, ORDERS_READ, ORDERS_WRITE
|
|
23
|
+
from .cache import ORDER_CACHE
|
|
24
|
+
from .tasks import ApprovalJob, ApprovalPayload
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _{{ cookiecutter.class_prefix }}ApplicationService(ApplicationService, {{ cookiecutter.class_prefix }}ApplicationService):
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
repository: OrderRepository,
|
|
33
|
+
cache: DistributedCache,
|
|
34
|
+
jobs: BackgroundJobEnqueuer,
|
|
35
|
+
user: CurrentUser,
|
|
36
|
+
) -> None:
|
|
37
|
+
self._repository, self._cache, self._jobs, self._user = repository, cache, jobs, user
|
|
38
|
+
|
|
39
|
+
@authorize(ORDERS_WRITE)
|
|
40
|
+
@http.post(status_code=201)
|
|
41
|
+
async def create(self, command: CreateOrder) -> OrderView:
|
|
42
|
+
order = Order.create(uuid4(), OrderTitle(value=command.title))
|
|
43
|
+
await self._repository.save(order)
|
|
44
|
+
logger.info("Order created")
|
|
45
|
+
return OrderView.model_validate(order)
|
|
46
|
+
|
|
47
|
+
@authorize(ORDERS_READ)
|
|
48
|
+
async def get(self, id: UUID) -> OrderView:
|
|
49
|
+
cached = await self._cache.get(ORDER_CACHE, str(id))
|
|
50
|
+
if cached is not None:
|
|
51
|
+
return cached
|
|
52
|
+
order = await self._repository.get(id)
|
|
53
|
+
if order is None:
|
|
54
|
+
raise ResourceNotFoundError("Order not found")
|
|
55
|
+
view = OrderView.model_validate(order)
|
|
56
|
+
await self._cache.set(ORDER_CACHE, str(id), view)
|
|
57
|
+
return view
|
|
58
|
+
|
|
59
|
+
@authorize(ORDERS_READ)
|
|
60
|
+
async def get_list(self) -> tuple[OrderView, ...]:
|
|
61
|
+
return tuple(OrderView.model_validate(order) for order in await self._repository.get_list())
|
|
62
|
+
|
|
63
|
+
@authorize(ORDERS_WRITE)
|
|
64
|
+
async def approve(self, id: UUID, command: ApproveOrder) -> OrderView:
|
|
65
|
+
if not await self.setting_provider.get(APPROVAL_ENABLED):
|
|
66
|
+
raise BusinessError(APPROVAL_DISABLED)
|
|
67
|
+
order = await self._repository.get(id)
|
|
68
|
+
if order is None:
|
|
69
|
+
raise ResourceNotFoundError("Order not found")
|
|
70
|
+
if order.version != command.expected_version:
|
|
71
|
+
raise ResourceConflictError("Order version changed")
|
|
72
|
+
order.approve(user_id=self._user.user_id)
|
|
73
|
+
await self._repository.save(order)
|
|
74
|
+
logger.info("Order approved")
|
|
75
|
+
return OrderView.model_validate(order)
|
|
76
|
+
|
|
77
|
+
@authorize(ORDERS_WRITE)
|
|
78
|
+
@http.post(status_code=202)
|
|
79
|
+
async def queue_approval(self, id: UUID) -> str:
|
|
80
|
+
if await self._repository.get(id) is None:
|
|
81
|
+
raise ResourceNotFoundError("Order not found")
|
|
82
|
+
# 入队参与本方法原生事务;失败时订单业务与队列写入一起回滚。
|
|
83
|
+
return await self._jobs.enqueue(ApprovalJob, ApprovalPayload(order_id=id))
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""非持久 Worker 与持久 Job 分开;Job 重复投递由业务状态保证幂等。"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
from python_ddd_framework import (
|
|
9
|
+
BackgroundWorker,
|
|
10
|
+
BackgroundWorkerContext,
|
|
11
|
+
Options,
|
|
12
|
+
)
|
|
13
|
+
from python_ddd_framework.background_jobs import (
|
|
14
|
+
BackgroundJobContext,
|
|
15
|
+
BackgroundJobHandler,
|
|
16
|
+
)
|
|
17
|
+
from python_ddd_framework.distributed_lock import DistributedLock
|
|
18
|
+
from python_ddd_framework.settings import SettingProvider
|
|
19
|
+
|
|
20
|
+
from ..domain.repository import OrderRepository
|
|
21
|
+
from ..domain.settings import APPROVAL_ENABLED
|
|
22
|
+
from ..domain_shared.definitions import MODULE_NAME, OrderStatus
|
|
23
|
+
from .options import {{ cookiecutter.class_prefix }}Options
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ApprovalPayload(BaseModel):
|
|
29
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
30
|
+
order_id: UUID
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ApprovalJob(BackgroundJobHandler[ApprovalPayload]):
|
|
34
|
+
name = f"{MODULE_NAME}.approve"
|
|
35
|
+
version = 1
|
|
36
|
+
current = True
|
|
37
|
+
timeout = timedelta(seconds=30)
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
repository: OrderRepository,
|
|
42
|
+
settings: SettingProvider,
|
|
43
|
+
options: Options[{{ cookiecutter.class_prefix }}Options],
|
|
44
|
+
) -> None:
|
|
45
|
+
self._repository, self._settings, self._options = repository, settings, options
|
|
46
|
+
|
|
47
|
+
async def execute(self, payload: ApprovalPayload, context: BackgroundJobContext) -> None:
|
|
48
|
+
if self._options.value.allow_background_approval and await self._settings.get(
|
|
49
|
+
APPROVAL_ENABLED
|
|
50
|
+
):
|
|
51
|
+
order = await self._repository.get(payload.order_id)
|
|
52
|
+
if order is not None and order.status is OrderStatus.PENDING:
|
|
53
|
+
order.approve()
|
|
54
|
+
await self._repository.save(order)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class OrderMaintenanceWorker(BackgroundWorker):
|
|
58
|
+
name = f"{MODULE_NAME}.maintenance"
|
|
59
|
+
enabled = False
|
|
60
|
+
interval = timedelta(seconds=30)
|
|
61
|
+
iteration_timeout = timedelta(seconds=10)
|
|
62
|
+
|
|
63
|
+
def __init__(self, repository: OrderRepository, lock: DistributedLock) -> None:
|
|
64
|
+
self._repository, self._lock = repository, lock
|
|
65
|
+
|
|
66
|
+
async def run_iteration(self, context: BackgroundWorkerContext) -> None:
|
|
67
|
+
# 租约失效由框架请求取消并等待本轮清理;业务不吞 CancelledError。
|
|
68
|
+
async with self._lock.acquire(f"{MODULE_NAME}:maintenance") as acquired:
|
|
69
|
+
if acquired:
|
|
70
|
+
orders = await self._repository.get_list()
|
|
71
|
+
logger.info("Order maintenance completed: %s", len(orders))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class OrderStatisticsWorker(BackgroundWorker):
|
|
75
|
+
name = f"{MODULE_NAME}.statistics"
|
|
76
|
+
enabled = False
|
|
77
|
+
interval = timedelta(minutes=5)
|
|
78
|
+
iteration_timeout = timedelta(seconds=10)
|
|
79
|
+
|
|
80
|
+
def __init__(self, repository: OrderRepository) -> None:
|
|
81
|
+
self._repository = repository
|
|
82
|
+
|
|
83
|
+
async def run_iteration(self, context: BackgroundWorkerContext) -> None:
|
|
84
|
+
logger.info("Order count: %s", len(await self._repository.get_list()))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""供调用者使用的 DTO 和服务契约。"""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""契约层不反向依赖应用实现或 provider。"""
|
|
2
|
+
|
|
3
|
+
from python_ddd_framework import AppModule
|
|
4
|
+
|
|
5
|
+
from ..domain_shared.module import {{ cookiecutter.class_prefix }}DomainSharedModule
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class {{ cookiecutter.class_prefix }}ApplicationContractsModule(AppModule):
|
|
9
|
+
dependencies = ({{ cookiecutter.class_prefix }}DomainSharedModule,)
|
|
10
|
+
scan_packages = (__package__,)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""DTO 只依赖共享定义;HTTP 与进程内调用共用同一验证管线。"""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
from python_ddd_framework import ApplicationServiceContract
|
|
8
|
+
|
|
9
|
+
from ..domain_shared.definitions import TITLE_MAX_LENGTH, OrderStatus
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CreateOrder(BaseModel):
|
|
13
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
14
|
+
title: str = Field(min_length=1, max_length=TITLE_MAX_LENGTH)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ApproveOrder(BaseModel):
|
|
18
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
19
|
+
expected_version: int = Field(ge=1)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OrderView(BaseModel):
|
|
23
|
+
model_config = ConfigDict(frozen=True, from_attributes=True)
|
|
24
|
+
id: UUID
|
|
25
|
+
title: str
|
|
26
|
+
status: OrderStatus
|
|
27
|
+
version: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class {{ cookiecutter.class_prefix }}ApplicationService(ApplicationServiceContract, Protocol):
|
|
31
|
+
async def create(self, command: CreateOrder) -> OrderView: ...
|
|
32
|
+
async def get(self, id: UUID) -> OrderView: ...
|
|
33
|
+
async def get_list(self) -> tuple[OrderView, ...]: ...
|
|
34
|
+
async def approve(self, id: UUID, command: ApproveOrder) -> OrderView: ...
|
|
35
|
+
async def queue_approval(self, id: UUID) -> str: ...
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""聚合、仓储契约、设置定义与幂等 Seed。"""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""扫描仅包含实际业务声明,模块 tests 不参与服务发现。"""
|
|
2
|
+
|
|
3
|
+
from python_ddd_framework import AppModule
|
|
4
|
+
|
|
5
|
+
from ..domain_shared.module import {{ cookiecutter.class_prefix }}DomainSharedModule
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class {{ cookiecutter.class_prefix }}DomainModule(AppModule):
|
|
9
|
+
dependencies = ({{ cookiecutter.class_prefix }}DomainSharedModule,)
|
|
10
|
+
scan_packages = (__package__,)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""聚合拥有审批规则、版本及事件;持久化实现只负责映射。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
from python_ddd_framework import AggregateRoot, ValueObject
|
|
10
|
+
from python_ddd_framework.errors import BusinessError
|
|
11
|
+
|
|
12
|
+
from ..domain_shared.definitions import ALREADY_APPROVED, TITLE_MAX_LENGTH, OrderStatus
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OrderTitle(ValueObject):
|
|
16
|
+
value: str = Field(min_length=1, max_length=TITLE_MAX_LENGTH)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class OrderChanged:
|
|
21
|
+
order_id: UUID
|
|
22
|
+
status: OrderStatus
|
|
23
|
+
user_id: UUID | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Order(AggregateRoot[UUID]):
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
id: UUID,
|
|
30
|
+
title: OrderTitle,
|
|
31
|
+
*,
|
|
32
|
+
status: OrderStatus = OrderStatus.PENDING,
|
|
33
|
+
version: int = 0,
|
|
34
|
+
) -> None:
|
|
35
|
+
super().__init__(id, version=version)
|
|
36
|
+
self._title, self._status = title, status
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def title(self) -> str:
|
|
40
|
+
return self._title.value
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def status(self) -> OrderStatus:
|
|
44
|
+
return self._status
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def create(cls, id: UUID, title: OrderTitle) -> Order:
|
|
48
|
+
order = cls(id, title)
|
|
49
|
+
order.raise_local_event(OrderChanged(id, order.status))
|
|
50
|
+
return order
|
|
51
|
+
|
|
52
|
+
def approve(self, *, user_id: UUID | None = None) -> None:
|
|
53
|
+
if self.status is OrderStatus.APPROVED:
|
|
54
|
+
raise BusinessError(ALREADY_APPROVED)
|
|
55
|
+
self._status = OrderStatus.APPROVED
|
|
56
|
+
self.raise_local_event(OrderChanged(self.id, self.status, user_id))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""领域需要的业务仓储边界;不暴露 Session 或容器。"""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from python_ddd_framework import RepositoryContract
|
|
7
|
+
|
|
8
|
+
from .orders import Order
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OrderRepository(RepositoryContract, Protocol):
|
|
12
|
+
async def get(self, id: UUID) -> Order | None: ...
|
|
13
|
+
async def get_list(self) -> tuple[Order, ...]: ...
|
|
14
|
+
async def save(self, order: Order) -> None: ...
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""仅 pddd db seed 显式调用;确定性业务 ID 保证重复初始化不产生新订单。"""
|
|
2
|
+
|
|
3
|
+
from uuid import NAMESPACE_URL, uuid5
|
|
4
|
+
|
|
5
|
+
from python_ddd_framework import DataSeedContributor
|
|
6
|
+
|
|
7
|
+
from ..domain_shared.definitions import MODULE_NAME
|
|
8
|
+
from .orders import Order, OrderTitle
|
|
9
|
+
from .repository import OrderRepository
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class {{ cookiecutter.class_prefix }}SeedContributor(DataSeedContributor):
|
|
13
|
+
def __init__(self, repository: OrderRepository) -> None:
|
|
14
|
+
self._repository = repository
|
|
15
|
+
|
|
16
|
+
async def seed(self) -> None:
|
|
17
|
+
id = uuid5(NAMESPACE_URL, f"{MODULE_NAME}:sample-order")
|
|
18
|
+
if await self._repository.get(id) is None:
|
|
19
|
+
await self._repository.save(Order.create(id, OrderTitle(value="Sample order")))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""设置默认值属于领域,业务通过 SettingProvider 获取冻结读取视图。"""
|
|
2
|
+
|
|
3
|
+
from python_ddd_framework.settings import (
|
|
4
|
+
SettingDefinition,
|
|
5
|
+
SettingDefinitionContext,
|
|
6
|
+
SettingDefinitionProvider,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from ..domain_shared.definitions import MODULE_NAME
|
|
10
|
+
|
|
11
|
+
APPROVAL_ENABLED = SettingDefinition(f"{MODULE_NAME}.approval_enabled", bool, default=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class {{ cookiecutter.class_prefix }}SettingDefinitionProvider(SettingDefinitionProvider):
|
|
15
|
+
def define(self, context: SettingDefinitionContext) -> None:
|
|
16
|
+
context.add(APPROVAL_ENABLED)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""业务共享定义,不依赖应用编排或 provider。"""
|