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,119 @@
|
|
|
1
|
+
"""Configuration 与 typed Options 的结构化失败。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .base import FrameworkError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pydantic import BaseModel, ValidationError
|
|
11
|
+
|
|
12
|
+
from ..diagnostics import SourceLocation
|
|
13
|
+
from ..modularity import ModuleKey
|
|
14
|
+
from ..options import OptionsValidationIssue
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigurationSourceError(FrameworkError):
|
|
18
|
+
"""配置来源加载或规范化失败;消息不复制可能含敏感值的底层异常文本。"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
source_id: str,
|
|
24
|
+
location: SourceLocation,
|
|
25
|
+
original_error: BaseException,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.source_id = source_id
|
|
28
|
+
self.location = location
|
|
29
|
+
self.original_error = original_error
|
|
30
|
+
self.__cause__ = original_error
|
|
31
|
+
self.__suppress_context__ = True
|
|
32
|
+
super().__init__(f"Unable to load configuration source {source_id!r}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InvalidOptionDefinitionError(FrameworkError):
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
owner: ModuleKey,
|
|
40
|
+
model_type: type[BaseModel] | None,
|
|
41
|
+
option_kind: str,
|
|
42
|
+
reason: str,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.owner = owner
|
|
45
|
+
self.model_type = model_type
|
|
46
|
+
self.option_kind = option_kind
|
|
47
|
+
self.reason = reason
|
|
48
|
+
model_name = (
|
|
49
|
+
f"{model_type.__module__}.{model_type.__qualname__}"
|
|
50
|
+
if model_type is not None
|
|
51
|
+
else "<invalid declaration>"
|
|
52
|
+
)
|
|
53
|
+
super().__init__(
|
|
54
|
+
f"Invalid {option_kind} definition {model_name} declared by {owner}: {reason}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DuplicateOptionDefinitionError(FrameworkError):
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
model_type: type[BaseModel],
|
|
63
|
+
option_kind: str,
|
|
64
|
+
first_owner: ModuleKey,
|
|
65
|
+
second_owner: ModuleKey,
|
|
66
|
+
) -> None:
|
|
67
|
+
self.model_type = model_type
|
|
68
|
+
self.option_kind = option_kind
|
|
69
|
+
self.first_owner = first_owner
|
|
70
|
+
self.second_owner = second_owner
|
|
71
|
+
model_name = f"{model_type.__module__}.{model_type.__qualname__}"
|
|
72
|
+
super().__init__(
|
|
73
|
+
f"Duplicate {option_kind} definition for {model_name}: {first_owner} and {second_owner}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class UnknownOptionsError(FrameworkError, KeyError):
|
|
78
|
+
def __init__(self, model_type: type[BaseModel], *, option_kind: str) -> None:
|
|
79
|
+
self.model_type = model_type
|
|
80
|
+
self.option_kind = option_kind
|
|
81
|
+
model_name = f"{model_type.__module__}.{model_type.__qualname__}"
|
|
82
|
+
super().__init__(f"Unknown {option_kind} model {model_name}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class OptionsContributionClosedError(FrameworkError):
|
|
86
|
+
def __init__(self, *, option_kind: str, stage: str) -> None:
|
|
87
|
+
self.option_kind = option_kind
|
|
88
|
+
self.stage = stage
|
|
89
|
+
super().__init__(
|
|
90
|
+
f"{option_kind} configuration contribution is no longer active after {stage}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class OptionsValidationError(FrameworkError):
|
|
95
|
+
"""对 Pydantic 错误增加 owner/source 语义,只保留已脱敏的 Pydantic cause。"""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
*,
|
|
100
|
+
model_type: type[BaseModel],
|
|
101
|
+
section: str,
|
|
102
|
+
owner: ModuleKey,
|
|
103
|
+
option_kind: str,
|
|
104
|
+
issues: tuple[OptionsValidationIssue, ...],
|
|
105
|
+
validation_error: ValidationError,
|
|
106
|
+
) -> None:
|
|
107
|
+
self.model_type = model_type
|
|
108
|
+
self.section = section
|
|
109
|
+
self.owner = owner
|
|
110
|
+
self.option_kind = option_kind
|
|
111
|
+
self.issues = issues
|
|
112
|
+
self.validation_error = validation_error
|
|
113
|
+
self.__cause__ = validation_error
|
|
114
|
+
self.__suppress_context__ = True
|
|
115
|
+
model_name = f"{model_type.__module__}.{model_type.__qualname__}"
|
|
116
|
+
paths = ", ".join(issue.field_path for issue in issues) or "<model>"
|
|
117
|
+
super().__init__(
|
|
118
|
+
f"Invalid {option_kind} {model_name} at section {section!r}; fields: {paths}"
|
|
119
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Diagnostic contract 自身的边界错误。"""
|
|
2
|
+
|
|
3
|
+
from .base import FrameworkError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InvalidDiagnosticCodeError(FrameworkError):
|
|
7
|
+
def __init__(self, code: str, *, max_length: int) -> None:
|
|
8
|
+
self.code = code
|
|
9
|
+
self.max_length = max_length
|
|
10
|
+
super().__init__(
|
|
11
|
+
f"Diagnostic code must be a stable identifier of at most {max_length} characters"
|
|
12
|
+
)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Application state、lifecycle invocation 与运行失败归属。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from .base import FrameworkError
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..diagnostics import LifecyclePhase, LifecycleStage, SourceLocation
|
|
12
|
+
from ..hosted_services.errors import HostedServiceLifecycleError
|
|
13
|
+
from ..lifecycle import ApplicationState
|
|
14
|
+
from ..modularity import ModuleKey
|
|
15
|
+
from .services import ContainerCleanupError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InvalidApplicationStateError(FrameworkError):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
operation: str,
|
|
23
|
+
current: ApplicationState,
|
|
24
|
+
allowed: tuple[ApplicationState, ...],
|
|
25
|
+
) -> None:
|
|
26
|
+
self.operation = operation
|
|
27
|
+
self.current = current
|
|
28
|
+
self.allowed = allowed
|
|
29
|
+
allowed_values = ", ".join(state.value for state in allowed)
|
|
30
|
+
super().__init__(
|
|
31
|
+
f"Operation {operation!r} is invalid in state {current.value}; "
|
|
32
|
+
f"allowed: {allowed_values}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CompositionResourceError(FrameworkError):
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
module: ModuleKey,
|
|
41
|
+
stage: LifecycleStage,
|
|
42
|
+
resource_kind: str,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.module = module
|
|
45
|
+
self.stage = stage
|
|
46
|
+
self.resource_kind = resource_kind
|
|
47
|
+
super().__init__(
|
|
48
|
+
f"Composition hook {module}/{stage.value} returned forbidden {resource_kind}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LifecycleReturnValueError(FrameworkError):
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
module: ModuleKey,
|
|
57
|
+
phase: LifecyclePhase,
|
|
58
|
+
stage: LifecycleStage,
|
|
59
|
+
returned_type: str,
|
|
60
|
+
) -> None:
|
|
61
|
+
self.module = module
|
|
62
|
+
self.phase = phase
|
|
63
|
+
self.stage = stage
|
|
64
|
+
self.returned_type = returned_type
|
|
65
|
+
super().__init__(
|
|
66
|
+
f"Lifecycle hook {module}/{stage.value} must return None, got {returned_type}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LifecycleInvocationError(FrameworkError):
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
failure_id: UUID,
|
|
75
|
+
module: ModuleKey,
|
|
76
|
+
phase: LifecyclePhase,
|
|
77
|
+
stage: LifecycleStage,
|
|
78
|
+
dependency_path: tuple[ModuleKey, ...],
|
|
79
|
+
duration_ns: int,
|
|
80
|
+
source: SourceLocation,
|
|
81
|
+
original_error: BaseException,
|
|
82
|
+
) -> None:
|
|
83
|
+
self.failure_id = failure_id
|
|
84
|
+
self.module = module
|
|
85
|
+
self.phase = phase
|
|
86
|
+
self.stage = stage
|
|
87
|
+
self.dependency_path = dependency_path
|
|
88
|
+
self.duration_ns = duration_ns
|
|
89
|
+
self.source = source
|
|
90
|
+
# cleanup 为 best-effort 会收集 wrapper,因此构造时显式建立因果链。
|
|
91
|
+
self.__cause__ = original_error
|
|
92
|
+
self.__suppress_context__ = True
|
|
93
|
+
self.original_error = original_error
|
|
94
|
+
super().__init__(
|
|
95
|
+
f"Lifecycle invocation failed for {module}/{stage.value} "
|
|
96
|
+
f"with {type(original_error).__name__}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _BackgroundExecutionStartFailure(FrameworkError):
|
|
101
|
+
"""保留原始 background start failure,同时不伪造 Module lifecycle 位置。"""
|
|
102
|
+
|
|
103
|
+
failure_id = None
|
|
104
|
+
stage = None
|
|
105
|
+
dependency_path: tuple[ModuleKey, ...] = ()
|
|
106
|
+
|
|
107
|
+
def __init__(self, *, original_error: BaseException) -> None:
|
|
108
|
+
self.original_error = original_error
|
|
109
|
+
self.__cause__ = original_error
|
|
110
|
+
self.__suppress_context__ = True
|
|
111
|
+
super().__init__(f"Background execution start failed with {type(original_error).__name__}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ApplicationRuntimeError(FrameworkError):
|
|
115
|
+
"""运行期基础设施 fatal fact;不携带第三方异常正文或原始 cause。"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, *, component: str, error_type: str) -> None:
|
|
118
|
+
self.component = component
|
|
119
|
+
self.error_type = error_type
|
|
120
|
+
# 标识一次失败事实,不持有原始异常、task 或 provider;同名失败仍是独立事实。
|
|
121
|
+
self._source = object()
|
|
122
|
+
super().__init__(f"Application runtime {component!r} failed with {error_type}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ApplicationStartError(FrameworkError):
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
primary: (
|
|
130
|
+
LifecycleInvocationError
|
|
131
|
+
| _BackgroundExecutionStartFailure
|
|
132
|
+
| HostedServiceLifecycleError
|
|
133
|
+
),
|
|
134
|
+
cleanup_failures: tuple[
|
|
135
|
+
LifecycleInvocationError
|
|
136
|
+
| ContainerCleanupError
|
|
137
|
+
| _BackgroundExecutionStartFailure
|
|
138
|
+
| HostedServiceLifecycleError,
|
|
139
|
+
...,
|
|
140
|
+
],
|
|
141
|
+
) -> None:
|
|
142
|
+
self.primary = primary
|
|
143
|
+
self.cleanup_failures = cleanup_failures
|
|
144
|
+
if isinstance(primary, LifecycleInvocationError):
|
|
145
|
+
location = f"{primary.module}/{primary.stage.value}"
|
|
146
|
+
elif isinstance(primary, _BackgroundExecutionStartFailure):
|
|
147
|
+
location = "background-execution"
|
|
148
|
+
else:
|
|
149
|
+
location = f"hosted-service:{primary.service}/{primary.operation}"
|
|
150
|
+
super().__init__(
|
|
151
|
+
f"Application start failed at {location}; cleanup failures: {len(cleanup_failures)}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _exclude_runtime_failure(
|
|
156
|
+
error: BaseException, primary: ApplicationRuntimeError
|
|
157
|
+
) -> BaseException | None:
|
|
158
|
+
# 发布 fatal 与观察 drain 可能竞争;终止 owner 才能确认哪个事实已成为主失败。
|
|
159
|
+
def same_source(item: BaseException) -> bool:
|
|
160
|
+
return isinstance(item, ApplicationRuntimeError) and item._source is primary._source
|
|
161
|
+
|
|
162
|
+
if isinstance(error, BaseExceptionGroup):
|
|
163
|
+
return error.split(same_source)[1]
|
|
164
|
+
return None if same_source(error) else error
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Module reference、graph 与 registry ownership 的结构化失败。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .base import FrameworkError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ..diagnostics import SourceLocation
|
|
11
|
+
from ..modularity import AppModule, ModuleDependencyEdge, ModuleKey
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidModuleReferenceError(FrameworkError):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
owner: ModuleKey | None,
|
|
19
|
+
reference: str,
|
|
20
|
+
reason: str,
|
|
21
|
+
source: SourceLocation | None,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.owner = owner
|
|
24
|
+
self.reference = reference
|
|
25
|
+
self.reason = reason
|
|
26
|
+
self.source = source
|
|
27
|
+
super().__init__(f"Invalid module reference {reference!r}: {reason}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ModuleResolutionError(FrameworkError):
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
owner: ModuleKey | None,
|
|
35
|
+
reference: str,
|
|
36
|
+
resolution_chain: tuple[str, ...],
|
|
37
|
+
source: SourceLocation | None,
|
|
38
|
+
original_error: BaseException,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.owner = owner
|
|
41
|
+
self.reference = reference
|
|
42
|
+
self.resolution_chain = resolution_chain
|
|
43
|
+
self.source = source
|
|
44
|
+
self.original_error = original_error
|
|
45
|
+
super().__init__(f"Unable to resolve module reference {reference!r}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ModuleCycleError(FrameworkError):
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
startup: ModuleKey,
|
|
53
|
+
path: tuple[ModuleKey, ...],
|
|
54
|
+
edges: tuple[ModuleDependencyEdge, ...],
|
|
55
|
+
) -> None:
|
|
56
|
+
self.startup = startup
|
|
57
|
+
self.path = path
|
|
58
|
+
self.edges = edges
|
|
59
|
+
rendered_path = " -> ".join(str(key) for key in path)
|
|
60
|
+
super().__init__(f"Module dependency cycle: {rendered_path}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class GraphInvariantError(FrameworkError):
|
|
64
|
+
def __init__(self, detail: str) -> None:
|
|
65
|
+
self.detail = detail
|
|
66
|
+
super().__init__(f"Module graph invariant failed: {detail}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DuplicateModuleIdentityError(FrameworkError):
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
*,
|
|
73
|
+
key: ModuleKey,
|
|
74
|
+
first_type: type[AppModule],
|
|
75
|
+
second_type: type[AppModule],
|
|
76
|
+
) -> None:
|
|
77
|
+
self.key = key
|
|
78
|
+
self.first_type = first_type
|
|
79
|
+
self.second_type = second_type
|
|
80
|
+
super().__init__(f"Different module classes resolved to the same key {key}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ModuleInstantiationError(FrameworkError):
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
module: ModuleKey,
|
|
88
|
+
source: SourceLocation,
|
|
89
|
+
original_error: BaseException,
|
|
90
|
+
) -> None:
|
|
91
|
+
self.module = module
|
|
92
|
+
self.source = source
|
|
93
|
+
self.original_error = original_error
|
|
94
|
+
super().__init__(f"Unable to instantiate module {module}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class UnknownModuleError(FrameworkError, KeyError):
|
|
98
|
+
def __init__(self, module: ModuleKey) -> None:
|
|
99
|
+
self.module = module
|
|
100
|
+
super().__init__(f"Unknown module {module}")
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Service composition、Convention 与 native container 的结构化失败。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from .base import FrameworkError
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from dishka import DependencyKey
|
|
12
|
+
|
|
13
|
+
from ..diagnostics import SourceLocation
|
|
14
|
+
from ..modularity import ModuleKey
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InvalidConventionDefinitionError(FrameworkError):
|
|
18
|
+
def __init__(self, *, symbol: str, reason: str) -> None:
|
|
19
|
+
self.symbol = symbol
|
|
20
|
+
self.reason = reason
|
|
21
|
+
super().__init__(f"Invalid convention definition {symbol}: {reason}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConventionScanError(FrameworkError):
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
owner: ModuleKey,
|
|
29
|
+
package: str | None,
|
|
30
|
+
source: SourceLocation,
|
|
31
|
+
reason: str,
|
|
32
|
+
original_error: BaseException | None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.owner = owner
|
|
35
|
+
self.package = package
|
|
36
|
+
self.source = source
|
|
37
|
+
self.reason = reason
|
|
38
|
+
self.original_error = original_error
|
|
39
|
+
if original_error is not None:
|
|
40
|
+
self.__cause__ = original_error
|
|
41
|
+
self.__suppress_context__ = True
|
|
42
|
+
target = package if package is not None else "<invalid metadata>"
|
|
43
|
+
super().__init__(f"Convention scan failed for {owner}/{target}: {reason}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ServiceContributorClosedError(FrameworkError):
|
|
47
|
+
def __init__(self, *, stage: str) -> None:
|
|
48
|
+
self.stage = stage
|
|
49
|
+
super().__init__(f"Service contributor is no longer active after {stage}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RegistrationConflictError(FrameworkError):
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
key: DependencyKey | None,
|
|
57
|
+
sources: tuple[SourceLocation, ...],
|
|
58
|
+
reason: str,
|
|
59
|
+
) -> None:
|
|
60
|
+
self.key = key
|
|
61
|
+
self.sources = sources
|
|
62
|
+
self.reason = reason
|
|
63
|
+
rendered_key = str(key) if key is not None else "<provider>"
|
|
64
|
+
super().__init__(f"Service registration conflict for {rendered_key}: {reason}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ContainerValidationError(FrameworkError):
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
sources: tuple[SourceLocation, ...],
|
|
72
|
+
original_error: Exception,
|
|
73
|
+
) -> None:
|
|
74
|
+
self.sources = sources
|
|
75
|
+
self.original_error = original_error
|
|
76
|
+
self.__cause__ = original_error
|
|
77
|
+
self.__suppress_context__ = True
|
|
78
|
+
super().__init__(
|
|
79
|
+
f"Dishka strict container validation failed with {type(original_error).__name__}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CatalogNativeGraphMismatchError(FrameworkError):
|
|
84
|
+
def __init__(self, *, differences: tuple[str, ...]) -> None:
|
|
85
|
+
self.differences = differences
|
|
86
|
+
super().__init__(f"Service catalog/native graph mismatch: {len(differences)} difference(s)")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ContainerCleanupError(FrameworkError):
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
*,
|
|
93
|
+
failure_id: UUID,
|
|
94
|
+
original_error: BaseException,
|
|
95
|
+
) -> None:
|
|
96
|
+
self.failure_id = failure_id
|
|
97
|
+
self.original_error = original_error
|
|
98
|
+
self.__cause__ = original_error
|
|
99
|
+
self.__suppress_context__ = True
|
|
100
|
+
super().__init__(f"Dishka container cleanup failed with {type(original_error).__name__}")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .aggregate import AggregateEventCollector
|
|
2
|
+
from .catalog import LocalEventCatalog, LocalEventSubscription
|
|
3
|
+
from .contracts import LocalEventPhase, local_event_handler
|
|
4
|
+
from .contribution import EventContributor
|
|
5
|
+
from .errors import (
|
|
6
|
+
EventContributionError,
|
|
7
|
+
EventCycleError,
|
|
8
|
+
LocalEventHandlerError,
|
|
9
|
+
PostCommitEventError,
|
|
10
|
+
)
|
|
11
|
+
from .runtime import LocalEventBus
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
"AggregateEventCollector",
|
|
15
|
+
"EventContributionError",
|
|
16
|
+
"EventContributor",
|
|
17
|
+
"EventCycleError",
|
|
18
|
+
"LocalEventBus",
|
|
19
|
+
"LocalEventCatalog",
|
|
20
|
+
"LocalEventHandlerError",
|
|
21
|
+
"LocalEventPhase",
|
|
22
|
+
"LocalEventSubscription",
|
|
23
|
+
"PostCommitEventError",
|
|
24
|
+
"local_event_handler",
|
|
25
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Aggregate pending events to UoW-owned local event queues."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
|
|
7
|
+
from ..domain import AggregateRoot
|
|
8
|
+
from ..unit_of_work.contracts import UnitOfWork
|
|
9
|
+
from .runtime import LocalEventBus
|
|
10
|
+
|
|
11
|
+
_TId = TypeVar("_TId")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AggregateEventCollector:
|
|
15
|
+
"""同一事件进入 DOMAIN 与 AFTER_COMMIT 队列,聚合只释放一次。"""
|
|
16
|
+
|
|
17
|
+
__slots__ = ("_events",)
|
|
18
|
+
|
|
19
|
+
def __init__(self, events: LocalEventBus) -> None:
|
|
20
|
+
self._events = events
|
|
21
|
+
|
|
22
|
+
def collect(self, aggregate: AggregateRoot[_TId]) -> tuple[object, ...]:
|
|
23
|
+
if not isinstance(aggregate, AggregateRoot):
|
|
24
|
+
raise TypeError("aggregate must be AggregateRoot")
|
|
25
|
+
# 发布资格先于提取;拒绝非事务发布不能消耗聚合尚未交付的事件。
|
|
26
|
+
work = self._events._manager.require_current(transactional=True)
|
|
27
|
+
return _collect_aggregate_events(work, aggregate)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _collect_aggregate_events(
|
|
31
|
+
work: UnitOfWork, aggregate: AggregateRoot[_TId]
|
|
32
|
+
) -> tuple[object, ...]:
|
|
33
|
+
# 两个公开入口先验证事务 owner;收集不触发 flush,也不恢复已释放事件或自动重放。
|
|
34
|
+
assert work._event_queue is not None
|
|
35
|
+
released = aggregate.release_local_events()
|
|
36
|
+
for event in released:
|
|
37
|
+
work._event_queue.publish_domain(event)
|
|
38
|
+
work._event_queue.publish_after_commit(event)
|
|
39
|
+
return released
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Local Event phase、subscription provenance 与 immutable catalog。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from dishka import BaseScope, Scope
|
|
12
|
+
|
|
13
|
+
from ..diagnostics import SourceLocation
|
|
14
|
+
from ..modularity import ModuleKey, ModuleRegistry
|
|
15
|
+
from .contracts import LocalEventPhase
|
|
16
|
+
from .errors import EventContributionError
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .contribution import EventContributionRegistry
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class LocalEventSubscription:
|
|
24
|
+
event_type: type[object]
|
|
25
|
+
handler_type: type[object] | Callable[..., Awaitable[None]]
|
|
26
|
+
phase: LocalEventPhase
|
|
27
|
+
owner: ModuleKey
|
|
28
|
+
source: SourceLocation
|
|
29
|
+
reason: str
|
|
30
|
+
declaration_index: int
|
|
31
|
+
order: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LocalEventCatalog:
|
|
35
|
+
__slots__ = ("_by_phase_and_type", "_subscriptions")
|
|
36
|
+
|
|
37
|
+
def __init__(self, subscriptions: tuple[LocalEventSubscription, ...]) -> None:
|
|
38
|
+
grouped: dict[tuple[LocalEventPhase, type[object]], list[LocalEventSubscription]] = {}
|
|
39
|
+
for subscription in subscriptions:
|
|
40
|
+
grouped.setdefault((subscription.phase, subscription.event_type), []).append(
|
|
41
|
+
subscription
|
|
42
|
+
)
|
|
43
|
+
self._subscriptions = subscriptions
|
|
44
|
+
self._by_phase_and_type: Mapping[
|
|
45
|
+
tuple[LocalEventPhase, type[object]], tuple[LocalEventSubscription, ...]
|
|
46
|
+
] = MappingProxyType({key: tuple(values) for key, values in grouped.items()})
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def subscriptions(self) -> tuple[LocalEventSubscription, ...]:
|
|
50
|
+
return self._subscriptions
|
|
51
|
+
|
|
52
|
+
def for_event(
|
|
53
|
+
self,
|
|
54
|
+
event_type: type[object],
|
|
55
|
+
phase: LocalEventPhase,
|
|
56
|
+
) -> tuple[LocalEventSubscription, ...]:
|
|
57
|
+
return self._by_phase_and_type.get((phase, event_type), ())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _build_event_catalog(
|
|
61
|
+
registry: ModuleRegistry,
|
|
62
|
+
contributions: EventContributionRegistry,
|
|
63
|
+
) -> LocalEventCatalog:
|
|
64
|
+
module_positions = {
|
|
65
|
+
descriptor.key: index for index, descriptor in enumerate(registry.dependency_order)
|
|
66
|
+
}
|
|
67
|
+
entries = sorted(
|
|
68
|
+
contributions.freeze(),
|
|
69
|
+
key=lambda item: (module_positions[item.owner], not item.automatic, item.declaration_index),
|
|
70
|
+
)
|
|
71
|
+
seen: dict[tuple[type[object], object, LocalEventPhase], SourceLocation] = {}
|
|
72
|
+
subscriptions: list[LocalEventSubscription] = []
|
|
73
|
+
for entry in entries:
|
|
74
|
+
identity = (entry.event_type, entry.handler_type, entry.phase)
|
|
75
|
+
if identity in seen:
|
|
76
|
+
raise EventContributionError(
|
|
77
|
+
reason=(
|
|
78
|
+
f"duplicate {entry.phase.value} subscription for "
|
|
79
|
+
f"{entry.event_type.__qualname__}/{entry.handler_type.__qualname__}; "
|
|
80
|
+
f"first={seen[identity]}, duplicate={entry.source}"
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
seen[identity] = entry.source
|
|
84
|
+
handler = (
|
|
85
|
+
inspect.getattr_static(entry.handler_type, "handle", None)
|
|
86
|
+
if isinstance(entry.handler_type, type)
|
|
87
|
+
else entry.handler_type
|
|
88
|
+
)
|
|
89
|
+
if isinstance(handler, (staticmethod, classmethod)) or not inspect.iscoroutinefunction(
|
|
90
|
+
handler
|
|
91
|
+
):
|
|
92
|
+
raise EventContributionError(
|
|
93
|
+
reason=(
|
|
94
|
+
f"{entry.handler_type.__module__}.{entry.handler_type.__qualname__}.handle "
|
|
95
|
+
"must be an instance async method"
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
subscriptions.append(
|
|
99
|
+
LocalEventSubscription(
|
|
100
|
+
event_type=entry.event_type,
|
|
101
|
+
handler_type=entry.handler_type,
|
|
102
|
+
phase=entry.phase,
|
|
103
|
+
owner=entry.owner,
|
|
104
|
+
source=entry.source,
|
|
105
|
+
reason=entry.reason,
|
|
106
|
+
declaration_index=entry.declaration_index,
|
|
107
|
+
order=len(subscriptions),
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
return LocalEventCatalog(tuple(subscriptions))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _validate_event_scopes(
|
|
114
|
+
catalog: LocalEventCatalog,
|
|
115
|
+
accepted_single_scopes: Mapping[type[object], BaseScope],
|
|
116
|
+
) -> None:
|
|
117
|
+
# 原生 alias 的生命周期只有最终 Dishka 图才能确定,不在贡献阶段猜测。
|
|
118
|
+
for subscription in catalog.subscriptions:
|
|
119
|
+
expected_scope = (
|
|
120
|
+
Scope.ACTION if subscription.phase is LocalEventPhase.DOMAIN else Scope.REQUEST
|
|
121
|
+
)
|
|
122
|
+
actual_scope = (
|
|
123
|
+
accepted_single_scopes.get(subscription.handler_type)
|
|
124
|
+
if isinstance(subscription.handler_type, type)
|
|
125
|
+
else expected_scope
|
|
126
|
+
)
|
|
127
|
+
if actual_scope is not expected_scope:
|
|
128
|
+
actual_label = "unregistered" if actual_scope is None else actual_scope.name
|
|
129
|
+
raise EventContributionError(
|
|
130
|
+
reason=(
|
|
131
|
+
f"{subscription.handler_type.__module__}."
|
|
132
|
+
f"{subscription.handler_type.__qualname__} must be "
|
|
133
|
+
f"independently resolvable from an accepted single registration in "
|
|
134
|
+
f"{expected_scope.name} scope, got {actual_label}"
|
|
135
|
+
)
|
|
136
|
+
)
|