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,647 @@
|
|
|
1
|
+
"""ApplicationServiceCatalog 驱动的约定式 FastAPI route 生成。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import re
|
|
7
|
+
import types
|
|
8
|
+
from collections.abc import Awaitable, Callable, Coroutine, Mapping
|
|
9
|
+
from dataclasses import dataclass, is_dataclass
|
|
10
|
+
from datetime import date, datetime, time
|
|
11
|
+
from decimal import Decimal
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Annotated, Any, Literal, Union, cast, get_args, get_origin, get_type_hints
|
|
14
|
+
from uuid import UUID
|
|
15
|
+
|
|
16
|
+
from dishka import AsyncContainer
|
|
17
|
+
from dishka.integrations.fastapi import DishkaRoute, FromDishka
|
|
18
|
+
from fastapi import APIRouter, Depends
|
|
19
|
+
from fastapi.params import Body, Param, Path
|
|
20
|
+
from fastapi.security import HTTPBearer
|
|
21
|
+
from pydantic import BaseModel
|
|
22
|
+
from starlette.requests import Request
|
|
23
|
+
from starlette.responses import Response
|
|
24
|
+
from starlette.routing import compile_path
|
|
25
|
+
|
|
26
|
+
from ..application import Application
|
|
27
|
+
from ..application_services import (
|
|
28
|
+
ApplicationServiceRegistration,
|
|
29
|
+
)
|
|
30
|
+
from ..application_services.catalog import _CompiledApplicationService, _CompiledMethod
|
|
31
|
+
from ..application_services.dispatcher import _ApplicationServiceDispatcher
|
|
32
|
+
from ..diagnostics import SourceLocation
|
|
33
|
+
from ..modularity import AppModule, ModuleDescriptor, ModuleKey
|
|
34
|
+
from .action import _HttpActionOwner, _request_parameter
|
|
35
|
+
from .contracts import (
|
|
36
|
+
ApplicationServiceRouteOverride,
|
|
37
|
+
_application_service_exposure_for,
|
|
38
|
+
_ApplicationServiceExposure,
|
|
39
|
+
)
|
|
40
|
+
from .errors import FastApiAdapterError
|
|
41
|
+
from .filters import _filters_for, _ordered_filters, _validate_filters
|
|
42
|
+
from .http import _metadata, _remote
|
|
43
|
+
from .http_router import _compile_parameter
|
|
44
|
+
from .parameters import _HttpParameterValidator
|
|
45
|
+
from .transfer import _manage_stream
|
|
46
|
+
from .uploads import _prepare_upload
|
|
47
|
+
|
|
48
|
+
_APPLICATION_SERVICE_SUFFIX = "ApplicationService"
|
|
49
|
+
_PATH_PARAMETER = re.compile(r"\{(?P<name>[^{}:]+)(?::[^{}]+)?\}")
|
|
50
|
+
_WORD_BOUNDARY_1 = re.compile(r"(.)([A-Z][a-z]+)")
|
|
51
|
+
_WORD_BOUNDARY_2 = re.compile(r"([a-z0-9])([A-Z])")
|
|
52
|
+
_ROUTE_SOURCE_ATTRIBUTE = "__python_platform_fastapi_route_source__"
|
|
53
|
+
_INVOCATION_IDENTITY_ATTRIBUTE = "__python_platform_fastapi_invocation_identity__"
|
|
54
|
+
_VERB_PREFIXES: tuple[tuple[str, str], ...] = (
|
|
55
|
+
("get_list", "GET"),
|
|
56
|
+
("get_all", "GET"),
|
|
57
|
+
("get", "GET"),
|
|
58
|
+
("create", "POST"),
|
|
59
|
+
("add", "POST"),
|
|
60
|
+
("insert", "POST"),
|
|
61
|
+
("post", "POST"),
|
|
62
|
+
("update", "PUT"),
|
|
63
|
+
("put", "PUT"),
|
|
64
|
+
("delete", "DELETE"),
|
|
65
|
+
("remove", "DELETE"),
|
|
66
|
+
("patch", "PATCH"),
|
|
67
|
+
)
|
|
68
|
+
_SCALAR_TYPES = (str, int, float, bool, UUID, Decimal, date, datetime, time)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class _GeneratedApplicationServiceRouter:
|
|
73
|
+
owner: ModuleKey
|
|
74
|
+
router: APIRouter
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class _RouteMethod:
|
|
79
|
+
signature: inspect.Signature
|
|
80
|
+
parameters: tuple[inspect.Parameter, ...]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True, slots=True)
|
|
84
|
+
class _ApplicationServiceRouteIdentity:
|
|
85
|
+
service: _CompiledApplicationService
|
|
86
|
+
method: _CompiledMethod
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _ApplicationServiceRoute(DishkaRoute):
|
|
90
|
+
"""在 native FastAPI binding 前建立 catalog-identity invocation boundary。"""
|
|
91
|
+
|
|
92
|
+
__slots__ = ("_compiled_method", "_compiled_service")
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
path: str,
|
|
97
|
+
endpoint: Callable[..., Any],
|
|
98
|
+
**kwargs: Any,
|
|
99
|
+
) -> None:
|
|
100
|
+
identity = getattr(endpoint, _INVOCATION_IDENTITY_ATTRIBUTE, None)
|
|
101
|
+
if not isinstance(identity, _ApplicationServiceRouteIdentity):
|
|
102
|
+
raise FastApiAdapterError("generated Application Service route lost its identity")
|
|
103
|
+
self._compiled_service = identity.service
|
|
104
|
+
self._compiled_method = identity.method
|
|
105
|
+
sources = getattr(endpoint, "__python_platform_http_sources__", None)
|
|
106
|
+
super().__init__(path, endpoint, **kwargs)
|
|
107
|
+
cast(Any, self.endpoint).__python_platform_http_sources__ = sources
|
|
108
|
+
# include_router 会以已注入 endpoint 重建 route;显式把 identity 传递给下一次构造。
|
|
109
|
+
setattr(self.endpoint, _INVOCATION_IDENTITY_ATTRIBUTE, identity)
|
|
110
|
+
|
|
111
|
+
def get_route_handler(
|
|
112
|
+
self,
|
|
113
|
+
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
|
114
|
+
native_handler = super().get_route_handler()
|
|
115
|
+
service = self._compiled_service
|
|
116
|
+
method = self._compiled_method
|
|
117
|
+
|
|
118
|
+
async def route_handler(request: Request) -> Response:
|
|
119
|
+
owner = await _HttpActionOwner.install(request)
|
|
120
|
+
container = cast(AsyncContainer, request.state.dishka_container)
|
|
121
|
+
dispatcher = await container.get(_ApplicationServiceDispatcher)
|
|
122
|
+
|
|
123
|
+
async def bind() -> Response:
|
|
124
|
+
await _prepare_upload(request, method.implementation_method, self)
|
|
125
|
+
return await native_handler(request)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
result = await dispatcher.invoke_http_request(
|
|
129
|
+
service,
|
|
130
|
+
method,
|
|
131
|
+
bind,
|
|
132
|
+
is_short_circuited=lambda: owner.short_circuited,
|
|
133
|
+
defer_audit=owner.audits.append,
|
|
134
|
+
)
|
|
135
|
+
owner.response_ready = isinstance(result, Response)
|
|
136
|
+
except BaseException as error:
|
|
137
|
+
owner.failure = error
|
|
138
|
+
raise
|
|
139
|
+
if not isinstance(result, Response):
|
|
140
|
+
raise FastApiAdapterError(
|
|
141
|
+
"an interceptor outside HTTP binding must return a native Response"
|
|
142
|
+
)
|
|
143
|
+
_manage_stream(request, result)
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
return route_handler
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _application_service_routers(
|
|
150
|
+
application: Application,
|
|
151
|
+
) -> tuple[_GeneratedApplicationServiceRouter, ...]:
|
|
152
|
+
"""按 live Module graph 顺序生成 Router,不保存进程级 exposure registry。"""
|
|
153
|
+
|
|
154
|
+
descriptors_by_type = {
|
|
155
|
+
descriptor.module_type: descriptor for descriptor in application.modules.dependency_order
|
|
156
|
+
}
|
|
157
|
+
generated: list[_GeneratedApplicationServiceRouter] = []
|
|
158
|
+
for http_descriptor in application.modules.dependency_order:
|
|
159
|
+
exposure = _application_service_exposure_for(http_descriptor.module_type)
|
|
160
|
+
if exposure is None:
|
|
161
|
+
continue
|
|
162
|
+
router = _create_exposure_router(
|
|
163
|
+
application,
|
|
164
|
+
http_descriptor=http_descriptor,
|
|
165
|
+
exposure=exposure,
|
|
166
|
+
descriptors_by_type=descriptors_by_type,
|
|
167
|
+
)
|
|
168
|
+
generated.append(
|
|
169
|
+
_GeneratedApplicationServiceRouter(owner=http_descriptor.key, router=router)
|
|
170
|
+
)
|
|
171
|
+
return tuple(generated)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _create_exposure_router(
|
|
175
|
+
application: Application,
|
|
176
|
+
*,
|
|
177
|
+
http_descriptor: ModuleDescriptor,
|
|
178
|
+
exposure: _ApplicationServiceExposure,
|
|
179
|
+
descriptors_by_type: Mapping[type[AppModule], ModuleDescriptor],
|
|
180
|
+
) -> APIRouter:
|
|
181
|
+
target_descriptors = _exposure_targets(http_descriptor, exposure, descriptors_by_type)
|
|
182
|
+
|
|
183
|
+
# metadata 决定稳定基序;最终仅按路径段具体性排序,避免参数路径遮蔽动作。
|
|
184
|
+
registrations = tuple(
|
|
185
|
+
registration
|
|
186
|
+
for target in target_descriptors
|
|
187
|
+
for registration in application.application_service_catalog.registrations
|
|
188
|
+
if registration.is_public_service and registration.owner == target.key
|
|
189
|
+
)
|
|
190
|
+
overrides = _index_overrides(exposure, registrations)
|
|
191
|
+
router = APIRouter(route_class=_ApplicationServiceRoute)
|
|
192
|
+
for registration in registrations:
|
|
193
|
+
if registration.is_integration_service:
|
|
194
|
+
continue
|
|
195
|
+
compiled_service = application.application_service_catalog._for_registration(registration)
|
|
196
|
+
service_override = overrides.get((registration.contract, None))
|
|
197
|
+
if service_override is not None:
|
|
198
|
+
continue
|
|
199
|
+
service_path = _service_path(exposure.root_path, registration.contract)
|
|
200
|
+
for method_registration in registration.methods:
|
|
201
|
+
compiled_method = compiled_service.methods[method_registration.name]
|
|
202
|
+
contract_method = cast(
|
|
203
|
+
Callable[..., Awaitable[object]],
|
|
204
|
+
getattr(registration.contract, method_registration.name),
|
|
205
|
+
)
|
|
206
|
+
method_override = overrides.get((registration.contract, contract_method))
|
|
207
|
+
if method_override is not None and not method_override.enabled:
|
|
208
|
+
continue
|
|
209
|
+
enabled, visible = _remote(
|
|
210
|
+
registration.implementation, compiled_method.implementation_method
|
|
211
|
+
)
|
|
212
|
+
if not enabled and method_override is None:
|
|
213
|
+
continue
|
|
214
|
+
projection, provenance = _route_projection(
|
|
215
|
+
compiled_method.implementation_method, method_override
|
|
216
|
+
)
|
|
217
|
+
route_method = _inspect_route_method(
|
|
218
|
+
registration.contract,
|
|
219
|
+
compiled_method.implementation_method,
|
|
220
|
+
method_registration.name,
|
|
221
|
+
)
|
|
222
|
+
path = _route_path(
|
|
223
|
+
service_path,
|
|
224
|
+
route_method.parameters,
|
|
225
|
+
projection.pop("path", None),
|
|
226
|
+
contract=registration.contract,
|
|
227
|
+
method_name=method_registration.name,
|
|
228
|
+
)
|
|
229
|
+
for parameter in route_method.parameters:
|
|
230
|
+
_compile_parameter(
|
|
231
|
+
parameter,
|
|
232
|
+
parameter.annotation,
|
|
233
|
+
compile_path(path)[2],
|
|
234
|
+
application._container_for_integration(),
|
|
235
|
+
infer_services=False,
|
|
236
|
+
)
|
|
237
|
+
verb = projection.pop("verb", _conventional_verb(method_registration.name))
|
|
238
|
+
operation_id = projection.pop(
|
|
239
|
+
"operation_id", _operation_id(registration.contract, method_registration.name)
|
|
240
|
+
)
|
|
241
|
+
status_code = projection.pop("status_code", None)
|
|
242
|
+
projection.setdefault("include_in_schema", visible)
|
|
243
|
+
_validate_filters(
|
|
244
|
+
_filters_for(
|
|
245
|
+
*reversed(registration.implementation.__mro__),
|
|
246
|
+
compiled_method.implementation_method,
|
|
247
|
+
),
|
|
248
|
+
application._container_for_integration(),
|
|
249
|
+
)
|
|
250
|
+
endpoint = _create_endpoint(
|
|
251
|
+
compiled_service,
|
|
252
|
+
compiled_method,
|
|
253
|
+
route_method,
|
|
254
|
+
method_registration.source,
|
|
255
|
+
)
|
|
256
|
+
from types import MappingProxyType
|
|
257
|
+
|
|
258
|
+
cast(Any, endpoint).__python_platform_http_sources__ = MappingProxyType(
|
|
259
|
+
{
|
|
260
|
+
key: provenance.get(key, "convention")
|
|
261
|
+
for key in ("path", "verb", "operation_id", "status_code", *projection)
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
try:
|
|
265
|
+
router.add_api_route(
|
|
266
|
+
path,
|
|
267
|
+
endpoint,
|
|
268
|
+
methods=[verb],
|
|
269
|
+
operation_id=operation_id,
|
|
270
|
+
status_code=status_code,
|
|
271
|
+
# 原生 dependency 声明 Bearer;认证和权限仍由正式 invocation 管线执行。
|
|
272
|
+
dependencies=[
|
|
273
|
+
*projection.pop("dependencies", ()),
|
|
274
|
+
*(
|
|
275
|
+
[Depends(HTTPBearer(auto_error=False))]
|
|
276
|
+
if compiled_method.authorization.requires_authenticated_user
|
|
277
|
+
else []
|
|
278
|
+
),
|
|
279
|
+
],
|
|
280
|
+
**projection,
|
|
281
|
+
)
|
|
282
|
+
except (AssertionError, TypeError, ValueError) as original_error:
|
|
283
|
+
raise FastApiAdapterError(
|
|
284
|
+
f"FastAPI rejected convention route {verb} {path} for "
|
|
285
|
+
f"{registration.contract.__module__}.{registration.contract.__qualname__}."
|
|
286
|
+
f"{method_registration.name}: {type(original_error).__name__}"
|
|
287
|
+
) from original_error
|
|
288
|
+
# Starlette 采用首次匹配:静态动作段必须先于参数段,否则 get_count 会落进 get(id)。
|
|
289
|
+
# 相同路径结构使用稳定排序,保留声明顺序及 endpoint 的 invocation identity。
|
|
290
|
+
router.routes.sort(
|
|
291
|
+
key=lambda route: tuple(
|
|
292
|
+
"{" in segment for segment in cast(_ApplicationServiceRoute, route).path.split("/")
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
return router
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _index_overrides(
|
|
299
|
+
exposure: _ApplicationServiceExposure,
|
|
300
|
+
registrations: tuple[ApplicationServiceRegistration, ...],
|
|
301
|
+
) -> Mapping[
|
|
302
|
+
tuple[type[object], Callable[..., Awaitable[object]] | None],
|
|
303
|
+
ApplicationServiceRouteOverride,
|
|
304
|
+
]:
|
|
305
|
+
"""用 Protocol/method identity 建索引,避免字符串方法名形成第二份 API 清单。"""
|
|
306
|
+
|
|
307
|
+
registrations_by_contract = {item.contract: item for item in registrations}
|
|
308
|
+
indexed: dict[
|
|
309
|
+
tuple[type[object], Callable[..., Awaitable[object]] | None],
|
|
310
|
+
ApplicationServiceRouteOverride,
|
|
311
|
+
] = {}
|
|
312
|
+
service_exclusions: set[type[object]] = set()
|
|
313
|
+
service_method_overrides: set[type[object]] = set()
|
|
314
|
+
for override in exposure.overrides:
|
|
315
|
+
registration = registrations_by_contract.get(override.service)
|
|
316
|
+
if registration is None:
|
|
317
|
+
raise FastApiAdapterError(
|
|
318
|
+
"route override targets an ApplicationServiceContract not owned by the "
|
|
319
|
+
f"declared target Modules: {override.service.__module__}."
|
|
320
|
+
f"{override.service.__qualname__}"
|
|
321
|
+
)
|
|
322
|
+
method = override.method
|
|
323
|
+
if registration.is_integration_service and override.enabled:
|
|
324
|
+
raise FastApiAdapterError("IntegrationService cannot be exposed over HTTP")
|
|
325
|
+
if method is not None:
|
|
326
|
+
method_names = {
|
|
327
|
+
item.name: cast(
|
|
328
|
+
Callable[..., Awaitable[object]],
|
|
329
|
+
getattr(registration.contract, item.name),
|
|
330
|
+
)
|
|
331
|
+
for item in registration.methods
|
|
332
|
+
}
|
|
333
|
+
if method not in method_names.values():
|
|
334
|
+
raise FastApiAdapterError(
|
|
335
|
+
f"route override method is not owned by {override.service.__module__}."
|
|
336
|
+
f"{override.service.__qualname__}"
|
|
337
|
+
)
|
|
338
|
+
service_method_overrides.add(override.service)
|
|
339
|
+
else:
|
|
340
|
+
service_exclusions.add(override.service)
|
|
341
|
+
key = (override.service, method)
|
|
342
|
+
if key in indexed:
|
|
343
|
+
raise FastApiAdapterError(
|
|
344
|
+
f"duplicate route override for {override.service.__module__}."
|
|
345
|
+
f"{override.service.__qualname__}"
|
|
346
|
+
)
|
|
347
|
+
indexed[key] = override
|
|
348
|
+
overlap = service_exclusions & service_method_overrides
|
|
349
|
+
if overlap:
|
|
350
|
+
contract = min(overlap, key=lambda item: (item.__module__, item.__qualname__))
|
|
351
|
+
raise FastApiAdapterError(
|
|
352
|
+
f"excluded service {contract.__module__}.{contract.__qualname__} cannot also "
|
|
353
|
+
"declare method overrides"
|
|
354
|
+
)
|
|
355
|
+
return indexed
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _inspect_route_method(
|
|
359
|
+
contract: type[object],
|
|
360
|
+
method: Callable[..., Awaitable[object]],
|
|
361
|
+
method_name: str,
|
|
362
|
+
) -> _RouteMethod:
|
|
363
|
+
"""在真实 FastAPI app 变更前解析并收窄动态 typing boundary。"""
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
annotations = get_type_hints(method, include_extras=True)
|
|
367
|
+
except (NameError, TypeError) as original_error:
|
|
368
|
+
raise FastApiAdapterError(
|
|
369
|
+
f"HTTP method annotations cannot be resolved for {contract.__module__}."
|
|
370
|
+
f"{contract.__qualname__}.{method_name}: {type(original_error).__name__}"
|
|
371
|
+
) from original_error
|
|
372
|
+
signature = inspect.signature(method)
|
|
373
|
+
resolved_parameters = tuple(
|
|
374
|
+
parameter.replace(annotation=annotations.get(name, parameter.annotation))
|
|
375
|
+
for name, parameter in signature.parameters.items()
|
|
376
|
+
)
|
|
377
|
+
signature = signature.replace(
|
|
378
|
+
parameters=resolved_parameters,
|
|
379
|
+
return_annotation=annotations.get("return", signature.return_annotation),
|
|
380
|
+
)
|
|
381
|
+
parameters = resolved_parameters[1:]
|
|
382
|
+
for parameter in parameters:
|
|
383
|
+
if parameter.kind in (
|
|
384
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
385
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
386
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
387
|
+
):
|
|
388
|
+
raise FastApiAdapterError(
|
|
389
|
+
f"HTTP method {contract.__module__}.{contract.__qualname__}.{method_name} "
|
|
390
|
+
"cannot use positional-only, *args, or **kwargs parameters"
|
|
391
|
+
)
|
|
392
|
+
if parameter.annotation is inspect.Signature.empty:
|
|
393
|
+
raise FastApiAdapterError(
|
|
394
|
+
f"HTTP method {contract.__module__}.{contract.__qualname__}.{method_name} "
|
|
395
|
+
f"parameter {parameter.name!r} requires a type annotation"
|
|
396
|
+
)
|
|
397
|
+
if signature.return_annotation is inspect.Signature.empty:
|
|
398
|
+
raise FastApiAdapterError(
|
|
399
|
+
f"HTTP method {contract.__module__}.{contract.__qualname__}.{method_name} "
|
|
400
|
+
"requires a return type annotation"
|
|
401
|
+
)
|
|
402
|
+
return _RouteMethod(signature=signature, parameters=parameters)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _uses_non_path_binding(parameter: inspect.Parameter) -> bool:
|
|
406
|
+
metadata = (
|
|
407
|
+
get_args(parameter.annotation)[1:] if get_origin(parameter.annotation) is Annotated else ()
|
|
408
|
+
)
|
|
409
|
+
return any(
|
|
410
|
+
isinstance(item, (Param, Body)) and not isinstance(item, Path)
|
|
411
|
+
for item in (*metadata, parameter.default)
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _route_path(
|
|
416
|
+
service_path: str,
|
|
417
|
+
parameters: tuple[inspect.Parameter, ...],
|
|
418
|
+
override_path: str | None,
|
|
419
|
+
*,
|
|
420
|
+
contract: type[object],
|
|
421
|
+
method_name: str,
|
|
422
|
+
) -> str:
|
|
423
|
+
# 只有唯一 required scalar id 才能无歧义进入 convention path;
|
|
424
|
+
# 其余情况保持 query 或要求 override。
|
|
425
|
+
id_candidates = tuple(
|
|
426
|
+
parameter
|
|
427
|
+
for parameter in parameters
|
|
428
|
+
if _is_required_path_parameter(parameter)
|
|
429
|
+
and (parameter.name == "id" or parameter.name.endswith("_id"))
|
|
430
|
+
and _is_scalar_annotation(parameter.annotation)
|
|
431
|
+
and not _uses_non_path_binding(parameter)
|
|
432
|
+
)
|
|
433
|
+
if override_path is None:
|
|
434
|
+
if len(id_candidates) > 1:
|
|
435
|
+
raise FastApiAdapterError(
|
|
436
|
+
f"HTTP method {contract.__module__}.{contract.__qualname__}.{method_name} "
|
|
437
|
+
"has ambiguous required id parameters and requires an explicit path override"
|
|
438
|
+
)
|
|
439
|
+
path = service_path
|
|
440
|
+
if len(id_candidates) == 1:
|
|
441
|
+
path = f"{path}/{{{id_candidates[0].name}}}"
|
|
442
|
+
# 只有集中定义的 CRUD 名称省略动作;审批/取消等领域动作保持其 contract 名称。
|
|
443
|
+
if method_name not in {prefix for prefix, _ in _VERB_PREFIXES}:
|
|
444
|
+
path = f"{path}/{method_name.replace('_', '-')}"
|
|
445
|
+
return path
|
|
446
|
+
|
|
447
|
+
if "?" in override_path or "#" in override_path:
|
|
448
|
+
raise FastApiAdapterError("route override path cannot contain query or fragment syntax")
|
|
449
|
+
rendered_parameters = tuple(
|
|
450
|
+
match.group("name") for match in _PATH_PARAMETER.finditer(override_path)
|
|
451
|
+
)
|
|
452
|
+
if override_path.count("{") != len(rendered_parameters) or override_path.count("}") != len(
|
|
453
|
+
rendered_parameters
|
|
454
|
+
):
|
|
455
|
+
raise FastApiAdapterError(f"route override path {override_path!r} is malformed")
|
|
456
|
+
parameters_by_name = {parameter.name: parameter for parameter in parameters}
|
|
457
|
+
for name in rendered_parameters:
|
|
458
|
+
parameter = parameters_by_name.get(name)
|
|
459
|
+
if (
|
|
460
|
+
parameter is None
|
|
461
|
+
or not _is_required_path_parameter(parameter)
|
|
462
|
+
or not _is_scalar_annotation(parameter.annotation)
|
|
463
|
+
or _uses_non_path_binding(parameter)
|
|
464
|
+
):
|
|
465
|
+
raise FastApiAdapterError(
|
|
466
|
+
f"route override path parameter {name!r} must name a required scalar "
|
|
467
|
+
f"Protocol parameter on {contract.__qualname__}.{method_name}"
|
|
468
|
+
)
|
|
469
|
+
if len(set(rendered_parameters)) != len(rendered_parameters):
|
|
470
|
+
raise FastApiAdapterError(f"route override path {override_path!r} repeats a path parameter")
|
|
471
|
+
return override_path
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _is_required_path_parameter(parameter: inspect.Parameter) -> bool:
|
|
475
|
+
return parameter.default is inspect.Signature.empty or isinstance(parameter.default, Path)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _is_scalar_annotation(annotation: object) -> bool:
|
|
479
|
+
current = annotation
|
|
480
|
+
if get_origin(current) is Annotated:
|
|
481
|
+
current = get_args(current)[0]
|
|
482
|
+
origin = get_origin(current)
|
|
483
|
+
if origin in (types.UnionType, Union):
|
|
484
|
+
members = tuple(item for item in get_args(current) if item is not type(None))
|
|
485
|
+
return len(members) == 1 and _is_scalar_annotation(members[0])
|
|
486
|
+
if origin is Literal:
|
|
487
|
+
return all(isinstance(item, (str, int, float, bool)) for item in get_args(current))
|
|
488
|
+
if not isinstance(current, type):
|
|
489
|
+
return False
|
|
490
|
+
if issubclass(current, BaseModel) or is_dataclass(current):
|
|
491
|
+
return False
|
|
492
|
+
return issubclass(current, (*_SCALAR_TYPES, Enum))
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _service_path(root_path: str, contract: type[object]) -> str:
|
|
496
|
+
service_name = contract.__name__
|
|
497
|
+
if service_name.endswith(_APPLICATION_SERVICE_SUFFIX):
|
|
498
|
+
service_name = service_name[: -len(_APPLICATION_SERVICE_SUFFIX)]
|
|
499
|
+
if not service_name:
|
|
500
|
+
raise FastApiAdapterError("application service contract has no routable name")
|
|
501
|
+
return f"/api/{root_path}/{_kebab_case(service_name)}"
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _operation_id(contract: type[object], method_name: str) -> str:
|
|
505
|
+
service_path = _service_path("app", contract).rsplit("/", maxsplit=1)[-1]
|
|
506
|
+
return f"{service_path.replace('-', '_')}_{method_name}"
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _kebab_case(value: str) -> str:
|
|
510
|
+
first_pass = _WORD_BOUNDARY_1.sub(r"\1-\2", value)
|
|
511
|
+
return _WORD_BOUNDARY_2.sub(r"\1-\2", first_pass).lower().replace("_", "-")
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _conventional_verb(method_name: str) -> str:
|
|
515
|
+
for prefix, verb in _VERB_PREFIXES:
|
|
516
|
+
if method_name == prefix or method_name.startswith(f"{prefix}_"):
|
|
517
|
+
return verb
|
|
518
|
+
return "POST"
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _create_endpoint(
|
|
522
|
+
service: _CompiledApplicationService,
|
|
523
|
+
method: _CompiledMethod,
|
|
524
|
+
route_method: _RouteMethod,
|
|
525
|
+
source: SourceLocation,
|
|
526
|
+
) -> Callable[..., Awaitable[object]]:
|
|
527
|
+
"""保留 Protocol 原签名供 FastAPI binding,并注入同一 REQUEST dispatcher。"""
|
|
528
|
+
|
|
529
|
+
contract = service.registration.contract
|
|
530
|
+
method_name = method.name
|
|
531
|
+
parameter_names = {parameter.name for parameter in route_method.parameters}
|
|
532
|
+
dispatcher_parameter_name = "_python_platform_application_service_dispatcher"
|
|
533
|
+
while dispatcher_parameter_name in parameter_names:
|
|
534
|
+
dispatcher_parameter_name = f"_{dispatcher_parameter_name}"
|
|
535
|
+
|
|
536
|
+
declared_filters = _ordered_filters(
|
|
537
|
+
_filters_for(*reversed(service.registration.implementation.__mro__)),
|
|
538
|
+
_filters_for(method.implementation_method),
|
|
539
|
+
)
|
|
540
|
+
validator = _HttpParameterValidator(
|
|
541
|
+
route_method.signature.replace(parameters=route_method.parameters)
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
async def endpoint(**values: object) -> object:
|
|
545
|
+
dispatcher = cast(_ApplicationServiceDispatcher, values.pop(dispatcher_parameter_name))
|
|
546
|
+
request = cast(
|
|
547
|
+
Request,
|
|
548
|
+
values.pop(request_parameter_name)
|
|
549
|
+
if injected_request
|
|
550
|
+
else values[request_parameter_name],
|
|
551
|
+
)
|
|
552
|
+
owner: _HttpActionOwner = request.state.python_platform_action
|
|
553
|
+
await owner.activate(
|
|
554
|
+
method.descriptor.unit_of_work, method.name, service.registration.connection_name
|
|
555
|
+
)
|
|
556
|
+
filters = _ordered_filters(
|
|
557
|
+
getattr(request.app.state, "python_platform_filters", ()), declared_filters
|
|
558
|
+
)
|
|
559
|
+
snapshots = validator.capture(values)
|
|
560
|
+
|
|
561
|
+
async def invoke(actual: dict[str, object]) -> object:
|
|
562
|
+
if filters:
|
|
563
|
+
validator.validate_changes(actual, snapshots)
|
|
564
|
+
return await dispatcher.invoke_http(service, method, actual)
|
|
565
|
+
|
|
566
|
+
return await owner.invoke(
|
|
567
|
+
method.implementation_method,
|
|
568
|
+
values,
|
|
569
|
+
filters,
|
|
570
|
+
invoke,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
request_parameter, injected_request = _request_parameter(route_method.parameters)
|
|
574
|
+
request_parameter_name = request_parameter.name
|
|
575
|
+
http_parameters = (
|
|
576
|
+
(*route_method.parameters, request_parameter)
|
|
577
|
+
if injected_request
|
|
578
|
+
else route_method.parameters
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
dispatcher_parameter = inspect.Parameter(
|
|
582
|
+
dispatcher_parameter_name,
|
|
583
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
584
|
+
annotation=cast(Any, FromDishka)[_ApplicationServiceDispatcher],
|
|
585
|
+
)
|
|
586
|
+
# 动态 endpoint 的 signature/annotations 同时发布,兼容 FastAPI 与 postponed annotations。
|
|
587
|
+
signature = route_method.signature.replace(parameters=(*http_parameters, dispatcher_parameter))
|
|
588
|
+
endpoint_name = f"{contract.__name__}_{method_name}"
|
|
589
|
+
endpoint.__name__ = endpoint_name
|
|
590
|
+
endpoint.__qualname__ = endpoint_name
|
|
591
|
+
endpoint.__module__ = contract.__module__
|
|
592
|
+
cast(Any, endpoint).__signature__ = signature
|
|
593
|
+
endpoint.__annotations__ = {
|
|
594
|
+
**{
|
|
595
|
+
parameter.name: parameter.annotation
|
|
596
|
+
for parameter in (*http_parameters, dispatcher_parameter)
|
|
597
|
+
},
|
|
598
|
+
"return": signature.return_annotation,
|
|
599
|
+
}
|
|
600
|
+
setattr(endpoint, _ROUTE_SOURCE_ATTRIBUTE, source)
|
|
601
|
+
setattr(
|
|
602
|
+
endpoint,
|
|
603
|
+
_INVOCATION_IDENTITY_ATTRIBUTE,
|
|
604
|
+
_ApplicationServiceRouteIdentity(service, method),
|
|
605
|
+
)
|
|
606
|
+
return endpoint
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _route_projection(
|
|
610
|
+
method: object, override: ApplicationServiceRouteOverride | None
|
|
611
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
612
|
+
projection = dict(_metadata(method))
|
|
613
|
+
provenance = {key: "method" for key in projection}
|
|
614
|
+
if override is not None:
|
|
615
|
+
from dataclasses import fields
|
|
616
|
+
|
|
617
|
+
for field in fields(override):
|
|
618
|
+
if field.name in ("service", "method", "enabled"):
|
|
619
|
+
continue
|
|
620
|
+
value = getattr(override, field.name)
|
|
621
|
+
if value is not None:
|
|
622
|
+
projection[field.name] = value
|
|
623
|
+
provenance[field.name] = "module_override"
|
|
624
|
+
return projection, provenance
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _exposure_targets(
|
|
628
|
+
http_descriptor: ModuleDescriptor,
|
|
629
|
+
exposure: _ApplicationServiceExposure,
|
|
630
|
+
descriptors_by_type: Mapping[type[AppModule], ModuleDescriptor],
|
|
631
|
+
) -> list[ModuleDescriptor]:
|
|
632
|
+
target_descriptors: list[ModuleDescriptor] = []
|
|
633
|
+
for target_type in exposure.target_modules:
|
|
634
|
+
target = descriptors_by_type.get(target_type)
|
|
635
|
+
if target is None:
|
|
636
|
+
raise FastApiAdapterError(
|
|
637
|
+
f"{http_descriptor.key} exposes unloaded target Module "
|
|
638
|
+
f"{target_type.__module__}:{target_type.__qualname__}"
|
|
639
|
+
)
|
|
640
|
+
if target.key not in http_descriptor.dependencies:
|
|
641
|
+
raise FastApiAdapterError(
|
|
642
|
+
f"{http_descriptor.key} may only expose a directly depended-on target Module; "
|
|
643
|
+
f"got {target.key}"
|
|
644
|
+
)
|
|
645
|
+
target_descriptors.append(target)
|
|
646
|
+
|
|
647
|
+
return target_descriptors
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""后台管理 HTTP 复用正式服务暴露、授权和响应准备。"""
|
|
2
|
+
|
|
3
|
+
from ..background_execution.management import BackgroundManagementApplicationService
|
|
4
|
+
from ..background_execution.module import BackgroundManagementModule
|
|
5
|
+
from ..modularity import AppModule
|
|
6
|
+
from .contracts import ApplicationServiceRouteOverride, exposes_application_services
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@exposes_application_services(
|
|
10
|
+
BackgroundManagementModule,
|
|
11
|
+
root_path="background",
|
|
12
|
+
overrides=(
|
|
13
|
+
ApplicationServiceRouteOverride(
|
|
14
|
+
BackgroundManagementApplicationService,
|
|
15
|
+
method=BackgroundManagementApplicationService.get_list,
|
|
16
|
+
path="/api/background/tasks",
|
|
17
|
+
verb="GET",
|
|
18
|
+
),
|
|
19
|
+
ApplicationServiceRouteOverride(
|
|
20
|
+
BackgroundManagementApplicationService,
|
|
21
|
+
method=BackgroundManagementApplicationService.get,
|
|
22
|
+
path="/api/background/tasks/{task_id}",
|
|
23
|
+
verb="GET",
|
|
24
|
+
),
|
|
25
|
+
ApplicationServiceRouteOverride(
|
|
26
|
+
BackgroundManagementApplicationService,
|
|
27
|
+
method=BackgroundManagementApplicationService.start,
|
|
28
|
+
path="/api/background/tasks/{task_id}/start",
|
|
29
|
+
verb="POST",
|
|
30
|
+
status_code=202,
|
|
31
|
+
),
|
|
32
|
+
ApplicationServiceRouteOverride(
|
|
33
|
+
BackgroundManagementApplicationService,
|
|
34
|
+
method=BackgroundManagementApplicationService.stop,
|
|
35
|
+
path="/api/background/tasks/{task_id}/stop",
|
|
36
|
+
verb="POST",
|
|
37
|
+
status_code=202,
|
|
38
|
+
),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
class BackgroundManagementHttpApiModule(AppModule):
|
|
42
|
+
dependencies = (BackgroundManagementModule,)
|