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,174 @@
|
|
|
1
|
+
"""单个 Application 拥有的 append-only diagnostic journal。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
from time import perf_counter_ns
|
|
7
|
+
from uuid import UUID, uuid4
|
|
8
|
+
|
|
9
|
+
from ..errors import InvalidDiagnosticCodeError
|
|
10
|
+
from .model import (
|
|
11
|
+
DiagnosticMessage,
|
|
12
|
+
DiagnosticSnapshot,
|
|
13
|
+
EventStatus,
|
|
14
|
+
FailureRecord,
|
|
15
|
+
LifecyclePhase,
|
|
16
|
+
LifecycleStage,
|
|
17
|
+
PhaseEvent,
|
|
18
|
+
SourceLocation,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_DIAGNOSTIC_CODE_PUNCTUATION = frozenset("._-")
|
|
22
|
+
_MAX_DIAGNOSTIC_CODE_LENGTH = 128
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class _ActiveEvent:
|
|
27
|
+
phase: LifecyclePhase
|
|
28
|
+
stage: LifecycleStage
|
|
29
|
+
subject: str
|
|
30
|
+
source: SourceLocation | None
|
|
31
|
+
started_ns: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _DiagnosticJournal:
|
|
35
|
+
"""Application-owned journal;外部只能取得不可变的时点快照。"""
|
|
36
|
+
|
|
37
|
+
__slots__ = ("_events", "_failure_order", "_failures", "_messages", "_sequence")
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
self._sequence = 0
|
|
41
|
+
self._events: list[PhaseEvent] = []
|
|
42
|
+
self._messages: list[DiagnosticMessage] = []
|
|
43
|
+
self._failure_order: list[UUID] = []
|
|
44
|
+
self._failures: dict[UUID, FailureRecord] = {}
|
|
45
|
+
|
|
46
|
+
def _next_sequence(self) -> int:
|
|
47
|
+
self._sequence += 1
|
|
48
|
+
return self._sequence
|
|
49
|
+
|
|
50
|
+
def start_event(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
phase: LifecyclePhase,
|
|
54
|
+
stage: LifecycleStage,
|
|
55
|
+
subject: str,
|
|
56
|
+
source: SourceLocation | None,
|
|
57
|
+
) -> _ActiveEvent:
|
|
58
|
+
started_ns = perf_counter_ns()
|
|
59
|
+
active_event = _ActiveEvent(phase, stage, subject, source, started_ns)
|
|
60
|
+
self._events.append(
|
|
61
|
+
PhaseEvent(
|
|
62
|
+
sequence=self._next_sequence(),
|
|
63
|
+
phase=phase,
|
|
64
|
+
stage=stage,
|
|
65
|
+
subject=subject,
|
|
66
|
+
status=EventStatus.STARTED,
|
|
67
|
+
started_monotonic_ns=started_ns,
|
|
68
|
+
duration_ns=None,
|
|
69
|
+
source=source,
|
|
70
|
+
failure_id=None,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
return active_event
|
|
74
|
+
|
|
75
|
+
def finish_event(
|
|
76
|
+
self,
|
|
77
|
+
active_event: _ActiveEvent,
|
|
78
|
+
*,
|
|
79
|
+
status: EventStatus,
|
|
80
|
+
failure_id: UUID | None,
|
|
81
|
+
) -> int:
|
|
82
|
+
# start 返回的 value object 是一次事件的唯一事实,finish 不重复传递上下文。
|
|
83
|
+
duration_ns = perf_counter_ns() - active_event.started_ns
|
|
84
|
+
self._events.append(
|
|
85
|
+
PhaseEvent(
|
|
86
|
+
sequence=self._next_sequence(),
|
|
87
|
+
phase=active_event.phase,
|
|
88
|
+
stage=active_event.stage,
|
|
89
|
+
subject=active_event.subject,
|
|
90
|
+
status=status,
|
|
91
|
+
started_monotonic_ns=active_event.started_ns,
|
|
92
|
+
duration_ns=duration_ns,
|
|
93
|
+
source=active_event.source,
|
|
94
|
+
failure_id=failure_id,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
return duration_ns
|
|
98
|
+
|
|
99
|
+
def begin_failure(
|
|
100
|
+
self,
|
|
101
|
+
*,
|
|
102
|
+
error_type: str,
|
|
103
|
+
phase: LifecyclePhase,
|
|
104
|
+
stage: LifecycleStage,
|
|
105
|
+
subject: str | None,
|
|
106
|
+
message: str,
|
|
107
|
+
source: SourceLocation | None,
|
|
108
|
+
dependency_path: tuple[str, ...],
|
|
109
|
+
) -> UUID:
|
|
110
|
+
failure_id = uuid4()
|
|
111
|
+
# 先发布不可变记录,rollback 后替换 cleanup 关联,snapshot 不出现悬空引用。
|
|
112
|
+
failure = FailureRecord(
|
|
113
|
+
id=failure_id,
|
|
114
|
+
error_type=error_type,
|
|
115
|
+
phase=phase,
|
|
116
|
+
stage=stage,
|
|
117
|
+
subject=subject,
|
|
118
|
+
message=message,
|
|
119
|
+
source=source,
|
|
120
|
+
dependency_path=dependency_path,
|
|
121
|
+
cleanup_failure_ids=(),
|
|
122
|
+
)
|
|
123
|
+
self._failure_order.append(failure_id)
|
|
124
|
+
self._failures[failure_id] = failure
|
|
125
|
+
return failure_id
|
|
126
|
+
|
|
127
|
+
def complete_failure(
|
|
128
|
+
self,
|
|
129
|
+
failure_id: UUID,
|
|
130
|
+
*,
|
|
131
|
+
cleanup_failure_ids: tuple[UUID, ...] = (),
|
|
132
|
+
) -> None:
|
|
133
|
+
self._failures[failure_id] = replace(
|
|
134
|
+
self._failures[failure_id], cleanup_failure_ids=cleanup_failure_ids
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def emit(
|
|
138
|
+
self,
|
|
139
|
+
*,
|
|
140
|
+
phase: LifecyclePhase,
|
|
141
|
+
stage: LifecycleStage,
|
|
142
|
+
subject: str,
|
|
143
|
+
code: str,
|
|
144
|
+
source: SourceLocation | None,
|
|
145
|
+
) -> None:
|
|
146
|
+
if not _is_diagnostic_code(code):
|
|
147
|
+
raise InvalidDiagnosticCodeError(code, max_length=_MAX_DIAGNOSTIC_CODE_LENGTH)
|
|
148
|
+
self._messages.append(
|
|
149
|
+
DiagnosticMessage(
|
|
150
|
+
sequence=self._next_sequence(),
|
|
151
|
+
phase=phase,
|
|
152
|
+
stage=stage,
|
|
153
|
+
subject=subject,
|
|
154
|
+
code=code,
|
|
155
|
+
source=source,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def snapshot(self, application_id: UUID) -> DiagnosticSnapshot:
|
|
160
|
+
failures = tuple(self._failures[failure_id] for failure_id in self._failure_order)
|
|
161
|
+
return DiagnosticSnapshot(
|
|
162
|
+
application_id=application_id,
|
|
163
|
+
events=tuple(self._events),
|
|
164
|
+
failures=failures,
|
|
165
|
+
messages=tuple(self._messages),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _is_diagnostic_code(code: str) -> bool:
|
|
170
|
+
if not code or len(code) > _MAX_DIAGNOSTIC_CODE_LENGTH or not code[0].isalpha():
|
|
171
|
+
return False
|
|
172
|
+
return all(
|
|
173
|
+
character.isalnum() or character in _DIAGNOSTIC_CODE_PUNCTUATION for character in code
|
|
174
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""不可变 diagnostic contract;不拥有 journal mutation。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LifecyclePhase(str, Enum):
|
|
11
|
+
COMPOSITION = "composition"
|
|
12
|
+
RUNTIME = "runtime"
|
|
13
|
+
CLEANUP = "cleanup"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LifecycleStage(str, Enum):
|
|
17
|
+
PRE_CONFIGURE = "pre_configure"
|
|
18
|
+
CONFIGURE = "configure"
|
|
19
|
+
POST_CONFIGURE = "post_configure"
|
|
20
|
+
PRE_INITIALIZE = "pre_initialize"
|
|
21
|
+
INITIALIZE = "initialize"
|
|
22
|
+
POST_INITIALIZE = "post_initialize"
|
|
23
|
+
SHUTDOWN = "shutdown"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EventStatus(str, Enum):
|
|
27
|
+
STARTED = "started"
|
|
28
|
+
SUCCEEDED = "succeeded"
|
|
29
|
+
FAILED = "failed"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class SourceLocation:
|
|
34
|
+
owner: str
|
|
35
|
+
file: str | None
|
|
36
|
+
line: int | None
|
|
37
|
+
symbol: str | None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class PhaseEvent:
|
|
42
|
+
sequence: int
|
|
43
|
+
phase: LifecyclePhase
|
|
44
|
+
stage: LifecycleStage
|
|
45
|
+
subject: str
|
|
46
|
+
status: EventStatus
|
|
47
|
+
started_monotonic_ns: int
|
|
48
|
+
duration_ns: int | None
|
|
49
|
+
source: SourceLocation | None
|
|
50
|
+
failure_id: UUID | None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class FailureRecord:
|
|
55
|
+
id: UUID
|
|
56
|
+
error_type: str
|
|
57
|
+
phase: LifecyclePhase
|
|
58
|
+
stage: LifecycleStage
|
|
59
|
+
subject: str | None
|
|
60
|
+
message: str
|
|
61
|
+
source: SourceLocation | None
|
|
62
|
+
dependency_path: tuple[str, ...]
|
|
63
|
+
cleanup_failure_ids: tuple[UUID, ...]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True)
|
|
67
|
+
class DiagnosticMessage:
|
|
68
|
+
sequence: int
|
|
69
|
+
phase: LifecyclePhase
|
|
70
|
+
stage: LifecycleStage
|
|
71
|
+
subject: str
|
|
72
|
+
code: str
|
|
73
|
+
source: SourceLocation | None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True, slots=True)
|
|
77
|
+
class DiagnosticSnapshot:
|
|
78
|
+
"""一次读取产生的不可变诊断视图,不随后续 lifecycle 变化。"""
|
|
79
|
+
|
|
80
|
+
application_id: UUID
|
|
81
|
+
events: tuple[PhaseEvent, ...]
|
|
82
|
+
failures: tuple[FailureRecord, ...]
|
|
83
|
+
messages: tuple[DiagnosticMessage, ...]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""贡献诊断的调用位置;depth 由公开贡献入口指定。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
|
|
7
|
+
from .model import SourceLocation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def caller_source(owner: str, *, depth: int) -> SourceLocation:
|
|
11
|
+
frame = inspect.currentframe()
|
|
12
|
+
try:
|
|
13
|
+
for _ in range(depth):
|
|
14
|
+
frame = None if frame is None else frame.f_back
|
|
15
|
+
if frame is None:
|
|
16
|
+
return SourceLocation(owner=owner, file=None, line=None, symbol=None)
|
|
17
|
+
return SourceLocation(
|
|
18
|
+
owner=owner,
|
|
19
|
+
file=frame.f_code.co_filename,
|
|
20
|
+
line=frame.f_lineno,
|
|
21
|
+
symbol=frame.f_code.co_name,
|
|
22
|
+
)
|
|
23
|
+
finally:
|
|
24
|
+
# 不让 frame 引用延长 Module/composition 对象的生命周期。
|
|
25
|
+
del frame
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""锁的业务使用与错误边界;provider 不进入稳定契约。"""
|
|
2
|
+
|
|
3
|
+
from contextlib import AbstractAsyncContextManager
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from ..errors.base import FrameworkError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DistributedLock(Protocol):
|
|
10
|
+
def acquire(self, key: str, /) -> AbstractAsyncContextManager[bool]:
|
|
11
|
+
"""进入后返回是否取得锁;持有者必须配合取消并在退出上下文前完成清理。"""
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DistributedLockError(FrameworkError):
|
|
16
|
+
def __init__(self, *, key: str, operation: str, error_type: str) -> None:
|
|
17
|
+
self.key = key
|
|
18
|
+
self.operation = operation
|
|
19
|
+
self.error_type = error_type
|
|
20
|
+
super().__init__(f"Distributed lock {key!r} {operation} failed with {error_type}")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""获取等待和租约时序的唯一默认值来源。"""
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, model_validator
|
|
6
|
+
|
|
7
|
+
from ..options import BaseOptions
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DistributedLockOptions(BaseOptions):
|
|
11
|
+
wait_timeout: timedelta = Field(default=timedelta(seconds=5), ge=timedelta(0))
|
|
12
|
+
lease_timeout: timedelta = Field(default=timedelta(seconds=30), ge=timedelta(milliseconds=1))
|
|
13
|
+
renewal_interval: timedelta = Field(default=timedelta(seconds=10), ge=timedelta(milliseconds=1))
|
|
14
|
+
|
|
15
|
+
@model_validator(mode="after")
|
|
16
|
+
def _validate_renewal(self) -> "DistributedLockOptions":
|
|
17
|
+
if self.renewal_interval >= self.lease_timeout:
|
|
18
|
+
raise ValueError("renewal_interval must be shorter than lease_timeout")
|
|
19
|
+
return self
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Provider-neutral DDD foundations owned by Python DDD Framework."""
|
|
2
|
+
|
|
3
|
+
from .aggregates import AggregateRoot
|
|
4
|
+
from .entities import Entity
|
|
5
|
+
from .errors import OptimisticConcurrencyError
|
|
6
|
+
from .value_objects import ValueObject
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
"AggregateRoot",
|
|
10
|
+
"Entity",
|
|
11
|
+
"OptimisticConcurrencyError",
|
|
12
|
+
"ValueObject",
|
|
13
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Aggregate identity, version and pending local-event ownership."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import is_dataclass
|
|
6
|
+
from typing import Generic, TypeVar
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from .entities import Entity
|
|
11
|
+
from .errors import OptimisticConcurrencyError
|
|
12
|
+
|
|
13
|
+
_TId = TypeVar("_TId")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AggregateRoot(Entity[_TId], Generic[_TId]):
|
|
17
|
+
"""一个 invocation 内聚合版本和待提交事件的唯一 mutable owner。"""
|
|
18
|
+
|
|
19
|
+
__slots__ = ("_local_events", "_version", "_version_staged")
|
|
20
|
+
|
|
21
|
+
def __init__(self, entity_id: _TId, *, version: int = 0) -> None:
|
|
22
|
+
super().__init__(entity_id)
|
|
23
|
+
if not isinstance(version, int) or isinstance(version, bool) or version < 0:
|
|
24
|
+
raise ValueError("aggregate version must be a non-negative integer")
|
|
25
|
+
self._version = version
|
|
26
|
+
self._version_staged = False
|
|
27
|
+
self._local_events: list[object] = []
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def version(self) -> int:
|
|
31
|
+
return self._version
|
|
32
|
+
|
|
33
|
+
def raise_local_event(self, event: object) -> None:
|
|
34
|
+
if not _is_immutable_event(event):
|
|
35
|
+
raise TypeError("aggregate local events must be immutable dataclass or Pydantic values")
|
|
36
|
+
self._local_events.append(event)
|
|
37
|
+
|
|
38
|
+
def release_local_events(self) -> tuple[object, ...]:
|
|
39
|
+
events = tuple(self._local_events)
|
|
40
|
+
self._local_events.clear()
|
|
41
|
+
return events
|
|
42
|
+
|
|
43
|
+
def stage_next_version(self, expected_version: int) -> int:
|
|
44
|
+
"""Repository SPI:一次聚合实例只允许暂存一个下一版本。"""
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
not isinstance(expected_version, int)
|
|
48
|
+
or isinstance(expected_version, bool)
|
|
49
|
+
or expected_version < 0
|
|
50
|
+
):
|
|
51
|
+
raise ValueError("expected_version must be a non-negative integer")
|
|
52
|
+
if self._version_staged or self._version != expected_version:
|
|
53
|
+
raise OptimisticConcurrencyError
|
|
54
|
+
self._version += 1
|
|
55
|
+
self._version_staged = True
|
|
56
|
+
return self._version
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_immutable_event(event: object) -> bool:
|
|
60
|
+
event_type = type(event)
|
|
61
|
+
dataclass_parameters = getattr(event_type, "__dataclass_params__", None)
|
|
62
|
+
if is_dataclass(event) and bool(getattr(dataclass_parameters, "frozen", False)):
|
|
63
|
+
return True
|
|
64
|
+
return isinstance(event, BaseModel) and event.model_config.get("frozen") is True
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Entity identity semantics independent from persistence providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
_TId = TypeVar("_TId")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Entity(Generic[_TId]):
|
|
11
|
+
"""Identity owner whose identifier cannot change after construction."""
|
|
12
|
+
|
|
13
|
+
__slots__ = ("_id",)
|
|
14
|
+
|
|
15
|
+
def __init__(self, entity_id: _TId) -> None:
|
|
16
|
+
if entity_id is None:
|
|
17
|
+
raise ValueError("entity id cannot be None")
|
|
18
|
+
self._id = entity_id
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def id(self) -> _TId:
|
|
22
|
+
return self._id
|
|
23
|
+
|
|
24
|
+
def __eq__(self, other: object) -> bool:
|
|
25
|
+
if self is other:
|
|
26
|
+
return True
|
|
27
|
+
if type(self) is not type(other):
|
|
28
|
+
return False
|
|
29
|
+
return self.id == other.id
|
|
30
|
+
|
|
31
|
+
def __hash__(self) -> int:
|
|
32
|
+
# 类型是 identity 的一部分,避免不同聚合偶然共用相同标识时发生碰撞语义。
|
|
33
|
+
return hash((type(self), self.id))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""DDD boundary failures safe for transport mapping."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..errors import ResourceConflictError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OptimisticConcurrencyError(ResourceConflictError):
|
|
9
|
+
"""调用方版本与持久化 authority 不一致;不携带真实行版本或 SQL。"""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
super().__init__("Aggregate version conflict")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Pydantic-backed immutable domain values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ValueObject(BaseModel):
|
|
11
|
+
"""按具体类型与字段值比较的领域值;它不是 transport DTO。"""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
|
|
17
|
+
super().__pydantic_init_subclass__(**kwargs)
|
|
18
|
+
if cls.model_config.get("frozen") is not True or cls.model_config.get("extra") != "forbid":
|
|
19
|
+
raise TypeError("ValueObject subclasses must remain frozen and forbid extra fields")
|
|
20
|
+
|
|
21
|
+
def __eq__(self, other: object) -> bool:
|
|
22
|
+
if type(self) is not type(other):
|
|
23
|
+
return False
|
|
24
|
+
return self._field_values() == other._field_values()
|
|
25
|
+
|
|
26
|
+
def __hash__(self) -> int:
|
|
27
|
+
# 字段中含 list/dict 等 mutable value 时由 Python 原生 hash fail closed。
|
|
28
|
+
return hash((type(self), self._field_values()))
|
|
29
|
+
|
|
30
|
+
def _field_values(self) -> tuple[object, ...]:
|
|
31
|
+
return tuple(getattr(self, name) for name in type(self).model_fields)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""按 capability 组织的 Python DDD Framework 结构化错误公开边界。"""
|
|
2
|
+
|
|
3
|
+
from .base import FrameworkError, ResourceConflictError, ResourceNotFoundError
|
|
4
|
+
from .business import BusinessError, BusinessErrorDefinition, BusinessErrorField
|
|
5
|
+
from .configuration import (
|
|
6
|
+
ConfigurationSourceError,
|
|
7
|
+
DuplicateOptionDefinitionError,
|
|
8
|
+
InvalidOptionDefinitionError,
|
|
9
|
+
OptionsContributionClosedError,
|
|
10
|
+
OptionsValidationError,
|
|
11
|
+
UnknownOptionsError,
|
|
12
|
+
)
|
|
13
|
+
from .diagnostics import InvalidDiagnosticCodeError
|
|
14
|
+
from .lifecycle import (
|
|
15
|
+
ApplicationRuntimeError,
|
|
16
|
+
ApplicationStartError,
|
|
17
|
+
CompositionResourceError,
|
|
18
|
+
InvalidApplicationStateError,
|
|
19
|
+
LifecycleInvocationError,
|
|
20
|
+
LifecycleReturnValueError,
|
|
21
|
+
)
|
|
22
|
+
from .modularity import (
|
|
23
|
+
DuplicateModuleIdentityError,
|
|
24
|
+
GraphInvariantError,
|
|
25
|
+
InvalidModuleReferenceError,
|
|
26
|
+
ModuleCycleError,
|
|
27
|
+
ModuleInstantiationError,
|
|
28
|
+
ModuleResolutionError,
|
|
29
|
+
UnknownModuleError,
|
|
30
|
+
)
|
|
31
|
+
from .services import (
|
|
32
|
+
CatalogNativeGraphMismatchError,
|
|
33
|
+
ContainerCleanupError,
|
|
34
|
+
ContainerValidationError,
|
|
35
|
+
ConventionScanError,
|
|
36
|
+
InvalidConventionDefinitionError,
|
|
37
|
+
RegistrationConflictError,
|
|
38
|
+
ServiceContributorClosedError,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = (
|
|
42
|
+
"ApplicationRuntimeError",
|
|
43
|
+
"ApplicationStartError",
|
|
44
|
+
"BusinessError",
|
|
45
|
+
"BusinessErrorDefinition",
|
|
46
|
+
"BusinessErrorField",
|
|
47
|
+
"CatalogNativeGraphMismatchError",
|
|
48
|
+
"CompositionResourceError",
|
|
49
|
+
"ConfigurationSourceError",
|
|
50
|
+
"ContainerCleanupError",
|
|
51
|
+
"ContainerValidationError",
|
|
52
|
+
"ConventionScanError",
|
|
53
|
+
"DuplicateModuleIdentityError",
|
|
54
|
+
"DuplicateOptionDefinitionError",
|
|
55
|
+
"FrameworkError",
|
|
56
|
+
"GraphInvariantError",
|
|
57
|
+
"InvalidApplicationStateError",
|
|
58
|
+
"InvalidConventionDefinitionError",
|
|
59
|
+
"InvalidDiagnosticCodeError",
|
|
60
|
+
"InvalidModuleReferenceError",
|
|
61
|
+
"InvalidOptionDefinitionError",
|
|
62
|
+
"LifecycleInvocationError",
|
|
63
|
+
"LifecycleReturnValueError",
|
|
64
|
+
"ModuleCycleError",
|
|
65
|
+
"ModuleInstantiationError",
|
|
66
|
+
"ModuleResolutionError",
|
|
67
|
+
"OptionsContributionClosedError",
|
|
68
|
+
"OptionsValidationError",
|
|
69
|
+
"RegistrationConflictError",
|
|
70
|
+
"ResourceConflictError",
|
|
71
|
+
"ResourceNotFoundError",
|
|
72
|
+
"ServiceContributorClosedError",
|
|
73
|
+
"UnknownModuleError",
|
|
74
|
+
"UnknownOptionsError",
|
|
75
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""所有 capability error 共享的最小命名根。"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FrameworkError(Exception):
|
|
5
|
+
"""所有 Python DDD Framework 结构化错误的命名基类。"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResourceConflictError(FrameworkError):
|
|
9
|
+
"""可信业务边界可映射为 HTTP 409 的静态冲突。"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ResourceNotFoundError(FrameworkError):
|
|
13
|
+
"""可信业务边界可映射为 HTTP 404 的静态缺失。"""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""业务模块集中定义的公开安全错误,不把异常正文或动态输入当作响应。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from .base import FrameworkError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class BusinessErrorDefinition:
|
|
14
|
+
code: str
|
|
15
|
+
message: str
|
|
16
|
+
|
|
17
|
+
def __post_init__(self) -> None:
|
|
18
|
+
if (
|
|
19
|
+
not isinstance(self.code, str)
|
|
20
|
+
or not self.code
|
|
21
|
+
or any(not (part.isascii() and (part.isalnum() or part in "._")) for part in self.code)
|
|
22
|
+
):
|
|
23
|
+
raise ValueError("business error code must be a stable non-empty identifier")
|
|
24
|
+
if not isinstance(self.message, str) or not self.message.strip():
|
|
25
|
+
raise ValueError("business error message must be a static safe message")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class BusinessErrorField:
|
|
30
|
+
model_type: type[BaseModel]
|
|
31
|
+
name: str
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
# 字段必须来自静态 DTO contract,不能把运行时字典键或用户输入混入定位信息。
|
|
35
|
+
if not isinstance(self.model_type, type) or not issubclass(self.model_type, BaseModel):
|
|
36
|
+
raise TypeError("business error field requires a Pydantic model type")
|
|
37
|
+
if self.name not in self.model_type.model_fields:
|
|
38
|
+
raise ValueError("business error field must name a declared model field")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BusinessError(FrameworkError):
|
|
42
|
+
def __init__(
|
|
43
|
+
self, definition: BusinessErrorDefinition, *, fields: tuple[BusinessErrorField, ...] = ()
|
|
44
|
+
) -> None:
|
|
45
|
+
if not isinstance(definition, BusinessErrorDefinition):
|
|
46
|
+
raise TypeError("business error requires a static definition")
|
|
47
|
+
if not isinstance(fields, tuple) or any(
|
|
48
|
+
not isinstance(field, BusinessErrorField) for field in fields
|
|
49
|
+
):
|
|
50
|
+
raise TypeError("business error fields must be an immutable tuple")
|
|
51
|
+
self.definition = definition
|
|
52
|
+
self.fields = fields
|
|
53
|
+
super().__init__(definition.code)
|