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
python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""原生上传与流响应;业务查询在返回响应前完成,不跨网络发送持有事务。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from fastapi import File, Form, UploadFile
|
|
11
|
+
from starlette.responses import Response, StreamingResponse
|
|
12
|
+
|
|
13
|
+
from python_platform.authorization import authorize
|
|
14
|
+
from python_platform.fastapi import ActionFilterContext, HttpRouter, service_filter
|
|
15
|
+
|
|
16
|
+
from ..application_contracts.orders import CreateOrder, {{ cookiecutter.class_prefix }}ApplicationService
|
|
17
|
+
from ..domain_shared.definitions import MODULE_NAME, ORDERS_READ, ORDERS_WRITE
|
|
18
|
+
|
|
19
|
+
router = HttpRouter(prefix=f"/api/{MODULE_NAME}/files", permissions=(ORDERS_READ,))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ExportFilter:
|
|
23
|
+
async def invoke(
|
|
24
|
+
self, context: ActionFilterContext, next_action: Callable[[], Awaitable[object]]
|
|
25
|
+
) -> object:
|
|
26
|
+
# Filter 的作用域只覆盖已绑定参数到响应准备;不假定流传输已经结束。
|
|
27
|
+
result = await next_action()
|
|
28
|
+
logging.getLogger(__name__).info("File response prepared")
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.post("/upload", status_code=201)
|
|
33
|
+
@authorize(ORDERS_WRITE)
|
|
34
|
+
async def upload(
|
|
35
|
+
service: {{ cookiecutter.class_prefix }}ApplicationService,
|
|
36
|
+
title: Annotated[str, Form()],
|
|
37
|
+
file: Annotated[UploadFile, File()],
|
|
38
|
+
) -> dict[str, object]:
|
|
39
|
+
# File/Form 使用框架读取期限额及原生 parser;UploadFile 资源由 REQUEST 关闭。
|
|
40
|
+
content = await file.read()
|
|
41
|
+
order = await service.create(CreateOrder(title=title))
|
|
42
|
+
return {
|
|
43
|
+
"order": order.model_dump(mode="json"),
|
|
44
|
+
"bytes": len(content),
|
|
45
|
+
"sha256": hashlib.sha256(content).hexdigest(),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.get("/{id}/download")
|
|
50
|
+
@service_filter(ExportFilter)
|
|
51
|
+
async def download(id: UUID, service: {{ cookiecutter.class_prefix }}ApplicationService) -> Response:
|
|
52
|
+
order = await service.get(id)
|
|
53
|
+
return Response(
|
|
54
|
+
order.model_dump_json(),
|
|
55
|
+
media_type="application/json",
|
|
56
|
+
headers={"Content-Disposition": f'attachment; filename="{id}.json"'},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@router.get("/export/stream")
|
|
61
|
+
@service_filter(ExportFilter)
|
|
62
|
+
async def export(service: {{ cookiecutter.class_prefix }}ApplicationService) -> StreamingResponse:
|
|
63
|
+
orders = await service.get_list()
|
|
64
|
+
|
|
65
|
+
async def produce() -> AsyncIterator[bytes]:
|
|
66
|
+
try:
|
|
67
|
+
for order in orders:
|
|
68
|
+
yield (order.model_dump_json() + "\n").encode()
|
|
69
|
+
await asyncio.sleep(0)
|
|
70
|
+
finally:
|
|
71
|
+
# 此示例只持有不可变 DTO,无悬挂 Session/文件;扩展外部资源时在此 shield 异步关闭。
|
|
72
|
+
logging.getLogger(__name__).info("Order export producer closed")
|
|
73
|
+
|
|
74
|
+
return StreamingResponse(produce(), media_type="application/x-ndjson")
|
python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""自动服务暴露与手写路由共用正式认证/REQUEST 管线。"""
|
|
2
|
+
|
|
3
|
+
from dishka import Scope
|
|
4
|
+
|
|
5
|
+
from python_platform import AppModule, ConfigureContext
|
|
6
|
+
from python_platform.fastapi import (
|
|
7
|
+
ApplicationServiceRouteOverride,
|
|
8
|
+
FastApiRealtimeModule,
|
|
9
|
+
contributes_routers,
|
|
10
|
+
exposes_application_services,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from ..application_contracts.module import {{ cookiecutter.class_prefix }}ApplicationContractsModule
|
|
14
|
+
from ..application_contracts.orders import {{ cookiecutter.class_prefix }}ApplicationService
|
|
15
|
+
from ..domain_shared.definitions import MODULE_NAME
|
|
16
|
+
from .files import ExportFilter
|
|
17
|
+
from .files import router as files_router
|
|
18
|
+
from .realtime import router as realtime_router
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@contributes_routers(files_router, realtime_router)
|
|
22
|
+
@exposes_application_services(
|
|
23
|
+
{{ cookiecutter.class_prefix }}ApplicationContractsModule,
|
|
24
|
+
overrides=(
|
|
25
|
+
ApplicationServiceRouteOverride(
|
|
26
|
+
{{ cookiecutter.class_prefix }}ApplicationService,
|
|
27
|
+
method={{ cookiecutter.class_prefix }}ApplicationService.create,
|
|
28
|
+
path=f"/api/{MODULE_NAME}",
|
|
29
|
+
status_code=201,
|
|
30
|
+
),
|
|
31
|
+
ApplicationServiceRouteOverride(
|
|
32
|
+
{{ cookiecutter.class_prefix }}ApplicationService,
|
|
33
|
+
method={{ cookiecutter.class_prefix }}ApplicationService.get_list,
|
|
34
|
+
path=f"/api/{MODULE_NAME}",
|
|
35
|
+
),
|
|
36
|
+
ApplicationServiceRouteOverride(
|
|
37
|
+
{{ cookiecutter.class_prefix }}ApplicationService,
|
|
38
|
+
method={{ cookiecutter.class_prefix }}ApplicationService.get,
|
|
39
|
+
path=f"/api/{MODULE_NAME}" + "/{id}",
|
|
40
|
+
),
|
|
41
|
+
ApplicationServiceRouteOverride(
|
|
42
|
+
{{ cookiecutter.class_prefix }}ApplicationService,
|
|
43
|
+
method={{ cookiecutter.class_prefix }}ApplicationService.approve,
|
|
44
|
+
path=f"/api/{MODULE_NAME}" + "/{id}/approve",
|
|
45
|
+
),
|
|
46
|
+
ApplicationServiceRouteOverride(
|
|
47
|
+
{{ cookiecutter.class_prefix }}ApplicationService,
|
|
48
|
+
method={{ cookiecutter.class_prefix }}ApplicationService.queue_approval,
|
|
49
|
+
path=f"/api/{MODULE_NAME}" + "/{id}/queue-approval",
|
|
50
|
+
status_code=202,
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
class {{ cookiecutter.class_prefix }}HttpApiModule(AppModule):
|
|
55
|
+
dependencies = ({{ cookiecutter.class_prefix }}ApplicationContractsModule, FastApiRealtimeModule)
|
|
56
|
+
|
|
57
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
58
|
+
context.services.add_scoped(ExportFilter, scope=Scope.REQUEST)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""单进程在线通知;连接认证由框架管理,业务授权仍经过服务契约。"""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends
|
|
6
|
+
from pydantic import BaseModel, ConfigDict
|
|
7
|
+
from starlette.websockets import WebSocketDisconnect
|
|
8
|
+
|
|
9
|
+
from python_platform import FrameworkError
|
|
10
|
+
from python_platform.fastapi import WebSocketConnection, websocket_connection
|
|
11
|
+
|
|
12
|
+
from ..application_contracts.orders import {{ cookiecutter.class_prefix }}ApplicationService
|
|
13
|
+
from ..domain_shared.definitions import MODULE_NAME, ORDER_SNAPSHOT_MESSAGE
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RefreshOrders(BaseModel):
|
|
19
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.websocket(f"/ws/{MODULE_NAME}")
|
|
23
|
+
async def orders_socket(
|
|
24
|
+
connection: Annotated[WebSocketConnection, Depends(websocket_connection)],
|
|
25
|
+
) -> None:
|
|
26
|
+
try:
|
|
27
|
+
while True:
|
|
28
|
+
orders = await connection.invoke({{ cookiecutter.class_prefix }}ApplicationService.get_list)
|
|
29
|
+
await connection.send(
|
|
30
|
+
ORDER_SNAPSHOT_MESSAGE,
|
|
31
|
+
{"orders": [item.model_dump(mode="json") for item in orders]},
|
|
32
|
+
)
|
|
33
|
+
await connection.receive(RefreshOrders)
|
|
34
|
+
except WebSocketDisconnect:
|
|
35
|
+
return
|
|
36
|
+
except FrameworkError as error:
|
|
37
|
+
await connection.send_problem(error)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""数据库实现由 consuming Host 选择。"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""初始无业务 revision;执行 pp db revision --module {{ cookiecutter.module_name }} 后人工审查。"""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""修改模型后显式生成并审查迁移,不在 Host 启动时建表。"""
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import Integer, String, Uuid
|
|
6
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from python_platform.sqlalchemy import AuditedEntityMixin
|
|
9
|
+
|
|
10
|
+
from ..domain_shared.definitions import MODULE_NAME, TITLE_MAX_LENGTH, OrderStatus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class {{ cookiecutter.class_prefix }}Base(DeclarativeBase):
|
|
14
|
+
"""本模块模型的原生 MetaData owner。"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OrderRow(AuditedEntityMixin, {{ cookiecutter.class_prefix }}Base):
|
|
18
|
+
__tablename__ = MODULE_NAME
|
|
19
|
+
id: Mapped[UUID] = mapped_column(Uuid, primary_key=True)
|
|
20
|
+
title: Mapped[str] = mapped_column(String(TITLE_MAX_LENGTH), nullable=False)
|
|
21
|
+
status: Mapped[str] = mapped_column(
|
|
22
|
+
String(max(len(item.value) for item in OrderStatus)), nullable=False
|
|
23
|
+
)
|
|
24
|
+
version: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
25
|
+
# SQLAlchemy 以行版本检查并发;聚合暂存版本与数据库提交版本保持一致。
|
|
26
|
+
__mapper_args__ = {"version_id_col": version, "version_id_generator": False}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""provider 依赖领域契约;Host 显式选择此实现。"""
|
|
2
|
+
|
|
3
|
+
from dishka import Provider, Scope
|
|
4
|
+
|
|
5
|
+
from python_platform import AppModule, ConfigureContext
|
|
6
|
+
from python_platform.sqlalchemy import (
|
|
7
|
+
SqlAlchemyModelRegistration,
|
|
8
|
+
SqlAlchemyPersistenceModule,
|
|
9
|
+
contributes_sqlalchemy_models,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from ..domain.module import {{ cookiecutter.class_prefix }}DomainModule
|
|
13
|
+
from ..domain.repository import OrderRepository
|
|
14
|
+
from ..domain_shared.definitions import MODULE_NAME
|
|
15
|
+
from .models import {{ cookiecutter.class_prefix }}Base
|
|
16
|
+
from .repository import SqlAlchemyOrderRepository
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@contributes_sqlalchemy_models(
|
|
20
|
+
SqlAlchemyModelRegistration(
|
|
21
|
+
metadata={{ cookiecutter.class_prefix }}Base.metadata,
|
|
22
|
+
migrations_package=__package__ + ".migrations",
|
|
23
|
+
branch_label=MODULE_NAME,
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
class {{ cookiecutter.class_prefix }}SqlAlchemyModule(AppModule):
|
|
27
|
+
dependencies = ({{ cookiecutter.class_prefix }}DomainModule, SqlAlchemyPersistenceModule)
|
|
28
|
+
|
|
29
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
30
|
+
provider = Provider()
|
|
31
|
+
provider.provide(SqlAlchemyOrderRepository, provides=OrderRepository, scope=Scope.REQUEST)
|
|
32
|
+
context.services.contribute(provider, reason="Module repository implementation")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""仓储参与当前 UoW,成功暂存后由 operation 收集聚合事件。"""
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import select
|
|
6
|
+
|
|
7
|
+
from python_platform import OptimisticConcurrencyError
|
|
8
|
+
from python_platform.sqlalchemy import SqlAlchemySessionProvider
|
|
9
|
+
|
|
10
|
+
from ..domain.orders import Order, OrderTitle
|
|
11
|
+
from ..domain_shared.definitions import OrderStatus
|
|
12
|
+
from .models import OrderRow
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SqlAlchemyOrderRepository:
|
|
16
|
+
def __init__(self, provider: SqlAlchemySessionProvider) -> None:
|
|
17
|
+
self._provider = provider
|
|
18
|
+
|
|
19
|
+
async def get(self, id: UUID) -> Order | None:
|
|
20
|
+
async with self._provider.operation("get"):
|
|
21
|
+
row = await (await self._provider.get_session()).get(OrderRow, id)
|
|
22
|
+
return None if row is None else _order(row)
|
|
23
|
+
|
|
24
|
+
async def get_list(self) -> tuple[Order, ...]:
|
|
25
|
+
async with self._provider.operation("get_list"):
|
|
26
|
+
rows = await (await self._provider.get_session()).scalars(
|
|
27
|
+
select(OrderRow).order_by(OrderRow.created_at, OrderRow.id)
|
|
28
|
+
)
|
|
29
|
+
return tuple(_order(row) for row in rows)
|
|
30
|
+
|
|
31
|
+
async def save(self, order: Order) -> None:
|
|
32
|
+
async with self._provider.operation("save", aggregate=order):
|
|
33
|
+
session = await self._provider.get_session()
|
|
34
|
+
row = await session.get(OrderRow, order.id)
|
|
35
|
+
if row is None:
|
|
36
|
+
if order.version:
|
|
37
|
+
raise OptimisticConcurrencyError
|
|
38
|
+
row = OrderRow(id=order.id)
|
|
39
|
+
session.add(row)
|
|
40
|
+
elif row.version != order.version:
|
|
41
|
+
raise OptimisticConcurrencyError
|
|
42
|
+
order.stage_next_version(order.version)
|
|
43
|
+
row.title, row.status, row.version = order.title, order.status.value, order.version
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _order(row: OrderRow) -> Order:
|
|
47
|
+
return Order(
|
|
48
|
+
row.id, OrderTitle(value=row.title), status=OrderStatus(row.status), version=row.version
|
|
49
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""模块内领域验收,不随生产 wheel 打包或参与服务扫描。"""
|
|
2
|
+
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from modules.{{ cookiecutter.module_name }}.domain.orders import Order, OrderTitle
|
|
7
|
+
from modules.{{ cookiecutter.module_name }}.domain_shared.definitions import OrderStatus
|
|
8
|
+
|
|
9
|
+
from python_platform.errors import BusinessError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_order_approval_rejects_repeat():
|
|
13
|
+
order = Order.create(uuid4(), OrderTitle(value="Example order"))
|
|
14
|
+
order.approve()
|
|
15
|
+
assert order.status is OrderStatus.APPROVED
|
|
16
|
+
assert len(order.release_local_events()) == 2
|
|
17
|
+
with pytest.raises(BusinessError):
|
|
18
|
+
order.approve()
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project_name": "application", "package_name": "application", "class_prefix": "Application",
|
|
3
|
+
"framework_version": "", "framework_source": "", "python_version": "", "build_version": "",
|
|
4
|
+
"database_password": "", "admin_password": "", "jwt_key": "",
|
|
5
|
+
"host_environment_variable": ""
|
|
6
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 仅开发使用;由 pp new 随机生成。生产通过环境或挂载配置提供独立凭据。
|
|
2
|
+
POSTGRES_IMAGE=postgres:18-bookworm
|
|
3
|
+
REDIS_IMAGE=redis:7-bookworm
|
|
4
|
+
POSTGRES_DB={{ cookiecutter.package_name }}
|
|
5
|
+
POSTGRES_USER={{ cookiecutter.package_name }}
|
|
6
|
+
POSTGRES_PASSWORD={{ cookiecutter.database_password }}
|
|
7
|
+
POSTGRES_PORT=5432
|
|
8
|
+
REDIS_PORT=6379
|
|
9
|
+
PP_CONNECTION_STRINGS__DEFAULT=postgresql+asyncpg://{{ cookiecutter.package_name }}:{{ cookiecutter.database_password }}@127.0.0.1:5432/{{ cookiecutter.package_name }}
|
|
10
|
+
PP_REDIS__URL=redis://127.0.0.1:6379/0
|
|
11
|
+
PP_IDENTITY__KEYS__development={{ cookiecutter.jwt_key }}
|
|
12
|
+
PP_IDENTITY__SEED_ADMIN_USERNAME=admin
|
|
13
|
+
PP_IDENTITY__SEED_ADMIN_PASSWORD={{ cookiecutter.admin_password }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{ cookiecutter.python_version }}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
FROM debian:bookworm-slim AS build
|
|
3
|
+
COPY --from=ghcr.io/astral-sh/uv:{{ cookiecutter.build_version }} /uv /usr/local/bin/uv
|
|
4
|
+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates build-essential
|
|
5
|
+
ENV UV_PYTHON_INSTALL_DIR=/opt/python UV_LINK_MODE=copy UV_COMPILE_BYTECODE=1
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
COPY . .
|
|
8
|
+
# Python 版本只从项目 .python-version 读取;PyPI 或 vendor wheel 均由 uv.lock 固定。
|
|
9
|
+
RUN uv python install && uv sync --locked --no-default-groups --no-editable
|
|
10
|
+
|
|
11
|
+
FROM debian:bookworm-slim AS runtime
|
|
12
|
+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libstdc++6 && useradd --create-home app
|
|
13
|
+
COPY --from=build /opt/python /opt/python
|
|
14
|
+
COPY --from=build /app/.venv /app/.venv
|
|
15
|
+
COPY --from=build /app/pyproject.toml /app/app.yaml /app/
|
|
16
|
+
WORKDIR /app
|
|
17
|
+
ENV PATH="/app/.venv/bin:$PATH" {{ cookiecutter.host_environment_variable }}=production PYTHONUNBUFFERED=1
|
|
18
|
+
RUN mkdir /app/.python-platform && chown app:app /app/.python-platform
|
|
19
|
+
USER app
|
|
20
|
+
EXPOSE 8000
|
|
21
|
+
# 独立 access handler 会绕过安全 formatter;请求诊断由框架审计和追踪负责。
|
|
22
|
+
CMD ["python", "-m", "host.main"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# {{ cookiecutter.project_name }}
|
|
2
|
+
|
|
3
|
+
纯 Host 骨架。`src/host` 负责 provider、配置及启动;`pp add module` 在 `src/modules` 生成六层业务。
|
|
4
|
+
模块内 tests 不进入生产 wheel 或服务扫描,Host 集成验收放在根级 tests/host。
|
|
5
|
+
|
|
6
|
+
## 开发次序
|
|
7
|
+
|
|
8
|
+
```powershell
|
|
9
|
+
uv run pp dev-init
|
|
10
|
+
uv run pp add module orders
|
|
11
|
+
uv run pp db upgrade --module identity
|
|
12
|
+
uv run pp db upgrade --module settings
|
|
13
|
+
uv run pp db upgrade --module auditing
|
|
14
|
+
uv run pp db upgrade --module background_jobs
|
|
15
|
+
uv run pp db revision --module orders
|
|
16
|
+
# 人工审查 revision,尤其表/列改名、数据回填和外部依赖。
|
|
17
|
+
uv run pp db upgrade --module orders
|
|
18
|
+
uv run pp db status --module orders
|
|
19
|
+
uv run pp db seed --module identity
|
|
20
|
+
uv run pp db seed --module orders
|
|
21
|
+
uv run pytest
|
|
22
|
+
uv run pp dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`dev-init` 只启动或复用开发 PostgreSQL/Redis,保留数据;不迁移、不 Seed。四个 db 命令必须指定
|
|
26
|
+
`--module`,别名来自标准 entry point,目标来自真实 Module 声明。数据库命令不启动 Worker/HostedService,
|
|
27
|
+
每个 Seed Contributor 独立事务。Host 启动同样不迁移、不 Seed。新增第二个模块时重复 add/revision/upgrade/seed。
|
|
28
|
+
无关模块未升级不会阻塞当前模块;缺少前置迁移需先显式升级其 owner。自动生成只比较目标当前模型的表及字段,
|
|
29
|
+
整表删除由开发者人工编写、审查其归属与数据影响。
|
|
30
|
+
|
|
31
|
+
## 业务位置与调用
|
|
32
|
+
|
|
33
|
+
- domain_shared:枚举、错误和权限名称;domain:聚合审批、事件、仓储契约、设置和幂等 Seed。
|
|
34
|
+
- application_contracts:DTO 与服务契约;application:应用服务、提交后缓存失效/通知、Job/Cron/Worker 和线程集成。
|
|
35
|
+
- sqlalchemy:模型、原生版本检查、仓储与迁移;http_api:自动服务暴露、上传/下载/流、Filter 和 WebSocket。
|
|
36
|
+
- 打开 `/docs`,用 `.env` 中随机生成的管理员凭据调用 `/api/auth/login`,在 Authorize 填写 access token。
|
|
37
|
+
`/api/orders` 创建/查询;`/{id}/approve` 审批,非法 DTO 在业务前失败,旧版本返回 409。
|
|
38
|
+
- `/api/orders/{id}/queue-approval` 在业务事务内入队;Job 对已审批状态保持幂等。Cron 每五分钟输出订单数量。
|
|
39
|
+
`/api/orders/files/upload` 使用 File/Form 及读取期限额;download、export/stream 在响应前完成短 UoW,
|
|
40
|
+
网络发送期间只持有不可变 DTO,断开后关闭 producer。
|
|
41
|
+
- 使用 Bearer 认证连接 `/ws/orders`,先收到快照,发送 `{}` 刷新;同一 Host 审批后收到 changed 消息。
|
|
42
|
+
不承诺跨进程在线通知或跨进程本地事件。`/api/settings/orders.approval_enabled` 支持修改与 reset,刷新使用框架通知。
|
|
43
|
+
- 审计、关联日志、tracing 使用框架默认管线;tracing 导出默认关闭,按环境配置启用已有 exporter。
|
|
44
|
+
|
|
45
|
+
## 后台与配置
|
|
46
|
+
|
|
47
|
+
模板 Worker 默认禁用。在模块 application/module.py 将 MAINTENANCE_WORKER 的 enabled 改为 true 后才执行。
|
|
48
|
+
Worker 用 DistributedLock 取得业务锁,未取得时跳过;租约失效会取消本轮并等待清理,业务不得吞取消。
|
|
49
|
+
BackgroundExecutionOptions.workers/jobs 按定义名称覆盖 mode:host、shared、exclusive;同逻辑 Job 所有版本共用位置。
|
|
50
|
+
`/api/background/tasks` 由 background.view/manage 授权,POST start/stop 返回 202,继续查询状态;代码禁用不能从 HTTP 启用。
|
|
51
|
+
停止 Job 保留排队数据。正式 Host 按目录/环境保持单实例,reload 等待旧进程实际退出,残留后台进程阻止新 Host。
|
|
52
|
+
|
|
53
|
+
模块 Options 放在 app.yaml 对应模块名下,例如:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
orders:
|
|
57
|
+
allow_background_approval: false
|
|
58
|
+
integration_enabled: true
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
线程集成示例 start 先演示受控调用,再由外部线程 submit_call 使用同一 DI/调用管线;stop 等待线程退出。
|
|
62
|
+
外部 SDK 的运行故障与恢复由 HostedService 自己管理。配置使用 app.yaml 与 app.<environment>.yaml;
|
|
63
|
+
凭据放环境或 .env,不提交开发/生产密钥。
|
|
64
|
+
|
|
65
|
+
## 打包与部署
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
uv build --no-sources
|
|
69
|
+
# APP_IMAGE 固定为验收过的同一镜像 digest,.env.production 使用独立凭据。
|
|
70
|
+
docker build -t <application-image> .
|
|
71
|
+
$env:MODULE = "orders"
|
|
72
|
+
docker compose -f compose.production.yaml run --rm migrate
|
|
73
|
+
docker compose -f compose.production.yaml up -d web
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
生产迁移按依赖顺序逐模块执行;Seed 独立使用 pp db seed --module <alias> --environment production。
|
|
77
|
+
镜像入口 python -m host.main;健康端点 /health/live 与 /health/ready;SIGTERM 等待后台、HostedService、业务与 REQUEST 清理。
|
|
78
|
+
版本、框架来源、构建工具版本和顶层包由标准项目 metadata 拥有。
|
|
79
|
+
|
|
80
|
+
## 框架升级
|
|
81
|
+
|
|
82
|
+
项目使用生成时 CLI 对应的框架精确版本,日常命令使用 `uv run pp`。
|
|
83
|
+
升级前阅读目标框架版本 README,将运行依赖和 developer-kit 同时更新到目标版本,保留 extras。
|
|
84
|
+
本地 wheel/旧 Git 项目首次转到 PyPI 时,仅移除 tool.uv.sources 的 python-platform 条目。
|
|
85
|
+
执行 `uv lock --upgrade-package python-platform`、`uv sync --locked` 和项目原有验证。
|
|
86
|
+
升级不重生成业务、Host 或模板;按框架升级说明手动迁移。全局 CLI 更新也不改变本项目版本。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
identity:
|
|
2
|
+
issuer: "{{ cookiecutter.project_name }}"
|
|
3
|
+
audience: "{{ cookiecutter.project_name }}"
|
|
4
|
+
active_kid: development
|
|
5
|
+
redis:
|
|
6
|
+
key_prefix: "{{ cookiecutter.project_name }}"
|
|
7
|
+
logging:
|
|
8
|
+
application: "{{ cookiecutter.project_name }}"
|
|
9
|
+
environment: development
|
|
10
|
+
format: text
|
|
11
|
+
level: INFO
|
|
12
|
+
tracing:
|
|
13
|
+
service_name: "{{ cookiecutter.project_name }}"
|
|
14
|
+
export_enabled: false
|
|
15
|
+
pgqueuer_background_jobs:
|
|
16
|
+
connection_name: default
|
python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: ${POSTGRES_IMAGE:?Set POSTGRES_IMAGE in .env}
|
|
4
|
+
environment:
|
|
5
|
+
POSTGRES_DB: ${POSTGRES_DB}
|
|
6
|
+
POSTGRES_USER: ${POSTGRES_USER}
|
|
7
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
8
|
+
ports: ["127.0.0.1:${POSTGRES_PORT}:5432"]
|
|
9
|
+
healthcheck:
|
|
10
|
+
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
|
11
|
+
interval: 2s
|
|
12
|
+
timeout: 2s
|
|
13
|
+
retries: 20
|
|
14
|
+
volumes: ["postgres-data:/var/lib/postgresql"]
|
|
15
|
+
redis:
|
|
16
|
+
image: ${REDIS_IMAGE:?Set REDIS_IMAGE in .env}
|
|
17
|
+
ports: ["127.0.0.1:${REDIS_PORT}:6379"]
|
|
18
|
+
healthcheck:
|
|
19
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
20
|
+
interval: 2s
|
|
21
|
+
timeout: 2s
|
|
22
|
+
retries: 20
|
|
23
|
+
volumes:
|
|
24
|
+
postgres-data:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# APP_IMAGE 使用验收通过的同一 digest;显式选择模块迁移,成功后启动 Host。
|
|
2
|
+
services:
|
|
3
|
+
migrate:
|
|
4
|
+
image: ${APP_IMAGE:?Provide the verified application image digest}
|
|
5
|
+
env_file: .env.production
|
|
6
|
+
command: ["pp", "db", "--environment", "production", "upgrade", "--module", "${MODULE:?Select the migration module alias}"]
|
|
7
|
+
profiles: ["operations"]
|
|
8
|
+
web:
|
|
9
|
+
image: ${APP_IMAGE:?Provide the verified application image digest}
|
|
10
|
+
env_file: .env.production
|
|
11
|
+
ports: ["${WEB_PORT:-8000}:8000"]
|
|
12
|
+
stop_grace_period: 45s
|
|
13
|
+
restart: unless-stopped
|
python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{{ cookiecutter.project_name }}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
requires-python = "=={{ cookiecutter.python_version }}"
|
|
5
|
+
dependencies = ["python-platform[fastapi,identity,logging,postgresql,redis,tracing,background-jobs]=={{ cookiecutter.framework_version }}"]
|
|
6
|
+
|
|
7
|
+
[project.entry-points."python_platform.hosts"]
|
|
8
|
+
application = "host.main:create_application"
|
|
9
|
+
web = "host.main:create_web"
|
|
10
|
+
[dependency-groups]
|
|
11
|
+
dev = ["python-platform[developer-kit]=={{ cookiecutter.framework_version }}", "pytest", "anyio", "testcontainers[postgres,redis]"]
|
|
12
|
+
|
|
13
|
+
{% if cookiecutter.framework_source %}[tool.uv.sources]
|
|
14
|
+
python-platform = {{ cookiecutter.framework_source }}
|
|
15
|
+
{% endif %}
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build=={{ cookiecutter.build_version }}"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[tool.uv.build-backend]
|
|
21
|
+
module-name = "host"
|
|
22
|
+
module-root = "src"
|
|
23
|
+
wheel-exclude = ["**/tests/**"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
anyio_mode = "auto"
|
|
27
|
+
testpaths = ["tests", "src/modules"]
|
|
28
|
+
addopts = "--import-mode=importlib"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""应用 composition root;业务代码由 pp add module 生成。"""
|
python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""标准入口一次 build/start/stop;生产和开发使用同一 factory。"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from starlette.middleware import Middleware
|
|
9
|
+
from starlette.middleware.gzip import GZipMiddleware
|
|
10
|
+
|
|
11
|
+
from python_platform import Application, ApplicationBuilder
|
|
12
|
+
from python_platform.cli import ENVIRONMENT_VARIABLE
|
|
13
|
+
from python_platform.fastapi import FastApiAdapter
|
|
14
|
+
|
|
15
|
+
from .module import {{ cookiecutter.class_prefix }}HostModule
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_application(
|
|
19
|
+
*, environment: str | None = None, cli_args: tuple[str, ...] = ()
|
|
20
|
+
) -> Application:
|
|
21
|
+
environment = environment or os.environ.get(ENVIRONMENT_VARIABLE, "development")
|
|
22
|
+
return ApplicationBuilder(
|
|
23
|
+
{{ cookiecutter.class_prefix }}HostModule,
|
|
24
|
+
environment=environment,
|
|
25
|
+
base_path=Path.cwd(),
|
|
26
|
+
env_prefix="PP_",
|
|
27
|
+
cli_args=cli_args,
|
|
28
|
+
dotenv_path=Path.cwd() / ".env" if environment == "development" else None,
|
|
29
|
+
explicit_overrides={
|
|
30
|
+
"logging": {
|
|
31
|
+
"environment": environment,
|
|
32
|
+
"format": "text" if environment == "development" else "json",
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
).build()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_web() -> FastAPI:
|
|
39
|
+
application = create_application()
|
|
40
|
+
# 全局 middleware 使用原生 Starlette 实现,在 adapter.install 前完成配置。
|
|
41
|
+
adapter = FastApiAdapter(application, middleware=(Middleware(GZipMiddleware),))
|
|
42
|
+
web = FastAPI(
|
|
43
|
+
title="{{ cookiecutter.project_name }}", version=version("{{ cookiecutter.project_name }}"), lifespan=adapter.lifespan
|
|
44
|
+
)
|
|
45
|
+
adapter.install(web)
|
|
46
|
+
return web
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
from python_platform.fastapi.server import _run_host
|
|
51
|
+
|
|
52
|
+
_run_host("host.main:create_web", host="0.0.0.0", port=8000)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Host 只选择 provider 与组合 Module,不拥有业务规则。"""
|
|
2
|
+
|
|
3
|
+
from dishka.integrations.fastapi import FastapiProvider
|
|
4
|
+
|
|
5
|
+
from python_platform import AppModule, ConfigureContext
|
|
6
|
+
from python_platform.auditing.sqlalchemy import SqlAlchemyAuditingModule
|
|
7
|
+
from python_platform.background_jobs.pgqueuer import PgQueuerBackgroundJobsModule
|
|
8
|
+
from python_platform.fastapi.background import BackgroundManagementHttpApiModule
|
|
9
|
+
from python_platform.fastapi.settings import SettingsManagementHttpApiModule
|
|
10
|
+
from python_platform.identity.http_api import IdentityHttpApiModule
|
|
11
|
+
from python_platform.identity.sqlalchemy import SqlAlchemyIdentityModule
|
|
12
|
+
from python_platform.observability.logging import StructuredLoggingModule
|
|
13
|
+
from python_platform.observability.tracing import TracingModule
|
|
14
|
+
from python_platform.redis import RedisDistributedLockModule, RedisModule
|
|
15
|
+
from python_platform.redis.notifications import RedisNotificationsModule
|
|
16
|
+
from python_platform.settings.notifications import SettingsNotificationsModule
|
|
17
|
+
from python_platform.settings.sqlalchemy import SqlAlchemyRuntimeSettingsModule
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class {{ cookiecutter.class_prefix }}HostModule(AppModule):
|
|
21
|
+
dependencies = (
|
|
22
|
+
SqlAlchemyIdentityModule,
|
|
23
|
+
IdentityHttpApiModule,
|
|
24
|
+
SqlAlchemyAuditingModule,
|
|
25
|
+
SqlAlchemyRuntimeSettingsModule,
|
|
26
|
+
SettingsManagementHttpApiModule,
|
|
27
|
+
RedisModule,
|
|
28
|
+
RedisDistributedLockModule,
|
|
29
|
+
RedisNotificationsModule,
|
|
30
|
+
SettingsNotificationsModule,
|
|
31
|
+
PgQueuerBackgroundJobsModule,
|
|
32
|
+
BackgroundManagementHttpApiModule,
|
|
33
|
+
StructuredLoggingModule,
|
|
34
|
+
TracingModule,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def configure(self, context: ConfigureContext) -> None:
|
|
38
|
+
context.services.contribute(FastapiProvider(), reason="Native HTTP request context")
|