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,46 @@
|
|
|
1
|
+
"""实现声明的验证开关;静态 metadata 不保存调用状态。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
|
|
7
|
+
from .errors import ApplicationServiceDefinitionError
|
|
8
|
+
|
|
9
|
+
_T = TypeVar("_T")
|
|
10
|
+
_VALIDATION_ATTRIBUTE = "__python_platform_validation_enabled__"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _declare_switch(target: _T, attribute: str, enabled: bool) -> _T:
|
|
14
|
+
if not isinstance(target, type) and not callable(target):
|
|
15
|
+
raise ApplicationServiceDefinitionError(
|
|
16
|
+
symbol=str(type(target)), reason="policy requires a class or method"
|
|
17
|
+
)
|
|
18
|
+
if attribute in vars(target):
|
|
19
|
+
raise ApplicationServiceDefinitionError(
|
|
20
|
+
symbol=getattr(target, "__qualname__", "<policy>"), reason="policy is already declared"
|
|
21
|
+
)
|
|
22
|
+
setattr(target, attribute, enabled)
|
|
23
|
+
return target
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def disable_validation(target: _T) -> _T:
|
|
27
|
+
return _declare_switch(target, _VALIDATION_ATTRIBUTE, False)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def enable_validation(target: _T) -> _T:
|
|
31
|
+
return _declare_switch(target, _VALIDATION_ATTRIBUTE, True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _effective_switch(
|
|
35
|
+
implementation: type[object], method: object, attribute: str, default: bool
|
|
36
|
+
) -> bool:
|
|
37
|
+
# 方法优先,再取 MRO 中最近的类声明;不把基类的禁用累加成无法恢复的开关。
|
|
38
|
+
for target in (method, *implementation.__mro__):
|
|
39
|
+
if attribute in vars(target):
|
|
40
|
+
value = vars(target)[attribute]
|
|
41
|
+
if not isinstance(value, bool):
|
|
42
|
+
raise ApplicationServiceDefinitionError(
|
|
43
|
+
symbol=implementation.__qualname__, reason="policy metadata must be bool"
|
|
44
|
+
)
|
|
45
|
+
return value
|
|
46
|
+
return default
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""显式 Migration Host 使用的 ordered、per-contributor UoW seeding。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from typing import TYPE_CHECKING, cast
|
|
7
|
+
|
|
8
|
+
from ..errors.base import FrameworkError
|
|
9
|
+
from ..modularity import AppModule
|
|
10
|
+
from ..modularity.selection import _dependency_keys
|
|
11
|
+
from .contracts import _InternalApplicationServiceContributor
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..application import Application
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DataSeedContributor(_InternalApplicationServiceContributor):
|
|
18
|
+
"""幂等性由具体 contributor 保证;框架只拥有顺序和独立 invocation。"""
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
async def seed(self) -> None:
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DataSeedError(FrameworkError):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
completed: tuple[type[DataSeedContributor], ...],
|
|
30
|
+
failed: type[DataSeedContributor],
|
|
31
|
+
original_error: BaseException,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.completed = completed
|
|
34
|
+
self.failed = failed
|
|
35
|
+
self.original_error = original_error
|
|
36
|
+
super().__init__(
|
|
37
|
+
f"Data seed contributor {failed.__module__}.{failed.__qualname__} failed after "
|
|
38
|
+
f"{len(completed)} completed contributor(s): {type(original_error).__name__}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DataSeeder:
|
|
43
|
+
__slots__ = ("_application",)
|
|
44
|
+
|
|
45
|
+
def __init__(self, application: Application) -> None:
|
|
46
|
+
self._application = application
|
|
47
|
+
|
|
48
|
+
async def seed_all(
|
|
49
|
+
self, *, module: type[AppModule] | None = None
|
|
50
|
+
) -> tuple[type[DataSeedContributor], ...]:
|
|
51
|
+
owners = _dependency_keys(self._application.modules, module) if module is not None else None
|
|
52
|
+
registrations = tuple(
|
|
53
|
+
registration
|
|
54
|
+
for registration in self._application.application_service_catalog.registrations
|
|
55
|
+
if issubclass(registration.implementation, DataSeedContributor)
|
|
56
|
+
and (owners is None or registration.implementation_owner in owners)
|
|
57
|
+
)
|
|
58
|
+
completed: list[type[DataSeedContributor]] = []
|
|
59
|
+
for registration in registrations:
|
|
60
|
+
contributor = cast(type[DataSeedContributor], registration.implementation)
|
|
61
|
+
# contributor 不发布 single Protocol,但仍复用同一 validation/UoW/event dispatcher。
|
|
62
|
+
try:
|
|
63
|
+
await self._application._invoke_registration(registration, "seed")
|
|
64
|
+
except Exception as original_error:
|
|
65
|
+
error = DataSeedError(
|
|
66
|
+
completed=tuple(completed),
|
|
67
|
+
failed=contributor,
|
|
68
|
+
original_error=original_error,
|
|
69
|
+
)
|
|
70
|
+
raise error from original_error
|
|
71
|
+
completed.append(contributor)
|
|
72
|
+
return tuple(completed)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""业务签名比较保留 Pydantic 约束,把 FieldInfo 的 wire projection 与业务值分开。"""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from copy import copy
|
|
5
|
+
from typing import Annotated, Any, get_args, get_origin
|
|
6
|
+
|
|
7
|
+
from pydantic.fields import FieldInfo
|
|
8
|
+
from pydantic_core import PydanticUndefined
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _business_annotation(annotation: Any) -> Any:
|
|
12
|
+
if get_origin(annotation) is not Annotated:
|
|
13
|
+
return annotation
|
|
14
|
+
base, *metadata = get_args(annotation)
|
|
15
|
+
constraints: list[object] = []
|
|
16
|
+
for item in metadata:
|
|
17
|
+
if isinstance(item, FieldInfo):
|
|
18
|
+
# Path(gt=...) 仍携带业务约束,不能整体丢掉 Annotated 或 FieldInfo。
|
|
19
|
+
constraints.extend(item.metadata)
|
|
20
|
+
for attribute in ("default_factory", "discriminator", "validate_default", "frozen"):
|
|
21
|
+
value = getattr(item, attribute)
|
|
22
|
+
if value is not None:
|
|
23
|
+
constraints.append((attribute, value))
|
|
24
|
+
if item.default is not PydanticUndefined:
|
|
25
|
+
constraints.append(("default", item.default))
|
|
26
|
+
else:
|
|
27
|
+
constraints.append(item)
|
|
28
|
+
return (base, tuple(constraints)) if constraints else base
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _business_signature(signature: inspect.Signature) -> inspect.Signature:
|
|
32
|
+
parameters = []
|
|
33
|
+
for parameter in signature.parameters.values():
|
|
34
|
+
default = parameter.default
|
|
35
|
+
annotation = parameter.annotation
|
|
36
|
+
if isinstance(default, FieldInfo):
|
|
37
|
+
binding = copy(default)
|
|
38
|
+
# 参数默认值由 Signature 比较,不能因 wire 声明的载体不同再比较一次。
|
|
39
|
+
binding.default = PydanticUndefined
|
|
40
|
+
annotation = Annotated[annotation, binding]
|
|
41
|
+
default = default.default
|
|
42
|
+
if default is PydanticUndefined:
|
|
43
|
+
default = inspect.Parameter.empty
|
|
44
|
+
parameters.append(
|
|
45
|
+
parameter.replace(annotation=_business_annotation(annotation), default=default)
|
|
46
|
+
)
|
|
47
|
+
return signature.replace(
|
|
48
|
+
parameters=parameters, return_annotation=_business_annotation(signature.return_annotation)
|
|
49
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Pydantic 调用参数边界与诊断脱敏;只保留静态签名允许的信息。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from .catalog import ApplicationServiceRegistration, _CompiledMethod
|
|
10
|
+
from .errors import ApplicationServiceValidationError, ApplicationServiceValidationIssue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def _validate_arguments(
|
|
14
|
+
method: _CompiledMethod,
|
|
15
|
+
registration: ApplicationServiceRegistration,
|
|
16
|
+
args: tuple[object, ...],
|
|
17
|
+
kwargs: dict[str, object],
|
|
18
|
+
) -> tuple[tuple[object, ...], dict[str, object]]:
|
|
19
|
+
validation_error: ApplicationServiceValidationError | None = None
|
|
20
|
+
try:
|
|
21
|
+
return await method.validator(None, *args, **kwargs)
|
|
22
|
+
except ValidationError as original_error:
|
|
23
|
+
validation_error = _sanitize_validation_error(
|
|
24
|
+
original_error,
|
|
25
|
+
registration=registration,
|
|
26
|
+
method_name=method.name,
|
|
27
|
+
parameter_names=method.parameter_names,
|
|
28
|
+
)
|
|
29
|
+
# 在 Pydantic except context 外抛出,避免 exception.__context__ 留住 raw input。
|
|
30
|
+
raise validation_error from None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _sanitize_validation_error(
|
|
34
|
+
error: ValidationError,
|
|
35
|
+
*,
|
|
36
|
+
registration: ApplicationServiceRegistration,
|
|
37
|
+
method_name: str,
|
|
38
|
+
parameter_names: frozenset[str],
|
|
39
|
+
) -> ApplicationServiceValidationError:
|
|
40
|
+
issues = tuple(
|
|
41
|
+
ApplicationServiceValidationIssue(
|
|
42
|
+
location=_sanitize_validation_location(detail.get("loc"), parameter_names),
|
|
43
|
+
error_type=_sanitize_validation_error_type(detail),
|
|
44
|
+
# 自定义 Pydantic validator 的 msg/type 可能自行拼入原值;公开错误只保留静态事实。
|
|
45
|
+
message="Input validation failed",
|
|
46
|
+
)
|
|
47
|
+
for detail in error.errors(
|
|
48
|
+
include_url=True,
|
|
49
|
+
include_context=False,
|
|
50
|
+
include_input=False,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
return ApplicationServiceValidationError(
|
|
54
|
+
service=registration.contract,
|
|
55
|
+
method_name=method_name,
|
|
56
|
+
issues=issues,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _sanitize_validation_location(
|
|
61
|
+
location: object,
|
|
62
|
+
parameter_names: frozenset[str],
|
|
63
|
+
) -> tuple[str | int, ...]:
|
|
64
|
+
if not isinstance(location, tuple) or not location:
|
|
65
|
+
return ()
|
|
66
|
+
first = location[0]
|
|
67
|
+
if isinstance(first, str) and first in parameter_names:
|
|
68
|
+
return (first,)
|
|
69
|
+
if isinstance(first, int) and not isinstance(first, bool) and first >= 0:
|
|
70
|
+
return (first,)
|
|
71
|
+
return ()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _sanitize_validation_error_type(detail: Mapping[str, object]) -> str:
|
|
75
|
+
error_type = detail.get("type")
|
|
76
|
+
url = detail.get("url")
|
|
77
|
+
if (
|
|
78
|
+
isinstance(error_type, str)
|
|
79
|
+
and isinstance(url, str)
|
|
80
|
+
and url.startswith("https://errors.pydantic.dev/")
|
|
81
|
+
and url.endswith(f"/v/{error_type}")
|
|
82
|
+
):
|
|
83
|
+
return error_type
|
|
84
|
+
return "value_error"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .contracts import ApplicationServiceAuditRecord, AuditSink, RequestAuditRecord
|
|
2
|
+
|
|
3
|
+
__all__ = (
|
|
4
|
+
"ApplicationServiceAuditRecord",
|
|
5
|
+
"AuditSink",
|
|
6
|
+
"AuditingControl",
|
|
7
|
+
"AuditingOptions",
|
|
8
|
+
"RequestAuditRecord",
|
|
9
|
+
"audited",
|
|
10
|
+
"disable_auditing",
|
|
11
|
+
)
|
|
12
|
+
from .control import AuditingControl, AuditingOptions, audited, disable_auditing
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""请求与 Application Service 审计只记录允许的静态事实。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class RequestAuditRecord:
|
|
13
|
+
occurred_at: datetime
|
|
14
|
+
user_id: UUID | None
|
|
15
|
+
session_id: UUID | None
|
|
16
|
+
correlation_id: str
|
|
17
|
+
route_template: str
|
|
18
|
+
status_code: int
|
|
19
|
+
duration_ns: int
|
|
20
|
+
exception_type: str | None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class ApplicationServiceAuditRecord:
|
|
25
|
+
occurred_at: datetime
|
|
26
|
+
user_id: UUID | None
|
|
27
|
+
session_id: UUID | None
|
|
28
|
+
correlation_id: str
|
|
29
|
+
contract: str
|
|
30
|
+
method: str
|
|
31
|
+
succeeded: bool
|
|
32
|
+
duration_ns: int
|
|
33
|
+
exception_type: str | None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AuditSink(Protocol):
|
|
37
|
+
async def write_request(self, record: RequestAuditRecord) -> None: ...
|
|
38
|
+
|
|
39
|
+
async def write_application_service(self, record: ApplicationServiceAuditRecord) -> None: ...
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""审计策略和 Application-local 运行时抑制;不改变外层请求审计。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from contextvars import ContextVar
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
from pydantic import ConfigDict
|
|
11
|
+
|
|
12
|
+
from ..application_services.policies import _declare_switch
|
|
13
|
+
from ..options import BaseOptions
|
|
14
|
+
|
|
15
|
+
_T = TypeVar("_T")
|
|
16
|
+
_AUDITING_ATTRIBUTE = "__python_platform_auditing_enabled__"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def disable_auditing(target: _T) -> _T:
|
|
20
|
+
return _declare_switch(target, _AUDITING_ATTRIBUTE, False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def audited(target: _T) -> _T:
|
|
24
|
+
return _declare_switch(target, _AUDITING_ATTRIBUTE, True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AuditingOptions(BaseOptions):
|
|
28
|
+
model_config = ConfigDict(extra="forbid")
|
|
29
|
+
is_enabled: bool = True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AuditingControl:
|
|
33
|
+
"""每个 Application 独立持有;只抑制当前异步上下文的方法审计。"""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self._disabled: ContextVar[bool] = ContextVar("method_auditing_disabled", default=False)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_enabled(self) -> bool:
|
|
40
|
+
return not self._disabled.get()
|
|
41
|
+
|
|
42
|
+
@contextmanager
|
|
43
|
+
def disable(self) -> Iterator[None]:
|
|
44
|
+
token = self._disabled.set(True)
|
|
45
|
+
try:
|
|
46
|
+
yield
|
|
47
|
+
finally:
|
|
48
|
+
self._disabled.reset(token)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Create the independent platform auditing migration branch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlalchemy as sa
|
|
6
|
+
from alembic import op
|
|
7
|
+
|
|
8
|
+
revision = "platform_auditing_0001"
|
|
9
|
+
down_revision = None
|
|
10
|
+
branch_labels = ("platform_auditing",)
|
|
11
|
+
depends_on = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def upgrade() -> None:
|
|
15
|
+
op.create_table(
|
|
16
|
+
"platform_audit_logs",
|
|
17
|
+
sa.Column("audit_id", sa.Uuid(), nullable=False),
|
|
18
|
+
sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False),
|
|
19
|
+
sa.Column("kind", sa.String(length=32), nullable=False),
|
|
20
|
+
sa.Column("user_id", sa.Uuid(), nullable=True),
|
|
21
|
+
sa.Column("session_id", sa.Uuid(), nullable=True),
|
|
22
|
+
sa.Column("correlation_id", sa.String(length=128), nullable=False),
|
|
23
|
+
sa.Column("route_template", sa.String(length=512), nullable=True),
|
|
24
|
+
sa.Column("contract", sa.String(length=512), nullable=True),
|
|
25
|
+
sa.Column("method", sa.String(length=128), nullable=True),
|
|
26
|
+
sa.Column("status_code", sa.Integer(), nullable=True),
|
|
27
|
+
sa.Column("succeeded", sa.Boolean(), nullable=True),
|
|
28
|
+
sa.Column("duration_ns", sa.BigInteger(), nullable=False),
|
|
29
|
+
sa.Column("exception_type", sa.String(length=256), nullable=True),
|
|
30
|
+
sa.PrimaryKeyConstraint("audit_id"),
|
|
31
|
+
)
|
|
32
|
+
op.create_index("ix_platform_audit_logs_kind", "platform_audit_logs", ["kind"])
|
|
33
|
+
op.create_index("ix_platform_audit_logs_user_id", "platform_audit_logs", ["user_id"])
|
|
34
|
+
op.create_index(
|
|
35
|
+
"ix_platform_audit_logs_correlation_id",
|
|
36
|
+
"platform_audit_logs",
|
|
37
|
+
["correlation_id"],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def downgrade() -> None:
|
|
42
|
+
op.drop_index("ix_platform_audit_logs_correlation_id", table_name="platform_audit_logs")
|
|
43
|
+
op.drop_index("ix_platform_audit_logs_user_id", table_name="platform_audit_logs")
|
|
44
|
+
op.drop_index("ix_platform_audit_logs_kind", table_name="platform_audit_logs")
|
|
45
|
+
op.drop_table("platform_audit_logs")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Platform auditing Alembic branch。"""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""审计日志的独立 metadata owner。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import BigInteger, Boolean, DateTime, Integer, String, Uuid
|
|
9
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuditingBase(DeclarativeBase):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuditLogRow(AuditingBase):
|
|
17
|
+
__tablename__ = "platform_audit_logs"
|
|
18
|
+
|
|
19
|
+
audit_id: Mapped[UUID] = mapped_column(Uuid, primary_key=True)
|
|
20
|
+
occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
21
|
+
kind: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
|
|
22
|
+
user_id: Mapped[UUID | None] = mapped_column(Uuid, nullable=True, index=True)
|
|
23
|
+
session_id: Mapped[UUID | None] = mapped_column(Uuid, nullable=True)
|
|
24
|
+
correlation_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
|
|
25
|
+
route_template: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
26
|
+
contract: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
27
|
+
method: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
28
|
+
status_code: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
29
|
+
succeeded: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
|
30
|
+
duration_ns: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
31
|
+
exception_type: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Host 显式选择的 SQLAlchemy 审计 provider Module。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dishka import Provider, Scope
|
|
6
|
+
|
|
7
|
+
from ...lifecycle import ConfigureContext
|
|
8
|
+
from ...modularity import AppModule
|
|
9
|
+
from ...sqlalchemy import (
|
|
10
|
+
SqlAlchemyModelRegistration,
|
|
11
|
+
SqlAlchemyPersistenceModule,
|
|
12
|
+
contributes_sqlalchemy_models,
|
|
13
|
+
)
|
|
14
|
+
from ..contracts import AuditSink
|
|
15
|
+
from .models import AuditingBase
|
|
16
|
+
from .store import SqlAlchemyAuditSink
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@contributes_sqlalchemy_models(
|
|
20
|
+
SqlAlchemyModelRegistration(
|
|
21
|
+
metadata=AuditingBase.metadata,
|
|
22
|
+
migrations_package="python_platform.auditing.sqlalchemy.migrations",
|
|
23
|
+
branch_label="platform_auditing",
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
class SqlAlchemyAuditingModule(AppModule):
|
|
27
|
+
dependencies = (SqlAlchemyPersistenceModule,)
|
|
28
|
+
|
|
29
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
30
|
+
provider = Provider()
|
|
31
|
+
provider.provide(SqlAlchemyAuditSink, provides=AuditSink, scope=Scope.APP)
|
|
32
|
+
context.services.contribute(provider, reason="SQLAlchemy independent audit sink")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""每条审计记录使用独立短事务,不加入业务 Unit of Work。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from ...sqlalchemy.auditing import PlatformAsyncSession
|
|
8
|
+
from ...sqlalchemy.runtime import _SqlAlchemyEngineRegistry
|
|
9
|
+
from ..contracts import ApplicationServiceAuditRecord, RequestAuditRecord
|
|
10
|
+
from .models import AuditLogRow
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SqlAlchemyAuditSink:
|
|
14
|
+
__slots__ = ("_engines",)
|
|
15
|
+
|
|
16
|
+
def __init__(self, engines: _SqlAlchemyEngineRegistry) -> None:
|
|
17
|
+
self._engines = engines
|
|
18
|
+
|
|
19
|
+
async def write_request(self, record: RequestAuditRecord) -> None:
|
|
20
|
+
await self._write(
|
|
21
|
+
AuditLogRow(
|
|
22
|
+
audit_id=uuid4(),
|
|
23
|
+
occurred_at=record.occurred_at,
|
|
24
|
+
kind="request",
|
|
25
|
+
user_id=record.user_id,
|
|
26
|
+
session_id=record.session_id,
|
|
27
|
+
correlation_id=record.correlation_id,
|
|
28
|
+
route_template=record.route_template,
|
|
29
|
+
contract=None,
|
|
30
|
+
method=None,
|
|
31
|
+
status_code=record.status_code,
|
|
32
|
+
succeeded=record.exception_type is None and record.status_code < 500,
|
|
33
|
+
duration_ns=record.duration_ns,
|
|
34
|
+
exception_type=record.exception_type,
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
async def write_application_service(self, record: ApplicationServiceAuditRecord) -> None:
|
|
39
|
+
await self._write(
|
|
40
|
+
AuditLogRow(
|
|
41
|
+
audit_id=uuid4(),
|
|
42
|
+
occurred_at=record.occurred_at,
|
|
43
|
+
kind="application_service",
|
|
44
|
+
user_id=record.user_id,
|
|
45
|
+
session_id=record.session_id,
|
|
46
|
+
correlation_id=record.correlation_id,
|
|
47
|
+
route_template=None,
|
|
48
|
+
contract=record.contract,
|
|
49
|
+
method=record.method,
|
|
50
|
+
status_code=None,
|
|
51
|
+
succeeded=record.succeeded,
|
|
52
|
+
duration_ns=record.duration_ns,
|
|
53
|
+
exception_type=record.exception_type,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def _write(self, row: AuditLogRow) -> None:
|
|
58
|
+
async with PlatformAsyncSession(
|
|
59
|
+
self._engines.get("default"),
|
|
60
|
+
expire_on_commit=False,
|
|
61
|
+
) as session:
|
|
62
|
+
async with session.begin():
|
|
63
|
+
session.add(row)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from .catalog import PermissionCatalog
|
|
2
|
+
from .contracts import (
|
|
3
|
+
AccessTokenAuthenticator,
|
|
4
|
+
CurrentUser,
|
|
5
|
+
PermissionChecker,
|
|
6
|
+
PermissionDefinition,
|
|
7
|
+
PermissionName,
|
|
8
|
+
RolePermissionVersion,
|
|
9
|
+
allow_anonymous,
|
|
10
|
+
authorize,
|
|
11
|
+
)
|
|
12
|
+
from .definitions import PermissionDefinitionContext, PermissionDefinitionProvider
|
|
13
|
+
from .errors import (
|
|
14
|
+
AuthorizationDefinitionError,
|
|
15
|
+
PermissionCheckerUnavailableError,
|
|
16
|
+
PermissionDefinitionError,
|
|
17
|
+
PermissionDeniedError,
|
|
18
|
+
UnauthenticatedError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = (
|
|
22
|
+
"AccessTokenAuthenticator",
|
|
23
|
+
"AuthorizationDefinitionError",
|
|
24
|
+
"CurrentUser",
|
|
25
|
+
"PermissionCatalog",
|
|
26
|
+
"PermissionChecker",
|
|
27
|
+
"PermissionCheckerUnavailableError",
|
|
28
|
+
"PermissionDefinition",
|
|
29
|
+
"PermissionDefinitionContext",
|
|
30
|
+
"PermissionDefinitionError",
|
|
31
|
+
"PermissionDefinitionProvider",
|
|
32
|
+
"PermissionDeniedError",
|
|
33
|
+
"PermissionName",
|
|
34
|
+
"RolePermissionVersion",
|
|
35
|
+
"UnauthenticatedError",
|
|
36
|
+
"allow_anonymous",
|
|
37
|
+
"authorize",
|
|
38
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""从 live Module metadata 聚合并冻结 Permission Catalog。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Iterator, Mapping
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
|
|
10
|
+
from ..modularity import ModuleRegistry
|
|
11
|
+
from ..modularity.discovery import _DiscoveredType
|
|
12
|
+
from .contracts import PermissionDefinition, PermissionName
|
|
13
|
+
from .definition_discovery import _collect_permission_definitions
|
|
14
|
+
from .errors import PermissionDefinitionError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PermissionCatalog:
|
|
18
|
+
__slots__ = ("_definitions", "_fingerprint")
|
|
19
|
+
|
|
20
|
+
def __init__(self, definitions: tuple[PermissionDefinition, ...]) -> None:
|
|
21
|
+
by_name: dict[PermissionName, PermissionDefinition] = {}
|
|
22
|
+
for definition in definitions:
|
|
23
|
+
if not isinstance(definition, PermissionDefinition):
|
|
24
|
+
raise PermissionDefinitionError(
|
|
25
|
+
permission=repr(definition),
|
|
26
|
+
reason="catalog entries must be PermissionDefinition values",
|
|
27
|
+
)
|
|
28
|
+
if definition.name in by_name:
|
|
29
|
+
raise PermissionDefinitionError(
|
|
30
|
+
permission=str(definition.name),
|
|
31
|
+
reason="permission is declared more than once",
|
|
32
|
+
)
|
|
33
|
+
by_name[definition.name] = definition
|
|
34
|
+
ordered = dict(sorted(by_name.items(), key=lambda item: item[0].value))
|
|
35
|
+
self._definitions: Mapping[PermissionName, PermissionDefinition] = MappingProxyType(ordered)
|
|
36
|
+
payload = tuple((item.name.value, item.display_name) for item in ordered.values())
|
|
37
|
+
self._fingerprint = hashlib.sha256(
|
|
38
|
+
json.dumps(payload, ensure_ascii=True, separators=(",", ":")).encode("utf-8")
|
|
39
|
+
).hexdigest()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def definitions(self) -> tuple[PermissionDefinition, ...]:
|
|
43
|
+
return tuple(self._definitions.values())
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def fingerprint(self) -> str:
|
|
47
|
+
return self._fingerprint
|
|
48
|
+
|
|
49
|
+
def __contains__(self, name: object) -> bool:
|
|
50
|
+
return name in self._definitions
|
|
51
|
+
|
|
52
|
+
def __iter__(self) -> Iterator[PermissionName]:
|
|
53
|
+
return iter(self._definitions)
|
|
54
|
+
|
|
55
|
+
def get(self, name: PermissionName) -> PermissionDefinition | None:
|
|
56
|
+
return self._definitions.get(name)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_permission_catalog(
|
|
60
|
+
registry: ModuleRegistry,
|
|
61
|
+
discoveries: tuple[_DiscoveredType, ...],
|
|
62
|
+
) -> PermissionCatalog:
|
|
63
|
+
return PermissionCatalog(_collect_permission_definitions(registry, discoveries))
|