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.
Files changed (344) hide show
  1. python_platform/__init__.py +445 -0
  2. python_platform/application/__init__.py +6 -0
  3. python_platform/application/build_spec.py +37 -0
  4. python_platform/application/builder.py +260 -0
  5. python_platform/application/composition.py +219 -0
  6. python_platform/application/runtime.py +546 -0
  7. python_platform/application/runtime_hooks.py +327 -0
  8. python_platform/application/state.py +216 -0
  9. python_platform/application_services/__init__.py +54 -0
  10. python_platform/application_services/catalog.py +500 -0
  11. python_platform/application_services/contracts.py +188 -0
  12. python_platform/application_services/dispatcher.py +423 -0
  13. python_platform/application_services/errors.py +89 -0
  14. python_platform/application_services/execution.py +199 -0
  15. python_platform/application_services/interceptors.py +44 -0
  16. python_platform/application_services/invocation.py +427 -0
  17. python_platform/application_services/policies.py +46 -0
  18. python_platform/application_services/seeding.py +72 -0
  19. python_platform/application_services/signature.py +49 -0
  20. python_platform/application_services/validation.py +84 -0
  21. python_platform/auditing/__init__.py +12 -0
  22. python_platform/auditing/contracts.py +39 -0
  23. python_platform/auditing/control.py +48 -0
  24. python_platform/auditing/sqlalchemy/__init__.py +6 -0
  25. python_platform/auditing/sqlalchemy/migrations/0001_platform_auditing.py +45 -0
  26. python_platform/auditing/sqlalchemy/migrations/__init__.py +1 -0
  27. python_platform/auditing/sqlalchemy/models.py +31 -0
  28. python_platform/auditing/sqlalchemy/module.py +32 -0
  29. python_platform/auditing/sqlalchemy/store.py +63 -0
  30. python_platform/authorization/__init__.py +38 -0
  31. python_platform/authorization/catalog.py +63 -0
  32. python_platform/authorization/contracts.py +241 -0
  33. python_platform/authorization/definition_discovery.py +81 -0
  34. python_platform/authorization/definitions.py +43 -0
  35. python_platform/authorization/errors.py +40 -0
  36. python_platform/background_execution/__init__.py +15 -0
  37. python_platform/background_execution/application.py +41 -0
  38. python_platform/background_execution/child.py +123 -0
  39. python_platform/background_execution/contracts.py +39 -0
  40. python_platform/background_execution/lifecycle.py +22 -0
  41. python_platform/background_execution/local.py +127 -0
  42. python_platform/background_execution/locks.py +31 -0
  43. python_platform/background_execution/management.py +17 -0
  44. python_platform/background_execution/module.py +10 -0
  45. python_platform/background_execution/permissions.py +14 -0
  46. python_platform/background_execution/processes.py +331 -0
  47. python_platform/background_jobs/__init__.py +31 -0
  48. python_platform/background_jobs/catalog.py +287 -0
  49. python_platform/background_jobs/contracts.py +202 -0
  50. python_platform/background_jobs/errors.py +28 -0
  51. python_platform/background_jobs/execution.py +201 -0
  52. python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
  53. python_platform/background_jobs/pgqueuer/__init__.py +6 -0
  54. python_platform/background_jobs/pgqueuer/enqueue.py +82 -0
  55. python_platform/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +63 -0
  56. python_platform/background_jobs/pgqueuer/migrations/__init__.py +1 -0
  57. python_platform/background_jobs/pgqueuer/module.py +102 -0
  58. python_platform/background_jobs/pgqueuer/options.py +70 -0
  59. python_platform/background_jobs/pgqueuer/runtime.py +479 -0
  60. python_platform/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
  61. python_platform/background_jobs/pgqueuer/supervision.py +201 -0
  62. python_platform/background_workers/__init__.py +22 -0
  63. python_platform/background_workers/catalog.py +96 -0
  64. python_platform/background_workers/contracts.py +124 -0
  65. python_platform/background_workers/errors.py +20 -0
  66. python_platform/background_workers/execution.py +133 -0
  67. python_platform/background_workers/runtime.py +203 -0
  68. python_platform/caching/__init__.py +11 -0
  69. python_platform/caching/catalog.py +73 -0
  70. python_platform/caching/contracts.py +73 -0
  71. python_platform/caching/errors.py +20 -0
  72. python_platform/cli/__init__.py +87 -0
  73. python_platform/cli/errors.py +5 -0
  74. python_platform/cli/project.py +69 -0
  75. python_platform/cli/runtime.py +68 -0
  76. python_platform/configuration/__init__.py +21 -0
  77. python_platform/configuration/composition.py +65 -0
  78. python_platform/configuration/contracts.py +68 -0
  79. python_platform/configuration/dotenv_source.py +51 -0
  80. python_platform/configuration/environment_source.py +38 -0
  81. python_platform/configuration/immutability.py +124 -0
  82. python_platform/configuration/input_shape.py +58 -0
  83. python_platform/configuration/merge.py +103 -0
  84. python_platform/configuration/root.py +215 -0
  85. python_platform/configuration/sources.py +519 -0
  86. python_platform/configuration/values.py +427 -0
  87. python_platform/configuration/yaml_source.py +51 -0
  88. python_platform/developer_kit/__init__.py +1 -0
  89. python_platform/developer_kit/generation.py +182 -0
  90. python_platform/developer_kit/project_metadata.py +45 -0
  91. python_platform/developer_kit/source.py +53 -0
  92. python_platform/developer_kit/templates/module/cookiecutter.json +1 -0
  93. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
  94. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
  95. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
  96. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
  97. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +46 -0
  98. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +29 -0
  99. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +8 -0
  100. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +81 -0
  101. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +86 -0
  102. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
  103. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
  104. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +36 -0
  105. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
  106. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
  107. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +57 -0
  108. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +12 -0
  109. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
  110. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
  111. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
  112. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
  113. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
  114. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
  115. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
  116. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +74 -0
  117. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +58 -0
  118. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +37 -0
  119. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
  120. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
  121. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models.py.jinja +26 -0
  122. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +32 -0
  123. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repository.py.jinja +49 -0
  124. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
  125. python_platform/developer_kit/templates/project/cookiecutter.json +6 -0
  126. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +8 -0
  127. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.env.jinja +13 -0
  128. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
  129. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
  130. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
  131. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +86 -0
  132. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/app.yaml.jinja +16 -0
  133. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
  134. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +13 -0
  135. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
  136. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
  137. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +52 -0
  138. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +38 -0
  139. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
  140. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +101 -0
  141. python_platform/developer_kit/wiring.py +85 -0
  142. python_platform/diagnostics/__init__.py +23 -0
  143. python_platform/diagnostics/journal.py +174 -0
  144. python_platform/diagnostics/model.py +83 -0
  145. python_platform/diagnostics/source.py +25 -0
  146. python_platform/distributed_lock/__init__.py +6 -0
  147. python_platform/distributed_lock/contracts.py +20 -0
  148. python_platform/distributed_lock/options.py +19 -0
  149. python_platform/domain/__init__.py +13 -0
  150. python_platform/domain/aggregates.py +64 -0
  151. python_platform/domain/entities.py +33 -0
  152. python_platform/domain/errors.py +12 -0
  153. python_platform/domain/value_objects.py +31 -0
  154. python_platform/errors/__init__.py +75 -0
  155. python_platform/errors/base.py +13 -0
  156. python_platform/errors/business.py +53 -0
  157. python_platform/errors/configuration.py +119 -0
  158. python_platform/errors/diagnostics.py +12 -0
  159. python_platform/errors/lifecycle.py +164 -0
  160. python_platform/errors/modularity.py +100 -0
  161. python_platform/errors/services.py +100 -0
  162. python_platform/events/__init__.py +25 -0
  163. python_platform/events/aggregate.py +39 -0
  164. python_platform/events/catalog.py +136 -0
  165. python_platform/events/contracts.py +39 -0
  166. python_platform/events/contribution.py +128 -0
  167. python_platform/events/discovery.py +105 -0
  168. python_platform/events/errors.py +48 -0
  169. python_platform/events/runtime.py +192 -0
  170. python_platform/fastapi/__init__.py +49 -0
  171. python_platform/fastapi/action.py +175 -0
  172. python_platform/fastapi/adapter.py +381 -0
  173. python_platform/fastapi/application_services.py +647 -0
  174. python_platform/fastapi/background.py +42 -0
  175. python_platform/fastapi/contracts.py +278 -0
  176. python_platform/fastapi/errors.py +32 -0
  177. python_platform/fastapi/filters.py +111 -0
  178. python_platform/fastapi/health.py +44 -0
  179. python_platform/fastapi/http.py +55 -0
  180. python_platform/fastapi/http_router.py +212 -0
  181. python_platform/fastapi/manual_action.py +105 -0
  182. python_platform/fastapi/middleware.py +93 -0
  183. python_platform/fastapi/parameters.py +97 -0
  184. python_platform/fastapi/realtime/__init__.py +13 -0
  185. python_platform/fastapi/realtime/authentication.py +181 -0
  186. python_platform/fastapi/realtime/connection.py +166 -0
  187. python_platform/fastapi/realtime/module.py +41 -0
  188. python_platform/fastapi/realtime/options.py +50 -0
  189. python_platform/fastapi/realtime/runtime.py +423 -0
  190. python_platform/fastapi/request_context.py +244 -0
  191. python_platform/fastapi/route_integrity.py +88 -0
  192. python_platform/fastapi/routing.py +410 -0
  193. python_platform/fastapi/server.py +181 -0
  194. python_platform/fastapi/settings.py +40 -0
  195. python_platform/fastapi/tracing.py +62 -0
  196. python_platform/fastapi/transfer.py +128 -0
  197. python_platform/fastapi/upload_limits.py +63 -0
  198. python_platform/fastapi/uploads.py +94 -0
  199. python_platform/hosted_services/__init__.py +29 -0
  200. python_platform/hosted_services/bridge.py +420 -0
  201. python_platform/hosted_services/catalog.py +259 -0
  202. python_platform/hosted_services/contracts.py +55 -0
  203. python_platform/hosted_services/errors.py +53 -0
  204. python_platform/hosted_services/options.py +17 -0
  205. python_platform/hosted_services/runtime.py +267 -0
  206. python_platform/hosted_services/state.py +40 -0
  207. python_platform/hosting/__init__.py +4 -0
  208. python_platform/hosting/instance.py +40 -0
  209. python_platform/identity/__init__.py +73 -0
  210. python_platform/identity/application.py +287 -0
  211. python_platform/identity/contracts.py +283 -0
  212. python_platform/identity/errors.py +28 -0
  213. python_platform/identity/http_api.py +90 -0
  214. python_platform/identity/module.py +46 -0
  215. python_platform/identity/passwords.py +34 -0
  216. python_platform/identity/permissions.py +14 -0
  217. python_platform/identity/services.py +108 -0
  218. python_platform/identity/sqlalchemy/__init__.py +5 -0
  219. python_platform/identity/sqlalchemy/migrations/0001_platform_identity.py +151 -0
  220. python_platform/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
  221. python_platform/identity/sqlalchemy/migrations/__init__.py +1 -0
  222. python_platform/identity/sqlalchemy/models.py +85 -0
  223. python_platform/identity/sqlalchemy/module.py +62 -0
  224. python_platform/identity/sqlalchemy/stores.py +526 -0
  225. python_platform/identity/tokens.py +106 -0
  226. python_platform/invocation/__init__.py +3 -0
  227. python_platform/invocation/callables.py +171 -0
  228. python_platform/invocation/contracts.py +33 -0
  229. python_platform/invocation/entries.py +54 -0
  230. python_platform/invocation/function_runtime.py +48 -0
  231. python_platform/invocation/interception.py +184 -0
  232. python_platform/lifecycle/__init__.py +25 -0
  233. python_platform/lifecycle/composition.py +35 -0
  234. python_platform/lifecycle/context.py +31 -0
  235. python_platform/lifecycle/runtime.py +43 -0
  236. python_platform/lifecycle/state.py +20 -0
  237. python_platform/modularity/__init__.py +15 -0
  238. python_platform/modularity/contracts.py +90 -0
  239. python_platform/modularity/discovery.py +143 -0
  240. python_platform/modularity/graph.py +334 -0
  241. python_platform/modularity/registry.py +100 -0
  242. python_platform/modularity/selection.py +16 -0
  243. python_platform/notifications/__init__.py +19 -0
  244. python_platform/notifications/catalog.py +46 -0
  245. python_platform/notifications/contracts.py +107 -0
  246. python_platform/observability/__init__.py +5 -0
  247. python_platform/observability/context.py +43 -0
  248. python_platform/observability/export.py +73 -0
  249. python_platform/observability/formatting.py +89 -0
  250. python_platform/observability/logging.py +118 -0
  251. python_platform/observability/options.py +45 -0
  252. python_platform/observability/tracing.py +80 -0
  253. python_platform/options/__init__.py +19 -0
  254. python_platform/options/aliases.py +331 -0
  255. python_platform/options/contribution.py +62 -0
  256. python_platform/options/immutability.py +48 -0
  257. python_platform/options/input_keys.py +214 -0
  258. python_platform/options/issues.py +335 -0
  259. python_platform/options/models.py +114 -0
  260. python_platform/options/registry.py +280 -0
  261. python_platform/options/schema.py +488 -0
  262. python_platform/options/validation.py +120 -0
  263. python_platform/py.typed +0 -0
  264. python_platform/realtime/__init__.py +12 -0
  265. python_platform/realtime/contracts.py +28 -0
  266. python_platform/realtime/diagnostics.py +37 -0
  267. python_platform/realtime/messages.py +113 -0
  268. python_platform/redis/__init__.py +6 -0
  269. python_platform/redis/distributed_lock.py +212 -0
  270. python_platform/redis/lease_lock.py +36 -0
  271. python_platform/redis/module.py +50 -0
  272. python_platform/redis/notification_runtime.py +182 -0
  273. python_platform/redis/notifications.py +25 -0
  274. python_platform/redis/options.py +35 -0
  275. python_platform/redis/runtime.py +116 -0
  276. python_platform/services/__init__.py +39 -0
  277. python_platform/services/application_bindings.py +221 -0
  278. python_platform/services/arbitration.py +381 -0
  279. python_platform/services/binding.py +102 -0
  280. python_platform/services/contribution.py +438 -0
  281. python_platform/services/convention.py +292 -0
  282. python_platform/services/convention_contracts.py +94 -0
  283. python_platform/services/exposure.py +23 -0
  284. python_platform/services/framework_provider.py +178 -0
  285. python_platform/services/native_graph.py +172 -0
  286. python_platform/services/provider.py +204 -0
  287. python_platform/services/registration.py +101 -0
  288. python_platform/services/runtime.py +298 -0
  289. python_platform/settings/__init__.py +30 -0
  290. python_platform/settings/application.py +95 -0
  291. python_platform/settings/binding.py +33 -0
  292. python_platform/settings/catalog.py +121 -0
  293. python_platform/settings/changes.py +14 -0
  294. python_platform/settings/contracts.py +115 -0
  295. python_platform/settings/definition_discovery.py +71 -0
  296. python_platform/settings/definitions.py +41 -0
  297. python_platform/settings/errors.py +22 -0
  298. python_platform/settings/handlers.py +43 -0
  299. python_platform/settings/management.py +57 -0
  300. python_platform/settings/manager.py +89 -0
  301. python_platform/settings/module.py +7 -0
  302. python_platform/settings/notifications.py +39 -0
  303. python_platform/settings/permissions.py +14 -0
  304. python_platform/settings/provider.py +74 -0
  305. python_platform/settings/refresh.py +140 -0
  306. python_platform/settings/refresh_module.py +45 -0
  307. python_platform/settings/sqlalchemy/__init__.py +5 -0
  308. python_platform/settings/sqlalchemy/migrations/0001_platform_settings.py +31 -0
  309. python_platform/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
  310. python_platform/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
  311. python_platform/settings/sqlalchemy/migrations/__init__.py +1 -0
  312. python_platform/settings/sqlalchemy/models.py +36 -0
  313. python_platform/settings/sqlalchemy/module.py +36 -0
  314. python_platform/settings/sqlalchemy/store.py +86 -0
  315. python_platform/settings/store.py +16 -0
  316. python_platform/settings/values.py +39 -0
  317. python_platform/sqlalchemy/__init__.py +51 -0
  318. python_platform/sqlalchemy/alembic_runtime/__init__.py +1 -0
  319. python_platform/sqlalchemy/alembic_runtime/env.py +27 -0
  320. python_platform/sqlalchemy/alembic_runtime/script.py.mako +14 -0
  321. python_platform/sqlalchemy/auditing.py +48 -0
  322. python_platform/sqlalchemy/errors.py +85 -0
  323. python_platform/sqlalchemy/metadata.py +538 -0
  324. python_platform/sqlalchemy/migration.py +346 -0
  325. python_platform/sqlalchemy/module.py +48 -0
  326. python_platform/sqlalchemy/module_migration.py +148 -0
  327. python_platform/sqlalchemy/options.py +66 -0
  328. python_platform/sqlalchemy/repository.py +33 -0
  329. python_platform/sqlalchemy/runtime.py +85 -0
  330. python_platform/sqlalchemy/session_provider.py +45 -0
  331. python_platform/sqlalchemy/unit_of_work.py +97 -0
  332. python_platform/testing/__init__.py +5 -0
  333. python_platform/testing/runtime.py +112 -0
  334. python_platform/unit_of_work/__init__.py +14 -0
  335. python_platform/unit_of_work/contracts.py +223 -0
  336. python_platform/unit_of_work/errors.py +27 -0
  337. python_platform/unit_of_work/manager.py +210 -0
  338. python_platform/unit_of_work/options.py +92 -0
  339. python_platform-0.1.1.dist-info/METADATA +122 -0
  340. python_platform-0.1.1.dist-info/RECORD +344 -0
  341. python_platform-0.1.1.dist-info/WHEEL +4 -0
  342. python_platform-0.1.1.dist-info/entry_points.txt +9 -0
  343. python_platform-0.1.1.dist-info/licenses/LICENSE +7 -0
  344. python_platform-0.1.1.dist-info/licenses/src/python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
@@ -0,0 +1,101 @@
1
+ """真实 Host 工作流;先按 README 生成并审查各业务模块 revision。"""
2
+
3
+ from importlib.metadata import distribution
4
+ from pathlib import Path
5
+
6
+ from dotenv import dotenv_values
7
+ from host.main import create_web
8
+ from host.module import {{ cookiecutter.class_prefix }}HostModule
9
+ from httpx import ASGITransport, AsyncClient
10
+ from testcontainers.community.postgres import PostgresContainer
11
+ from testcontainers.community.redis import RedisContainer
12
+
13
+ from python_platform import ApplicationBuilder, DataSeeder
14
+ from python_platform.sqlalchemy import SqlAlchemyMigrator
15
+ from python_platform.testing import TestApplication as PlatformTestApplication
16
+
17
+
18
+ async def test_host_http_workflow(monkeypatch):
19
+ configuration = dotenv_values(Path.cwd() / ".env")
20
+ with PostgresContainer(configuration["POSTGRES_IMAGE"], driver="asyncpg") as postgres:
21
+ with RedisContainer(configuration["REDIS_IMAGE"]) as redis:
22
+ monkeypatch.setenv(
23
+ "PP_CONNECTION_STRINGS__DEFAULT", postgres.get_connection_url(driver="asyncpg")
24
+ )
25
+ monkeypatch.setenv(
26
+ "PP_REDIS__URL",
27
+ f"redis://{redis.get_container_host_ip()}:{redis.get_exposed_port(redis.port)}/0",
28
+ )
29
+ test_application = PlatformTestApplication(
30
+ ApplicationBuilder(
31
+ {{ cookiecutter.class_prefix }}HostModule,
32
+ environment="development",
33
+ base_path=Path.cwd(),
34
+ env_prefix="PP_",
35
+ dotenv_path=Path.cwd() / ".env",
36
+ explicit_overrides={"logging": {"format": "text"}},
37
+ )
38
+ )
39
+ application = test_application.build()
40
+ try:
41
+ await SqlAlchemyMigrator(application).upgrade_heads()
42
+ await test_application.start()
43
+ await DataSeeder(application).seed_all()
44
+ finally:
45
+ await test_application.stop()
46
+ web = create_web()
47
+ async with web.router.lifespan_context(web):
48
+ async with AsyncClient(
49
+ transport=ASGITransport(web), base_url="http://test"
50
+ ) as client:
51
+ assert (await client.get("/health/ready")).status_code == 200
52
+ login = await client.post(
53
+ "/api/auth/login",
54
+ json={
55
+ "username": configuration["PP_IDENTITY__SEED_ADMIN_USERNAME"],
56
+ "password": configuration["PP_IDENTITY__SEED_ADMIN_PASSWORD"],
57
+ },
58
+ )
59
+ assert login.status_code == 200
60
+ client.headers["Authorization"] = "Bearer " + login.json()["access_token"]
61
+ modules = (
62
+ entry.name
63
+ for entry in distribution("{{ cookiecutter.project_name }}").entry_points
64
+ if entry.group == "python_platform.modules"
65
+ )
66
+ for module in modules:
67
+ path = f"/api/{module}"
68
+ created = await client.post(path, json={"title": "Sample order"})
69
+ assert created.status_code == 201
70
+ order = created.json()
71
+ assert (await client.get(f"{path}/{order['id']}")).json() == order
72
+ approved = await client.post(
73
+ f"{path}/{order['id']}/approve",
74
+ json={"expected_version": order["version"]},
75
+ )
76
+ assert (
77
+ approved.status_code == 200 and approved.json()["status"] == "approved"
78
+ )
79
+ conflict = await client.post(
80
+ f"{path}/{order['id']}/approve",
81
+ json={"expected_version": order["version"]},
82
+ )
83
+ assert conflict.status_code == 409
84
+ assert (await client.get(f"{path}/{order['id']}")).json()[
85
+ "status"
86
+ ] == "approved"
87
+ settings_path = f"/api/settings/{module}.approval_enabled"
88
+ setting = (await client.get(settings_path)).json()
89
+ assert (
90
+ await client.put(
91
+ settings_path,
92
+ json={"value": False, "expected_version": setting["version"]},
93
+ )
94
+ ).status_code == 200
95
+ second = (await client.post(path, json={"title": "Second"})).json()
96
+ assert (
97
+ await client.post(
98
+ f"{path}/{second['id']}/approve",
99
+ json={"expected_version": second["version"]},
100
+ )
101
+ ).status_code == 400
@@ -0,0 +1,85 @@
1
+ """LibCST 只修改已识别 startup class 的依赖;不重写用户文件的格式和注释。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import cast
6
+
7
+ import libcst as cst
8
+ import libcst.matchers as matchers
9
+ from libcst.codemod import CodemodContext
10
+ from libcst.codemod.visitors import AddImportsVisitor
11
+ from libcst.metadata import ClassScope, MetadataWrapper, ScopeProvider
12
+
13
+ from ..cli.errors import CommandError
14
+
15
+
16
+ class _AddDependency(cst.CSTTransformer):
17
+ def __init__(self, startup: cst.ClassDef, dependency: str) -> None:
18
+ self._startup = startup
19
+ self._dependency = dependency
20
+ self.matches = 0
21
+
22
+ def leave_ClassDef(
23
+ self, original_node: cst.ClassDef, updated_node: cst.ClassDef
24
+ ) -> cst.ClassDef:
25
+ if original_node is not self._startup:
26
+ return updated_node
27
+ body = cast(cst.IndentedBlock, updated_node.body)
28
+ statements = list(body.body)
29
+ for index, statement in enumerate(statements):
30
+ if not isinstance(statement, cst.SimpleStatementLine) or len(statement.body) != 1:
31
+ continue
32
+ assignment = statement.body[0]
33
+ if isinstance(assignment, cst.Assign) and len(assignment.targets) == 1:
34
+ target = assignment.targets[0].target
35
+ elif isinstance(assignment, cst.AnnAssign):
36
+ target = assignment.target
37
+ else:
38
+ continue
39
+ if not isinstance(target, cst.Name) or target.value != "dependencies":
40
+ continue
41
+ value = assignment.value
42
+ if not isinstance(value, cst.Tuple) or any(
43
+ isinstance(item, cst.StarredElement) for item in value.elements
44
+ ):
45
+ raise CommandError(
46
+ "Startup dependencies must be an explicit tuple without unpacking"
47
+ )
48
+ self.matches += 1
49
+ replacement = value.with_changes(
50
+ elements=(*value.elements, cst.Element(cst.Name(self._dependency)))
51
+ )
52
+ statements[index] = statement.with_changes(
53
+ body=(assignment.with_changes(value=replacement),)
54
+ )
55
+ return updated_node.with_changes(body=body.with_changes(body=statements))
56
+
57
+
58
+ def add_dependency(source: str, startup: str, import_path: str, dependency: str) -> str:
59
+ wrapper = MetadataWrapper(cst.parse_module(source))
60
+ module = wrapper.module
61
+ # 星号导出的绑定无法由静态 scope 确定,不能冒险覆盖用户的既有依赖。
62
+ if matchers.findall(module, matchers.ImportStar()):
63
+ raise CommandError("Star imports prevent safe startup dependency wiring")
64
+ # 原生作用域绑定防止覆盖用户符号;仅修改实际顶层节点,不触及嵌套同名类。
65
+ candidates = tuple(
66
+ statement
67
+ for statement in module.body
68
+ if isinstance(statement, cst.ClassDef) and statement.name.value == startup
69
+ )
70
+ if len(candidates) != 1:
71
+ raise CommandError("Startup Module must be a unique top-level class")
72
+ candidate = candidates[0]
73
+ if not isinstance(candidate.body, cst.IndentedBlock):
74
+ raise CommandError("Startup Module must use an indented class body")
75
+ # 类体的名称查找包含本类和外层模块;两层绑定均不得被新增裸名称遮蔽。
76
+ scope = cast(ClassScope, wrapper.resolve(ScopeProvider)[candidate.body.body[0]])
77
+ if dependency in scope:
78
+ raise CommandError(f"Dependency name already bound in startup module: {dependency}")
79
+ transformer = _AddDependency(candidate, dependency)
80
+ updated = module.visit(transformer)
81
+ if transformer.matches != 1:
82
+ raise CommandError("Cannot uniquely identify the startup Module dependencies assignment")
83
+ context = CodemodContext()
84
+ AddImportsVisitor.add_needed_import(context, import_path, dependency)
85
+ return AddImportsVisitor(context).transform_module(updated).code
@@ -0,0 +1,23 @@
1
+ """Diagnostic contracts 与 Application-owned journal 的 capability 边界。"""
2
+
3
+ from .model import (
4
+ DiagnosticMessage,
5
+ DiagnosticSnapshot,
6
+ EventStatus,
7
+ FailureRecord,
8
+ LifecyclePhase,
9
+ LifecycleStage,
10
+ PhaseEvent,
11
+ SourceLocation,
12
+ )
13
+
14
+ __all__ = (
15
+ "DiagnosticMessage",
16
+ "DiagnosticSnapshot",
17
+ "EventStatus",
18
+ "FailureRecord",
19
+ "LifecyclePhase",
20
+ "LifecycleStage",
21
+ "PhaseEvent",
22
+ "SourceLocation",
23
+ )
@@ -0,0 +1,174 @@
1
+ """单个 Application 拥有的 append-only diagnostic journal。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, replace
6
+ from time import perf_counter_ns
7
+ from uuid import UUID, uuid4
8
+
9
+ from ..errors import InvalidDiagnosticCodeError
10
+ from .model import (
11
+ DiagnosticMessage,
12
+ DiagnosticSnapshot,
13
+ EventStatus,
14
+ FailureRecord,
15
+ LifecyclePhase,
16
+ LifecycleStage,
17
+ PhaseEvent,
18
+ SourceLocation,
19
+ )
20
+
21
+ _DIAGNOSTIC_CODE_PUNCTUATION = frozenset("._-")
22
+ _MAX_DIAGNOSTIC_CODE_LENGTH = 128
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class _ActiveEvent:
27
+ phase: LifecyclePhase
28
+ stage: LifecycleStage
29
+ subject: str
30
+ source: SourceLocation | None
31
+ started_ns: int
32
+
33
+
34
+ class _DiagnosticJournal:
35
+ """Application-owned journal;外部只能取得不可变的时点快照。"""
36
+
37
+ __slots__ = ("_events", "_failure_order", "_failures", "_messages", "_sequence")
38
+
39
+ def __init__(self) -> None:
40
+ self._sequence = 0
41
+ self._events: list[PhaseEvent] = []
42
+ self._messages: list[DiagnosticMessage] = []
43
+ self._failure_order: list[UUID] = []
44
+ self._failures: dict[UUID, FailureRecord] = {}
45
+
46
+ def _next_sequence(self) -> int:
47
+ self._sequence += 1
48
+ return self._sequence
49
+
50
+ def start_event(
51
+ self,
52
+ *,
53
+ phase: LifecyclePhase,
54
+ stage: LifecycleStage,
55
+ subject: str,
56
+ source: SourceLocation | None,
57
+ ) -> _ActiveEvent:
58
+ started_ns = perf_counter_ns()
59
+ active_event = _ActiveEvent(phase, stage, subject, source, started_ns)
60
+ self._events.append(
61
+ PhaseEvent(
62
+ sequence=self._next_sequence(),
63
+ phase=phase,
64
+ stage=stage,
65
+ subject=subject,
66
+ status=EventStatus.STARTED,
67
+ started_monotonic_ns=started_ns,
68
+ duration_ns=None,
69
+ source=source,
70
+ failure_id=None,
71
+ )
72
+ )
73
+ return active_event
74
+
75
+ def finish_event(
76
+ self,
77
+ active_event: _ActiveEvent,
78
+ *,
79
+ status: EventStatus,
80
+ failure_id: UUID | None,
81
+ ) -> int:
82
+ # start 返回的 value object 是一次事件的唯一事实,finish 不重复传递上下文。
83
+ duration_ns = perf_counter_ns() - active_event.started_ns
84
+ self._events.append(
85
+ PhaseEvent(
86
+ sequence=self._next_sequence(),
87
+ phase=active_event.phase,
88
+ stage=active_event.stage,
89
+ subject=active_event.subject,
90
+ status=status,
91
+ started_monotonic_ns=active_event.started_ns,
92
+ duration_ns=duration_ns,
93
+ source=active_event.source,
94
+ failure_id=failure_id,
95
+ )
96
+ )
97
+ return duration_ns
98
+
99
+ def begin_failure(
100
+ self,
101
+ *,
102
+ error_type: str,
103
+ phase: LifecyclePhase,
104
+ stage: LifecycleStage,
105
+ subject: str | None,
106
+ message: str,
107
+ source: SourceLocation | None,
108
+ dependency_path: tuple[str, ...],
109
+ ) -> UUID:
110
+ failure_id = uuid4()
111
+ # 先发布不可变记录,rollback 后替换 cleanup 关联,snapshot 不出现悬空引用。
112
+ failure = FailureRecord(
113
+ id=failure_id,
114
+ error_type=error_type,
115
+ phase=phase,
116
+ stage=stage,
117
+ subject=subject,
118
+ message=message,
119
+ source=source,
120
+ dependency_path=dependency_path,
121
+ cleanup_failure_ids=(),
122
+ )
123
+ self._failure_order.append(failure_id)
124
+ self._failures[failure_id] = failure
125
+ return failure_id
126
+
127
+ def complete_failure(
128
+ self,
129
+ failure_id: UUID,
130
+ *,
131
+ cleanup_failure_ids: tuple[UUID, ...] = (),
132
+ ) -> None:
133
+ self._failures[failure_id] = replace(
134
+ self._failures[failure_id], cleanup_failure_ids=cleanup_failure_ids
135
+ )
136
+
137
+ def emit(
138
+ self,
139
+ *,
140
+ phase: LifecyclePhase,
141
+ stage: LifecycleStage,
142
+ subject: str,
143
+ code: str,
144
+ source: SourceLocation | None,
145
+ ) -> None:
146
+ if not _is_diagnostic_code(code):
147
+ raise InvalidDiagnosticCodeError(code, max_length=_MAX_DIAGNOSTIC_CODE_LENGTH)
148
+ self._messages.append(
149
+ DiagnosticMessage(
150
+ sequence=self._next_sequence(),
151
+ phase=phase,
152
+ stage=stage,
153
+ subject=subject,
154
+ code=code,
155
+ source=source,
156
+ )
157
+ )
158
+
159
+ def snapshot(self, application_id: UUID) -> DiagnosticSnapshot:
160
+ failures = tuple(self._failures[failure_id] for failure_id in self._failure_order)
161
+ return DiagnosticSnapshot(
162
+ application_id=application_id,
163
+ events=tuple(self._events),
164
+ failures=failures,
165
+ messages=tuple(self._messages),
166
+ )
167
+
168
+
169
+ def _is_diagnostic_code(code: str) -> bool:
170
+ if not code or len(code) > _MAX_DIAGNOSTIC_CODE_LENGTH or not code[0].isalpha():
171
+ return False
172
+ return all(
173
+ character.isalnum() or character in _DIAGNOSTIC_CODE_PUNCTUATION for character in code
174
+ )
@@ -0,0 +1,83 @@
1
+ """不可变 diagnostic contract;不拥有 journal mutation。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from uuid import UUID
8
+
9
+
10
+ class LifecyclePhase(str, Enum):
11
+ COMPOSITION = "composition"
12
+ RUNTIME = "runtime"
13
+ CLEANUP = "cleanup"
14
+
15
+
16
+ class LifecycleStage(str, Enum):
17
+ PRE_CONFIGURE = "pre_configure"
18
+ CONFIGURE = "configure"
19
+ POST_CONFIGURE = "post_configure"
20
+ PRE_INITIALIZE = "pre_initialize"
21
+ INITIALIZE = "initialize"
22
+ POST_INITIALIZE = "post_initialize"
23
+ SHUTDOWN = "shutdown"
24
+
25
+
26
+ class EventStatus(str, Enum):
27
+ STARTED = "started"
28
+ SUCCEEDED = "succeeded"
29
+ FAILED = "failed"
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class SourceLocation:
34
+ owner: str
35
+ file: str | None
36
+ line: int | None
37
+ symbol: str | None
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class PhaseEvent:
42
+ sequence: int
43
+ phase: LifecyclePhase
44
+ stage: LifecycleStage
45
+ subject: str
46
+ status: EventStatus
47
+ started_monotonic_ns: int
48
+ duration_ns: int | None
49
+ source: SourceLocation | None
50
+ failure_id: UUID | None
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class FailureRecord:
55
+ id: UUID
56
+ error_type: str
57
+ phase: LifecyclePhase
58
+ stage: LifecycleStage
59
+ subject: str | None
60
+ message: str
61
+ source: SourceLocation | None
62
+ dependency_path: tuple[str, ...]
63
+ cleanup_failure_ids: tuple[UUID, ...]
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class DiagnosticMessage:
68
+ sequence: int
69
+ phase: LifecyclePhase
70
+ stage: LifecycleStage
71
+ subject: str
72
+ code: str
73
+ source: SourceLocation | None
74
+
75
+
76
+ @dataclass(frozen=True, slots=True)
77
+ class DiagnosticSnapshot:
78
+ """一次读取产生的不可变诊断视图,不随后续 lifecycle 变化。"""
79
+
80
+ application_id: UUID
81
+ events: tuple[PhaseEvent, ...]
82
+ failures: tuple[FailureRecord, ...]
83
+ messages: tuple[DiagnosticMessage, ...]
@@ -0,0 +1,25 @@
1
+ """贡献诊断的调用位置;depth 由公开贡献入口指定。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+
7
+ from .model import SourceLocation
8
+
9
+
10
+ def caller_source(owner: str, *, depth: int) -> SourceLocation:
11
+ frame = inspect.currentframe()
12
+ try:
13
+ for _ in range(depth):
14
+ frame = None if frame is None else frame.f_back
15
+ if frame is None:
16
+ return SourceLocation(owner=owner, file=None, line=None, symbol=None)
17
+ return SourceLocation(
18
+ owner=owner,
19
+ file=frame.f_code.co_filename,
20
+ line=frame.f_lineno,
21
+ symbol=frame.f_code.co_name,
22
+ )
23
+ finally:
24
+ # 不让 frame 引用延长 Module/composition 对象的生命周期。
25
+ del frame
@@ -0,0 +1,6 @@
1
+ """Provider-neutral 业务互斥与租约失败契约。"""
2
+
3
+ from .contracts import DistributedLock, DistributedLockError
4
+ from .options import DistributedLockOptions
5
+
6
+ __all__ = ("DistributedLock", "DistributedLockError", "DistributedLockOptions")
@@ -0,0 +1,20 @@
1
+ """锁的业务使用与错误边界;provider 不进入稳定契约。"""
2
+
3
+ from contextlib import AbstractAsyncContextManager
4
+ from typing import Protocol
5
+
6
+ from ..errors.base import FrameworkError
7
+
8
+
9
+ class DistributedLock(Protocol):
10
+ def acquire(self, key: str, /) -> AbstractAsyncContextManager[bool]:
11
+ """进入后返回是否取得锁;持有者必须配合取消并在退出上下文前完成清理。"""
12
+ ...
13
+
14
+
15
+ class DistributedLockError(FrameworkError):
16
+ def __init__(self, *, key: str, operation: str, error_type: str) -> None:
17
+ self.key = key
18
+ self.operation = operation
19
+ self.error_type = error_type
20
+ super().__init__(f"Distributed lock {key!r} {operation} failed with {error_type}")
@@ -0,0 +1,19 @@
1
+ """获取等待和租约时序的唯一默认值来源。"""
2
+
3
+ from datetime import timedelta
4
+
5
+ from pydantic import Field, model_validator
6
+
7
+ from ..options import BaseOptions
8
+
9
+
10
+ class DistributedLockOptions(BaseOptions):
11
+ wait_timeout: timedelta = Field(default=timedelta(seconds=5), ge=timedelta(0))
12
+ lease_timeout: timedelta = Field(default=timedelta(seconds=30), ge=timedelta(milliseconds=1))
13
+ renewal_interval: timedelta = Field(default=timedelta(seconds=10), ge=timedelta(milliseconds=1))
14
+
15
+ @model_validator(mode="after")
16
+ def _validate_renewal(self) -> "DistributedLockOptions":
17
+ if self.renewal_interval >= self.lease_timeout:
18
+ raise ValueError("renewal_interval must be shorter than lease_timeout")
19
+ return self
@@ -0,0 +1,13 @@
1
+ """Provider-neutral DDD foundations owned by Python Platform."""
2
+
3
+ from .aggregates import AggregateRoot
4
+ from .entities import Entity
5
+ from .errors import OptimisticConcurrencyError
6
+ from .value_objects import ValueObject
7
+
8
+ __all__ = (
9
+ "AggregateRoot",
10
+ "Entity",
11
+ "OptimisticConcurrencyError",
12
+ "ValueObject",
13
+ )
@@ -0,0 +1,64 @@
1
+ """Aggregate identity, version and pending local-event ownership."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import is_dataclass
6
+ from typing import Generic, TypeVar
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from .entities import Entity
11
+ from .errors import OptimisticConcurrencyError
12
+
13
+ _TId = TypeVar("_TId")
14
+
15
+
16
+ class AggregateRoot(Entity[_TId], Generic[_TId]):
17
+ """一个 invocation 内聚合版本和待提交事件的唯一 mutable owner。"""
18
+
19
+ __slots__ = ("_local_events", "_version", "_version_staged")
20
+
21
+ def __init__(self, entity_id: _TId, *, version: int = 0) -> None:
22
+ super().__init__(entity_id)
23
+ if not isinstance(version, int) or isinstance(version, bool) or version < 0:
24
+ raise ValueError("aggregate version must be a non-negative integer")
25
+ self._version = version
26
+ self._version_staged = False
27
+ self._local_events: list[object] = []
28
+
29
+ @property
30
+ def version(self) -> int:
31
+ return self._version
32
+
33
+ def raise_local_event(self, event: object) -> None:
34
+ if not _is_immutable_event(event):
35
+ raise TypeError("aggregate local events must be immutable dataclass or Pydantic values")
36
+ self._local_events.append(event)
37
+
38
+ def release_local_events(self) -> tuple[object, ...]:
39
+ events = tuple(self._local_events)
40
+ self._local_events.clear()
41
+ return events
42
+
43
+ def stage_next_version(self, expected_version: int) -> int:
44
+ """Repository SPI:一次聚合实例只允许暂存一个下一版本。"""
45
+
46
+ if (
47
+ not isinstance(expected_version, int)
48
+ or isinstance(expected_version, bool)
49
+ or expected_version < 0
50
+ ):
51
+ raise ValueError("expected_version must be a non-negative integer")
52
+ if self._version_staged or self._version != expected_version:
53
+ raise OptimisticConcurrencyError
54
+ self._version += 1
55
+ self._version_staged = True
56
+ return self._version
57
+
58
+
59
+ def _is_immutable_event(event: object) -> bool:
60
+ event_type = type(event)
61
+ dataclass_parameters = getattr(event_type, "__dataclass_params__", None)
62
+ if is_dataclass(event) and bool(getattr(dataclass_parameters, "frozen", False)):
63
+ return True
64
+ return isinstance(event, BaseModel) and event.model_config.get("frozen") is True
@@ -0,0 +1,33 @@
1
+ """Entity identity semantics independent from persistence providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Generic, TypeVar
6
+
7
+ _TId = TypeVar("_TId")
8
+
9
+
10
+ class Entity(Generic[_TId]):
11
+ """Identity owner whose identifier cannot change after construction."""
12
+
13
+ __slots__ = ("_id",)
14
+
15
+ def __init__(self, entity_id: _TId) -> None:
16
+ if entity_id is None:
17
+ raise ValueError("entity id cannot be None")
18
+ self._id = entity_id
19
+
20
+ @property
21
+ def id(self) -> _TId:
22
+ return self._id
23
+
24
+ def __eq__(self, other: object) -> bool:
25
+ if self is other:
26
+ return True
27
+ if type(self) is not type(other):
28
+ return False
29
+ return self.id == other.id
30
+
31
+ def __hash__(self) -> int:
32
+ # 类型是 identity 的一部分,避免不同聚合偶然共用相同标识时发生碰撞语义。
33
+ return hash((type(self), self.id))
@@ -0,0 +1,12 @@
1
+ """DDD boundary failures safe for transport mapping."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..errors import ResourceConflictError
6
+
7
+
8
+ class OptimisticConcurrencyError(ResourceConflictError):
9
+ """调用方版本与持久化 authority 不一致;不携带真实行版本或 SQL。"""
10
+
11
+ def __init__(self) -> None:
12
+ super().__init__("Aggregate version conflict")