python-platform 0.1.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_platform/__init__.py +445 -0
- python_platform/application/__init__.py +6 -0
- python_platform/application/build_spec.py +37 -0
- python_platform/application/builder.py +260 -0
- python_platform/application/composition.py +219 -0
- python_platform/application/runtime.py +546 -0
- python_platform/application/runtime_hooks.py +327 -0
- python_platform/application/state.py +216 -0
- python_platform/application_services/__init__.py +54 -0
- python_platform/application_services/catalog.py +500 -0
- python_platform/application_services/contracts.py +188 -0
- python_platform/application_services/dispatcher.py +423 -0
- python_platform/application_services/errors.py +89 -0
- python_platform/application_services/execution.py +199 -0
- python_platform/application_services/interceptors.py +44 -0
- python_platform/application_services/invocation.py +427 -0
- python_platform/application_services/policies.py +46 -0
- python_platform/application_services/seeding.py +72 -0
- python_platform/application_services/signature.py +49 -0
- python_platform/application_services/validation.py +84 -0
- python_platform/auditing/__init__.py +12 -0
- python_platform/auditing/contracts.py +39 -0
- python_platform/auditing/control.py +48 -0
- python_platform/auditing/sqlalchemy/__init__.py +6 -0
- python_platform/auditing/sqlalchemy/migrations/0001_platform_auditing.py +45 -0
- python_platform/auditing/sqlalchemy/migrations/__init__.py +1 -0
- python_platform/auditing/sqlalchemy/models.py +31 -0
- python_platform/auditing/sqlalchemy/module.py +32 -0
- python_platform/auditing/sqlalchemy/store.py +63 -0
- python_platform/authorization/__init__.py +38 -0
- python_platform/authorization/catalog.py +63 -0
- python_platform/authorization/contracts.py +241 -0
- python_platform/authorization/definition_discovery.py +81 -0
- python_platform/authorization/definitions.py +43 -0
- python_platform/authorization/errors.py +40 -0
- python_platform/background_execution/__init__.py +15 -0
- python_platform/background_execution/application.py +41 -0
- python_platform/background_execution/child.py +123 -0
- python_platform/background_execution/contracts.py +39 -0
- python_platform/background_execution/lifecycle.py +22 -0
- python_platform/background_execution/local.py +127 -0
- python_platform/background_execution/locks.py +31 -0
- python_platform/background_execution/management.py +17 -0
- python_platform/background_execution/module.py +10 -0
- python_platform/background_execution/permissions.py +14 -0
- python_platform/background_execution/processes.py +331 -0
- python_platform/background_jobs/__init__.py +31 -0
- python_platform/background_jobs/catalog.py +287 -0
- python_platform/background_jobs/contracts.py +202 -0
- python_platform/background_jobs/errors.py +28 -0
- python_platform/background_jobs/execution.py +201 -0
- python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
- python_platform/background_jobs/pgqueuer/__init__.py +6 -0
- python_platform/background_jobs/pgqueuer/enqueue.py +82 -0
- python_platform/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +63 -0
- python_platform/background_jobs/pgqueuer/migrations/__init__.py +1 -0
- python_platform/background_jobs/pgqueuer/module.py +102 -0
- python_platform/background_jobs/pgqueuer/options.py +70 -0
- python_platform/background_jobs/pgqueuer/runtime.py +479 -0
- python_platform/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
- python_platform/background_jobs/pgqueuer/supervision.py +201 -0
- python_platform/background_workers/__init__.py +22 -0
- python_platform/background_workers/catalog.py +96 -0
- python_platform/background_workers/contracts.py +124 -0
- python_platform/background_workers/errors.py +20 -0
- python_platform/background_workers/execution.py +133 -0
- python_platform/background_workers/runtime.py +203 -0
- python_platform/caching/__init__.py +11 -0
- python_platform/caching/catalog.py +73 -0
- python_platform/caching/contracts.py +73 -0
- python_platform/caching/errors.py +20 -0
- python_platform/cli/__init__.py +87 -0
- python_platform/cli/errors.py +5 -0
- python_platform/cli/project.py +69 -0
- python_platform/cli/runtime.py +68 -0
- python_platform/configuration/__init__.py +21 -0
- python_platform/configuration/composition.py +65 -0
- python_platform/configuration/contracts.py +68 -0
- python_platform/configuration/dotenv_source.py +51 -0
- python_platform/configuration/environment_source.py +38 -0
- python_platform/configuration/immutability.py +124 -0
- python_platform/configuration/input_shape.py +58 -0
- python_platform/configuration/merge.py +103 -0
- python_platform/configuration/root.py +215 -0
- python_platform/configuration/sources.py +519 -0
- python_platform/configuration/values.py +427 -0
- python_platform/configuration/yaml_source.py +51 -0
- python_platform/developer_kit/__init__.py +1 -0
- python_platform/developer_kit/generation.py +182 -0
- python_platform/developer_kit/project_metadata.py +45 -0
- python_platform/developer_kit/source.py +53 -0
- python_platform/developer_kit/templates/module/cookiecutter.json +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +46 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +29 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +8 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +81 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +86 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +36 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +57 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +12 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +74 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +58 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +37 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models.py.jinja +26 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +32 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repository.py.jinja +49 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
- python_platform/developer_kit/templates/project/cookiecutter.json +6 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +8 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.env.jinja +13 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +86 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/app.yaml.jinja +16 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +13 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +52 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +38 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +101 -0
- python_platform/developer_kit/wiring.py +85 -0
- python_platform/diagnostics/__init__.py +23 -0
- python_platform/diagnostics/journal.py +174 -0
- python_platform/diagnostics/model.py +83 -0
- python_platform/diagnostics/source.py +25 -0
- python_platform/distributed_lock/__init__.py +6 -0
- python_platform/distributed_lock/contracts.py +20 -0
- python_platform/distributed_lock/options.py +19 -0
- python_platform/domain/__init__.py +13 -0
- python_platform/domain/aggregates.py +64 -0
- python_platform/domain/entities.py +33 -0
- python_platform/domain/errors.py +12 -0
- python_platform/domain/value_objects.py +31 -0
- python_platform/errors/__init__.py +75 -0
- python_platform/errors/base.py +13 -0
- python_platform/errors/business.py +53 -0
- python_platform/errors/configuration.py +119 -0
- python_platform/errors/diagnostics.py +12 -0
- python_platform/errors/lifecycle.py +164 -0
- python_platform/errors/modularity.py +100 -0
- python_platform/errors/services.py +100 -0
- python_platform/events/__init__.py +25 -0
- python_platform/events/aggregate.py +39 -0
- python_platform/events/catalog.py +136 -0
- python_platform/events/contracts.py +39 -0
- python_platform/events/contribution.py +128 -0
- python_platform/events/discovery.py +105 -0
- python_platform/events/errors.py +48 -0
- python_platform/events/runtime.py +192 -0
- python_platform/fastapi/__init__.py +49 -0
- python_platform/fastapi/action.py +175 -0
- python_platform/fastapi/adapter.py +381 -0
- python_platform/fastapi/application_services.py +647 -0
- python_platform/fastapi/background.py +42 -0
- python_platform/fastapi/contracts.py +278 -0
- python_platform/fastapi/errors.py +32 -0
- python_platform/fastapi/filters.py +111 -0
- python_platform/fastapi/health.py +44 -0
- python_platform/fastapi/http.py +55 -0
- python_platform/fastapi/http_router.py +212 -0
- python_platform/fastapi/manual_action.py +105 -0
- python_platform/fastapi/middleware.py +93 -0
- python_platform/fastapi/parameters.py +97 -0
- python_platform/fastapi/realtime/__init__.py +13 -0
- python_platform/fastapi/realtime/authentication.py +181 -0
- python_platform/fastapi/realtime/connection.py +166 -0
- python_platform/fastapi/realtime/module.py +41 -0
- python_platform/fastapi/realtime/options.py +50 -0
- python_platform/fastapi/realtime/runtime.py +423 -0
- python_platform/fastapi/request_context.py +244 -0
- python_platform/fastapi/route_integrity.py +88 -0
- python_platform/fastapi/routing.py +410 -0
- python_platform/fastapi/server.py +181 -0
- python_platform/fastapi/settings.py +40 -0
- python_platform/fastapi/tracing.py +62 -0
- python_platform/fastapi/transfer.py +128 -0
- python_platform/fastapi/upload_limits.py +63 -0
- python_platform/fastapi/uploads.py +94 -0
- python_platform/hosted_services/__init__.py +29 -0
- python_platform/hosted_services/bridge.py +420 -0
- python_platform/hosted_services/catalog.py +259 -0
- python_platform/hosted_services/contracts.py +55 -0
- python_platform/hosted_services/errors.py +53 -0
- python_platform/hosted_services/options.py +17 -0
- python_platform/hosted_services/runtime.py +267 -0
- python_platform/hosted_services/state.py +40 -0
- python_platform/hosting/__init__.py +4 -0
- python_platform/hosting/instance.py +40 -0
- python_platform/identity/__init__.py +73 -0
- python_platform/identity/application.py +287 -0
- python_platform/identity/contracts.py +283 -0
- python_platform/identity/errors.py +28 -0
- python_platform/identity/http_api.py +90 -0
- python_platform/identity/module.py +46 -0
- python_platform/identity/passwords.py +34 -0
- python_platform/identity/permissions.py +14 -0
- python_platform/identity/services.py +108 -0
- python_platform/identity/sqlalchemy/__init__.py +5 -0
- python_platform/identity/sqlalchemy/migrations/0001_platform_identity.py +151 -0
- python_platform/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
- python_platform/identity/sqlalchemy/migrations/__init__.py +1 -0
- python_platform/identity/sqlalchemy/models.py +85 -0
- python_platform/identity/sqlalchemy/module.py +62 -0
- python_platform/identity/sqlalchemy/stores.py +526 -0
- python_platform/identity/tokens.py +106 -0
- python_platform/invocation/__init__.py +3 -0
- python_platform/invocation/callables.py +171 -0
- python_platform/invocation/contracts.py +33 -0
- python_platform/invocation/entries.py +54 -0
- python_platform/invocation/function_runtime.py +48 -0
- python_platform/invocation/interception.py +184 -0
- python_platform/lifecycle/__init__.py +25 -0
- python_platform/lifecycle/composition.py +35 -0
- python_platform/lifecycle/context.py +31 -0
- python_platform/lifecycle/runtime.py +43 -0
- python_platform/lifecycle/state.py +20 -0
- python_platform/modularity/__init__.py +15 -0
- python_platform/modularity/contracts.py +90 -0
- python_platform/modularity/discovery.py +143 -0
- python_platform/modularity/graph.py +334 -0
- python_platform/modularity/registry.py +100 -0
- python_platform/modularity/selection.py +16 -0
- python_platform/notifications/__init__.py +19 -0
- python_platform/notifications/catalog.py +46 -0
- python_platform/notifications/contracts.py +107 -0
- python_platform/observability/__init__.py +5 -0
- python_platform/observability/context.py +43 -0
- python_platform/observability/export.py +73 -0
- python_platform/observability/formatting.py +89 -0
- python_platform/observability/logging.py +118 -0
- python_platform/observability/options.py +45 -0
- python_platform/observability/tracing.py +80 -0
- python_platform/options/__init__.py +19 -0
- python_platform/options/aliases.py +331 -0
- python_platform/options/contribution.py +62 -0
- python_platform/options/immutability.py +48 -0
- python_platform/options/input_keys.py +214 -0
- python_platform/options/issues.py +335 -0
- python_platform/options/models.py +114 -0
- python_platform/options/registry.py +280 -0
- python_platform/options/schema.py +488 -0
- python_platform/options/validation.py +120 -0
- python_platform/py.typed +0 -0
- python_platform/realtime/__init__.py +12 -0
- python_platform/realtime/contracts.py +28 -0
- python_platform/realtime/diagnostics.py +37 -0
- python_platform/realtime/messages.py +113 -0
- python_platform/redis/__init__.py +6 -0
- python_platform/redis/distributed_lock.py +212 -0
- python_platform/redis/lease_lock.py +36 -0
- python_platform/redis/module.py +50 -0
- python_platform/redis/notification_runtime.py +182 -0
- python_platform/redis/notifications.py +25 -0
- python_platform/redis/options.py +35 -0
- python_platform/redis/runtime.py +116 -0
- python_platform/services/__init__.py +39 -0
- python_platform/services/application_bindings.py +221 -0
- python_platform/services/arbitration.py +381 -0
- python_platform/services/binding.py +102 -0
- python_platform/services/contribution.py +438 -0
- python_platform/services/convention.py +292 -0
- python_platform/services/convention_contracts.py +94 -0
- python_platform/services/exposure.py +23 -0
- python_platform/services/framework_provider.py +178 -0
- python_platform/services/native_graph.py +172 -0
- python_platform/services/provider.py +204 -0
- python_platform/services/registration.py +101 -0
- python_platform/services/runtime.py +298 -0
- python_platform/settings/__init__.py +30 -0
- python_platform/settings/application.py +95 -0
- python_platform/settings/binding.py +33 -0
- python_platform/settings/catalog.py +121 -0
- python_platform/settings/changes.py +14 -0
- python_platform/settings/contracts.py +115 -0
- python_platform/settings/definition_discovery.py +71 -0
- python_platform/settings/definitions.py +41 -0
- python_platform/settings/errors.py +22 -0
- python_platform/settings/handlers.py +43 -0
- python_platform/settings/management.py +57 -0
- python_platform/settings/manager.py +89 -0
- python_platform/settings/module.py +7 -0
- python_platform/settings/notifications.py +39 -0
- python_platform/settings/permissions.py +14 -0
- python_platform/settings/provider.py +74 -0
- python_platform/settings/refresh.py +140 -0
- python_platform/settings/refresh_module.py +45 -0
- python_platform/settings/sqlalchemy/__init__.py +5 -0
- python_platform/settings/sqlalchemy/migrations/0001_platform_settings.py +31 -0
- python_platform/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
- python_platform/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
- python_platform/settings/sqlalchemy/migrations/__init__.py +1 -0
- python_platform/settings/sqlalchemy/models.py +36 -0
- python_platform/settings/sqlalchemy/module.py +36 -0
- python_platform/settings/sqlalchemy/store.py +86 -0
- python_platform/settings/store.py +16 -0
- python_platform/settings/values.py +39 -0
- python_platform/sqlalchemy/__init__.py +51 -0
- python_platform/sqlalchemy/alembic_runtime/__init__.py +1 -0
- python_platform/sqlalchemy/alembic_runtime/env.py +27 -0
- python_platform/sqlalchemy/alembic_runtime/script.py.mako +14 -0
- python_platform/sqlalchemy/auditing.py +48 -0
- python_platform/sqlalchemy/errors.py +85 -0
- python_platform/sqlalchemy/metadata.py +538 -0
- python_platform/sqlalchemy/migration.py +346 -0
- python_platform/sqlalchemy/module.py +48 -0
- python_platform/sqlalchemy/module_migration.py +148 -0
- python_platform/sqlalchemy/options.py +66 -0
- python_platform/sqlalchemy/repository.py +33 -0
- python_platform/sqlalchemy/runtime.py +85 -0
- python_platform/sqlalchemy/session_provider.py +45 -0
- python_platform/sqlalchemy/unit_of_work.py +97 -0
- python_platform/testing/__init__.py +5 -0
- python_platform/testing/runtime.py +112 -0
- python_platform/unit_of_work/__init__.py +14 -0
- python_platform/unit_of_work/contracts.py +223 -0
- python_platform/unit_of_work/errors.py +27 -0
- python_platform/unit_of_work/manager.py +210 -0
- python_platform/unit_of_work/options.py +92 -0
- python_platform-0.1.1.dist-info/METADATA +122 -0
- python_platform-0.1.1.dist-info/RECORD +344 -0
- python_platform-0.1.1.dist-info/WHEEL +4 -0
- python_platform-0.1.1.dist-info/entry_points.txt +9 -0
- python_platform-0.1.1.dist-info/licenses/LICENSE +7 -0
- python_platform-0.1.1.dist-info/licenses/src/python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""APP 独占 engine registry;Session 由具体 UoW 按需创建与释放。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator, Mapping
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import text
|
|
9
|
+
from sqlalchemy.ext.asyncio import (
|
|
10
|
+
AsyncEngine,
|
|
11
|
+
create_async_engine,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from ..options import Options
|
|
15
|
+
from .errors import SqlAlchemyConnectionError, UnknownSqlAlchemyConnectionError
|
|
16
|
+
from .metadata import SqlAlchemyModelCatalog
|
|
17
|
+
from .options import ConnectionStringsOptions
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _SqlAlchemyEngineRegistry:
|
|
21
|
+
__slots__ = ("_engines",)
|
|
22
|
+
|
|
23
|
+
def __init__(self, engines: Mapping[str, AsyncEngine]) -> None:
|
|
24
|
+
self._engines: Mapping[str, AsyncEngine] = MappingProxyType(dict(engines))
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def connection_names(self) -> tuple[str, ...]:
|
|
28
|
+
return tuple(self._engines)
|
|
29
|
+
|
|
30
|
+
def get(self, connection_name: str) -> AsyncEngine:
|
|
31
|
+
try:
|
|
32
|
+
return self._engines[connection_name]
|
|
33
|
+
except KeyError:
|
|
34
|
+
raise UnknownSqlAlchemyConnectionError(connection_name) from None
|
|
35
|
+
|
|
36
|
+
async def ping_all(self) -> None:
|
|
37
|
+
for connection_name, engine in self._engines.items():
|
|
38
|
+
try:
|
|
39
|
+
async with engine.connect() as connection:
|
|
40
|
+
await connection.execute(text("SELECT 1"))
|
|
41
|
+
except Exception as original_error:
|
|
42
|
+
raise SqlAlchemyConnectionError(
|
|
43
|
+
connection_name=connection_name,
|
|
44
|
+
operation="startup ping",
|
|
45
|
+
error_type=type(original_error).__name__,
|
|
46
|
+
) from None
|
|
47
|
+
|
|
48
|
+
async def dispose_all(self) -> None:
|
|
49
|
+
failures: list[SqlAlchemyConnectionError] = []
|
|
50
|
+
for connection_name in reversed(self.connection_names):
|
|
51
|
+
try:
|
|
52
|
+
await self._engines[connection_name].dispose()
|
|
53
|
+
except BaseException as original_error:
|
|
54
|
+
failures.append(
|
|
55
|
+
SqlAlchemyConnectionError(
|
|
56
|
+
connection_name=connection_name,
|
|
57
|
+
operation="engine disposal",
|
|
58
|
+
error_type=type(original_error).__name__,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
if failures:
|
|
62
|
+
raise BaseExceptionGroup(
|
|
63
|
+
"SQLAlchemy engine disposal failed",
|
|
64
|
+
tuple(failures),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def _engine_registry(
|
|
69
|
+
options: Options[ConnectionStringsOptions],
|
|
70
|
+
models: SqlAlchemyModelCatalog,
|
|
71
|
+
) -> AsyncIterator[_SqlAlchemyEngineRegistry]:
|
|
72
|
+
models._assert_unchanged()
|
|
73
|
+
configured_names = set(options.value.connection_names)
|
|
74
|
+
missing = set(models.connection_names) - configured_names
|
|
75
|
+
if missing:
|
|
76
|
+
raise UnknownSqlAlchemyConnectionError(sorted(missing)[0])
|
|
77
|
+
engines = {
|
|
78
|
+
name: create_async_engine(options.value._url_for(name))
|
|
79
|
+
for name in options.value.connection_names
|
|
80
|
+
}
|
|
81
|
+
registry = _SqlAlchemyEngineRegistry(engines)
|
|
82
|
+
try:
|
|
83
|
+
yield registry
|
|
84
|
+
finally:
|
|
85
|
+
await registry.dispose_all()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""只解析当前有效 UoW 的原生 Session,无 owner 时明确失败。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
|
|
9
|
+
from ..domain import AggregateRoot
|
|
10
|
+
from ..events.aggregate import _collect_aggregate_events
|
|
11
|
+
from ..unit_of_work.manager import UnitOfWorkManager
|
|
12
|
+
from ..unit_of_work.options import UnitOfWorkDeclaration
|
|
13
|
+
from .unit_of_work import _SqlAlchemyUnitOfWork
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SqlAlchemySessionProvider:
|
|
17
|
+
def __init__(self, manager: UnitOfWorkManager) -> None:
|
|
18
|
+
self._manager = manager
|
|
19
|
+
|
|
20
|
+
async def get_session(self) -> AsyncSession:
|
|
21
|
+
work = self._manager.require_current()
|
|
22
|
+
if not isinstance(work, _SqlAlchemyUnitOfWork):
|
|
23
|
+
raise TypeError("current UnitOfWork does not own SQLAlchemy persistence")
|
|
24
|
+
return work.get_session()
|
|
25
|
+
|
|
26
|
+
@asynccontextmanager
|
|
27
|
+
async def operation(
|
|
28
|
+
self,
|
|
29
|
+
method_name: str,
|
|
30
|
+
*,
|
|
31
|
+
aggregate: AggregateRoot[Any] | None = None,
|
|
32
|
+
) -> AsyncIterator[None]:
|
|
33
|
+
"""仓储暂存边界;成功后收集聚合事件,Session 仍由 get_session 懒创建。"""
|
|
34
|
+
if aggregate is not None and not isinstance(aggregate, AggregateRoot):
|
|
35
|
+
raise TypeError("aggregate must be AggregateRoot")
|
|
36
|
+
options = self._manager._resolve_options(None, UnitOfWorkDeclaration(), method_name)
|
|
37
|
+
async with self._manager._begin(options) as work:
|
|
38
|
+
owner = self._manager.require_current(transactional=aggregate is not None)
|
|
39
|
+
yield
|
|
40
|
+
if aggregate is not None:
|
|
41
|
+
# 参与者不拥有提交权;成功暂存后必须仍在进入时的同一有效事务中。
|
|
42
|
+
if self._manager.require_current(transactional=True) is not owner:
|
|
43
|
+
raise RuntimeError("aggregate operation changed its UnitOfWork owner")
|
|
44
|
+
_collect_aggregate_events(owner, aggregate)
|
|
45
|
+
await work.complete()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""一个逻辑 UoW 拥有按需 Session;原生事务与连接配置归 SQLAlchemy。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import Connection, event, text
|
|
6
|
+
from sqlalchemy.orm import Session, SessionTransaction
|
|
7
|
+
from sqlalchemy.orm.exc import StaleDataError
|
|
8
|
+
|
|
9
|
+
from ..application_services.invocation import _InvocationRuntime
|
|
10
|
+
from ..domain import OptimisticConcurrencyError
|
|
11
|
+
from ..unit_of_work import UnitOfWork
|
|
12
|
+
from ..unit_of_work.contracts import _UnitOfWorkFactory
|
|
13
|
+
from .auditing import PlatformAsyncSession, _bind_current_user
|
|
14
|
+
from .runtime import _SqlAlchemyEngineRegistry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _SqlAlchemyUnitOfWork(UnitOfWork):
|
|
18
|
+
def __init__(self, engines: _SqlAlchemyEngineRegistry, runtime: _InvocationRuntime) -> None:
|
|
19
|
+
super().__init__()
|
|
20
|
+
self._engines = engines
|
|
21
|
+
self._runtime = runtime
|
|
22
|
+
self._session: PlatformAsyncSession | None = None
|
|
23
|
+
self._connections: list[Connection] = []
|
|
24
|
+
|
|
25
|
+
def get_session(self) -> PlatformAsyncSession:
|
|
26
|
+
self._require_active("get_session", allow_completing=True)
|
|
27
|
+
if self._session is None:
|
|
28
|
+
options = self._require_options()
|
|
29
|
+
engine = self._engines.get(options.connection_name)
|
|
30
|
+
if options.isolation_level is not None:
|
|
31
|
+
engine = engine.execution_options(isolation_level=options.isolation_level)
|
|
32
|
+
# close_resets_only=False 阻止已关闭 raw Session 再借连接;不替代 owner 检查。
|
|
33
|
+
self._session = PlatformAsyncSession(
|
|
34
|
+
engine, expire_on_commit=False, autoflush=False, close_resets_only=False
|
|
35
|
+
)
|
|
36
|
+
_bind_current_user(self._session, self._runtime.current_user())
|
|
37
|
+
event.listen(self._session.sync_session, "after_begin", self._after_begin)
|
|
38
|
+
return self._session
|
|
39
|
+
|
|
40
|
+
def _after_begin(
|
|
41
|
+
self, session: Session, transaction: SessionTransaction, connection: Connection
|
|
42
|
+
) -> None:
|
|
43
|
+
# 保留合法取得的公开 Connection 引用;不访问 Session 私有连接缓存。
|
|
44
|
+
self._connections[:] = [item for item in self._connections if not item.closed]
|
|
45
|
+
self._connections.append(connection)
|
|
46
|
+
timeout = self._require_options().command_timeout_ms
|
|
47
|
+
if timeout is not None:
|
|
48
|
+
connection.execute(
|
|
49
|
+
text("SELECT set_config('statement_timeout', :timeout, true)"),
|
|
50
|
+
{"timeout": str(timeout)},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async def _flush(self) -> None:
|
|
54
|
+
if self._session is not None:
|
|
55
|
+
try:
|
|
56
|
+
await self._session.flush()
|
|
57
|
+
except StaleDataError:
|
|
58
|
+
raise OptimisticConcurrencyError from None
|
|
59
|
+
|
|
60
|
+
async def _commit(self) -> None:
|
|
61
|
+
if self._session is not None:
|
|
62
|
+
try:
|
|
63
|
+
await self._session.commit()
|
|
64
|
+
except StaleDataError:
|
|
65
|
+
raise OptimisticConcurrencyError from None
|
|
66
|
+
|
|
67
|
+
async def _rollback(self) -> None:
|
|
68
|
+
if self._session is not None:
|
|
69
|
+
await self._session.rollback()
|
|
70
|
+
|
|
71
|
+
async def _close(self) -> None:
|
|
72
|
+
if self._session is not None:
|
|
73
|
+
await self._session.close()
|
|
74
|
+
|
|
75
|
+
async def _invalidate(self) -> None:
|
|
76
|
+
if self._session is None:
|
|
77
|
+
return
|
|
78
|
+
# Session 部分关闭失败时仍沿已登记的公开连接失效,防止丢失活动句柄。
|
|
79
|
+
try:
|
|
80
|
+
await self._session.invalidate()
|
|
81
|
+
finally:
|
|
82
|
+
for connection in self._connections:
|
|
83
|
+
if not connection.closed:
|
|
84
|
+
|
|
85
|
+
def invalidate_owned(session: Session, owned: Connection = connection) -> None:
|
|
86
|
+
owned.invalidate()
|
|
87
|
+
|
|
88
|
+
await self._session.run_sync(invalidate_owned)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _SqlAlchemyUnitOfWorkFactory(_UnitOfWorkFactory):
|
|
92
|
+
def __init__(self, engines: _SqlAlchemyEngineRegistry, runtime: _InvocationRuntime) -> None:
|
|
93
|
+
self._engines = engines
|
|
94
|
+
self._runtime = runtime
|
|
95
|
+
|
|
96
|
+
def create(self) -> UnitOfWork:
|
|
97
|
+
return _SqlAlchemyUnitOfWork(self._engines, self._runtime)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""正式 ApplicationBuilder 上的窄测试生命周期,不建立第二套配置或 DI DSL。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Concatenate, ParamSpec, TypeVar
|
|
8
|
+
|
|
9
|
+
from ..application import Application, ApplicationBuilder
|
|
10
|
+
from ..application_services import ApplicationServiceContract
|
|
11
|
+
from ..authorization import CurrentUser
|
|
12
|
+
|
|
13
|
+
_P = ParamSpec("_P")
|
|
14
|
+
_R = TypeVar("_R")
|
|
15
|
+
_TContract = TypeVar("_TContract", bound=ApplicationServiceContract)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestApplication:
|
|
19
|
+
"""测试拥有的 Application owner;build/start/stop 仍走正式生产路径。"""
|
|
20
|
+
|
|
21
|
+
__slots__ = ("_application", "_build_called", "_builder")
|
|
22
|
+
|
|
23
|
+
def __init__(self, builder: ApplicationBuilder) -> None:
|
|
24
|
+
if not isinstance(builder, ApplicationBuilder):
|
|
25
|
+
raise TypeError("builder must be python_platform.ApplicationBuilder")
|
|
26
|
+
self._builder = builder
|
|
27
|
+
self._build_called = False
|
|
28
|
+
self._application: Application | None = None
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def application(self) -> Application:
|
|
32
|
+
if self._application is None:
|
|
33
|
+
raise RuntimeError("TestApplication has not been built")
|
|
34
|
+
return self._application
|
|
35
|
+
|
|
36
|
+
def build(self) -> Application:
|
|
37
|
+
if self._build_called:
|
|
38
|
+
raise RuntimeError("TestApplication may only build once")
|
|
39
|
+
self._build_called = True
|
|
40
|
+
self._application = self._builder.build()
|
|
41
|
+
return self._application
|
|
42
|
+
|
|
43
|
+
async def start(self) -> None:
|
|
44
|
+
application = self._application if self._application is not None else self.build()
|
|
45
|
+
await application.start()
|
|
46
|
+
|
|
47
|
+
async def stop(self) -> None:
|
|
48
|
+
application = self._application if self._application is not None else self.build()
|
|
49
|
+
await application.stop()
|
|
50
|
+
|
|
51
|
+
async def __aenter__(self) -> TestApplication:
|
|
52
|
+
await self.start()
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
async def __aexit__(
|
|
56
|
+
self,
|
|
57
|
+
exception_type: type[BaseException] | None,
|
|
58
|
+
exception: BaseException | None,
|
|
59
|
+
traceback: TracebackType | None,
|
|
60
|
+
) -> bool:
|
|
61
|
+
del exception_type, traceback
|
|
62
|
+
cleanup_failure: BaseException | None = None
|
|
63
|
+
try:
|
|
64
|
+
await self.stop()
|
|
65
|
+
except BaseException as original_error:
|
|
66
|
+
cleanup_failure = original_error
|
|
67
|
+
if exception is not None and cleanup_failure is not None:
|
|
68
|
+
raise BaseExceptionGroup(
|
|
69
|
+
"TestApplication body and cleanup failed",
|
|
70
|
+
(exception, cleanup_failure),
|
|
71
|
+
) from None
|
|
72
|
+
if cleanup_failure is not None:
|
|
73
|
+
raise cleanup_failure
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
async def call(
|
|
77
|
+
self,
|
|
78
|
+
function: Callable[..., Awaitable[_R]],
|
|
79
|
+
/,
|
|
80
|
+
*args: object,
|
|
81
|
+
**kwargs: object,
|
|
82
|
+
) -> _R:
|
|
83
|
+
return await self.application.call(function, *args, **kwargs)
|
|
84
|
+
|
|
85
|
+
async def call_as(
|
|
86
|
+
self,
|
|
87
|
+
current_user: CurrentUser,
|
|
88
|
+
function: Callable[..., Awaitable[_R]],
|
|
89
|
+
/,
|
|
90
|
+
*args: object,
|
|
91
|
+
**kwargs: object,
|
|
92
|
+
) -> _R:
|
|
93
|
+
return await self.application.call_as(current_user, function, *args, **kwargs)
|
|
94
|
+
|
|
95
|
+
async def invoke(
|
|
96
|
+
self,
|
|
97
|
+
method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
|
|
98
|
+
/,
|
|
99
|
+
*args: _P.args,
|
|
100
|
+
**kwargs: _P.kwargs,
|
|
101
|
+
) -> _R:
|
|
102
|
+
return await self.application.invoke(method, *args, **kwargs)
|
|
103
|
+
|
|
104
|
+
async def invoke_as(
|
|
105
|
+
self,
|
|
106
|
+
current_user: CurrentUser,
|
|
107
|
+
method: Callable[Concatenate[_TContract, _P], Awaitable[_R]],
|
|
108
|
+
/,
|
|
109
|
+
*args: _P.args,
|
|
110
|
+
**kwargs: _P.kwargs,
|
|
111
|
+
) -> _R:
|
|
112
|
+
return await self.application.invoke_as(current_user, method, *args, **kwargs)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .contracts import UnitOfWork, UnitOfWorkState
|
|
2
|
+
from .errors import UnitOfWorkStateError
|
|
3
|
+
from .manager import UnitOfWorkManager
|
|
4
|
+
from .options import UnitOfWorkOptions, UnitOfWorkTransactionBehavior, unit_of_work
|
|
5
|
+
|
|
6
|
+
__all__ = (
|
|
7
|
+
"UnitOfWork",
|
|
8
|
+
"UnitOfWorkManager",
|
|
9
|
+
"UnitOfWorkOptions",
|
|
10
|
+
"UnitOfWorkState",
|
|
11
|
+
"UnitOfWorkStateError",
|
|
12
|
+
"UnitOfWorkTransactionBehavior",
|
|
13
|
+
"unit_of_work",
|
|
14
|
+
)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""具体 UoW 的工作状态、提交事实与资源释放事实分别保存。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from .errors import UnitOfWorkStateError
|
|
11
|
+
from .options import _ResolvedUnitOfWorkOptions
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..events.runtime import _LocalEventQueue
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UnitOfWorkState(str, Enum):
|
|
18
|
+
ACTIVE = "active"
|
|
19
|
+
COMMITTED = "committed"
|
|
20
|
+
ROLLED_BACK = "rolled_back"
|
|
21
|
+
FAILED = "failed"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UnitOfWork:
|
|
25
|
+
"""手动 owner 显式完成;参与者只确认参与,不提交或关闭共享资源。"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._state = UnitOfWorkState.ACTIVE
|
|
29
|
+
self._task = asyncio.current_task()
|
|
30
|
+
self._completing = False
|
|
31
|
+
self._released = False
|
|
32
|
+
self._committed = False
|
|
33
|
+
self._commit_unknown = False
|
|
34
|
+
self._saved_batches = 0
|
|
35
|
+
self._cleanup_failures: tuple[BaseException, ...] = ()
|
|
36
|
+
self._options: _ResolvedUnitOfWorkOptions | None = None
|
|
37
|
+
self._event_queue: _LocalEventQueue | None = None
|
|
38
|
+
self._after_commit: Callable[[], Awaitable[None]] | None = None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def state(self) -> UnitOfWorkState:
|
|
42
|
+
return self._state
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def is_transactional(self) -> bool:
|
|
46
|
+
return self._require_options().is_transactional
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def connection_name(self) -> str:
|
|
50
|
+
return self._require_options().connection_name
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def committed(self) -> bool:
|
|
54
|
+
return self._committed
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def commit_outcome_unknown(self) -> bool:
|
|
58
|
+
return self._commit_unknown
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def saved_batches(self) -> int:
|
|
62
|
+
return self._saved_batches
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def resources_released(self) -> bool:
|
|
66
|
+
return self._released
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def cleanup_failures(self) -> tuple[BaseException, ...]:
|
|
70
|
+
return self._cleanup_failures
|
|
71
|
+
|
|
72
|
+
def _require_options(self) -> _ResolvedUnitOfWorkOptions:
|
|
73
|
+
if self._options is None:
|
|
74
|
+
raise RuntimeError("UnitOfWork has no manager owner")
|
|
75
|
+
return self._options
|
|
76
|
+
|
|
77
|
+
def _require_active(self, operation: str, *, allow_completing: bool = False) -> None:
|
|
78
|
+
from ..application_services.errors import CrossTaskInvocationError
|
|
79
|
+
|
|
80
|
+
if asyncio.current_task() is not self._task:
|
|
81
|
+
raise CrossTaskInvocationError
|
|
82
|
+
if self._released or (self._completing and not allow_completing):
|
|
83
|
+
raise RuntimeError(f"UnitOfWork.{operation} is no longer available")
|
|
84
|
+
self._require_state(operation, (UnitOfWorkState.ACTIVE,))
|
|
85
|
+
|
|
86
|
+
async def flush(self) -> None:
|
|
87
|
+
self._require_active("flush", allow_completing=True)
|
|
88
|
+
try:
|
|
89
|
+
await self._flush()
|
|
90
|
+
except BaseException:
|
|
91
|
+
self._state = UnitOfWorkState.FAILED
|
|
92
|
+
raise
|
|
93
|
+
|
|
94
|
+
async def save_changes(self) -> None:
|
|
95
|
+
self._require_active("save_changes")
|
|
96
|
+
await self.flush()
|
|
97
|
+
if not self.is_transactional:
|
|
98
|
+
await self._save_batch()
|
|
99
|
+
|
|
100
|
+
async def complete(self) -> None:
|
|
101
|
+
self._require_active("complete")
|
|
102
|
+
self._completing = True
|
|
103
|
+
try:
|
|
104
|
+
if self._event_queue is not None:
|
|
105
|
+
await self._event_queue._dispatch_domain()
|
|
106
|
+
await self.flush()
|
|
107
|
+
await self._save_batch()
|
|
108
|
+
self._committed = True
|
|
109
|
+
self._state = UnitOfWorkState.COMMITTED
|
|
110
|
+
if self._after_commit is not None:
|
|
111
|
+
await self._after_commit()
|
|
112
|
+
except BaseException:
|
|
113
|
+
if not self._committed:
|
|
114
|
+
self._state = UnitOfWorkState.FAILED
|
|
115
|
+
raise
|
|
116
|
+
finally:
|
|
117
|
+
self._completing = False
|
|
118
|
+
|
|
119
|
+
async def _save_batch(self) -> None:
|
|
120
|
+
# await commit 抛错不证明服务端未提交;诊断独立于后续 rollback 成败。
|
|
121
|
+
self._commit_unknown = True
|
|
122
|
+
try:
|
|
123
|
+
await self._commit()
|
|
124
|
+
except BaseException:
|
|
125
|
+
self._state = UnitOfWorkState.FAILED
|
|
126
|
+
raise
|
|
127
|
+
self._commit_unknown = False
|
|
128
|
+
if self._options is not None and not self.is_transactional:
|
|
129
|
+
self._saved_batches += 1
|
|
130
|
+
|
|
131
|
+
async def rollback(self) -> None:
|
|
132
|
+
self._require_active("rollback")
|
|
133
|
+
await self._rollback_owned()
|
|
134
|
+
|
|
135
|
+
async def _rollback_owned(self) -> None:
|
|
136
|
+
try:
|
|
137
|
+
await self._rollback()
|
|
138
|
+
except BaseException:
|
|
139
|
+
self._state = UnitOfWorkState.FAILED
|
|
140
|
+
raise
|
|
141
|
+
if not self._commit_unknown:
|
|
142
|
+
self._state = UnitOfWorkState.ROLLED_BACK
|
|
143
|
+
|
|
144
|
+
async def _flush(self) -> None:
|
|
145
|
+
"""未选择持久化 provider 时无需物理资源。"""
|
|
146
|
+
|
|
147
|
+
async def _commit(self) -> None:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
async def _rollback(self) -> None:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
async def _close(self) -> None:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
async def _invalidate(self) -> None:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
async def _cleanup(self) -> None:
|
|
160
|
+
failures: list[BaseException] = []
|
|
161
|
+
suspect = self._commit_unknown
|
|
162
|
+
if not self._committed and self._state is not UnitOfWorkState.ROLLED_BACK:
|
|
163
|
+
try:
|
|
164
|
+
await self._rollback_owned()
|
|
165
|
+
except BaseException as error:
|
|
166
|
+
failures.append(error)
|
|
167
|
+
suspect = True
|
|
168
|
+
# 有疑问的连接在 close 归池前失效;close 自身失败仍沿原生资源引用继续回收。
|
|
169
|
+
if suspect:
|
|
170
|
+
try:
|
|
171
|
+
await self._invalidate()
|
|
172
|
+
except BaseException as error:
|
|
173
|
+
failures.append(error)
|
|
174
|
+
try:
|
|
175
|
+
await self._close()
|
|
176
|
+
self._released = True
|
|
177
|
+
except BaseException as error:
|
|
178
|
+
failures.append(error)
|
|
179
|
+
try:
|
|
180
|
+
await self._invalidate()
|
|
181
|
+
self._released = True
|
|
182
|
+
except BaseException as invalidate_error:
|
|
183
|
+
failures.append(invalidate_error)
|
|
184
|
+
self._cleanup_failures = tuple(failures)
|
|
185
|
+
|
|
186
|
+
def _require_state(self, operation: str, allowed: tuple[UnitOfWorkState, ...]) -> None:
|
|
187
|
+
if self._state not in allowed:
|
|
188
|
+
raise UnitOfWorkStateError(operation=operation, current=self._state, allowed=allowed)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class _UnitOfWorkParticipant(UnitOfWork):
|
|
192
|
+
def __init__(self, owner: UnitOfWork) -> None:
|
|
193
|
+
super().__init__()
|
|
194
|
+
self._owner = owner
|
|
195
|
+
self._options = owner._options
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def state(self) -> UnitOfWorkState:
|
|
199
|
+
return self._owner.state
|
|
200
|
+
|
|
201
|
+
async def complete(self) -> None:
|
|
202
|
+
self._require_active("complete")
|
|
203
|
+
self._owner._require_active("participate", allow_completing=True)
|
|
204
|
+
self._state = UnitOfWorkState.COMMITTED
|
|
205
|
+
|
|
206
|
+
async def flush(self) -> None:
|
|
207
|
+
self._require_active("flush")
|
|
208
|
+
await self._owner.flush()
|
|
209
|
+
|
|
210
|
+
async def save_changes(self) -> None:
|
|
211
|
+
self._require_active("save_changes")
|
|
212
|
+
await self._owner.save_changes()
|
|
213
|
+
|
|
214
|
+
async def rollback(self) -> None:
|
|
215
|
+
self._require_active("rollback")
|
|
216
|
+
await self._owner.rollback()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class _UnitOfWorkFactory:
|
|
220
|
+
"""原生 DI 的 provider 替换点:无数据库与 SQLAlchemy 两种实际实现。"""
|
|
221
|
+
|
|
222
|
+
def create(self) -> UnitOfWork:
|
|
223
|
+
return UnitOfWork()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Unit of Work 公开失败契约与状态诊断。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ..errors.base import FrameworkError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .contracts import UnitOfWorkState
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnitOfWorkStateError(FrameworkError):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
operation: str,
|
|
18
|
+
current: UnitOfWorkState,
|
|
19
|
+
allowed: tuple[UnitOfWorkState, ...],
|
|
20
|
+
) -> None:
|
|
21
|
+
self.operation = operation
|
|
22
|
+
self.current = current
|
|
23
|
+
self.allowed = allowed
|
|
24
|
+
allowed_values = ", ".join(item.value for item in allowed)
|
|
25
|
+
super().__init__(
|
|
26
|
+
f"UnitOfWork.{operation} requires state in ({allowed_values}), got {current.value}"
|
|
27
|
+
)
|