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,84 @@
|
|
|
1
|
+
# Application architecture
|
|
2
|
+
|
|
3
|
+
This application was generated with Python DDD Framework {{ cookiecutter.framework_version }}. This document records the application's initial boundaries; maintain it as the application evolves. Installed versions and module registrations remain owned by project metadata and source code.
|
|
4
|
+
|
|
5
|
+
[Run the application](../README.md) · [Development guide](development.md) · [Working rules](../AGENTS.md)
|
|
6
|
+
|
|
7
|
+
## Composition and ownership
|
|
8
|
+
|
|
9
|
+
The initial project is a Host without business modules. `pddd add module <name>` adds an example module that the application team adapts to its domain. Generated order-management behavior is a sample, not a declaration of the application's business requirements.
|
|
10
|
+
|
|
11
|
+
| Owner | Responsibility |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `src/host/main.py` | Application and FastAPI factories, configuration inputs, middleware assembly, and the server entry point. |
|
|
14
|
+
| `src/host/module.py` | Explicit module dependencies and provider selection for this deployment. |
|
|
15
|
+
| `src/modules/<name>/` | A business capability, its public contracts, implementation, persistence, transport, and tests. |
|
|
16
|
+
| `app.development.yaml` | Local environment inputs and generated development credentials. |
|
|
17
|
+
| Deployment configuration | Production connections, secrets, environment configuration, and image selection. |
|
|
18
|
+
| `pyproject.toml` and `uv.lock` | Package dependencies, entry points, Python requirements, and resolved versions. |
|
|
19
|
+
|
|
20
|
+
The Host initially selects PostgreSQL persistence, identity, auditing, Redis capabilities, settings, background jobs, and observability integrations. Read its dependency declarations or run `uv run pddd inspect` for the actual composition. Installing a framework dependency does not enable a module or start its resources.
|
|
21
|
+
|
|
22
|
+
## Business-module boundaries
|
|
23
|
+
|
|
24
|
+
Each generated module uses the following dependency direction:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
application -> domain -> domain_shared
|
|
28
|
+
application -> application_contracts -> domain_shared
|
|
29
|
+
http_api -> application_contracts
|
|
30
|
+
sqlalchemy -> domain
|
|
31
|
+
|
|
32
|
+
Host composes application, sqlalchemy, and http_api modules.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Layer | Owns | Keep outside this layer |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `domain_shared` | Shared values, error definitions, permission names, and business constants. | Host configuration, persistence, and transport behavior. |
|
|
38
|
+
| `domain` | Aggregates, state transitions, events, repository contracts, setting definitions, and seed contributors. | ORM models, HTTP requests, and concrete infrastructure providers. |
|
|
39
|
+
| `application_contracts` | Input/output DTOs and public service Protocols. | Service policies, implementation details, and ORM types. |
|
|
40
|
+
| `application` | Use cases, authorization policies, event handlers, typed job handlers, workers, and integration behavior. | Database mappings and Host-specific provider selection. |
|
|
41
|
+
| `sqlalchemy` | ORM models, repository implementations, model registration, and owned migrations. | Business policy belonging to aggregates or application services. |
|
|
42
|
+
| `http_api` | Service exposure and explicit HTTP/WebSocket transport declarations. | Direct persistence access and duplicated use-case logic. |
|
|
43
|
+
|
|
44
|
+
The application layer depends on domain and application contracts. The HTTP layer consumes contracts; it does not import the application implementation. The persistence layer implements domain repository contracts. Framework types and selected native Python libraries remain available where appropriate to these responsibilities.
|
|
45
|
+
|
|
46
|
+
Cross-module access uses public service contracts and explicit Module dependencies. A module owns its own data and migrations. Shared database deployment does not authorize another module to query its tables. Keep internal collaboration contracts separate from end-user service contracts where the caller and authorization semantics differ; `IntegrationService` requires its own contract and is not exposed over HTTP.
|
|
47
|
+
|
|
48
|
+
## Calls, scopes, and transactions
|
|
49
|
+
|
|
50
|
+
A managed service call passes through authorization, validation, ACTION scope, unit-of-work handling, and the final implementation. HTTP and direct service invocation share this pipeline. Inject the public service contract; do not construct implementations to bypass it. A self-call is an ordinary Python call and does not re-enter the pipeline.
|
|
51
|
+
|
|
52
|
+
Dishka owns service lifetimes. Repository implementations use cached REQUEST scope, application-service implementations use cached ACTION scope, and hosted services use cached APP scope. A unit of work independently owns its transaction and Session; REQUEST and ACTION scopes are not themselves transactions.
|
|
53
|
+
|
|
54
|
+
Default read/write transaction behavior follows framework method/HTTP conventions and explicit policies. For a manually opened unit of work inside a managed invocation, call `complete()` before exiting. A flush is not a commit. Keep sessions local to the owning invocation and avoid transactions across external I/O or long waits.
|
|
55
|
+
|
|
56
|
+
Repositories map aggregates and ORM models explicitly. Their `SqlAlchemySessionProvider.operation(..., aggregate=...)` boundary stages version changes and collects aggregate events once. Database optimistic concurrency still protects competing writes; do not replace it with a cache or a distributed lock.
|
|
57
|
+
|
|
58
|
+
Local transactional events run in this order: DOMAIN handlers, flush/version checks, database commit, then AFTER_COMMIT handlers. A post-commit failure does not roll back committed data. Local events provide neither distributed delivery nor crash recovery.
|
|
59
|
+
|
|
60
|
+
## Configuration and lifecycle
|
|
61
|
+
|
|
62
|
+
The Host selects the environment through `{{ cookiecutter.host_environment_variable }}` and uses `PDDD_` for business environment variables. Nested fields use `__`. Optional `app.yaml` supplies shared values; `app.<environment>.yaml` supplies environment-specific values. Environment variables override file inputs; explicit Host CLI inputs and overrides have their documented higher precedence.
|
|
63
|
+
|
|
64
|
+
Modules bind typed `BaseOptions` models with `context.configure(...)`; services inject `Options[T]`. Startup options and mutable runtime settings have separate purposes. `SettingProvider` reads settings; conditional management updates use the current version token.
|
|
65
|
+
|
|
66
|
+
An Application owns its runtime state, DI container, connections, and background execution. Module definitions can be shared; mutable runtime state cannot. No global application registry or service locator is required.
|
|
67
|
+
|
|
68
|
+
Module initialization precedes selected hosted services and background execution. Shutdown waits for real cleanup. The Host does not migrate or seed the database. `dev-init` prepares local infrastructure, while explicit `db` commands own migration and seed operations.
|
|
69
|
+
|
|
70
|
+
## Background work and external boundaries
|
|
71
|
+
|
|
72
|
+
- Durable jobs use typed, versioned payloads and the PostgreSQL provider. Transactional enqueue must use the same configured connection as the surrounding unit of work. External side effects remain the job's idempotency responsibility.
|
|
73
|
+
- Periodic workers are explicitly registered. Generated example workers are disabled in their class declarations; configuration cannot enable a code-disabled worker.
|
|
74
|
+
- Execution modes are `host`, `shared`, and `exclusive`. Spawned processes compose their own Application; they do not inherit a live Session or container.
|
|
75
|
+
- A `HostedService` owns long-lived SDKs or threads, their failures, recovery, and actual shutdown. It is opt-in; background-process instances need explicit selection. External threads use its supported submission methods.
|
|
76
|
+
- Redis business locks are leases. Preserve cancellation and cleanup when ownership is lost; a lease is not a transaction, fencing guarantee, or exactly-once guarantee.
|
|
77
|
+
- WebSocket delivery targets currently connected clients. The generated sample has no cross-process backplane, offline replay, or delivery acknowledgment. HTTP streaming must release business transactions before network transmission and clean up producers on disconnect.
|
|
78
|
+
- Liveness and readiness report lifecycle state, not continuous infrastructure availability. Trace export is disabled in the generated development configuration until explicitly configured.
|
|
79
|
+
|
|
80
|
+
## Documentation ownership
|
|
81
|
+
|
|
82
|
+
`README.md` owns setup and operations; `AGENTS.md` owns development instructions; this file owns architecture boundaries; `development.md` owns usage recipes. Each module README owns its actual business rules, public surface, data, and verification entry points.
|
|
83
|
+
|
|
84
|
+
Update the affected document with the implementation. Do not duplicate complete dependency, command, route, or module inventories: inspect metadata, CLI help, OpenAPI, and source. Generated documentation belongs to the application after creation and is not overwritten by a framework upgrade.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Development guide
|
|
2
|
+
|
|
3
|
+
Use this guide with the project's [architecture](architecture.md) and [working rules](../AGENTS.md). It describes the generated application's use of Python DDD Framework {{ cookiecutter.framework_version }}. Follow the installed version when adapting examples after an upgrade.
|
|
4
|
+
|
|
5
|
+
[Add a module](#add-a-module) · [Services and permissions](#services-and-permissions) · [Options and settings](#options-and-settings) · [Persistence](#persistence-and-migrations) · [Events and background work](#events-and-background-work) · [Verification](#verification)
|
|
6
|
+
|
|
7
|
+
## Add a module
|
|
8
|
+
|
|
9
|
+
From the application root:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
uv run pddd add module orders --dry-run
|
|
13
|
+
uv run pddd add module orders
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The CLI generates `src/modules/orders/`, registers its application/persistence/HTTP modules in the Host, updates standard package metadata, and synchronizes dependencies. It also creates the module's README. Existing module directories are not overwritten. Repeat with another valid lowercase name when a separate business owner is needed.
|
|
17
|
+
|
|
18
|
+
Read the generated module README before replacing the order example with your domain. The examples below assume an `orders` module; substitute your actual package and contract names. The application remains usable as a pure Host before adding a module.
|
|
19
|
+
|
|
20
|
+
## Services and permissions
|
|
21
|
+
|
|
22
|
+
1. Define shared business values and permission names in `domain_shared/`.
|
|
23
|
+
2. Put aggregate invariants and repository contracts in `domain/`.
|
|
24
|
+
3. Define Pydantic DTOs and an `ApplicationServiceContract, Protocol` in `application_contracts/`.
|
|
25
|
+
4. Implement the contract with `ApplicationService` in `application/`. Keep permission and HTTP method policies on the final implementation.
|
|
26
|
+
5. Keep `scan_packages` scoped to the owning package. The generated application module already scans its package; the HTTP module explicitly exposes its contracts.
|
|
27
|
+
|
|
28
|
+
The generated implementation illustrates a managed write:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from uuid import uuid4
|
|
32
|
+
|
|
33
|
+
from python_ddd_framework.authorization import authorize
|
|
34
|
+
from python_ddd_framework.fastapi import http
|
|
35
|
+
|
|
36
|
+
from ..application_contracts.orders import CreateOrder, OrderView
|
|
37
|
+
from ..domain.orders import Order, OrderTitle
|
|
38
|
+
from ..domain_shared.definitions import ORDERS_WRITE
|
|
39
|
+
|
|
40
|
+
# Method inside the generated ApplicationService implementation:
|
|
41
|
+
@authorize(ORDERS_WRITE)
|
|
42
|
+
@http.post(status_code=201)
|
|
43
|
+
async def create(self, command: CreateOrder) -> OrderView:
|
|
44
|
+
order = Order.create(uuid4(), OrderTitle(value=command.title))
|
|
45
|
+
await self._repository.save(order)
|
|
46
|
+
return OrderView.model_validate(order)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Reuse the existing permission definition provider when adding a permission. A new permission definition does not replace the application's grant policy. Do not grant end-user permissions to background components to repair an incorrectly chosen service boundary.
|
|
50
|
+
|
|
51
|
+
In DI-managed code, inject the public contract and call `await service.method(...)`. Outside DI, use `await application.invoke(ServiceContract.method, ...)`; it is anonymous by default. Trusted Host/test callers can supply a validated identity through `invoke_as`. Use `application.call` for ordinary async callables that need DI. Do not resolve or instantiate the private service implementation yourself.
|
|
52
|
+
|
|
53
|
+
For ordinary services, use the module's existing registration methods or native Dishka providers. Repository, application-service, and hosted-service scopes are fixed by the framework. Do not add a wrapper or custom container around those roles.
|
|
54
|
+
|
|
55
|
+
## Options and settings
|
|
56
|
+
|
|
57
|
+
Startup configuration belongs in a `BaseOptions` type and is bound by the owning module:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from python_ddd_framework import BaseOptions, Options
|
|
61
|
+
|
|
62
|
+
class OrdersOptions(BaseOptions):
|
|
63
|
+
allow_background_approval: bool = True
|
|
64
|
+
|
|
65
|
+
# In the module's configure(context) method:
|
|
66
|
+
# context.configure(OrdersOptions, section="orders")
|
|
67
|
+
|
|
68
|
+
class ApprovalPolicy:
|
|
69
|
+
def __init__(self, options: Options[OrdersOptions]) -> None:
|
|
70
|
+
self._options = options
|
|
71
|
+
|
|
72
|
+
def background_approval_allowed(self) -> bool:
|
|
73
|
+
return self._options.value.allow_background_approval
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This demonstrates the existing Options declaration and injection pattern; extend the generated types rather than defining a second `OrdersOptions`. Register any new ordinary service in the owning module. Configure the generated option in `app.development.yaml`:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
orders:
|
|
80
|
+
allow_background_approval: false
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The corresponding environment key is `PDDD_ORDERS__ALLOW_BACKGROUND_APPROVAL`. `PDDD_HTTP__API_PREFIX` changes the business API prefix. `{{ cookiecutter.host_environment_variable }}` selects the Host environment and is separate from business configuration. Values that affect composition require the framework's pre-configuration phase; do not read unbound Options during construction.
|
|
84
|
+
|
|
85
|
+
Use runtime settings when a value must be changed while the application is running. Add definitions through a scanned `SettingDefinitionProvider`, read them through `SettingProvider`, and use `SettingManager` or the management API for conditional updates/reset with the queried version token. Application services can use `self.setting_provider` during managed invocation, not during construction. The sample's approval setting is defined in `domain/settings.py`.
|
|
86
|
+
|
|
87
|
+
Inspect configuration sources and service registrations with:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
uv run pddd inspect --environment development
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This composes and closes a new Application without starting it. Output contains redacted configuration inputs, not running-process state or the final Options values after defaults and module contributions. Mark secrets through the framework's existing secret types/metadata; do not log configuration objects or credentials.
|
|
94
|
+
|
|
95
|
+
## Persistence and migrations
|
|
96
|
+
|
|
97
|
+
The generated repository interface inherits `RepositoryContract, Protocol`. Its SQLAlchemy implementation nominally implements that contract under `sqlalchemy/repositories/`, which the persistence module scans. Keep Sessions and ORM types inside this layer.
|
|
98
|
+
|
|
99
|
+
Map aggregates and rows explicitly within `SqlAlchemySessionProvider.operation`. Pass `aggregate=order` when staging an aggregate write, keep the optimistic version checks, and let that boundary collect the aggregate's events. Avoid collecting the same events again in the service.
|
|
100
|
+
|
|
101
|
+
Place model files in `sqlalchemy/models/` and inherit the package's Base. `SqlAlchemyModelRegistration.from_package` imports that package before freezing metadata, so a new model file does not need a separate import registry. Keep migration scripts in the separate `sqlalchemy/migrations/` package.
|
|
102
|
+
|
|
103
|
+
After the initial provider setup in the [README](../README.md#initialize-the-database):
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
uv run pddd db revision --module orders
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Review the revision, especially renames, data backfills, external revision dependencies, and destructive operations. Autogeneration does not decide whole-table deletion or another owner's migration for you. Then run:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
uv run pddd db upgrade --module orders
|
|
113
|
+
uv run pddd db status --module orders
|
|
114
|
+
uv run pddd db seed --module orders
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Upgrade prerequisites explicitly. Do not modify already applied revisions or write migrations into the installed framework package. Seed contributors run in independent transactions and should be idempotent; seeding a business module does not seed identity on its behalf.
|
|
118
|
+
|
|
119
|
+
Managed application-service calls provide the normal unit-of-work boundary. Open an explicit short unit of work only when the operation requires it, inside a managed invocation, and complete it explicitly. Never hold it across a device call, long-running task, external network wait, or streamed response. A child task or thread cannot reuse the parent's Session.
|
|
120
|
+
|
|
121
|
+
For shared databases, configure installation-specific Alembic version table locations, queue schema/object prefixes, and Redis prefixes through their existing Options. A queue schema has one migration owner; it cannot contain the Alembic version table. Changing installation identity after data exists requires an explicit data and migration-record plan.
|
|
122
|
+
|
|
123
|
+
## Events and background work
|
|
124
|
+
|
|
125
|
+
Raise domain events on the aggregate. Put typed handlers in a scanned package and select the required `LocalEventPhase` with `@local_event_handler`. Use DOMAIN for transactional rules and AFTER_COMMIT for work such as the sample's cache invalidation and online notifications. Post-commit failures cannot undo a committed order; local events are not a durable message bus.
|
|
126
|
+
|
|
127
|
+
Jobs inherit `BackgroundJobHandler[Payload]`, declare their durable name/version/current/timeout on the class, and are discovered by module scanning. The generated `ApprovalJob` and `ApprovalPayload` show an idempotent business handler. Enqueue by type from a managed invocation, using the same configured database connection for transactional enqueue:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from uuid import UUID
|
|
131
|
+
|
|
132
|
+
from python_ddd_framework.background_jobs import BackgroundJobEnqueuer
|
|
133
|
+
|
|
134
|
+
from modules.orders.application.tasks import ApprovalJob, ApprovalPayload
|
|
135
|
+
|
|
136
|
+
async def queue_approval(jobs: BackgroundJobEnqueuer, order_id: UUID) -> str:
|
|
137
|
+
return await jobs.enqueue(ApprovalJob, ApprovalPayload(order_id=order_id))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Keep persisted payload versions until their queued jobs are drained or explicitly migrated. A job's external side effects must tolerate repeat execution.
|
|
141
|
+
|
|
142
|
+
Workers implement `BackgroundWorker.run_iteration` and are registered in `Module.background_workers`; their period, timeout, and enabled state live on the class. Both generated workers are disabled. Change a worker's code declaration deliberately before expecting it to run; configuration and management endpoints cannot enable a code-disabled worker.
|
|
143
|
+
|
|
144
|
+
Background execution Options use catalog names to select `host`, `shared`, or `exclusive` placement. Only worker configuration accepts `enabled`; job configuration does not. Management start/stop is asynchronous: query status after a request. Stopping a job processor preserves its queued data.
|
|
145
|
+
|
|
146
|
+
Use the existing `DistributedLock` provider for explicit business leases. If acquisition returns false, skip or report the operation as appropriate. Preserve cancellation and cleanup on lease loss. Use database concurrency and idempotency for their separate guarantees.
|
|
147
|
+
|
|
148
|
+
A `HostedService` manages a long-lived SDK or thread through async `start`/`stop`, including actual thread termination. Register it explicitly in `Module.hosted_services`; the generated integration example is not registered by default. External threads submit through the hosted-service context, and the component owns runtime failures and recovery.
|
|
149
|
+
|
|
150
|
+
## HTTP, files, and real-time communication
|
|
151
|
+
|
|
152
|
+
The Host passes `http.api_prefix` to `FastApiAdapter`, defaulting to `/api`. The generated HTTP module exposes its application contracts; special method behavior uses `http` decorators on implementations. Check `/docs` after route changes. Keep method paths relative to their service; module overrides do not repeat the global prefix.
|
|
153
|
+
|
|
154
|
+
For explicit business routes, use `HttpRouter(use_api_prefix=True, ...)` and inject public service contracts. Independent endpoints such as WebSocket and health routes retain separate paths. Native FastAPI parameter and response types remain supported.
|
|
155
|
+
|
|
156
|
+
Use `UploadFile`, `File`, and `Form` for uploads and native response types for downloads/streams. Keep byte limits enforced while reading. Release database work before sending a stream, pass detached data to the producer, and protect asynchronous cleanup in its `finally` block; never carry the previous business Session across yields.
|
|
157
|
+
|
|
158
|
+
The example WebSocket accepts authenticated clients, sends an initial snapshot, and handles `{}` as a refresh request. Invocations recheck identity and permissions. Non-loopback deployments require WSS; query-token authentication requires an explicit Origin policy. Sample notifications stay within the Host process and do not promise replay or delivery acknowledgment.
|
|
159
|
+
|
|
160
|
+
Use `logging.getLogger(__name__)` for application logs. Configure logging in the Host's YAML, keep credentials and payloads out of messages, and enable existing tracing export only when its destination is configured. Avoid process-global logging/tracing setup in a business module.
|
|
161
|
+
|
|
162
|
+
## Verification
|
|
163
|
+
|
|
164
|
+
Run commands from the application root. Read fixtures first: Host tests use disposable PostgreSQL and Redis containers, so Docker must be available.
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
# After adding an orders module: its domain tests do not need Docker.
|
|
168
|
+
uv run pytest src/modules/orders/tests
|
|
169
|
+
|
|
170
|
+
# Full application tests include real Host infrastructure.
|
|
171
|
+
uv run pytest
|
|
172
|
+
|
|
173
|
+
# Check the application distribution without local source overrides.
|
|
174
|
+
uv build --no-sources
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
For custom service tests, use `TestApplication(ApplicationBuilder(...))`, its async context manager, and typed `invoke`/`invoke_as`. The wrapper uses normal composition and cleanup; choose only the modules and providers required by the behavior being tested. Preserve identity and transaction boundaries in integration tests.
|
|
178
|
+
|
|
179
|
+
The template does not install Ruff or mypy as application dependencies and does not add an architecture-test suite. If those gates are needed, add and configure them as an explicit project decision. Do not describe a static check or a mock as live database, HTTP, or deployment verification.
|
|
180
|
+
|
|
181
|
+
## Maintain the application
|
|
182
|
+
|
|
183
|
+
Update a module README for changed business rules, contracts, persistence, or background behavior. Keep architecture and usage instructions in their respective documents. Record a new architectural decision only when there is an actual decision to preserve; do not pre-generate empty plans or status logs.
|
|
184
|
+
|
|
185
|
+
Framework upgrades update dependencies, not application source or documentation. Follow the target release's migration notes and update these documents deliberately. Add application dependencies with `uv add <package>`; preserve their source overrides and version choices when changing the framework dependency.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{{ cookiecutter.project_name }}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
requires-python = "=={{ cookiecutter.python_version }}"
|
|
5
|
+
dependencies = ["python-ddd-framework=={{ cookiecutter.framework_version }}"]
|
|
6
|
+
|
|
7
|
+
[project.entry-points."python_ddd_framework.hosts"]
|
|
8
|
+
application = "host.main:create_application"
|
|
9
|
+
web = "host.main:create_web"
|
|
10
|
+
[dependency-groups]
|
|
11
|
+
dev = ["python-ddd-framework[developer-kit]=={{ cookiecutter.framework_version }}", "pytest", "anyio", "testcontainers[postgres,redis]"]
|
|
12
|
+
|
|
13
|
+
{% if cookiecutter.framework_source %}[tool.uv.sources]
|
|
14
|
+
python-ddd-framework = {{ cookiecutter.framework_source }}
|
|
15
|
+
{% endif %}
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build=={{ cookiecutter.build_version }}"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[tool.uv.build-backend]
|
|
21
|
+
module-name = "host"
|
|
22
|
+
module-root = "src"
|
|
23
|
+
wheel-exclude = ["**/tests/**"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
anyio_mode = "auto"
|
|
27
|
+
testpaths = ["tests", "src/modules"]
|
|
28
|
+
addopts = "--import-mode=importlib"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""应用 composition root;业务代码由 pddd add module 生成。"""
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""标准入口一次 build/start/stop;生产和开发使用同一 factory。"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from python_ddd_framework import Application, ApplicationBuilder
|
|
9
|
+
from python_ddd_framework.cli import ENVIRONMENT_VARIABLE
|
|
10
|
+
from python_ddd_framework.fastapi import FastApiAdapter
|
|
11
|
+
from starlette.middleware import Middleware
|
|
12
|
+
from starlette.middleware.gzip import GZipMiddleware
|
|
13
|
+
|
|
14
|
+
from .module import {{ cookiecutter.class_prefix }}HostModule
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_application(
|
|
18
|
+
*, environment: str | None = None, cli_args: tuple[str, ...] = ()
|
|
19
|
+
) -> Application:
|
|
20
|
+
environment = environment or os.environ.get(ENVIRONMENT_VARIABLE, "development")
|
|
21
|
+
return ApplicationBuilder(
|
|
22
|
+
{{ cookiecutter.class_prefix }}HostModule,
|
|
23
|
+
environment=environment,
|
|
24
|
+
base_path=Path.cwd(),
|
|
25
|
+
env_prefix="PDDD_",
|
|
26
|
+
cli_args=cli_args,
|
|
27
|
+
).build()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_web() -> FastAPI:
|
|
31
|
+
application = create_application()
|
|
32
|
+
# 全局 middleware 使用原生 Starlette 实现,在 adapter.install 前完成配置。
|
|
33
|
+
adapter = FastApiAdapter(
|
|
34
|
+
application, middleware=(Middleware(GZipMiddleware),),
|
|
35
|
+
api_prefix=application.configuration.get_path("http.api_prefix"),
|
|
36
|
+
)
|
|
37
|
+
web = FastAPI(
|
|
38
|
+
title="{{ cookiecutter.project_name }}", version=version("{{ cookiecutter.project_name }}"), lifespan=adapter.lifespan
|
|
39
|
+
)
|
|
40
|
+
adapter.install(web)
|
|
41
|
+
return web
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
from python_ddd_framework.fastapi.server import run_host
|
|
46
|
+
|
|
47
|
+
run_host("host.main:create_web", host="0.0.0.0", port=8000)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Host 只选择 provider 与组合 Module,不拥有业务规则。"""
|
|
2
|
+
|
|
3
|
+
from dishka.integrations.fastapi import FastapiProvider
|
|
4
|
+
from python_ddd_framework import AppModule, ConfigureContext
|
|
5
|
+
from python_ddd_framework.auditing.sqlalchemy import SqlAlchemyAuditingModule
|
|
6
|
+
from python_ddd_framework.background_jobs.pgqueuer import PgQueuerBackgroundJobsModule
|
|
7
|
+
from python_ddd_framework.fastapi.background import BackgroundManagementHttpApiModule
|
|
8
|
+
from python_ddd_framework.fastapi.settings import SettingsManagementHttpApiModule
|
|
9
|
+
from python_ddd_framework.identity.http_api import IdentityHttpApiModule
|
|
10
|
+
from python_ddd_framework.identity.sqlalchemy import SqlAlchemyIdentityModule
|
|
11
|
+
from python_ddd_framework.observability.logging import StructuredLoggingModule
|
|
12
|
+
from python_ddd_framework.observability.tracing import TracingModule
|
|
13
|
+
from python_ddd_framework.redis import RedisDistributedLockModule, RedisModule
|
|
14
|
+
from python_ddd_framework.redis.notifications import RedisNotificationsModule
|
|
15
|
+
from python_ddd_framework.settings.notifications import SettingsNotificationsModule
|
|
16
|
+
from python_ddd_framework.settings.sqlalchemy import SqlAlchemyRuntimeSettingsModule
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class {{ cookiecutter.class_prefix }}HostModule(AppModule):
|
|
20
|
+
dependencies = (
|
|
21
|
+
SqlAlchemyIdentityModule,
|
|
22
|
+
IdentityHttpApiModule,
|
|
23
|
+
SqlAlchemyAuditingModule,
|
|
24
|
+
SqlAlchemyRuntimeSettingsModule,
|
|
25
|
+
SettingsManagementHttpApiModule,
|
|
26
|
+
RedisModule,
|
|
27
|
+
RedisDistributedLockModule,
|
|
28
|
+
RedisNotificationsModule,
|
|
29
|
+
SettingsNotificationsModule,
|
|
30
|
+
PgQueuerBackgroundJobsModule,
|
|
31
|
+
BackgroundManagementHttpApiModule,
|
|
32
|
+
StructuredLoggingModule,
|
|
33
|
+
TracingModule,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
37
|
+
context.services.contribute(FastapiProvider(), reason="Native HTTP request context")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Host 只验证通用健康和身份流程;业务断言归各模块。"""
|
|
2
|
+
|
|
3
|
+
from host.main import create_application, create_web
|
|
4
|
+
from httpx import ASGITransport, AsyncClient
|
|
5
|
+
from testcontainers.community.postgres import PostgresContainer
|
|
6
|
+
from testcontainers.community.redis import RedisContainer
|
|
7
|
+
|
|
8
|
+
from python_ddd_framework import DataSeeder
|
|
9
|
+
from python_ddd_framework.cli import ENVIRONMENT_VARIABLE
|
|
10
|
+
from python_ddd_framework.identity import IdentityOptions
|
|
11
|
+
from python_ddd_framework.sqlalchemy import SqlAlchemyMigrator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def test_host_http_workflow(monkeypatch):
|
|
15
|
+
monkeypatch.setenv(ENVIRONMENT_VARIABLE, "development")
|
|
16
|
+
composed = create_application()
|
|
17
|
+
try:
|
|
18
|
+
configuration = composed.configuration
|
|
19
|
+
identity = composed.options.get(IdentityOptions)
|
|
20
|
+
postgres_image = configuration.get_path("development.compose.postgres_image")
|
|
21
|
+
redis_image = configuration.get_path("development.compose.redis_image")
|
|
22
|
+
api_prefix = configuration.get_path("http.api_prefix")
|
|
23
|
+
finally:
|
|
24
|
+
await composed.stop()
|
|
25
|
+
with PostgresContainer(postgres_image, driver="asyncpg") as postgres:
|
|
26
|
+
with RedisContainer(redis_image) as redis:
|
|
27
|
+
monkeypatch.setenv(
|
|
28
|
+
"PDDD_CONNECTION_STRINGS__DEFAULT", postgres.get_connection_url(driver="asyncpg")
|
|
29
|
+
)
|
|
30
|
+
monkeypatch.setenv(
|
|
31
|
+
"PDDD_REDIS__URL",
|
|
32
|
+
f"redis://{redis.get_container_host_ip()}:{redis.get_exposed_port(redis.port)}/0",
|
|
33
|
+
)
|
|
34
|
+
application = create_application()
|
|
35
|
+
try:
|
|
36
|
+
await SqlAlchemyMigrator(application).upgrade_heads()
|
|
37
|
+
await application.start()
|
|
38
|
+
await DataSeeder(application).seed_all()
|
|
39
|
+
finally:
|
|
40
|
+
await application.stop()
|
|
41
|
+
web = create_web()
|
|
42
|
+
async with web.router.lifespan_context(web):
|
|
43
|
+
async with AsyncClient(
|
|
44
|
+
transport=ASGITransport(web), base_url="http://test"
|
|
45
|
+
) as client:
|
|
46
|
+
assert (await client.get("/health/live")).status_code == 200
|
|
47
|
+
assert (await client.get("/health/ready")).status_code == 200
|
|
48
|
+
login = await client.post(
|
|
49
|
+
f"{api_prefix}/auth/login",
|
|
50
|
+
json={
|
|
51
|
+
"username": identity.seed_admin_username,
|
|
52
|
+
"password": identity.seed_admin_password.get_secret_value(),
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
assert login.status_code == 200
|
|
56
|
+
client.headers["Authorization"] = "Bearer " + login.json()["access_token"]
|
|
57
|
+
assert (await client.get(f"{api_prefix}/auth/me")).status_code == 200
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""LibCST 只修改已识别 startup class 的依赖;不重写用户文件的格式和注释。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
import libcst as cst
|
|
8
|
+
import libcst.matchers as matchers
|
|
9
|
+
from libcst.codemod import CodemodContext
|
|
10
|
+
from libcst.codemod.visitors import AddImportsVisitor
|
|
11
|
+
from libcst.helpers import get_full_name_for_node
|
|
12
|
+
from libcst.metadata import ClassScope, MetadataWrapper, ScopeProvider
|
|
13
|
+
|
|
14
|
+
from ..cli.errors import CommandError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _AddDependency(cst.CSTTransformer):
|
|
18
|
+
def __init__(self, startup: cst.ClassDef, dependency: str) -> None:
|
|
19
|
+
self._startup = startup
|
|
20
|
+
self._dependency = dependency
|
|
21
|
+
self.matches = 0
|
|
22
|
+
|
|
23
|
+
def leave_ClassDef(
|
|
24
|
+
self, original_node: cst.ClassDef, updated_node: cst.ClassDef
|
|
25
|
+
) -> cst.ClassDef:
|
|
26
|
+
if original_node is not self._startup:
|
|
27
|
+
return updated_node
|
|
28
|
+
body = cast(cst.IndentedBlock, updated_node.body)
|
|
29
|
+
statements = list(body.body)
|
|
30
|
+
for index, statement in enumerate(statements):
|
|
31
|
+
if not isinstance(statement, cst.SimpleStatementLine) or len(statement.body) != 1:
|
|
32
|
+
continue
|
|
33
|
+
assignment = statement.body[0]
|
|
34
|
+
if isinstance(assignment, cst.Assign) and len(assignment.targets) == 1:
|
|
35
|
+
target = assignment.targets[0].target
|
|
36
|
+
elif isinstance(assignment, cst.AnnAssign):
|
|
37
|
+
target = assignment.target
|
|
38
|
+
else:
|
|
39
|
+
continue
|
|
40
|
+
if not isinstance(target, cst.Name) or target.value != "dependencies":
|
|
41
|
+
continue
|
|
42
|
+
value = assignment.value
|
|
43
|
+
if not isinstance(value, cst.Tuple) or any(
|
|
44
|
+
isinstance(item, cst.StarredElement) for item in value.elements
|
|
45
|
+
):
|
|
46
|
+
raise CommandError(
|
|
47
|
+
"Startup dependencies must be an explicit tuple without unpacking"
|
|
48
|
+
)
|
|
49
|
+
self.matches += 1
|
|
50
|
+
replacement = _append_dependency(value, self._dependency)
|
|
51
|
+
statements[index] = statement.with_changes(
|
|
52
|
+
body=(assignment.with_changes(value=replacement),)
|
|
53
|
+
)
|
|
54
|
+
return updated_node.with_changes(body=body.with_changes(body=statements))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _append_dependency(value: cst.Tuple, dependency: str) -> cst.Tuple:
|
|
58
|
+
elements = list(value.elements)
|
|
59
|
+
right = value.rpar
|
|
60
|
+
if elements and value.lpar and right:
|
|
61
|
+
opening = value.lpar[0].whitespace_after
|
|
62
|
+
closing = right[0].whitespace_before
|
|
63
|
+
if isinstance(opening, cst.ParenthesizedWhitespace) and isinstance(
|
|
64
|
+
closing, cst.ParenthesizedWhitespace
|
|
65
|
+
):
|
|
66
|
+
# LibCST 将最后一项的行尾注释归右括号;把它移到原项之后,保留所属关系。
|
|
67
|
+
last = elements[-1]
|
|
68
|
+
comma = last.comma if isinstance(last.comma, cst.Comma) else cst.Comma()
|
|
69
|
+
elements[-1] = last.with_changes(
|
|
70
|
+
comma=comma.with_changes(
|
|
71
|
+
whitespace_after=closing.with_changes(last_line=opening.last_line)
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
right = (
|
|
75
|
+
right[0].with_changes(
|
|
76
|
+
whitespace_before=closing.with_changes(
|
|
77
|
+
first_line=cst.TrailingWhitespace(newline=closing.first_line.newline),
|
|
78
|
+
empty_lines=(),
|
|
79
|
+
)
|
|
80
|
+
),
|
|
81
|
+
*right[1:],
|
|
82
|
+
)
|
|
83
|
+
elements.append(cst.Element(cst.Name(dependency), comma=cst.Comma()))
|
|
84
|
+
return value.with_changes(elements=elements, rpar=right)
|
|
85
|
+
return value.with_changes(elements=(*elements, cst.Element(cst.Name(dependency))))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def add_dependency(source: str, startup: str, import_path: str, dependency: str) -> str:
|
|
89
|
+
wrapper = MetadataWrapper(cst.parse_module(source))
|
|
90
|
+
module = wrapper.module
|
|
91
|
+
# 星号导出的绑定无法由静态 scope 确定,不能冒险覆盖用户的既有依赖。
|
|
92
|
+
if matchers.findall(module, matchers.ImportStar()):
|
|
93
|
+
raise CommandError("Star imports prevent safe startup dependency wiring")
|
|
94
|
+
# 原生作用域绑定防止覆盖用户符号;仅修改实际顶层节点,不触及嵌套同名类。
|
|
95
|
+
candidates = tuple(
|
|
96
|
+
statement
|
|
97
|
+
for statement in module.body
|
|
98
|
+
if isinstance(statement, cst.ClassDef) and statement.name.value == startup
|
|
99
|
+
)
|
|
100
|
+
if len(candidates) != 1:
|
|
101
|
+
raise CommandError("Startup Module must be a unique top-level class")
|
|
102
|
+
candidate = candidates[0]
|
|
103
|
+
if not isinstance(candidate.body, cst.IndentedBlock):
|
|
104
|
+
raise CommandError("Startup Module must use an indented class body")
|
|
105
|
+
# 类体的名称查找包含本类和外层模块;两层绑定均不得被新增裸名称遮蔽。
|
|
106
|
+
scope = cast(ClassScope, wrapper.resolve(ScopeProvider)[candidate.body.body[0]])
|
|
107
|
+
if dependency in scope:
|
|
108
|
+
raise CommandError(f"Dependency name already bound in startup module: {dependency}")
|
|
109
|
+
transformer = _AddDependency(candidate, dependency)
|
|
110
|
+
updated = module.visit(transformer)
|
|
111
|
+
if transformer.matches != 1:
|
|
112
|
+
raise CommandError("Cannot uniquely identify the startup Module dependencies assignment")
|
|
113
|
+
context = CodemodContext()
|
|
114
|
+
AddImportsVisitor.add_needed_import(context, import_path, dependency)
|
|
115
|
+
imports = AddImportsVisitor(context)
|
|
116
|
+
imported = imports.transform_module(updated)
|
|
117
|
+
if any(
|
|
118
|
+
isinstance(node, cst.ImportFrom)
|
|
119
|
+
and not node.relative
|
|
120
|
+
and node.module is not None
|
|
121
|
+
and get_full_name_for_node(node.module) == import_path
|
|
122
|
+
for node in imports.all_imports
|
|
123
|
+
):
|
|
124
|
+
return imported.code
|
|
125
|
+
return _place_dependency_import(imported, import_path).code
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _place_dependency_import(module: cst.Module, import_path: str) -> cst.Module:
|
|
129
|
+
# LibCST 原生负责添加/合并 import;这里只定位同一业务 package 内的新接线。
|
|
130
|
+
# 不排序已有语句,也不重写其注释;首次接线与第三方 imports 之间留空行。
|
|
131
|
+
body = list(module.body)
|
|
132
|
+
package = import_path.partition(".")[0]
|
|
133
|
+
imports: list[tuple[int, str]] = []
|
|
134
|
+
for index, statement in enumerate(body):
|
|
135
|
+
if not isinstance(statement, cst.SimpleStatementLine) or len(statement.body) != 1:
|
|
136
|
+
break
|
|
137
|
+
node = statement.body[0]
|
|
138
|
+
# 原生 visitor 可在首行 docstring 或 __strict__ 声明之后插入 import。
|
|
139
|
+
if index == 0 and not isinstance(node, (cst.Import, cst.ImportFrom)):
|
|
140
|
+
continue
|
|
141
|
+
if isinstance(node, cst.ImportFrom) and not node.relative and node.module is not None:
|
|
142
|
+
name = get_full_name_for_node(node.module)
|
|
143
|
+
if name is not None and name.partition(".")[0] == package:
|
|
144
|
+
imports.append((index, name))
|
|
145
|
+
elif not isinstance(node, (cst.Import, cst.ImportFrom)):
|
|
146
|
+
break
|
|
147
|
+
target_index = next(index for index, name in imports if name == import_path)
|
|
148
|
+
target = cast(cst.SimpleStatementLine, body[target_index])
|
|
149
|
+
siblings = [(index, name) for index, name in imports if index != target_index]
|
|
150
|
+
before = next((index for index, name in siblings if name > import_path), None)
|
|
151
|
+
insertion = before if before is not None else siblings[-1][0] + 1 if siblings else target_index
|
|
152
|
+
if before is not None:
|
|
153
|
+
following = cast(cst.SimpleStatementLine, body[before])
|
|
154
|
+
lines = following.leading_lines
|
|
155
|
+
if lines and lines[0].comment is None:
|
|
156
|
+
target = target.with_changes(leading_lines=(lines[0], *target.leading_lines))
|
|
157
|
+
body[before] = following.with_changes(leading_lines=lines[1:])
|
|
158
|
+
elif not siblings and target_index and not target.leading_lines:
|
|
159
|
+
target = target.with_changes(leading_lines=(cst.EmptyLine(),))
|
|
160
|
+
body.pop(target_index)
|
|
161
|
+
body.insert(insertion - (target_index < insertion), target)
|
|
162
|
+
return module.with_changes(body=body)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Diagnostic contracts 与 Application-owned journal 的 capability 边界。"""
|
|
2
|
+
|
|
3
|
+
from .model import (
|
|
4
|
+
DiagnosticMessage,
|
|
5
|
+
DiagnosticSnapshot,
|
|
6
|
+
EventStatus,
|
|
7
|
+
FailureRecord,
|
|
8
|
+
LifecyclePhase,
|
|
9
|
+
LifecycleStage,
|
|
10
|
+
PhaseEvent,
|
|
11
|
+
SourceLocation,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = (
|
|
15
|
+
"DiagnosticMessage",
|
|
16
|
+
"DiagnosticSnapshot",
|
|
17
|
+
"EventStatus",
|
|
18
|
+
"FailureRecord",
|
|
19
|
+
"LifecyclePhase",
|
|
20
|
+
"LifecycleStage",
|
|
21
|
+
"PhaseEvent",
|
|
22
|
+
"SourceLocation",
|
|
23
|
+
)
|