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,24 @@
|
|
|
1
|
+
"""修改业务名称、权限和约束的单一位置。"""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from python_ddd_framework import PermissionName
|
|
6
|
+
from python_ddd_framework.errors import BusinessErrorDefinition
|
|
7
|
+
|
|
8
|
+
MODULE_NAME = "{{ cookiecutter.module_name }}"
|
|
9
|
+
ORDER_CHANGED_MESSAGE = f"{MODULE_NAME}.changed.v1"
|
|
10
|
+
ORDER_SNAPSHOT_MESSAGE = f"{MODULE_NAME}.snapshot.v1"
|
|
11
|
+
TITLE_MAX_LENGTH = 120
|
|
12
|
+
ORDERS_READ = PermissionName(f"{MODULE_NAME}.read")
|
|
13
|
+
ORDERS_WRITE = PermissionName(f"{MODULE_NAME}.write")
|
|
14
|
+
ALREADY_APPROVED = BusinessErrorDefinition(
|
|
15
|
+
f"{MODULE_NAME}.already_approved", "Order is already approved"
|
|
16
|
+
)
|
|
17
|
+
APPROVAL_DISABLED = BusinessErrorDefinition(
|
|
18
|
+
f"{MODULE_NAME}.approval_disabled", "Order approval is disabled"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OrderStatus(str, Enum):
|
|
23
|
+
PENDING = "pending"
|
|
24
|
+
APPROVED = "approved"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""权限名称由共享定义拥有;Provider 仅发布定义。"""
|
|
2
|
+
|
|
3
|
+
from python_ddd_framework import (
|
|
4
|
+
PermissionDefinition,
|
|
5
|
+
PermissionDefinitionContext,
|
|
6
|
+
PermissionDefinitionProvider,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from .definitions import ORDERS_READ, ORDERS_WRITE
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class {{ cookiecutter.class_prefix }}PermissionDefinitionProvider(PermissionDefinitionProvider):
|
|
13
|
+
def define(self, context: PermissionDefinitionContext) -> None:
|
|
14
|
+
context.add(PermissionDefinition(ORDERS_READ, "Read orders"))
|
|
15
|
+
context.add(PermissionDefinition(ORDERS_WRITE, "Create and approve orders"))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HTTP adapter 只依赖服务契约,不反向导入 application 或 sqlalchemy。"""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""原生上传与流响应;业务查询在返回响应前完成,不跨网络发送持有事务。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from fastapi import File, Form, UploadFile
|
|
11
|
+
from python_ddd_framework.authorization import authorize
|
|
12
|
+
from python_ddd_framework.fastapi import ActionFilterContext, HttpRouter, service_filter
|
|
13
|
+
from starlette.responses import Response, StreamingResponse
|
|
14
|
+
|
|
15
|
+
from ..application_contracts.orders import CreateOrder, {{ cookiecutter.class_prefix }}ApplicationService
|
|
16
|
+
from ..domain_shared.definitions import MODULE_NAME, ORDERS_READ, ORDERS_WRITE
|
|
17
|
+
|
|
18
|
+
router = HttpRouter(use_api_prefix=True, prefix=f"/{MODULE_NAME}/files", permissions=(ORDERS_READ,))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExportFilter:
|
|
22
|
+
async def invoke(
|
|
23
|
+
self, context: ActionFilterContext, next_action: Callable[[], Awaitable[object]]
|
|
24
|
+
) -> object:
|
|
25
|
+
# Filter 的作用域只覆盖已绑定参数到响应准备;不假定流传输已经结束。
|
|
26
|
+
result = await next_action()
|
|
27
|
+
logging.getLogger(__name__).info("File response prepared")
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.post("/upload", status_code=201)
|
|
32
|
+
@authorize(ORDERS_WRITE)
|
|
33
|
+
async def upload(
|
|
34
|
+
service: {{ cookiecutter.class_prefix }}ApplicationService,
|
|
35
|
+
title: Annotated[str, Form()],
|
|
36
|
+
file: Annotated[UploadFile, File()],
|
|
37
|
+
) -> dict[str, object]:
|
|
38
|
+
# File/Form 使用框架读取期限额及原生 parser;UploadFile 资源由 REQUEST 关闭。
|
|
39
|
+
content = await file.read()
|
|
40
|
+
order = await service.create(CreateOrder(title=title))
|
|
41
|
+
return {
|
|
42
|
+
"order": order.model_dump(mode="json"),
|
|
43
|
+
"bytes": len(content),
|
|
44
|
+
"sha256": hashlib.sha256(content).hexdigest(),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.get("/{id}/download")
|
|
49
|
+
@service_filter(ExportFilter)
|
|
50
|
+
async def download(id: UUID, service: {{ cookiecutter.class_prefix }}ApplicationService) -> Response:
|
|
51
|
+
order = await service.get(id)
|
|
52
|
+
return Response(
|
|
53
|
+
order.model_dump_json(),
|
|
54
|
+
media_type="application/json",
|
|
55
|
+
headers={"Content-Disposition": f'attachment; filename="{id}.json"'},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.get("/export/stream")
|
|
60
|
+
@service_filter(ExportFilter)
|
|
61
|
+
async def export(service: {{ cookiecutter.class_prefix }}ApplicationService) -> StreamingResponse:
|
|
62
|
+
orders = await service.get_list()
|
|
63
|
+
|
|
64
|
+
async def produce() -> AsyncIterator[bytes]:
|
|
65
|
+
try:
|
|
66
|
+
for order in orders:
|
|
67
|
+
yield (order.model_dump_json() + "\n").encode()
|
|
68
|
+
await asyncio.sleep(0)
|
|
69
|
+
finally:
|
|
70
|
+
# 此示例只持有不可变 DTO,无悬挂 Session/文件;扩展外部资源时在此 shield 异步关闭。
|
|
71
|
+
logging.getLogger(__name__).info("Order export producer closed")
|
|
72
|
+
|
|
73
|
+
return StreamingResponse(produce(), media_type="application/x-ndjson")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""自动服务暴露与手写路由共用正式认证/REQUEST 管线。"""
|
|
2
|
+
|
|
3
|
+
from dishka import Scope
|
|
4
|
+
from python_ddd_framework import AppModule, ConfigureContext
|
|
5
|
+
from python_ddd_framework.fastapi import (
|
|
6
|
+
FastApiRealtimeModule,
|
|
7
|
+
contributes_routers,
|
|
8
|
+
exposes_application_services,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from ..application_contracts.module import {{ cookiecutter.class_prefix }}ApplicationContractsModule
|
|
12
|
+
from .files import ExportFilter
|
|
13
|
+
from .files import router as files_router
|
|
14
|
+
from .realtime import router as realtime_router
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contributes_routers(files_router, realtime_router)
|
|
18
|
+
@exposes_application_services(
|
|
19
|
+
{{ cookiecutter.class_prefix }}ApplicationContractsModule,
|
|
20
|
+
)
|
|
21
|
+
class {{ cookiecutter.class_prefix }}HttpApiModule(AppModule):
|
|
22
|
+
dependencies = ({{ cookiecutter.class_prefix }}ApplicationContractsModule, FastApiRealtimeModule)
|
|
23
|
+
|
|
24
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
25
|
+
context.services.add_scoped(ExportFilter, scope=Scope.REQUEST)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""单进程在线通知;连接认证由框架管理,业务授权仍经过服务契约。"""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends
|
|
6
|
+
from pydantic import BaseModel, ConfigDict
|
|
7
|
+
from python_ddd_framework import FrameworkError
|
|
8
|
+
from python_ddd_framework.fastapi import WebSocketConnection, websocket_connection
|
|
9
|
+
from starlette.websockets import WebSocketDisconnect
|
|
10
|
+
|
|
11
|
+
from ..application_contracts.orders import {{ cookiecutter.class_prefix }}ApplicationService
|
|
12
|
+
from ..domain_shared.definitions import MODULE_NAME, ORDER_SNAPSHOT_MESSAGE
|
|
13
|
+
|
|
14
|
+
router = APIRouter()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RefreshOrders(BaseModel):
|
|
18
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.websocket(f"/ws/{MODULE_NAME}")
|
|
22
|
+
async def orders_socket(
|
|
23
|
+
connection: Annotated[WebSocketConnection, Depends(websocket_connection)],
|
|
24
|
+
) -> None:
|
|
25
|
+
try:
|
|
26
|
+
while True:
|
|
27
|
+
orders = await connection.invoke({{ cookiecutter.class_prefix }}ApplicationService.get_list)
|
|
28
|
+
await connection.send(
|
|
29
|
+
ORDER_SNAPSHOT_MESSAGE,
|
|
30
|
+
{"orders": [item.model_dump(mode="json") for item in orders]},
|
|
31
|
+
)
|
|
32
|
+
await connection.receive(RefreshOrders)
|
|
33
|
+
except WebSocketDisconnect:
|
|
34
|
+
return
|
|
35
|
+
except FrameworkError as error:
|
|
36
|
+
await connection.send_problem(error)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""数据库实现由 consuming Host 选择。"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""初始无业务 revision;执行 pddd db revision --module {{ cookiecutter.module_name }} 后人工审查。"""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""修改模型后显式生成并审查迁移,不在 Host 启动时建表。"""
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from python_ddd_framework.sqlalchemy import AuditedEntityMixin
|
|
6
|
+
from sqlalchemy import Integer, String, Uuid
|
|
7
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
8
|
+
|
|
9
|
+
from ...domain_shared.definitions import MODULE_NAME, TITLE_MAX_LENGTH, OrderStatus
|
|
10
|
+
from . import {{ cookiecutter.class_prefix }}Base
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OrderRow(AuditedEntityMixin, {{ cookiecutter.class_prefix }}Base):
|
|
14
|
+
__tablename__ = MODULE_NAME
|
|
15
|
+
id: Mapped[UUID] = mapped_column(Uuid, primary_key=True)
|
|
16
|
+
title: Mapped[str] = mapped_column(String(TITLE_MAX_LENGTH), nullable=False)
|
|
17
|
+
status: Mapped[str] = mapped_column(
|
|
18
|
+
String(max(len(item.value) for item in OrderStatus)), nullable=False
|
|
19
|
+
)
|
|
20
|
+
version: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
21
|
+
# SQLAlchemy 以行版本检查并发;聚合暂存版本与数据库提交版本保持一致。
|
|
22
|
+
__mapper_args__ = { # noqa: RUF012 -- SQLAlchemy 原生声明属性,基类不允许 ClassVar。
|
|
23
|
+
"version_id_col": version,
|
|
24
|
+
"version_id_generator": False,
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""provider 依赖领域契约;Host 显式选择此实现。"""
|
|
2
|
+
|
|
3
|
+
from python_ddd_framework import AppModule
|
|
4
|
+
from python_ddd_framework.sqlalchemy import (
|
|
5
|
+
SqlAlchemyModelRegistration,
|
|
6
|
+
SqlAlchemyPersistenceModule,
|
|
7
|
+
contributes_sqlalchemy_models,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from ..domain.module import {{ cookiecutter.class_prefix }}DomainModule
|
|
11
|
+
from ..domain_shared.definitions import MODULE_NAME
|
|
12
|
+
from .models import {{ cookiecutter.class_prefix }}Base
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@contributes_sqlalchemy_models(
|
|
16
|
+
SqlAlchemyModelRegistration.from_package(
|
|
17
|
+
models_package=__package__ + ".models",
|
|
18
|
+
metadata={{ cookiecutter.class_prefix }}Base.metadata,
|
|
19
|
+
migrations_package=__package__ + ".migrations",
|
|
20
|
+
branch_label=MODULE_NAME,
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
class {{ cookiecutter.class_prefix }}SqlAlchemyModule(AppModule):
|
|
24
|
+
dependencies = ({{ cookiecutter.class_prefix }}DomainModule, SqlAlchemyPersistenceModule)
|
|
25
|
+
|
|
26
|
+
scan_packages = (__package__ + ".repositories",)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""本模块的仓储实现发现边界。"""
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""仓储参与当前 UoW,成功暂存后由 operation 收集聚合事件。"""
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from python_ddd_framework import OptimisticConcurrencyError
|
|
6
|
+
from python_ddd_framework.sqlalchemy import SqlAlchemySessionProvider
|
|
7
|
+
from sqlalchemy import select
|
|
8
|
+
|
|
9
|
+
from ...domain.orders import Order, OrderTitle
|
|
10
|
+
from ...domain.repository import OrderRepository
|
|
11
|
+
from ...domain_shared.definitions import OrderStatus
|
|
12
|
+
from ..models.orders import OrderRow
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SqlAlchemyOrderRepository(OrderRepository):
|
|
16
|
+
def __init__(self, provider: SqlAlchemySessionProvider) -> None:
|
|
17
|
+
self._provider = provider
|
|
18
|
+
|
|
19
|
+
async def get(self, id: UUID) -> Order | None:
|
|
20
|
+
async with self._provider.operation("get"):
|
|
21
|
+
row = await (await self._provider.get_session()).get(OrderRow, id)
|
|
22
|
+
return None if row is None else _order(row)
|
|
23
|
+
|
|
24
|
+
async def get_list(self) -> tuple[Order, ...]:
|
|
25
|
+
async with self._provider.operation("get_list"):
|
|
26
|
+
rows = await (await self._provider.get_session()).scalars(
|
|
27
|
+
select(OrderRow).order_by(OrderRow.created_at, OrderRow.id)
|
|
28
|
+
)
|
|
29
|
+
return tuple(_order(row) for row in rows)
|
|
30
|
+
|
|
31
|
+
async def save(self, order: Order) -> None:
|
|
32
|
+
async with self._provider.operation("save", aggregate=order):
|
|
33
|
+
session = await self._provider.get_session()
|
|
34
|
+
row = await session.get(OrderRow, order.id)
|
|
35
|
+
if row is None:
|
|
36
|
+
if order.version:
|
|
37
|
+
raise OptimisticConcurrencyError
|
|
38
|
+
row = OrderRow(id=order.id)
|
|
39
|
+
session.add(row)
|
|
40
|
+
elif row.version != order.version:
|
|
41
|
+
raise OptimisticConcurrencyError
|
|
42
|
+
order.stage_next_version(order.version)
|
|
43
|
+
row.title, row.status, row.version = order.title, order.status.value, order.version
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _order(row: OrderRow) -> Order:
|
|
47
|
+
return Order(
|
|
48
|
+
row.id, OrderTitle(value=row.title), status=OrderStatus(row.status), version=row.version
|
|
49
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""模块内领域验收,不随生产 wheel 打包或参与服务扫描。"""
|
|
2
|
+
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from python_ddd_framework.errors import BusinessError
|
|
7
|
+
|
|
8
|
+
from modules.{{ cookiecutter.module_name }}.domain.orders import Order, OrderTitle
|
|
9
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.definitions import OrderStatus
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_order_approval_rejects_repeat():
|
|
13
|
+
order = Order.create(uuid4(), OrderTitle(value="Example order"))
|
|
14
|
+
order.approve()
|
|
15
|
+
assert order.status is OrderStatus.APPROVED
|
|
16
|
+
assert len(order.release_local_events()) == 2
|
|
17
|
+
with pytest.raises(BusinessError):
|
|
18
|
+
order.approve()
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project_name": "application", "package_name": "application", "class_prefix": "Application",
|
|
3
|
+
"framework_version": "", "framework_source": "", "python_version": "", "build_version": "",
|
|
4
|
+
"database_password": "", "admin_password": "", "jwt_key": "",
|
|
5
|
+
"host_environment_variable": ""
|
|
6
|
+
}
|
python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{ cookiecutter.python_version }}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Project development guidance
|
|
2
|
+
|
|
3
|
+
These instructions apply to this application and its business modules. Follow the user's task scope and any more specific instructions in the affected directory.
|
|
4
|
+
|
|
5
|
+
## Start with the affected capability
|
|
6
|
+
|
|
7
|
+
1. Check the branch, worktree, requested outcome, and existing changes before editing. Analysis and review requests are read-only unless implementation is requested.
|
|
8
|
+
2. Use [README](README.md) for setup and commands, [architecture](docs/architecture.md) for ownership and dependencies, and the relevant section of [development](docs/development.md) for framework usage.
|
|
9
|
+
3. Before changing a business module, read its `src/modules/<name>/README.md`, then inspect the affected source and direct callers. Expand the investigation only when those facts reveal another affected boundary.
|
|
10
|
+
4. Read versions and dependencies from `pyproject.toml`, exact resolutions from `uv.lock`, and commands from `uv run pddd --help`. Verify behavior against the installed framework version when the application documentation is insufficient.
|
|
11
|
+
|
|
12
|
+
## Preserve ownership and contracts
|
|
13
|
+
|
|
14
|
+
- The Host composes modules, selects providers, and supplies configuration. Keep business state and rules in their owning modules.
|
|
15
|
+
- Domain and application contracts stay independent of Host, HTTP, and persistence implementations. Cross-module calls use explicitly declared dependencies and public contracts; do not access another module's implementation, ORM models, or tables directly.
|
|
16
|
+
- Use framework application services, repositories, Options, permissions, units of work, events, jobs, and lifecycle hooks before adding custom infrastructure. Do not introduce a parallel DI container, service locator, transaction manager, or wrapper without a missing business capability.
|
|
17
|
+
- Keep public contracts, mutable state, orchestration, and external I/O under clear owners. Split for independent responsibilities, not a fixed line count. Avoid generic `common`, `utils`, or `helpers` collections and speculative abstractions.
|
|
18
|
+
- Resolve conflicting requirements, documented contracts, and implementation before changing the affected boundary. Describe the conflict to the user; do not rewrite documentation to justify an unapproved design.
|
|
19
|
+
- Confirm changes to dependencies, public contracts, persistent data, external protocols, or deployment infrastructure when they extend beyond the requested task. Internal simplification does not authorize breaking consumers or stored data.
|
|
20
|
+
|
|
21
|
+
## Use the framework safely
|
|
22
|
+
|
|
23
|
+
- Invoke application services through their injected public contracts or the application's public invocation methods. Manual construction and self-calls do not create a new managed invocation.
|
|
24
|
+
- Keep transactions short. Never carry a Session, unit of work, or request scope into an ordinary child task, external thread, network wait, or streamed response.
|
|
25
|
+
- Keep aggregate rules and optimistic concurrency checks intact. Use repository operations to stage changes and collect events; do not publish the same aggregate events twice.
|
|
26
|
+
- Review generated migrations. Do not edit applied revisions, bypass migration guards, or treat Host startup as migration or seeding.
|
|
27
|
+
- Keep background side effects idempotent. Local events and WebSocket messages do not provide durable distributed delivery; distributed locks do not replace database concurrency or idempotency.
|
|
28
|
+
- Validate external inputs, propagate meaningful failures, and preserve cancellation and cleanup. Add defaults, retries, or fallbacks only for an identified requirement.
|
|
29
|
+
- Use typed public APIs and native framework extension points. Keep dynamic data narrowing at external boundaries; do not use `Any`, unchecked casts, or broad exception handlers to hide contract mistakes.
|
|
30
|
+
|
|
31
|
+
## Verify and maintain
|
|
32
|
+
|
|
33
|
+
- Preserve unrelated changes. Do not edit the installed framework, generated dependency files by hand, or another repository to make an application change pass.
|
|
34
|
+
- Use the smallest relevant existing checks, and add focused regression coverage when the requested behavior needs it. Read fixtures before running tests that start infrastructure. See [verification](docs/development.md#verification) for available commands.
|
|
35
|
+
- Distinguish source checks, unit tests, real infrastructure tests, and distribution validation. Report actual results and anything not verified; never claim success from a mock or an unexecuted command.
|
|
36
|
+
- Update the owning module README when business rules or public behavior change. Update architecture for dependency or ownership changes, development for usage changes, and README for setup or operations changes. Replace outdated statements rather than appending a work log.
|
|
37
|
+
- Documentation guides development; it is not an executable architecture gate. Use type checks and appropriate boundary tests when the project has them.
|
|
38
|
+
- Commit or push only when requested. Review the final diff, choose owned paths explicitly, and exclude secrets, caches, temporary artifacts, and unrelated changes.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
FROM debian:bookworm-slim AS build
|
|
3
|
+
COPY --from=ghcr.io/astral-sh/uv:{{ cookiecutter.build_version }} /uv /usr/local/bin/uv
|
|
4
|
+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates build-essential
|
|
5
|
+
ENV UV_PYTHON_INSTALL_DIR=/opt/python UV_LINK_MODE=copy UV_COMPILE_BYTECODE=1
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
COPY . .
|
|
8
|
+
# Python 版本只从项目 .python-version 读取;PyPI 或 vendor wheel 均由 uv.lock 固定。
|
|
9
|
+
RUN uv python install && uv sync --locked --no-default-groups --no-editable
|
|
10
|
+
|
|
11
|
+
FROM debian:bookworm-slim AS runtime
|
|
12
|
+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libstdc++6 && useradd --create-home app
|
|
13
|
+
COPY --from=build /opt/python /opt/python
|
|
14
|
+
COPY --from=build /app/.venv /app/.venv
|
|
15
|
+
COPY --from=build /app/pyproject.toml /app/
|
|
16
|
+
WORKDIR /app
|
|
17
|
+
ENV PATH="/app/.venv/bin:$PATH" {{ cookiecutter.host_environment_variable }}=production PYTHONUNBUFFERED=1
|
|
18
|
+
RUN mkdir /app/.python-ddd-framework && chown app:app /app/.python-ddd-framework
|
|
19
|
+
USER app
|
|
20
|
+
EXPOSE 8000
|
|
21
|
+
# 独立 access handler 会绕过安全 formatter;请求诊断由框架审计和追踪负责。
|
|
22
|
+
CMD ["python", "-m", "host.main"]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# {{ cookiecutter.project_name }}
|
|
2
|
+
|
|
3
|
+
A modular Python application generated with Python DDD Framework {{ cookiecutter.framework_version }}. The initial project contains a Host; add business modules with `pddd add module` and adapt their examples to your requirements.
|
|
4
|
+
|
|
5
|
+
| Document | Purpose |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| [AGENTS.md](AGENTS.md) | Development instructions and reading order for Codex and other contributors. |
|
|
8
|
+
| [Architecture](docs/architecture.md) | Ownership, dependency direction, transaction boundaries, and lifecycle guarantees. |
|
|
9
|
+
| [Development guide](docs/development.md) | Recipes for services, permissions, persistence, configuration, events, and background work. |
|
|
10
|
+
|
|
11
|
+
Each generated business module also has its own README under `src/modules/<name>/`. This README owns application setup and operations.
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
Use the Python version declared by this project's metadata, uv, and a running Docker installation with Compose. `pddd new` has already synchronized dependencies. On an existing checkout, run `uv sync --locked`.
|
|
16
|
+
|
|
17
|
+
Run project commands from this directory with `uv run pddd` so they use the project's framework version. Use `uv run pddd` or a subcommand's `--help` to see available commands and options.
|
|
18
|
+
|
|
19
|
+
## Prepare local development
|
|
20
|
+
|
|
21
|
+
Review `app.development.yaml`, then start the local infrastructure:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
uv run pddd dev-init
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This starts or reuses PostgreSQL and Redis from the Host's development configuration and preserves data. It does not start the application, migrate, seed, or create an `.env` file. The bundled Compose setup requires local URLs with explicit ports; its Redis service does not configure authentication or TLS.
|
|
28
|
+
|
|
29
|
+
To add the example business module:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
uv run pddd add module orders
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Read `src/modules/orders/README.md` before changing the sample. The project is also usable as a pure Host without this step.
|
|
36
|
+
|
|
37
|
+
## Initialize the database
|
|
38
|
+
|
|
39
|
+
Apply the migrations for the providers selected by the generated Host:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
uv run pddd db upgrade --module identity
|
|
43
|
+
uv run pddd db upgrade --module settings
|
|
44
|
+
uv run pddd db upgrade --module auditing
|
|
45
|
+
uv run pddd db upgrade --module background_jobs
|
|
46
|
+
uv run pddd db seed --module identity
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If you added `orders`, generate its first revision:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
uv run pddd db revision --module orders
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Review the generated file in `src/modules/orders/sqlalchemy/migrations/`, then apply and seed it:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
uv run pddd db upgrade --module orders
|
|
59
|
+
uv run pddd db status --module orders
|
|
60
|
+
uv run pddd db seed --module orders
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
All database operations select an explicit module. Upgrade prerequisite owners explicitly; commands do not migrate unrelated modules. Host startup does not migrate or seed. Database commands do not start workers or hosted services. See [persistence and migrations](docs/development.md#persistence-and-migrations) before changing models or existing data.
|
|
64
|
+
|
|
65
|
+
## Run and inspect
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
uv run pddd dev
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Open **http://127.0.0.1:8000/docs**. Call `/api/auth/login` using the generated `identity.seed_admin_username` and `identity.seed_admin_password` from `app.development.yaml`, then enter the access token in **Authorize**. Local credentials must not be reused for production.
|
|
72
|
+
|
|
73
|
+
With the `orders` example installed, `/api/orders` supports create/query, `/api/orders/{id}/approve` approves an order, and `/api/orders/{id}/queue-approval` enqueues approval. Read the module README for its rules and OpenAPI for request schemas. A stale version produces a conflict response.
|
|
74
|
+
|
|
75
|
+
`http.api_prefix` controls business HTTP addresses and defaults to `/api`; `/ws`, `/health/live`, `/health/ready`, and `/docs` retain independent paths. Readiness reports Application lifecycle state, not continuous infrastructure health.
|
|
76
|
+
|
|
77
|
+
To inspect module composition, service registrations, and redacted configuration inputs:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
uv run pddd inspect --environment development
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Inspection composes and closes a new Application without starting it. It does not query an existing process, and its input snapshot is not the final Options view. Configuration, workers, jobs, real-time communication, and logging are covered in the [development guide](docs/development.md).
|
|
84
|
+
|
|
85
|
+
## Test and package
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
uv run pytest
|
|
89
|
+
uv build --no-sources
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The application tests include real Host infrastructure and require Docker. Module domain tests can be run separately; see [verification](docs/development.md#verification). Module tests are excluded from production wheels and service discovery.
|
|
93
|
+
|
|
94
|
+
## Deploy
|
|
95
|
+
|
|
96
|
+
The deployment supplies `app.production.yaml`, following the development file's key structure with independent database, Redis, JWT, and administrator credentials. Set the production logging environment and format. Keep the file out of Git and the image; production Compose mounts it read-only at `/app/app.production.yaml` for both web and migration services.
|
|
97
|
+
|
|
98
|
+
Build an image, validate it, and set `APP_IMAGE` to its immutable digest. In PowerShell, run the following for each required module, replacing the module alias and digest:
|
|
99
|
+
|
|
100
|
+
```powershell
|
|
101
|
+
docker build -t {{ cookiecutter.project_name }}:candidate .
|
|
102
|
+
$env:APP_IMAGE = "<verified-image>@sha256:<digest>"
|
|
103
|
+
$env:MODULE = "orders"
|
|
104
|
+
docker compose -f compose.production.yaml run --rm migrate
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Complete all required module migrations in dependency order and run initial seeds explicitly with `pddd db seed --module <alias> --environment production` in the same application environment. Then start the web service:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
docker compose -f compose.production.yaml up -d web
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The image starts `python -m host.main`, using the public framework server entry point without reload. Web, migration, and background processes use the same application image. Shutdown waits for actual cleanup. `APP_IMAGE`, `MODULE`, and `WEB_PORT` are deployment controls; they do not replace the application's configuration sources.
|
|
114
|
+
|
|
115
|
+
The base framework dependency includes runtime capabilities. Production does not require `developer-kit`; generation tools, tests, and development tools are excluded from the final image. Configure TLS, secrets, external health monitoring, capacity, and database recovery for the deployment's actual requirements.
|
|
116
|
+
|
|
117
|
+
## Upgrade the framework
|
|
118
|
+
|
|
119
|
+
Read the target framework release's migration instructions. Pin `python-ddd-framework` in runtime dependencies and `python-ddd-framework[developer-kit]` in the development group to the same target version. Preserve the application's own dependencies and source overrides.
|
|
120
|
+
|
|
121
|
+
For a first move from a local wheel or old Git source to PyPI, remove only the framework entry in `[tool.uv.sources]`; retain other dependencies and vendor files. Then run:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
uv lock --upgrade-package python-ddd-framework
|
|
125
|
+
uv sync --locked
|
|
126
|
+
uv run pytest
|
|
127
|
+
uv build --no-sources
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A framework upgrade does not regenerate or overwrite application source, migrations, or these documents. A global CLI upgrade also does not update this project. Apply required adaptations and update the relevant application and module documentation deliberately.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# pddd new 生成的随机凭据仅供本机开发;共享环境和生产必须使用独立凭据。
|
|
2
|
+
development:
|
|
3
|
+
compose:
|
|
4
|
+
postgres_image: postgres:18-bookworm
|
|
5
|
+
redis_image: redis:7-bookworm
|
|
6
|
+
connection_strings:
|
|
7
|
+
default: "postgresql+asyncpg://{{ cookiecutter.package_name }}:{{ cookiecutter.database_password }}@127.0.0.1:5432/{{ cookiecutter.package_name }}"
|
|
8
|
+
identity:
|
|
9
|
+
issuer: "{{ cookiecutter.project_name }}"
|
|
10
|
+
audience: "{{ cookiecutter.project_name }}"
|
|
11
|
+
active_kid: development
|
|
12
|
+
keys:
|
|
13
|
+
development: "{{ cookiecutter.jwt_key }}"
|
|
14
|
+
seed_admin_username: admin
|
|
15
|
+
seed_admin_password: "{{ cookiecutter.admin_password }}"
|
|
16
|
+
redis:
|
|
17
|
+
url: redis://127.0.0.1:6379/0
|
|
18
|
+
key_prefix: "{{ cookiecutter.project_name }}"
|
|
19
|
+
logging:
|
|
20
|
+
application: "{{ cookiecutter.project_name }}"
|
|
21
|
+
environment: development
|
|
22
|
+
format: text
|
|
23
|
+
level: INFO
|
|
24
|
+
tracing:
|
|
25
|
+
service_name: "{{ cookiecutter.project_name }}"
|
|
26
|
+
export_enabled: false
|
|
27
|
+
pgqueuer_background_jobs:
|
|
28
|
+
connection_name: default
|
|
29
|
+
http:
|
|
30
|
+
api_prefix: /api
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: ${POSTGRES_IMAGE:?Run pddd dev-init to load development configuration}
|
|
4
|
+
environment:
|
|
5
|
+
POSTGRES_DB: ${POSTGRES_DB}
|
|
6
|
+
POSTGRES_USER: ${POSTGRES_USER}
|
|
7
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
8
|
+
ports: ["127.0.0.1:${POSTGRES_PORT}:5432"]
|
|
9
|
+
healthcheck:
|
|
10
|
+
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
|
11
|
+
interval: 2s
|
|
12
|
+
timeout: 2s
|
|
13
|
+
retries: 20
|
|
14
|
+
volumes: ["postgres-data:/var/lib/postgresql"]
|
|
15
|
+
redis:
|
|
16
|
+
image: ${REDIS_IMAGE:?Run pddd dev-init to load development configuration}
|
|
17
|
+
ports: ["127.0.0.1:${REDIS_PORT}:6379"]
|
|
18
|
+
healthcheck:
|
|
19
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
20
|
+
interval: 2s
|
|
21
|
+
timeout: 2s
|
|
22
|
+
retries: 20
|
|
23
|
+
volumes:
|
|
24
|
+
postgres-data:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# APP_IMAGE 使用验收通过的同一 digest;显式选择模块迁移,成功后启动 Host。
|
|
2
|
+
x-configuration: &configuration
|
|
3
|
+
type: bind
|
|
4
|
+
source: ./app.production.yaml
|
|
5
|
+
target: /app/app.production.yaml
|
|
6
|
+
read_only: true
|
|
7
|
+
bind:
|
|
8
|
+
create_host_path: false
|
|
9
|
+
services:
|
|
10
|
+
migrate:
|
|
11
|
+
image: ${APP_IMAGE:?Provide the verified application image digest}
|
|
12
|
+
volumes: [*configuration]
|
|
13
|
+
command: ["pddd", "db", "--environment", "production", "upgrade", "--module", "${MODULE:?Select the migration module alias}"]
|
|
14
|
+
profiles: ["operations"]
|
|
15
|
+
web:
|
|
16
|
+
image: ${APP_IMAGE:?Provide the verified application image digest}
|
|
17
|
+
volumes: [*configuration]
|
|
18
|
+
ports: ["${WEB_PORT:-8000}:8000"]
|
|
19
|
+
stop_grace_period: 45s
|
|
20
|
+
restart: unless-stopped
|