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,40 @@
|
|
|
1
|
+
"""管理路由复用正式 Application Service 认证、权限与参数绑定。"""
|
|
2
|
+
|
|
3
|
+
from ..modularity import AppModule
|
|
4
|
+
from ..settings.management import SettingsManagementApplicationService
|
|
5
|
+
from ..settings.module import SettingsManagementModule
|
|
6
|
+
from . import ApplicationServiceRouteOverride, exposes_application_services
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@exposes_application_services(
|
|
10
|
+
SettingsManagementModule,
|
|
11
|
+
root_path="settings",
|
|
12
|
+
overrides=(
|
|
13
|
+
ApplicationServiceRouteOverride(
|
|
14
|
+
SettingsManagementApplicationService,
|
|
15
|
+
method=SettingsManagementApplicationService.get_list,
|
|
16
|
+
path="/api/settings",
|
|
17
|
+
verb="GET",
|
|
18
|
+
),
|
|
19
|
+
ApplicationServiceRouteOverride(
|
|
20
|
+
SettingsManagementApplicationService,
|
|
21
|
+
method=SettingsManagementApplicationService.get,
|
|
22
|
+
path="/api/settings/{name}",
|
|
23
|
+
verb="GET",
|
|
24
|
+
),
|
|
25
|
+
ApplicationServiceRouteOverride(
|
|
26
|
+
SettingsManagementApplicationService,
|
|
27
|
+
method=SettingsManagementApplicationService.update,
|
|
28
|
+
path="/api/settings/{name}",
|
|
29
|
+
verb="PUT",
|
|
30
|
+
),
|
|
31
|
+
ApplicationServiceRouteOverride(
|
|
32
|
+
SettingsManagementApplicationService,
|
|
33
|
+
method=SettingsManagementApplicationService.reset,
|
|
34
|
+
path="/api/settings/{name}/reset",
|
|
35
|
+
verb="POST",
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
class SettingsManagementHttpApiModule(AppModule):
|
|
40
|
+
dependencies = (SettingsManagementModule,)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""FastAPI 官方 instrumentation 的 Application provider 接线。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from starlette.background import BackgroundTask
|
|
10
|
+
|
|
11
|
+
from ..application import Application
|
|
12
|
+
from ..observability.options import TracingOptions
|
|
13
|
+
from ..options import Options
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@contextmanager
|
|
17
|
+
def _tracing_rollback(app: FastAPI) -> Iterator[None]:
|
|
18
|
+
already_instrumented = getattr(app, "_is_instrumented_by_opentelemetry", False)
|
|
19
|
+
background_call = BackgroundTask.__call__
|
|
20
|
+
original_call = getattr(BackgroundTask, "_otel_original_call", None)
|
|
21
|
+
try:
|
|
22
|
+
yield
|
|
23
|
+
except BaseException:
|
|
24
|
+
if not already_instrumented and getattr(app, "_is_instrumented_by_opentelemetry", False):
|
|
25
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
26
|
+
|
|
27
|
+
# 官方撤销会重建栈;失败的 Host 构造器不能在撤销时再次执行。
|
|
28
|
+
# 外层安装事务随后恢复原 middleware、stack 和实例属性。
|
|
29
|
+
app.user_middleware = []
|
|
30
|
+
try:
|
|
31
|
+
FastAPIInstrumentor.uninstrument_app(app)
|
|
32
|
+
finally:
|
|
33
|
+
# 0.65b0 撤销会修改全局 BackgroundTask;保留其他 Host 已有接线。
|
|
34
|
+
BackgroundTask.__call__ = background_call # type: ignore[method-assign]
|
|
35
|
+
if original_call is not None:
|
|
36
|
+
BackgroundTask._otel_original_call = original_call # type: ignore[attr-defined]
|
|
37
|
+
raise
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _install_tracing(application: Application, app: FastAPI) -> None:
|
|
41
|
+
if TracingOptions not in application.options.values or getattr(
|
|
42
|
+
app, "_is_instrumented_by_opentelemetry", False
|
|
43
|
+
):
|
|
44
|
+
return
|
|
45
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
46
|
+
|
|
47
|
+
from ..observability.tracing import TracingModule
|
|
48
|
+
|
|
49
|
+
# provider 来自正式 Module 实例;不建立应用级 tracer registry 或全局 SDK provider。
|
|
50
|
+
module = next(
|
|
51
|
+
descriptor.instance
|
|
52
|
+
for descriptor in application.modules.dependency_order
|
|
53
|
+
if isinstance(descriptor.instance, TracingModule)
|
|
54
|
+
)
|
|
55
|
+
provider = module._prepare(Options(application.options.get(TracingOptions)))
|
|
56
|
+
FastAPIInstrumentor.instrument_app(
|
|
57
|
+
app,
|
|
58
|
+
tracer_provider=provider,
|
|
59
|
+
http_capture_headers_server_request=[],
|
|
60
|
+
http_capture_headers_server_response=[],
|
|
61
|
+
exclude_spans=["receive", "send"],
|
|
62
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""原生传输资源的退出边界,早于 REQUEST 退出,晚于业务 UoW。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
from contextlib import AsyncExitStack
|
|
9
|
+
|
|
10
|
+
from anyio import CancelScope
|
|
11
|
+
from starlette.concurrency import iterate_in_threadpool, run_in_threadpool
|
|
12
|
+
from starlette.datastructures import FormData, UploadFile
|
|
13
|
+
from starlette.requests import ClientDisconnect, Request
|
|
14
|
+
from starlette.responses import StreamingResponse
|
|
15
|
+
from starlette.types import Send
|
|
16
|
+
|
|
17
|
+
from ..application_services.invocation import _InvocationRuntime
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def _close_form(form: FormData) -> None:
|
|
21
|
+
# native 正常 close 已执行时无需再关;取消导致 native close 中断时补齐剩余文件。
|
|
22
|
+
with CancelScope(shield=True):
|
|
23
|
+
async with AsyncExitStack() as stack:
|
|
24
|
+
for _, value in form.multi_items():
|
|
25
|
+
if isinstance(value, UploadFile) and not value.file.closed:
|
|
26
|
+
stack.push_async_callback(value.close)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _manage_stream(request: Request, response: object) -> None:
|
|
30
|
+
if not isinstance(response, StreamingResponse):
|
|
31
|
+
return
|
|
32
|
+
if isinstance(getattr(response.stream_response, "__self__", None), _StreamLifetime):
|
|
33
|
+
return
|
|
34
|
+
iterator = response.body_iterator
|
|
35
|
+
close: Callable[[], Awaitable[None]] | None = None
|
|
36
|
+
# Starlette 同步流已经转换为原生线程池生成器;在第一次迭代前取得其输入。
|
|
37
|
+
# 按实际函数签名读取,不复制参数名称或线程池算法。此处是锁定版本的集成接点。
|
|
38
|
+
if inspect.isasyncgen(iterator) and iterator.ag_code is iterate_in_threadpool.__code__:
|
|
39
|
+
frame = iterator.ag_frame
|
|
40
|
+
assert frame is not None
|
|
41
|
+
arguments = inspect.getargvalues(frame)
|
|
42
|
+
source = iter(arguments.locals[arguments.args[0]])
|
|
43
|
+
response.body_iterator = iterate_in_threadpool(source)
|
|
44
|
+
sync_close = getattr(source, "close", None)
|
|
45
|
+
if sync_close is not None:
|
|
46
|
+
|
|
47
|
+
async def close() -> None:
|
|
48
|
+
await run_in_threadpool(sync_close)
|
|
49
|
+
else:
|
|
50
|
+
iterator = aiter(iterator)
|
|
51
|
+
response.body_iterator = iterator
|
|
52
|
+
close = getattr(iterator, "aclose", None)
|
|
53
|
+
lifetime = _StreamLifetime(request, response.stream_response, close)
|
|
54
|
+
response.stream_response = lifetime.send # type: ignore[method-assign]
|
|
55
|
+
request.scope["fastapi_middleware_astack"].push_async_callback(lifetime.close)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _StreamLifetime:
|
|
59
|
+
"""producer 串行拥有迭代与关闭;请求监督它结束后才允许 REQUEST/stop 退出。"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
request: Request,
|
|
64
|
+
send: Callable[[Send], Awaitable[None]],
|
|
65
|
+
close: Callable[[], Awaitable[None]] | None,
|
|
66
|
+
) -> None:
|
|
67
|
+
self._request = request
|
|
68
|
+
self._send = send
|
|
69
|
+
self._close = close
|
|
70
|
+
|
|
71
|
+
async def close(self) -> None:
|
|
72
|
+
# 先取走回调,避免 producer finally 与请求退出栈重复关闭同一资源。
|
|
73
|
+
# 这里只保护平台调用的 close;生成器在迭代中进入的 finally 必须自行 shield。
|
|
74
|
+
close, self._close = self._close, None
|
|
75
|
+
if close is not None:
|
|
76
|
+
with CancelScope(shield=True):
|
|
77
|
+
await close()
|
|
78
|
+
|
|
79
|
+
async def send(self, send: Send) -> None:
|
|
80
|
+
# scope 可在 producer 启动前取消,但只能由 producer 自己进入/退出。
|
|
81
|
+
cancel_scope = CancelScope()
|
|
82
|
+
task = asyncio.create_task(self._run(send, cancel_scope))
|
|
83
|
+
try:
|
|
84
|
+
# wait 不向 producer 传播 raw cancel,也不把已监督的清理异常重复报告给 loop。
|
|
85
|
+
await asyncio.wait((task,))
|
|
86
|
+
task.result()
|
|
87
|
+
except asyncio.CancelledError as primary:
|
|
88
|
+
# ASGI <2.4 的原生 task group 会吞掉 disconnect 引发的取消;保留请求失败事实,
|
|
89
|
+
# 审计可以记录它,但不能在已发送响应后重新生成错误 body。
|
|
90
|
+
self._request.state.python_platform_transfer_failure = ClientDisconnect()
|
|
91
|
+
# 原生 scope 尊重线程池等待和生成器自己的 shield;raw task.cancel 会绕过它们。
|
|
92
|
+
# 强引用并等待 producer,重复请求取消不得使 REQUEST 提前退出。
|
|
93
|
+
cancel_scope.cancel()
|
|
94
|
+
with CancelScope(shield=True):
|
|
95
|
+
while not task.done():
|
|
96
|
+
try:
|
|
97
|
+
await asyncio.wait((task,))
|
|
98
|
+
except asyncio.CancelledError:
|
|
99
|
+
# 原生 shield 阻挡 AnyIO 的重复取消;raw asyncio 取消仍可能到达监督者。
|
|
100
|
+
# 继续等待同一个 producer,不重发取消、不重启或放弃清理。
|
|
101
|
+
pass
|
|
102
|
+
try:
|
|
103
|
+
task.result()
|
|
104
|
+
except asyncio.CancelledError:
|
|
105
|
+
# producer 已结束;对调用方保留首次取消,而不是替换成内部 scope 的取消。
|
|
106
|
+
pass
|
|
107
|
+
except BaseException as failure:
|
|
108
|
+
# ASGI <2.4 的 task group 会吞 CancelledError,即使它带有失败 cause。
|
|
109
|
+
# 让清理失败穿过原生 task group,原始取消保留在异常链中,不重发响应。
|
|
110
|
+
raise failure from primary
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
async def _run(self, send: Send, cancel_scope: CancelScope) -> None:
|
|
114
|
+
container = self._request.state.dishka_container
|
|
115
|
+
runtime = await container.get(_InvocationRuntime)
|
|
116
|
+
user, correlation = self._request.state.python_platform_identity
|
|
117
|
+
# 可信 REQUEST 许可覆盖发送和关闭;内层取消 scope 只控制发送。
|
|
118
|
+
# 先退出内层 scope 再执行 finalizer,避免重复取消破坏关闭顺序。
|
|
119
|
+
with (
|
|
120
|
+
CancelScope(shield=True),
|
|
121
|
+
runtime.trusted_http_task(container, user, correlation, self._request.method),
|
|
122
|
+
):
|
|
123
|
+
try:
|
|
124
|
+
with cancel_scope:
|
|
125
|
+
await self._send(send)
|
|
126
|
+
finally:
|
|
127
|
+
# 发送/next 已退出才能串行关闭;生成器内部 finally 的 await 由其自己 shield。
|
|
128
|
+
await self.close()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""不可变上传策略;默认值、字段和覆盖语义的唯一权威。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, fields, replace
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import TypeVar
|
|
10
|
+
|
|
11
|
+
_F = TypeVar("_F", bound=Callable[..., object])
|
|
12
|
+
_LIMITS = "__python_platform_upload_limits__"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _Unset(Enum):
|
|
16
|
+
VALUE = "unset"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class UploadLimits:
|
|
21
|
+
"""Host 默认;总量含编码开销,max_file_size=None 表示不另限单文件。"""
|
|
22
|
+
|
|
23
|
+
max_request_size: int = 30_000_000
|
|
24
|
+
max_file_size: int | None = None
|
|
25
|
+
max_files: int = 1000
|
|
26
|
+
max_fields: int = 1000
|
|
27
|
+
max_part_size: int = 1024 * 1024
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
for field in fields(self):
|
|
31
|
+
value = getattr(self, field.name)
|
|
32
|
+
if value is None and field.default is None:
|
|
33
|
+
continue
|
|
34
|
+
if type(value) is not int or value < 0:
|
|
35
|
+
raise ValueError(f"{field.name} must be a non-negative integer")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def upload_limits(
|
|
39
|
+
*,
|
|
40
|
+
max_request_size: int | _Unset = _Unset.VALUE,
|
|
41
|
+
max_file_size: int | _Unset | None = _Unset.VALUE,
|
|
42
|
+
max_files: int | _Unset = _Unset.VALUE,
|
|
43
|
+
max_fields: int | _Unset = _Unset.VALUE,
|
|
44
|
+
max_part_size: int | _Unset = _Unset.VALUE,
|
|
45
|
+
) -> Callable[[_F], _F]:
|
|
46
|
+
"""只覆盖显式字段;单文件 None 显式取消 Host 的单文件上限。"""
|
|
47
|
+
values = locals()
|
|
48
|
+
overrides = {
|
|
49
|
+
field.name: values[field.name]
|
|
50
|
+
for field in fields(UploadLimits)
|
|
51
|
+
if values[field.name] is not _Unset.VALUE
|
|
52
|
+
}
|
|
53
|
+
replace(UploadLimits(), **overrides)
|
|
54
|
+
|
|
55
|
+
def decorate(method: _F) -> _F:
|
|
56
|
+
setattr(method, _LIMITS, MappingProxyType(overrides))
|
|
57
|
+
return method
|
|
58
|
+
|
|
59
|
+
return decorate
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _limits_for(host: UploadLimits, method: object) -> UploadLimits:
|
|
63
|
+
return replace(host, **getattr(method, _LIMITS, {}))
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Starlette 上传 parser 接线;不拥有业务持久化文件。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
|
|
7
|
+
from fastapi.params import Form
|
|
8
|
+
from fastapi.routing import APIRoute
|
|
9
|
+
from python_multipart.exceptions import MultipartParseError
|
|
10
|
+
from python_multipart.multipart import parse_options_header
|
|
11
|
+
from starlette.exceptions import HTTPException
|
|
12
|
+
from starlette.formparsers import FormParser, MultiPartException, MultiPartParser
|
|
13
|
+
from starlette.requests import Request
|
|
14
|
+
|
|
15
|
+
from .transfer import _close_form
|
|
16
|
+
from .upload_limits import UploadLimits, _limits_for
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _LimitedMultipartParser(MultiPartParser):
|
|
20
|
+
def __init__(
|
|
21
|
+
self, request: Request, stream: AsyncGenerator[bytes, None], limits: UploadLimits
|
|
22
|
+
) -> None:
|
|
23
|
+
self._ended = False
|
|
24
|
+
super().__init__(
|
|
25
|
+
request.headers,
|
|
26
|
+
self._complete_stream(stream),
|
|
27
|
+
max_files=limits.max_files,
|
|
28
|
+
max_fields=limits.max_fields,
|
|
29
|
+
max_part_size=limits.max_part_size,
|
|
30
|
+
)
|
|
31
|
+
self._max_file_size = limits.max_file_size
|
|
32
|
+
self._file_size = 0
|
|
33
|
+
|
|
34
|
+
async def _complete_stream(
|
|
35
|
+
self, stream: AsyncGenerator[bytes, None]
|
|
36
|
+
) -> AsyncGenerator[bytes, None]:
|
|
37
|
+
async for chunk in stream:
|
|
38
|
+
yield chunk
|
|
39
|
+
# 上游 finalize 不拒绝被截断的最后一个 part;在 native parse 的 try 内拒绝,
|
|
40
|
+
# 仍由它的 BaseException 路径关闭全部已创建 spool。
|
|
41
|
+
if not self._ended:
|
|
42
|
+
raise MultiPartException("Incomplete multipart body")
|
|
43
|
+
|
|
44
|
+
def on_end(self) -> None:
|
|
45
|
+
super().on_end()
|
|
46
|
+
self._ended = True
|
|
47
|
+
|
|
48
|
+
def on_part_begin(self) -> None:
|
|
49
|
+
super().on_part_begin()
|
|
50
|
+
self._file_size = 0
|
|
51
|
+
|
|
52
|
+
def on_part_data(self, data: bytes, start: int, end: int) -> None:
|
|
53
|
+
if self._current_part.file is not None:
|
|
54
|
+
self._file_size += end - start
|
|
55
|
+
if self._max_file_size is not None and self._file_size > self._max_file_size:
|
|
56
|
+
raise HTTPException(413, "Upload size limit exceeded")
|
|
57
|
+
super().on_part_data(data, start, end)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _prepare_upload(request: Request, method: object, route: APIRoute) -> None:
|
|
61
|
+
# 只接管原生表单绑定;原始 Request/body endpoint 保留自己的读取语义。
|
|
62
|
+
if route.body_field is None or not isinstance(route.body_field.field_info, Form):
|
|
63
|
+
return
|
|
64
|
+
content_type, _ = parse_options_header(request.headers.get("content-type"))
|
|
65
|
+
if content_type not in (b"multipart/form-data", b"application/x-www-form-urlencoded"):
|
|
66
|
+
return
|
|
67
|
+
limits = _limits_for(request.app.state.python_platform_upload_limits, method)
|
|
68
|
+
|
|
69
|
+
async def counted() -> AsyncGenerator[bytes, None]:
|
|
70
|
+
total = 0
|
|
71
|
+
async for chunk in request.stream():
|
|
72
|
+
total += len(chunk)
|
|
73
|
+
if total > limits.max_request_size:
|
|
74
|
+
raise HTTPException(413, "Upload size limit exceeded")
|
|
75
|
+
yield chunk
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
parser = (
|
|
79
|
+
_LimitedMultipartParser(request, counted(), limits)
|
|
80
|
+
if content_type == b"multipart/form-data"
|
|
81
|
+
else FormParser(
|
|
82
|
+
request.headers,
|
|
83
|
+
counted(),
|
|
84
|
+
max_fields=limits.max_fields,
|
|
85
|
+
max_part_size=limits.max_part_size,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
form = await parser.parse()
|
|
89
|
+
except (MultiPartException, MultipartParseError) as error:
|
|
90
|
+
raise HTTPException(400, "Invalid form data") from error
|
|
91
|
+
# 唯一缓存接点:原生绑定复用已解析表单;原生文件栈拥有请求结束时的关闭。
|
|
92
|
+
# 先登记受保护的关闭,再进入 native,覆盖绑定前失败;关闭只处理尚未释放的句柄。
|
|
93
|
+
request._form = form
|
|
94
|
+
request.scope["fastapi_middleware_astack"].push_async_callback(_close_form, form)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""通用外部 Hosted Service、readiness 与 Application Service bridge。"""
|
|
2
|
+
|
|
3
|
+
from .bridge import HostedServiceContext
|
|
4
|
+
from .catalog import HostedServiceCatalog, HostedServiceRegistration
|
|
5
|
+
from .contracts import HostedService, HostedServiceStatus
|
|
6
|
+
from .errors import (
|
|
7
|
+
HostedServiceContractError,
|
|
8
|
+
HostedServiceDefinitionError,
|
|
9
|
+
HostedServiceInvocationRejectedError,
|
|
10
|
+
HostedServiceLifecycleError,
|
|
11
|
+
)
|
|
12
|
+
from .options import HOSTED_SERVICE_OPTIONS_SECTION, HostedServiceOptions
|
|
13
|
+
from .state import ApplicationReadinessSnapshot, HostedServiceSnapshot
|
|
14
|
+
|
|
15
|
+
__all__ = (
|
|
16
|
+
"HOSTED_SERVICE_OPTIONS_SECTION",
|
|
17
|
+
"ApplicationReadinessSnapshot",
|
|
18
|
+
"HostedService",
|
|
19
|
+
"HostedServiceCatalog",
|
|
20
|
+
"HostedServiceContext",
|
|
21
|
+
"HostedServiceContractError",
|
|
22
|
+
"HostedServiceDefinitionError",
|
|
23
|
+
"HostedServiceInvocationRejectedError",
|
|
24
|
+
"HostedServiceLifecycleError",
|
|
25
|
+
"HostedServiceOptions",
|
|
26
|
+
"HostedServiceRegistration",
|
|
27
|
+
"HostedServiceSnapshot",
|
|
28
|
+
"HostedServiceStatus",
|
|
29
|
+
)
|