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
|
+
"""SQLAlchemy capability 的安全失败契约;任何消息都不得包含连接 URL。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..errors.base import FrameworkError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SqlAlchemyModelRegistrationError(FrameworkError):
|
|
9
|
+
def __init__(self, *, owner: str, reason: str) -> None:
|
|
10
|
+
self.owner = owner
|
|
11
|
+
self.reason = reason
|
|
12
|
+
super().__init__(f"Invalid SQLAlchemy model registration for {owner}: {reason}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SqlAlchemyMetadataChangedError(FrameworkError):
|
|
16
|
+
def __init__(self, *, owner: str) -> None:
|
|
17
|
+
self.owner = owner
|
|
18
|
+
super().__init__(
|
|
19
|
+
f"SQLAlchemy MetaData owned by {owner} changed after its registration was declared"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SqlAlchemyConnectionError(FrameworkError):
|
|
24
|
+
def __init__(self, *, connection_name: str, operation: str, error_type: str) -> None:
|
|
25
|
+
self.connection_name = connection_name
|
|
26
|
+
self.operation = operation
|
|
27
|
+
self.error_type = error_type
|
|
28
|
+
super().__init__(
|
|
29
|
+
f"SQLAlchemy connection {connection_name!r} failed during {operation}: {error_type}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UnknownSqlAlchemyConnectionError(FrameworkError, KeyError):
|
|
34
|
+
def __init__(self, connection_name: str) -> None:
|
|
35
|
+
self.connection_name = connection_name
|
|
36
|
+
super().__init__(f"SQLAlchemy connection {connection_name!r} is not configured")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SoftDeleteMigrationBlockedError(FrameworkError):
|
|
40
|
+
def __init__(self, affected_tables: tuple[tuple[str, int], ...]) -> None:
|
|
41
|
+
self.affected_tables = affected_tables
|
|
42
|
+
super().__init__(
|
|
43
|
+
f"Resolve historical soft-deleted rows before upgrading: {affected_tables}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SqlAlchemyMigrationError(FrameworkError):
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
operation: str,
|
|
52
|
+
failed_connection: str,
|
|
53
|
+
completed_connections: tuple[str, ...],
|
|
54
|
+
error_type: str,
|
|
55
|
+
cleanup_error_types: tuple[str, ...],
|
|
56
|
+
affected_tables: tuple[tuple[str, int], ...] = (),
|
|
57
|
+
) -> None:
|
|
58
|
+
self.operation = operation
|
|
59
|
+
self.failed_connection = failed_connection
|
|
60
|
+
self.completed_connections = completed_connections
|
|
61
|
+
self.error_type = error_type
|
|
62
|
+
self.cleanup_error_types = cleanup_error_types
|
|
63
|
+
self.affected_tables = affected_tables
|
|
64
|
+
super().__init__(
|
|
65
|
+
f"SQLAlchemy migration {operation} failed for {failed_connection!r} after "
|
|
66
|
+
f"{len(completed_connections)} completed connection(s): {error_type}; "
|
|
67
|
+
f"cleanup failures: {len(cleanup_error_types)}; affected tables: {affected_tables}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MigrationHeadsMismatchError(FrameworkError):
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
connection_name: str,
|
|
76
|
+
current_heads: tuple[str, ...],
|
|
77
|
+
expected_heads: tuple[str, ...],
|
|
78
|
+
) -> None:
|
|
79
|
+
self.connection_name = connection_name
|
|
80
|
+
self.current_heads = current_heads
|
|
81
|
+
self.expected_heads = expected_heads
|
|
82
|
+
super().__init__(
|
|
83
|
+
f"Connection {connection_name!r} is not at current Alembic heads: "
|
|
84
|
+
f"current={current_heads!r}, expected={expected_heads!r}"
|
|
85
|
+
)
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"""Module-owned MetaData/migration declarations 与 Application-local aggregation。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TypeVar
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import Constraint, ForeignKey, MetaData, Table
|
|
12
|
+
|
|
13
|
+
from ..modularity import AppModule, ModuleDescriptor, ModuleKey, ModuleRegistry
|
|
14
|
+
from .errors import SqlAlchemyMetadataChangedError, SqlAlchemyModelRegistrationError
|
|
15
|
+
from .options import _validate_connection_name
|
|
16
|
+
|
|
17
|
+
_MODEL_REGISTRATIONS_ATTRIBUTE = "__python_platform_sqlalchemy_model_registrations__"
|
|
18
|
+
_MIGRATION_REGISTRATIONS_ATTRIBUTE = "__python_platform_sqlalchemy_migration_registrations__"
|
|
19
|
+
_TModule = TypeVar("_TModule", bound=type[AppModule])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class SqlAlchemyModelRegistration:
|
|
24
|
+
metadata: MetaData
|
|
25
|
+
migrations_package: str
|
|
26
|
+
branch_label: str
|
|
27
|
+
connection_name: str = "default"
|
|
28
|
+
_declared_fingerprint: tuple[object, ...] = field(init=False, repr=False, compare=False)
|
|
29
|
+
|
|
30
|
+
def __post_init__(self) -> None:
|
|
31
|
+
if not isinstance(self.metadata, MetaData):
|
|
32
|
+
raise TypeError("metadata must be sqlalchemy.MetaData")
|
|
33
|
+
if not isinstance(self.migrations_package, str) or not self.migrations_package.strip():
|
|
34
|
+
raise ValueError("migrations_package must be a non-empty import path")
|
|
35
|
+
if not isinstance(self.branch_label, str) or not self.branch_label.strip():
|
|
36
|
+
raise ValueError("branch_label must be a non-empty string")
|
|
37
|
+
if self.branch_label != self.branch_label.strip():
|
|
38
|
+
raise ValueError("branch_label must not contain surrounding whitespace")
|
|
39
|
+
_validate_connection_name(self.connection_name)
|
|
40
|
+
object.__setattr__(self, "_declared_fingerprint", _metadata_fingerprint(self.metadata))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class SqlAlchemyModelContribution:
|
|
45
|
+
owner: ModuleKey
|
|
46
|
+
registration: SqlAlchemyModelRegistration
|
|
47
|
+
migration_path: Path
|
|
48
|
+
declaration_index: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class SqlAlchemyMigrationRegistration:
|
|
53
|
+
"""不拥有 ORM metadata 的 external raw-SQL migration branch。"""
|
|
54
|
+
|
|
55
|
+
migrations_package: str
|
|
56
|
+
branch_label: str
|
|
57
|
+
owned_schemas: tuple[str, ...]
|
|
58
|
+
connection_name: str = "default"
|
|
59
|
+
|
|
60
|
+
def __post_init__(self) -> None:
|
|
61
|
+
if not isinstance(self.migrations_package, str) or not self.migrations_package.strip():
|
|
62
|
+
raise ValueError("migrations_package must be a non-empty import path")
|
|
63
|
+
if (
|
|
64
|
+
not isinstance(self.branch_label, str)
|
|
65
|
+
or not self.branch_label
|
|
66
|
+
or self.branch_label != self.branch_label.strip()
|
|
67
|
+
):
|
|
68
|
+
raise ValueError("branch_label must be a non-empty trimmed string")
|
|
69
|
+
_validate_connection_name(self.connection_name)
|
|
70
|
+
if (
|
|
71
|
+
not isinstance(self.owned_schemas, tuple)
|
|
72
|
+
or not self.owned_schemas
|
|
73
|
+
or any(not _is_postgres_identifier(item) for item in self.owned_schemas)
|
|
74
|
+
):
|
|
75
|
+
raise ValueError("owned_schemas must be a non-empty tuple of PostgreSQL identifiers")
|
|
76
|
+
normalized = tuple(item.lower() for item in self.owned_schemas)
|
|
77
|
+
if len(set(normalized)) != len(normalized):
|
|
78
|
+
raise ValueError("owned_schemas cannot contain duplicates")
|
|
79
|
+
object.__setattr__(self, "owned_schemas", normalized)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True, slots=True)
|
|
83
|
+
class SqlAlchemyMigrationContribution:
|
|
84
|
+
owner: ModuleKey
|
|
85
|
+
registration: SqlAlchemyMigrationRegistration
|
|
86
|
+
migration_path: Path
|
|
87
|
+
declaration_index: int
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class _SqlAlchemyMigrationSource:
|
|
92
|
+
owner: ModuleKey
|
|
93
|
+
migrations_package: str
|
|
94
|
+
branch_label: str
|
|
95
|
+
migration_path: Path
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SqlAlchemyModelCatalog:
|
|
99
|
+
__slots__ = ("_contributions", "_migration_contributions", "_migration_declarations")
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
contributions: tuple[SqlAlchemyModelContribution, ...],
|
|
104
|
+
migration_contributions: tuple[SqlAlchemyMigrationContribution, ...],
|
|
105
|
+
migration_declarations: tuple[
|
|
106
|
+
tuple[type[AppModule], tuple[SqlAlchemyMigrationRegistration, ...]], ...
|
|
107
|
+
],
|
|
108
|
+
) -> None:
|
|
109
|
+
self._contributions = contributions
|
|
110
|
+
self._migration_contributions = migration_contributions
|
|
111
|
+
self._migration_declarations = migration_declarations
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def contributions(self) -> tuple[SqlAlchemyModelContribution, ...]:
|
|
115
|
+
return self._contributions
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def migration_contributions(self) -> tuple[SqlAlchemyMigrationContribution, ...]:
|
|
119
|
+
return self._migration_contributions
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def connection_names(self) -> tuple[str, ...]:
|
|
123
|
+
names = {item.registration.connection_name for item in self._contributions}
|
|
124
|
+
names.update(item.registration.connection_name for item in self._migration_contributions)
|
|
125
|
+
return tuple(name for name in ("default", *sorted(names - {"default"})) if name in names)
|
|
126
|
+
|
|
127
|
+
def for_connection(
|
|
128
|
+
self,
|
|
129
|
+
connection_name: str,
|
|
130
|
+
) -> tuple[SqlAlchemyModelContribution, ...]:
|
|
131
|
+
return tuple(
|
|
132
|
+
item
|
|
133
|
+
for item in self._contributions
|
|
134
|
+
if item.registration.connection_name == connection_name
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def migrations_for_connection(
|
|
138
|
+
self,
|
|
139
|
+
connection_name: str,
|
|
140
|
+
) -> tuple[SqlAlchemyMigrationContribution, ...]:
|
|
141
|
+
return tuple(
|
|
142
|
+
item
|
|
143
|
+
for item in self._migration_contributions
|
|
144
|
+
if item.registration.connection_name == connection_name
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _migration_sources_for_connection(
|
|
148
|
+
self,
|
|
149
|
+
connection_name: str,
|
|
150
|
+
) -> tuple[_SqlAlchemyMigrationSource, ...]:
|
|
151
|
+
model_sources = tuple(
|
|
152
|
+
_SqlAlchemyMigrationSource(
|
|
153
|
+
owner=item.owner,
|
|
154
|
+
migrations_package=item.registration.migrations_package,
|
|
155
|
+
branch_label=item.registration.branch_label,
|
|
156
|
+
migration_path=item.migration_path,
|
|
157
|
+
)
|
|
158
|
+
for item in self.for_connection(connection_name)
|
|
159
|
+
)
|
|
160
|
+
external_sources = tuple(
|
|
161
|
+
_SqlAlchemyMigrationSource(
|
|
162
|
+
owner=item.owner,
|
|
163
|
+
migrations_package=item.registration.migrations_package,
|
|
164
|
+
branch_label=item.registration.branch_label,
|
|
165
|
+
migration_path=item.migration_path,
|
|
166
|
+
)
|
|
167
|
+
for item in self.migrations_for_connection(connection_name)
|
|
168
|
+
)
|
|
169
|
+
return (*model_sources, *external_sources)
|
|
170
|
+
|
|
171
|
+
def _assert_unchanged(self) -> None:
|
|
172
|
+
for contribution in self._contributions:
|
|
173
|
+
registration = contribution.registration
|
|
174
|
+
if _metadata_fingerprint(registration.metadata) != registration._declared_fingerprint:
|
|
175
|
+
raise SqlAlchemyMetadataChangedError(owner=str(contribution.owner))
|
|
176
|
+
for module_type, declared in self._migration_declarations:
|
|
177
|
+
if module_type.__dict__.get(_MIGRATION_REGISTRATIONS_ATTRIBUTE, ()) is not declared:
|
|
178
|
+
raise SqlAlchemyMetadataChangedError(
|
|
179
|
+
owner=f"{module_type.__module__}:{module_type.__qualname__}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def contributes_sqlalchemy_models(
|
|
184
|
+
*registrations: SqlAlchemyModelRegistration,
|
|
185
|
+
) -> Callable[[_TModule], _TModule]:
|
|
186
|
+
declared = tuple(registrations)
|
|
187
|
+
if not declared or any(not isinstance(item, SqlAlchemyModelRegistration) for item in declared):
|
|
188
|
+
raise TypeError("contributes_sqlalchemy_models requires model registrations")
|
|
189
|
+
|
|
190
|
+
def decorate(module_type: _TModule) -> _TModule:
|
|
191
|
+
if not isinstance(module_type, type) or not issubclass(module_type, AppModule):
|
|
192
|
+
raise TypeError("contributes_sqlalchemy_models can only decorate AppModule classes")
|
|
193
|
+
if _MODEL_REGISTRATIONS_ATTRIBUTE in module_type.__dict__:
|
|
194
|
+
raise TypeError("SQLAlchemy model registrations may only be declared once per Module")
|
|
195
|
+
setattr(module_type, _MODEL_REGISTRATIONS_ATTRIBUTE, declared)
|
|
196
|
+
return module_type
|
|
197
|
+
|
|
198
|
+
return decorate
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def contributes_sqlalchemy_migrations(
|
|
202
|
+
*registrations: SqlAlchemyMigrationRegistration,
|
|
203
|
+
) -> Callable[[_TModule], _TModule]:
|
|
204
|
+
declared = tuple(registrations)
|
|
205
|
+
if not declared or any(
|
|
206
|
+
not isinstance(item, SqlAlchemyMigrationRegistration) for item in declared
|
|
207
|
+
):
|
|
208
|
+
raise TypeError("contributes_sqlalchemy_migrations requires migration registrations")
|
|
209
|
+
|
|
210
|
+
def decorate(module_type: _TModule) -> _TModule:
|
|
211
|
+
if not isinstance(module_type, type) or not issubclass(module_type, AppModule):
|
|
212
|
+
raise TypeError("contributes_sqlalchemy_migrations can only decorate AppModule classes")
|
|
213
|
+
if _MIGRATION_REGISTRATIONS_ATTRIBUTE in module_type.__dict__:
|
|
214
|
+
raise TypeError(
|
|
215
|
+
"SQLAlchemy migration registrations may only be declared once per Module"
|
|
216
|
+
)
|
|
217
|
+
setattr(module_type, _MIGRATION_REGISTRATIONS_ATTRIBUTE, declared)
|
|
218
|
+
return module_type
|
|
219
|
+
|
|
220
|
+
return decorate
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _build_model_catalog(registry: ModuleRegistry) -> SqlAlchemyModelCatalog:
|
|
224
|
+
builder = _SqlAlchemyModelCatalogBuilder()
|
|
225
|
+
for descriptor in registry.dependency_order:
|
|
226
|
+
builder.add_module(descriptor)
|
|
227
|
+
return builder.build()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class _SqlAlchemyModelCatalogBuilder:
|
|
231
|
+
"""拥有 model/raw-SQL migration arbitration 的 build-time mutable state。"""
|
|
232
|
+
|
|
233
|
+
__slots__ = (
|
|
234
|
+
"_branch_owners",
|
|
235
|
+
"_contributions",
|
|
236
|
+
"_migration_contributions",
|
|
237
|
+
"_migration_declarations",
|
|
238
|
+
"_schema_owners",
|
|
239
|
+
"_table_owners",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def __init__(self) -> None:
|
|
243
|
+
self._contributions: list[SqlAlchemyModelContribution] = []
|
|
244
|
+
self._migration_contributions: list[SqlAlchemyMigrationContribution] = []
|
|
245
|
+
self._migration_declarations: list[
|
|
246
|
+
tuple[type[AppModule], tuple[SqlAlchemyMigrationRegistration, ...]]
|
|
247
|
+
] = []
|
|
248
|
+
self._branch_owners: dict[str, ModuleKey] = {}
|
|
249
|
+
self._table_owners: dict[tuple[str, str], ModuleKey] = {}
|
|
250
|
+
self._schema_owners: dict[tuple[str, str], tuple[ModuleKey, str]] = {}
|
|
251
|
+
|
|
252
|
+
def add_module(self, descriptor: ModuleDescriptor) -> None:
|
|
253
|
+
self._add_model_declarations(descriptor)
|
|
254
|
+
self._add_migration_declarations(descriptor)
|
|
255
|
+
|
|
256
|
+
def build(self) -> SqlAlchemyModelCatalog:
|
|
257
|
+
return SqlAlchemyModelCatalog(
|
|
258
|
+
tuple(self._contributions),
|
|
259
|
+
tuple(self._migration_contributions),
|
|
260
|
+
tuple(self._migration_declarations),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def _add_model_declarations(self, descriptor: ModuleDescriptor) -> None:
|
|
264
|
+
declared = descriptor.module_type.__dict__.get(_MODEL_REGISTRATIONS_ATTRIBUTE, ())
|
|
265
|
+
if not isinstance(declared, tuple):
|
|
266
|
+
raise SqlAlchemyModelRegistrationError(
|
|
267
|
+
owner=str(descriptor.key),
|
|
268
|
+
reason="model registration metadata must be an immutable tuple",
|
|
269
|
+
)
|
|
270
|
+
for declaration_index, candidate in enumerate(declared):
|
|
271
|
+
self._add_model(candidate, descriptor.key, declaration_index)
|
|
272
|
+
|
|
273
|
+
def _add_model(
|
|
274
|
+
self,
|
|
275
|
+
candidate: object,
|
|
276
|
+
owner: ModuleKey,
|
|
277
|
+
declaration_index: int,
|
|
278
|
+
) -> None:
|
|
279
|
+
if not isinstance(candidate, SqlAlchemyModelRegistration):
|
|
280
|
+
raise SqlAlchemyModelRegistrationError(
|
|
281
|
+
owner=str(owner),
|
|
282
|
+
reason="model registration metadata contains an invalid item",
|
|
283
|
+
)
|
|
284
|
+
registration = candidate
|
|
285
|
+
if _metadata_fingerprint(registration.metadata) != registration._declared_fingerprint:
|
|
286
|
+
raise SqlAlchemyMetadataChangedError(owner=str(owner))
|
|
287
|
+
self._claim_branch(registration.branch_label, owner)
|
|
288
|
+
migration_path = _migration_package_path(
|
|
289
|
+
registration.migrations_package,
|
|
290
|
+
owner=owner,
|
|
291
|
+
)
|
|
292
|
+
for table in registration.metadata.sorted_tables:
|
|
293
|
+
self._claim_table(registration, table, owner)
|
|
294
|
+
self._contributions.append(
|
|
295
|
+
SqlAlchemyModelContribution(
|
|
296
|
+
owner=owner,
|
|
297
|
+
registration=registration,
|
|
298
|
+
migration_path=migration_path,
|
|
299
|
+
declaration_index=declaration_index,
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def _claim_table(
|
|
304
|
+
self,
|
|
305
|
+
registration: SqlAlchemyModelRegistration,
|
|
306
|
+
table: Table,
|
|
307
|
+
owner: ModuleKey,
|
|
308
|
+
) -> None:
|
|
309
|
+
identity = (registration.connection_name, table.key)
|
|
310
|
+
previous_owner = self._table_owners.get(identity)
|
|
311
|
+
if previous_owner is not None:
|
|
312
|
+
raise SqlAlchemyModelRegistrationError(
|
|
313
|
+
owner=str(owner),
|
|
314
|
+
reason=(
|
|
315
|
+
f"table {table.key!r} on connection {registration.connection_name!r} "
|
|
316
|
+
f"is already owned by {previous_owner}"
|
|
317
|
+
),
|
|
318
|
+
)
|
|
319
|
+
self._table_owners[identity] = owner
|
|
320
|
+
if table.schema is not None:
|
|
321
|
+
_claim_schema(
|
|
322
|
+
self._schema_owners,
|
|
323
|
+
connection_name=registration.connection_name,
|
|
324
|
+
schema=table.schema,
|
|
325
|
+
owner=owner,
|
|
326
|
+
kind="model",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def _add_migration_declarations(self, descriptor: ModuleDescriptor) -> None:
|
|
330
|
+
declared_migrations = descriptor.module_type.__dict__.get(
|
|
331
|
+
_MIGRATION_REGISTRATIONS_ATTRIBUTE,
|
|
332
|
+
(),
|
|
333
|
+
)
|
|
334
|
+
if not isinstance(declared_migrations, tuple):
|
|
335
|
+
raise SqlAlchemyModelRegistrationError(
|
|
336
|
+
owner=str(descriptor.key),
|
|
337
|
+
reason="migration registration metadata must be an immutable tuple",
|
|
338
|
+
)
|
|
339
|
+
self._migration_declarations.append((descriptor.module_type, declared_migrations))
|
|
340
|
+
for declaration_index, candidate in enumerate(declared_migrations):
|
|
341
|
+
self._add_migration(candidate, descriptor.key, declaration_index)
|
|
342
|
+
|
|
343
|
+
def _add_migration(
|
|
344
|
+
self,
|
|
345
|
+
candidate: object,
|
|
346
|
+
owner: ModuleKey,
|
|
347
|
+
declaration_index: int,
|
|
348
|
+
) -> None:
|
|
349
|
+
if not isinstance(candidate, SqlAlchemyMigrationRegistration):
|
|
350
|
+
raise SqlAlchemyModelRegistrationError(
|
|
351
|
+
owner=str(owner),
|
|
352
|
+
reason="migration registration metadata contains an invalid item",
|
|
353
|
+
)
|
|
354
|
+
registration = candidate
|
|
355
|
+
self._claim_branch(registration.branch_label, owner)
|
|
356
|
+
migration_path = _migration_package_path(
|
|
357
|
+
registration.migrations_package,
|
|
358
|
+
owner=owner,
|
|
359
|
+
)
|
|
360
|
+
for schema in registration.owned_schemas:
|
|
361
|
+
_claim_schema(
|
|
362
|
+
self._schema_owners,
|
|
363
|
+
connection_name=registration.connection_name,
|
|
364
|
+
schema=schema,
|
|
365
|
+
owner=owner,
|
|
366
|
+
kind="migration",
|
|
367
|
+
)
|
|
368
|
+
self._migration_contributions.append(
|
|
369
|
+
SqlAlchemyMigrationContribution(
|
|
370
|
+
owner=owner,
|
|
371
|
+
registration=registration,
|
|
372
|
+
migration_path=migration_path,
|
|
373
|
+
declaration_index=declaration_index,
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def _claim_branch(self, branch_label: str, owner: ModuleKey) -> None:
|
|
378
|
+
previous_owner = self._branch_owners.get(branch_label)
|
|
379
|
+
if previous_owner is not None:
|
|
380
|
+
raise SqlAlchemyModelRegistrationError(
|
|
381
|
+
owner=str(owner),
|
|
382
|
+
reason=f"branch label {branch_label!r} is already owned by {previous_owner}",
|
|
383
|
+
)
|
|
384
|
+
self._branch_owners[branch_label] = owner
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _claim_schema(
|
|
388
|
+
owners: dict[tuple[str, str], tuple[ModuleKey, str]],
|
|
389
|
+
*,
|
|
390
|
+
connection_name: str,
|
|
391
|
+
schema: str,
|
|
392
|
+
owner: ModuleKey,
|
|
393
|
+
kind: str,
|
|
394
|
+
) -> None:
|
|
395
|
+
identity = (connection_name, schema.lower())
|
|
396
|
+
previous = owners.get(identity)
|
|
397
|
+
if previous is not None:
|
|
398
|
+
previous_owner, previous_kind = previous
|
|
399
|
+
if previous_owner == owner and previous_kind == kind == "model":
|
|
400
|
+
return
|
|
401
|
+
raise SqlAlchemyModelRegistrationError(
|
|
402
|
+
owner=str(owner),
|
|
403
|
+
reason=(
|
|
404
|
+
f"schema {schema!r} on connection {connection_name!r} is already owned by "
|
|
405
|
+
f"{previous_owner} ({previous_kind})"
|
|
406
|
+
),
|
|
407
|
+
)
|
|
408
|
+
owners[identity] = (owner, kind)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _is_postgres_identifier(value: object) -> bool:
|
|
412
|
+
return (
|
|
413
|
+
isinstance(value, str)
|
|
414
|
+
and bool(value)
|
|
415
|
+
and (value[0].isalpha() or value[0] == "_")
|
|
416
|
+
and all(character.isalnum() or character == "_" for character in value)
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _migration_package_path(package_name: str, *, owner: ModuleKey) -> Path:
|
|
421
|
+
try:
|
|
422
|
+
spec = importlib.util.find_spec(package_name)
|
|
423
|
+
except (ImportError, AttributeError, ValueError):
|
|
424
|
+
spec = None
|
|
425
|
+
locations = (
|
|
426
|
+
()
|
|
427
|
+
if spec is None or spec.submodule_search_locations is None
|
|
428
|
+
else tuple(spec.submodule_search_locations)
|
|
429
|
+
)
|
|
430
|
+
if len(locations) != 1:
|
|
431
|
+
raise SqlAlchemyModelRegistrationError(
|
|
432
|
+
owner=str(owner),
|
|
433
|
+
reason=f"migration package {package_name!r} must resolve to one package directory",
|
|
434
|
+
)
|
|
435
|
+
return Path(locations[0]).resolve()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _metadata_fingerprint(metadata: MetaData) -> tuple[object, ...]:
|
|
439
|
+
return tuple(
|
|
440
|
+
_table_fingerprint(table)
|
|
441
|
+
for _, table in sorted(metadata.tables.items(), key=lambda item: item[0])
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _table_fingerprint(table: Table) -> tuple[object, ...]:
|
|
446
|
+
return (
|
|
447
|
+
table.key,
|
|
448
|
+
table.comment,
|
|
449
|
+
tuple(
|
|
450
|
+
(
|
|
451
|
+
column.key,
|
|
452
|
+
column.name,
|
|
453
|
+
str(column.type),
|
|
454
|
+
column.nullable,
|
|
455
|
+
column.primary_key,
|
|
456
|
+
column.unique,
|
|
457
|
+
column.index,
|
|
458
|
+
column.autoincrement,
|
|
459
|
+
column.comment,
|
|
460
|
+
_schema_value_fingerprint(column.default),
|
|
461
|
+
_schema_value_fingerprint(column.server_default),
|
|
462
|
+
_schema_value_fingerprint(column.onupdate),
|
|
463
|
+
_schema_value_fingerprint(column.server_onupdate),
|
|
464
|
+
tuple(sorted(_foreign_key_fingerprint(item) for item in column.foreign_keys)),
|
|
465
|
+
)
|
|
466
|
+
for column in table.columns
|
|
467
|
+
),
|
|
468
|
+
tuple(sorted(_constraint_fingerprint(constraint) for constraint in table.constraints)),
|
|
469
|
+
tuple(
|
|
470
|
+
sorted(
|
|
471
|
+
(
|
|
472
|
+
index.name or "",
|
|
473
|
+
index.unique,
|
|
474
|
+
tuple(str(expression) for expression in index.expressions),
|
|
475
|
+
tuple(
|
|
476
|
+
sorted(
|
|
477
|
+
(str(key), repr(value)) for key, value in index.dialect_kwargs.items()
|
|
478
|
+
)
|
|
479
|
+
),
|
|
480
|
+
)
|
|
481
|
+
for index in table.indexes
|
|
482
|
+
)
|
|
483
|
+
),
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _constraint_fingerprint(constraint: Constraint) -> tuple[object, ...]:
|
|
488
|
+
return (
|
|
489
|
+
type(constraint).__module__,
|
|
490
|
+
type(constraint).__qualname__,
|
|
491
|
+
str(constraint.name or ""),
|
|
492
|
+
_ddl_value_fingerprint(getattr(constraint, "deferrable", None)),
|
|
493
|
+
_ddl_value_fingerprint(getattr(constraint, "initially", None)),
|
|
494
|
+
_ddl_value_fingerprint(getattr(constraint, "comment", None)),
|
|
495
|
+
_ddl_value_fingerprint(getattr(constraint, "onupdate", None)),
|
|
496
|
+
_ddl_value_fingerprint(getattr(constraint, "ondelete", None)),
|
|
497
|
+
_ddl_value_fingerprint(getattr(constraint, "use_alter", None)),
|
|
498
|
+
_ddl_value_fingerprint(getattr(constraint, "link_to_name", None)),
|
|
499
|
+
_ddl_value_fingerprint(getattr(constraint, "match", None)),
|
|
500
|
+
tuple(column.key for column in getattr(constraint, "columns", ())),
|
|
501
|
+
str(getattr(constraint, "sqltext", "")),
|
|
502
|
+
tuple(
|
|
503
|
+
sorted(
|
|
504
|
+
_foreign_key_fingerprint(element)
|
|
505
|
+
for element in getattr(constraint, "elements", ())
|
|
506
|
+
if isinstance(element, ForeignKey)
|
|
507
|
+
)
|
|
508
|
+
),
|
|
509
|
+
tuple(sorted((str(key), repr(value)) for key, value in constraint.dialect_kwargs.items())),
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _foreign_key_fingerprint(foreign_key: ForeignKey) -> tuple[object, ...]:
|
|
514
|
+
return (
|
|
515
|
+
str(foreign_key.target_fullname),
|
|
516
|
+
str(foreign_key.name or ""),
|
|
517
|
+
_ddl_value_fingerprint(foreign_key.onupdate),
|
|
518
|
+
_ddl_value_fingerprint(foreign_key.ondelete),
|
|
519
|
+
_ddl_value_fingerprint(foreign_key.deferrable),
|
|
520
|
+
_ddl_value_fingerprint(foreign_key.initially),
|
|
521
|
+
_ddl_value_fingerprint(foreign_key.use_alter),
|
|
522
|
+
_ddl_value_fingerprint(foreign_key.link_to_name),
|
|
523
|
+
_ddl_value_fingerprint(foreign_key.match),
|
|
524
|
+
_ddl_value_fingerprint(foreign_key.comment),
|
|
525
|
+
tuple(sorted((str(key), repr(value)) for key, value in foreign_key.dialect_kwargs.items())),
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _ddl_value_fingerprint(value: object | None) -> tuple[str, str, str]:
|
|
530
|
+
# constraint fingerprints 需要排序;连 None 也编码为同形 tuple,避免显式值与 None 无法比较。
|
|
531
|
+
return type(value).__module__, type(value).__qualname__, repr(value)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _schema_value_fingerprint(value: object | None) -> tuple[str, str, str] | None:
|
|
535
|
+
if value is None:
|
|
536
|
+
return None
|
|
537
|
+
argument = getattr(value, "arg", value)
|
|
538
|
+
return type(value).__module__, type(value).__qualname__, str(argument)
|