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,219 @@
|
|
|
1
|
+
"""Application-owned 任务编排;目录提供任务事实,执行 owner 各自管理本地或子进程资源。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
|
|
9
|
+
from dishka import AsyncContainer
|
|
10
|
+
|
|
11
|
+
from ..application.build_spec import _ApplicationBuildSpec
|
|
12
|
+
from ..application_services.invocation import _InvocationRuntime
|
|
13
|
+
from ..background_execution import (
|
|
14
|
+
BackgroundExecutionMode,
|
|
15
|
+
BackgroundJobOptions,
|
|
16
|
+
BackgroundTaskOptions,
|
|
17
|
+
BackgroundTaskSnapshot,
|
|
18
|
+
)
|
|
19
|
+
from ..background_execution.lifecycle import _BackgroundRuntimeFailure, _TaskExecution
|
|
20
|
+
from ..background_execution.local import _LocalTask
|
|
21
|
+
from ..background_execution.locks import _check_previous_owners
|
|
22
|
+
from ..background_execution.processes import _ProcessSlot
|
|
23
|
+
from ..background_jobs import BackgroundJobCatalog
|
|
24
|
+
from ..invocation import BackgroundSystemCaller
|
|
25
|
+
from .catalog import BackgroundWorkerCatalog
|
|
26
|
+
from .contracts import BackgroundExecutionOptions, BackgroundExecutionProfile
|
|
27
|
+
from .execution import _WorkerExecution
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _BackgroundCoordinator:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
worker_catalog: BackgroundWorkerCatalog,
|
|
35
|
+
job_catalog: BackgroundJobCatalog,
|
|
36
|
+
options: BackgroundExecutionOptions,
|
|
37
|
+
invocation_runtime: _InvocationRuntime,
|
|
38
|
+
build_spec: _ApplicationBuildSpec,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._worker_catalog = worker_catalog
|
|
41
|
+
self._job_catalog = job_catalog
|
|
42
|
+
self._options = options
|
|
43
|
+
self._invocation_runtime = invocation_runtime
|
|
44
|
+
self._spec = build_spec
|
|
45
|
+
self._caller = BackgroundSystemCaller()
|
|
46
|
+
self._container: AsyncContainer | None = None
|
|
47
|
+
self._started = False
|
|
48
|
+
self._prepared = False
|
|
49
|
+
self._child = False
|
|
50
|
+
self._database_only = False
|
|
51
|
+
self._failures: asyncio.Queue[_BackgroundRuntimeFailure] = asyncio.Queue()
|
|
52
|
+
self._tasks: dict[str, _LocalTask] = {}
|
|
53
|
+
self._slots: dict[str, _ProcessSlot] = {}
|
|
54
|
+
self._task_slots: dict[str, _ProcessSlot] = {}
|
|
55
|
+
self._register_tasks(
|
|
56
|
+
"worker",
|
|
57
|
+
{item.name: item.enabled for item in worker_catalog.definitions},
|
|
58
|
+
options.workers,
|
|
59
|
+
)
|
|
60
|
+
self._register_tasks(
|
|
61
|
+
"job", {item.name: True for item in job_catalog.definitions}, options.jobs
|
|
62
|
+
)
|
|
63
|
+
self._local = {
|
|
64
|
+
key
|
|
65
|
+
for key, task in self._tasks.items()
|
|
66
|
+
if task.options.mode is BackgroundExecutionMode.HOST
|
|
67
|
+
}
|
|
68
|
+
self._initial = {key for key in self._local if self._tasks[key].options.enabled}
|
|
69
|
+
|
|
70
|
+
def _register_tasks(
|
|
71
|
+
self,
|
|
72
|
+
kind: str,
|
|
73
|
+
declarations: Mapping[str, bool],
|
|
74
|
+
configured: Mapping[str, BackgroundJobOptions],
|
|
75
|
+
) -> None:
|
|
76
|
+
unknown = set(configured) - declarations.keys()
|
|
77
|
+
if unknown:
|
|
78
|
+
raise ValueError(f"Unknown background {kind} configuration: {sorted(unknown)}")
|
|
79
|
+
for name, admitted in sorted(declarations.items()):
|
|
80
|
+
key = f"{kind}:{name}"
|
|
81
|
+
configured_options = configured.get(name, BackgroundJobOptions())
|
|
82
|
+
enabled = (
|
|
83
|
+
configured_options.enabled
|
|
84
|
+
if isinstance(configured_options, BackgroundTaskOptions)
|
|
85
|
+
else True
|
|
86
|
+
)
|
|
87
|
+
options = BackgroundTaskOptions(
|
|
88
|
+
mode=configured_options.mode, enabled=admitted and enabled
|
|
89
|
+
)
|
|
90
|
+
self._tasks[key] = _LocalTask(
|
|
91
|
+
key,
|
|
92
|
+
options,
|
|
93
|
+
self._options.graceful_shutdown_timeout.total_seconds(),
|
|
94
|
+
self._failures.put_nowait,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def profile(self) -> BackgroundExecutionProfile:
|
|
99
|
+
return BackgroundExecutionProfile.DISABLED if self._database_only else self._options.profile
|
|
100
|
+
|
|
101
|
+
def disable_for_database(self) -> None:
|
|
102
|
+
if self._started or self._prepared:
|
|
103
|
+
raise RuntimeError("Database execution must be selected before initialization")
|
|
104
|
+
self._database_only = True
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def system_caller(self) -> BackgroundSystemCaller:
|
|
108
|
+
return self._caller
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def has_runtime_failure_source(self) -> bool:
|
|
112
|
+
return any(
|
|
113
|
+
task.execution is not None and task.execution.wait_for_failure is not None
|
|
114
|
+
for key, task in self._tasks.items()
|
|
115
|
+
if key in self._local
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def local_job_names(self) -> tuple[str, ...]:
|
|
120
|
+
if self.profile is BackgroundExecutionProfile.DISABLED:
|
|
121
|
+
return ()
|
|
122
|
+
return tuple(
|
|
123
|
+
key.removeprefix("job:")
|
|
124
|
+
for key in self._local
|
|
125
|
+
if key.startswith("job:") and self._tasks[key].options.enabled
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def select_child(
|
|
129
|
+
self, tasks: dict[str, BackgroundTaskOptions], initial: tuple[str, ...]
|
|
130
|
+
) -> None:
|
|
131
|
+
if self._prepared or self._started:
|
|
132
|
+
raise RuntimeError("Child task assignment is frozen before startup")
|
|
133
|
+
for key, options in tasks.items():
|
|
134
|
+
if key not in self._tasks or self._tasks[key].options != options:
|
|
135
|
+
raise RuntimeError("Child task catalog/configuration differs from parent")
|
|
136
|
+
self._child = True
|
|
137
|
+
self._local = set(tasks)
|
|
138
|
+
self._initial = set(initial)
|
|
139
|
+
|
|
140
|
+
def prepare(self) -> None:
|
|
141
|
+
if self._prepared:
|
|
142
|
+
raise RuntimeError("Background coordinator is already prepared")
|
|
143
|
+
self._prepared = True
|
|
144
|
+
self._worker_catalog._assert_unchanged()
|
|
145
|
+
self._job_catalog._assert_unchanged()
|
|
146
|
+
if self._child or self.profile is BackgroundExecutionProfile.DISABLED:
|
|
147
|
+
return
|
|
148
|
+
groups: dict[str, dict[str, BackgroundTaskOptions]] = defaultdict(dict)
|
|
149
|
+
for key, task in self._tasks.items():
|
|
150
|
+
mode = task.options.mode
|
|
151
|
+
if mode is not BackgroundExecutionMode.HOST:
|
|
152
|
+
position = "shared" if mode is BackgroundExecutionMode.SHARED else key
|
|
153
|
+
groups[position][key] = task.options
|
|
154
|
+
if groups:
|
|
155
|
+
_check_previous_owners(self._spec.project, self._spec.environment)
|
|
156
|
+
for position, tasks in groups.items():
|
|
157
|
+
slot = _ProcessSlot(position, tasks, self._spec, self._options)
|
|
158
|
+
slot.reserve()
|
|
159
|
+
self._slots[position] = slot
|
|
160
|
+
self._task_slots.update({key: slot for key in tasks})
|
|
161
|
+
|
|
162
|
+
def attach_container(self, container: AsyncContainer) -> None:
|
|
163
|
+
if self._container is not None:
|
|
164
|
+
raise RuntimeError("Background coordinator container is already attached")
|
|
165
|
+
self._container = container
|
|
166
|
+
for definition in self._worker_catalog.definitions:
|
|
167
|
+
self._tasks[f"worker:{definition.name}"].execution = _WorkerExecution(
|
|
168
|
+
definition, container, self._invocation_runtime, self._caller
|
|
169
|
+
).binding
|
|
170
|
+
|
|
171
|
+
def bind_durable(self, name: str, binding: _TaskExecution) -> None:
|
|
172
|
+
task = self._tasks[f"job:{name}"]
|
|
173
|
+
if self._started or task.execution is not None:
|
|
174
|
+
raise RuntimeError("Durable background task may only be bound once before start")
|
|
175
|
+
task.execution = binding
|
|
176
|
+
|
|
177
|
+
async def start(self) -> None:
|
|
178
|
+
if self._started:
|
|
179
|
+
raise RuntimeError("Background coordinator is already started")
|
|
180
|
+
self._started = True
|
|
181
|
+
self.prepare()
|
|
182
|
+
if self.profile is BackgroundExecutionProfile.DISABLED:
|
|
183
|
+
return
|
|
184
|
+
for key in self._tasks:
|
|
185
|
+
if key in self._initial:
|
|
186
|
+
await self._tasks[key].start()
|
|
187
|
+
for slot in self._slots.values():
|
|
188
|
+
await slot.start()
|
|
189
|
+
|
|
190
|
+
def snapshots(self) -> tuple[BackgroundTaskSnapshot, ...]:
|
|
191
|
+
remote = {item.id: item for slot in self._slots.values() for item in slot.snapshots()}
|
|
192
|
+
return tuple(
|
|
193
|
+
remote.get(key, task.snapshot())
|
|
194
|
+
for key, task in self._tasks.items()
|
|
195
|
+
if not self._child or key in self._local
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def request(self, key: str, *, start: bool) -> BackgroundTaskSnapshot:
|
|
199
|
+
if not self._started or self.profile is BackgroundExecutionProfile.DISABLED:
|
|
200
|
+
raise ValueError("Background execution is not active")
|
|
201
|
+
if key not in self._tasks or (self._child and key not in self._local):
|
|
202
|
+
raise KeyError(key)
|
|
203
|
+
slot = self._task_slots.get(key)
|
|
204
|
+
if slot is not None:
|
|
205
|
+
return slot.request(key, start)
|
|
206
|
+
return self._tasks[key].request(start)
|
|
207
|
+
|
|
208
|
+
async def stop(self) -> None:
|
|
209
|
+
# 即使启动前的预占或 Module 初始化失败,也必须释放已取得的位置。
|
|
210
|
+
operations = [task.shutdown() for key, task in self._tasks.items() if key in self._local]
|
|
211
|
+
operations.extend(slot.shutdown() for slot in self._slots.values())
|
|
212
|
+
outcomes = await asyncio.gather(*operations, return_exceptions=True)
|
|
213
|
+
self._started = False
|
|
214
|
+
failures = tuple(item for item in outcomes if isinstance(item, BaseException))
|
|
215
|
+
if failures:
|
|
216
|
+
raise BaseExceptionGroup("Background runtime drain failed", failures)
|
|
217
|
+
|
|
218
|
+
async def wait_for_runtime_failure(self) -> _BackgroundRuntimeFailure:
|
|
219
|
+
return await self._failures.get()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .catalog import CacheCatalog
|
|
2
|
+
from .contracts import CacheDefinition, DistributedCache
|
|
3
|
+
from .errors import CacheDefinitionError, DistributedCacheError
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
"CacheCatalog",
|
|
7
|
+
"CacheDefinition",
|
|
8
|
+
"CacheDefinitionError",
|
|
9
|
+
"DistributedCache",
|
|
10
|
+
"DistributedCacheError",
|
|
11
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Cache metadata 的 Application-local 动态聚合与 immutable view。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Mapping
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..modularity import ModuleRegistry
|
|
10
|
+
from .contracts import CacheDefinition
|
|
11
|
+
from .errors import CacheDefinitionError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CacheCatalog:
|
|
15
|
+
__slots__ = ("_definitions",)
|
|
16
|
+
|
|
17
|
+
def __init__(self, definitions: tuple[CacheDefinition[Any], ...]) -> None:
|
|
18
|
+
by_name: dict[str, CacheDefinition[Any]] = {}
|
|
19
|
+
for definition in definitions:
|
|
20
|
+
if not isinstance(definition, CacheDefinition):
|
|
21
|
+
raise CacheDefinitionError(
|
|
22
|
+
cache=repr(definition),
|
|
23
|
+
reason="catalog entries must be CacheDefinition values",
|
|
24
|
+
)
|
|
25
|
+
if definition.name in by_name:
|
|
26
|
+
raise CacheDefinitionError(
|
|
27
|
+
cache=definition.name,
|
|
28
|
+
reason="cache is declared more than once",
|
|
29
|
+
)
|
|
30
|
+
by_name[definition.name] = definition
|
|
31
|
+
self._definitions: Mapping[str, CacheDefinition[Any]] = MappingProxyType(
|
|
32
|
+
dict(sorted(by_name.items()))
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def definitions(self) -> tuple[CacheDefinition[Any], ...]:
|
|
37
|
+
return tuple(self._definitions.values())
|
|
38
|
+
|
|
39
|
+
def __iter__(self) -> Iterator[str]:
|
|
40
|
+
return iter(self._definitions)
|
|
41
|
+
|
|
42
|
+
def get(self, name: str) -> CacheDefinition[Any] | None:
|
|
43
|
+
return self._definitions.get(name)
|
|
44
|
+
|
|
45
|
+
def owns(self, definition: CacheDefinition[Any]) -> bool:
|
|
46
|
+
return self._definitions.get(definition.name) is definition
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _build_cache_catalog(registry: ModuleRegistry) -> CacheCatalog:
|
|
50
|
+
definitions: list[CacheDefinition[Any]] = []
|
|
51
|
+
owners: dict[str, str] = {}
|
|
52
|
+
for descriptor in registry.dependency_order:
|
|
53
|
+
declared = descriptor.module_type.cache_definitions
|
|
54
|
+
if not isinstance(declared, tuple):
|
|
55
|
+
raise CacheDefinitionError(
|
|
56
|
+
cache=str(descriptor.key),
|
|
57
|
+
reason="Module cache_definitions must be an immutable tuple",
|
|
58
|
+
)
|
|
59
|
+
for definition in declared:
|
|
60
|
+
if not isinstance(definition, CacheDefinition):
|
|
61
|
+
raise CacheDefinitionError(
|
|
62
|
+
cache=str(descriptor.key),
|
|
63
|
+
reason="Module cache_definitions contains an invalid item",
|
|
64
|
+
)
|
|
65
|
+
previous = owners.get(definition.name)
|
|
66
|
+
if previous is not None:
|
|
67
|
+
raise CacheDefinitionError(
|
|
68
|
+
cache=definition.name,
|
|
69
|
+
reason=f"cache is already owned by {previous}",
|
|
70
|
+
)
|
|
71
|
+
owners[definition.name] = str(descriptor.key)
|
|
72
|
+
definitions.append(definition)
|
|
73
|
+
return CacheCatalog(tuple(definitions))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""强制 TTL 的 typed cache definition 与最小读写契约。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from typing import Any, Generic, Protocol, TypeVar
|
|
7
|
+
|
|
8
|
+
from pydantic import TypeAdapter
|
|
9
|
+
|
|
10
|
+
from .errors import CacheDefinitionError
|
|
11
|
+
|
|
12
|
+
_T = TypeVar("_T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CacheDefinition(Generic[_T]):
|
|
16
|
+
__slots__ = ("_adapter", "_name", "_ttl", "_value_type")
|
|
17
|
+
|
|
18
|
+
def __init__(self, name: str, value_type: type[_T], *, ttl: timedelta) -> None:
|
|
19
|
+
if (
|
|
20
|
+
not isinstance(name, str)
|
|
21
|
+
or not name
|
|
22
|
+
or name != name.strip()
|
|
23
|
+
or name.startswith(":")
|
|
24
|
+
or name.endswith(":")
|
|
25
|
+
or "::" in name
|
|
26
|
+
# ':' 是 Redis key 的结构分隔符,definition name 不得制造跨 cache key 歧义。
|
|
27
|
+
or any(not (item.isalnum() or item in "._-") for item in name)
|
|
28
|
+
):
|
|
29
|
+
raise CacheDefinitionError(
|
|
30
|
+
cache=name if isinstance(name, str) else repr(name),
|
|
31
|
+
reason="name must be a trimmed cache identifier",
|
|
32
|
+
)
|
|
33
|
+
if not isinstance(value_type, type):
|
|
34
|
+
raise CacheDefinitionError(cache=name, reason="value_type must be a runtime type")
|
|
35
|
+
if not isinstance(ttl, timedelta) or ttl.total_seconds() <= 0:
|
|
36
|
+
raise CacheDefinitionError(cache=name, reason="ttl must be a positive timedelta")
|
|
37
|
+
if ttl.total_seconds() > 2_147_483_647:
|
|
38
|
+
raise CacheDefinitionError(cache=name, reason="ttl exceeds the supported Redis range")
|
|
39
|
+
self._name = name
|
|
40
|
+
self._value_type = value_type
|
|
41
|
+
self._ttl = ttl
|
|
42
|
+
self._adapter: TypeAdapter[_T] = TypeAdapter(value_type)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def name(self) -> str:
|
|
46
|
+
return self._name
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def value_type(self) -> type[_T]:
|
|
50
|
+
return self._value_type
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def ttl(self) -> timedelta:
|
|
54
|
+
return self._ttl
|
|
55
|
+
|
|
56
|
+
def __repr__(self) -> str:
|
|
57
|
+
return (
|
|
58
|
+
f"CacheDefinition(name={self.name!r}, value_type={self.value_type!r}, ttl={self.ttl!r})"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def _serialize(self, value: _T) -> bytes:
|
|
62
|
+
return self._adapter.dump_json(self._adapter.validate_python(value))
|
|
63
|
+
|
|
64
|
+
def _deserialize(self, value: bytes | str) -> _T:
|
|
65
|
+
return self._adapter.validate_json(value)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DistributedCache(Protocol):
|
|
69
|
+
async def get(self, definition: CacheDefinition[_T], key: str) -> _T | None: ...
|
|
70
|
+
|
|
71
|
+
async def set(self, definition: CacheDefinition[_T], key: str, value: _T) -> None: ...
|
|
72
|
+
|
|
73
|
+
async def remove(self, definition: CacheDefinition[Any], key: str) -> None: ...
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Typed distributed cache definition 与 serialization failures。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..errors.base import FrameworkError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CacheDefinitionError(FrameworkError):
|
|
9
|
+
def __init__(self, *, cache: str, reason: str) -> None:
|
|
10
|
+
self.cache = cache
|
|
11
|
+
self.reason = reason
|
|
12
|
+
super().__init__(f"Invalid cache {cache!r}: {reason}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DistributedCacheError(FrameworkError):
|
|
16
|
+
def __init__(self, *, operation: str, cache: str, error_type: str) -> None:
|
|
17
|
+
self.operation = operation
|
|
18
|
+
self.cache = cache
|
|
19
|
+
self.error_type = error_type
|
|
20
|
+
super().__init__(f"Distributed cache {operation} failed for {cache!r} with {error_type}")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""pddd 的轻量 argparse 入口;只有生成命令才加载 Developer Kit。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ..hosting import ENVIRONMENT_VARIABLE as ENVIRONMENT_VARIABLE
|
|
14
|
+
from .errors import CommandError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _Command(str, Enum):
|
|
18
|
+
NEW = "new"
|
|
19
|
+
ADD = "add"
|
|
20
|
+
DEV = "dev"
|
|
21
|
+
DEV_INIT = "dev-init"
|
|
22
|
+
DB = "db"
|
|
23
|
+
INSPECT = "inspect"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _parser() -> argparse.ArgumentParser:
|
|
27
|
+
parser = argparse.ArgumentParser(prog="pddd", description="Tools for modular DDD applications")
|
|
28
|
+
commands = parser.add_subparsers(dest="command", title="commands")
|
|
29
|
+
new = commands.add_parser(_Command.NEW.value, help="Create a project from bundled templates")
|
|
30
|
+
new.add_argument("name")
|
|
31
|
+
new.add_argument("--framework-wheel", type=Path, help="Validate with a local framework wheel")
|
|
32
|
+
new.add_argument("--dry-run", action="store_true")
|
|
33
|
+
add = commands.add_parser(
|
|
34
|
+
_Command.ADD.value, help="Add a business module to the current project"
|
|
35
|
+
).add_subparsers(dest="kind", required=True)
|
|
36
|
+
module = add.add_parser("module", help="Generate and register a business module")
|
|
37
|
+
module.add_argument("name")
|
|
38
|
+
module.add_argument("--dry-run", action="store_true")
|
|
39
|
+
dev = commands.add_parser(_Command.DEV.value, help="Run the development web server")
|
|
40
|
+
dev.add_argument("--host", default="127.0.0.1")
|
|
41
|
+
dev.add_argument("--port", type=int, default=8000)
|
|
42
|
+
for command, description in (
|
|
43
|
+
(_Command.DEV_INIT, "Prepare local development infrastructure"),
|
|
44
|
+
(_Command.DB, "Manage module migrations and seed data"),
|
|
45
|
+
(_Command.INSPECT, "Inspect modules, services and redacted configuration"),
|
|
46
|
+
):
|
|
47
|
+
action = commands.add_parser(command.value, help=description, description=description)
|
|
48
|
+
action.add_argument(
|
|
49
|
+
"--environment",
|
|
50
|
+
default=os.environ.get(ENVIRONMENT_VARIABLE, "development"),
|
|
51
|
+
help="Host environment (default: %(default)s)",
|
|
52
|
+
)
|
|
53
|
+
if command is _Command.DB:
|
|
54
|
+
action.add_argument("operation", choices=("revision", "upgrade", "status", "seed"))
|
|
55
|
+
action.add_argument("--module", required=True)
|
|
56
|
+
return parser
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
60
|
+
parser = _parser()
|
|
61
|
+
args = parser.parse_args(argv)
|
|
62
|
+
if args.command is None:
|
|
63
|
+
parser.print_help()
|
|
64
|
+
return 0
|
|
65
|
+
try:
|
|
66
|
+
return _execute(args)
|
|
67
|
+
except CommandError as error:
|
|
68
|
+
parser.exit(2, f"pddd: {error}\n")
|
|
69
|
+
except subprocess.CalledProcessError as error:
|
|
70
|
+
parser.exit(
|
|
71
|
+
error.returncode, "pddd: external command failed; diagnostic files were retained\n"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _execute(args: argparse.Namespace) -> int:
|
|
76
|
+
command = _Command(args.command)
|
|
77
|
+
if command in (_Command.NEW, _Command.ADD):
|
|
78
|
+
try:
|
|
79
|
+
from ..developer_kit.generation import add_module, new_project
|
|
80
|
+
except ModuleNotFoundError as error:
|
|
81
|
+
if error.name not in {"cookiecutter", "libcst"}:
|
|
82
|
+
raise
|
|
83
|
+
raise CommandError(
|
|
84
|
+
"Install python-ddd-framework[developer-kit] to use generation commands"
|
|
85
|
+
) from None
|
|
86
|
+
if command is _Command.NEW:
|
|
87
|
+
new_project(args.name, args.framework_wheel, args.dry_run)
|
|
88
|
+
else:
|
|
89
|
+
add_module(args.name, args.dry_run)
|
|
90
|
+
else:
|
|
91
|
+
from .runtime import database, development_server
|
|
92
|
+
|
|
93
|
+
if command is _Command.DEV:
|
|
94
|
+
development_server(args.host, args.port)
|
|
95
|
+
elif command is _Command.DEV_INIT:
|
|
96
|
+
from .development import dev_init
|
|
97
|
+
|
|
98
|
+
asyncio.run(dev_init(args.environment))
|
|
99
|
+
elif command is _Command.INSPECT:
|
|
100
|
+
from .inspection import inspect_application
|
|
101
|
+
|
|
102
|
+
asyncio.run(inspect_application(args.environment))
|
|
103
|
+
else:
|
|
104
|
+
asyncio.run(database(args.operation, args.environment, args.module))
|
|
105
|
+
return 0
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""开发依赖的 Compose 接线;配置只来自当前 Host 构建出的快照。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from ..application import Application
|
|
9
|
+
from .errors import CommandError
|
|
10
|
+
from .project import Project
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def dev_init(environment: str) -> None:
|
|
14
|
+
if environment != "development":
|
|
15
|
+
raise CommandError("dev-init is only available in the development environment")
|
|
16
|
+
project = Project.current()
|
|
17
|
+
application = project.host("application").load()(environment=environment)
|
|
18
|
+
if not isinstance(application, Application):
|
|
19
|
+
raise CommandError("The application entry point must return Application")
|
|
20
|
+
try:
|
|
21
|
+
subprocess.run(
|
|
22
|
+
[
|
|
23
|
+
"docker",
|
|
24
|
+
"compose",
|
|
25
|
+
"-f",
|
|
26
|
+
str(project.root / "compose.dev.yaml"),
|
|
27
|
+
"up",
|
|
28
|
+
"-d",
|
|
29
|
+
"--wait",
|
|
30
|
+
],
|
|
31
|
+
cwd=project.root,
|
|
32
|
+
env=_compose_environment(application),
|
|
33
|
+
check=True,
|
|
34
|
+
)
|
|
35
|
+
finally:
|
|
36
|
+
# build 只组合,关闭仍交给 Application;解析和子进程失败也不得遗留 APP owner。
|
|
37
|
+
await application.stop()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _compose_environment(application: Application) -> dict[str, str]:
|
|
41
|
+
from sqlalchemy.engine import make_url
|
|
42
|
+
from sqlalchemy.exc import ArgumentError
|
|
43
|
+
|
|
44
|
+
from ..redis import RedisOptions
|
|
45
|
+
from ..sqlalchemy import ConnectionStringsOptions
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
postgres = make_url(
|
|
49
|
+
application.options.get(ConnectionStringsOptions).default.get_secret_value()
|
|
50
|
+
)
|
|
51
|
+
redis = make_url(application.options.get(RedisOptions).url.get_secret_value())
|
|
52
|
+
images = {
|
|
53
|
+
"POSTGRES_IMAGE": application.configuration.get_path(
|
|
54
|
+
"development.compose.postgres_image"
|
|
55
|
+
),
|
|
56
|
+
"REDIS_IMAGE": application.configuration.get_path("development.compose.redis_image"),
|
|
57
|
+
}
|
|
58
|
+
if any(not isinstance(value, str) or not value.strip() for value in images.values()):
|
|
59
|
+
raise ValueError("Compose images require non-empty strings")
|
|
60
|
+
# 模板只提供本机 TCP 依赖,拒绝无法兑现的远端、TLS 或查询参数覆盖。
|
|
61
|
+
for url in (postgres, redis):
|
|
62
|
+
if url.host not in ("127.0.0.1", "localhost") or not url.port or url.query:
|
|
63
|
+
raise ValueError("Compose requires local URLs with explicit ports and no query")
|
|
64
|
+
if not postgres.username or not postgres.password or not postgres.database:
|
|
65
|
+
raise ValueError("PostgreSQL URL requires credentials and database")
|
|
66
|
+
if redis.drivername != "redis" or redis.username or redis.password:
|
|
67
|
+
raise ValueError("Development Redis requires a plain local connection")
|
|
68
|
+
except (ArgumentError, KeyError, TypeError, ValueError):
|
|
69
|
+
# 原生 parser 异常可能包含完整 URL,CLI 只公开需要检查的配置位置。
|
|
70
|
+
raise CommandError(
|
|
71
|
+
"Invalid development Compose configuration; check connection_strings.default, "
|
|
72
|
+
"redis.url and development.compose (local URLs require explicit ports)"
|
|
73
|
+
) from None
|
|
74
|
+
environment = dict(os.environ)
|
|
75
|
+
environment.pop("COMPOSE_ENV_FILES", None)
|
|
76
|
+
environment.update(
|
|
77
|
+
COMPOSE_DISABLE_ENV_FILE="1",
|
|
78
|
+
POSTGRES_DB=postgres.database,
|
|
79
|
+
POSTGRES_USER=postgres.username,
|
|
80
|
+
POSTGRES_PASSWORD=postgres.password,
|
|
81
|
+
POSTGRES_PORT=str(postgres.port),
|
|
82
|
+
REDIS_PORT=str(redis.port),
|
|
83
|
+
)
|
|
84
|
+
environment.update({key: str(value) for key, value in images.items()})
|
|
85
|
+
return environment
|