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,65 @@
1
+ """Configuration source loading 与 merge 的 Application composition owner。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import replace
7
+ from pathlib import Path
8
+ from typing import cast
9
+
10
+ from .contracts import _ConfigurationDefaultContribution
11
+ from .merge import _ConfigurationMerger
12
+ from .root import ConfigurationRoot
13
+ from .sources import _ConfigurationSourceLoader
14
+ from .values import freeze_value, normalize_mapping
15
+
16
+
17
+ class _ConfigurationComposer:
18
+ """组合已验证参数,不暴露 source 或 mutable merge state 给 Application。"""
19
+
20
+ __slots__ = ("_loader",)
21
+
22
+ def __init__(
23
+ self,
24
+ *,
25
+ environment: str,
26
+ base_path: Path,
27
+ env_prefix: str,
28
+ dotenv_path: Path | None,
29
+ explicit_overrides: Mapping[str, object],
30
+ cli_args: tuple[str, ...],
31
+ ) -> None:
32
+ self._loader = _ConfigurationSourceLoader(
33
+ environment=environment,
34
+ base_path=base_path,
35
+ env_prefix=env_prefix,
36
+ dotenv_path=dotenv_path,
37
+ explicit_overrides=explicit_overrides,
38
+ cli_args=cli_args,
39
+ )
40
+
41
+ def build(
42
+ self,
43
+ defaults: tuple[_ConfigurationDefaultContribution, ...],
44
+ ) -> ConfigurationRoot:
45
+ payloads, diagnostics = self._loader.load(defaults)
46
+ frozen_payloads = tuple(
47
+ replace(payload, values=cast(Mapping[str, object], freeze_value(payload.values)))
48
+ for payload in payloads
49
+ )
50
+ initial = _ConfigurationMerger().merge(
51
+ payloads=tuple(
52
+ replace(payload, values=normalize_mapping(payload.values, pending=True))
53
+ for payload in frozen_payloads
54
+ ),
55
+ diagnostics=diagnostics,
56
+ secret_paths=frozenset(key for payload in payloads for key in payload.values),
57
+ )
58
+ # 临时配置视图用于 composition;类型明确后只重解释此快照,绝不重读来源。
59
+ return ConfigurationRoot(
60
+ tree=initial,
61
+ provenance=initial._provenance,
62
+ secret_paths=frozenset(initial),
63
+ sources=initial.sources,
64
+ payloads=frozen_payloads,
65
+ )
@@ -0,0 +1,68 @@
1
+ """Configuration source、provenance 与 diagnostic value contracts。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+
9
+ from ..diagnostics import SourceLocation
10
+
11
+
12
+ class ConfigurationSource(str, Enum):
13
+ MODULE_DEFAULTS = "module-defaults"
14
+ YAML_BASE = "yaml-base"
15
+ YAML_ENVIRONMENT = "yaml-environment"
16
+ DOTENV = "dotenv"
17
+ OS_ENVIRONMENT = "os-environment"
18
+ CLI = "cli"
19
+ EXPLICIT = "explicit"
20
+
21
+
22
+ class ConfigurationSourceStatus(str, Enum):
23
+ LOADED = "loaded"
24
+ SKIPPED = "skipped"
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class ConfigurationSourceDiagnostic:
29
+ source: ConfigurationSource
30
+ status: ConfigurationSourceStatus
31
+ location: SourceLocation
32
+ reason: str | None = None
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class ConfigurationContribution:
37
+ source_id: ConfigurationSource
38
+ source_location: SourceLocation
39
+ raw_kind: str
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class ConfigurationProvenance:
44
+ path: str
45
+ chain: tuple[ConfigurationContribution, ...]
46
+ effective_source: ConfigurationContribution
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class ConfigurationValueDiagnostic:
51
+ path: str
52
+ value: object
53
+ redacted: bool
54
+ provenance: ConfigurationProvenance
55
+
56
+
57
+ @dataclass(frozen=True, slots=True)
58
+ class _ConfigurationDefaultContribution:
59
+ source_location: SourceLocation
60
+ values: Mapping[str, object]
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class _SourcePayload:
65
+ source: ConfigurationSource
66
+ location: SourceLocation
67
+ values: Mapping[str, object]
68
+ input_depth: int = 0
@@ -0,0 +1,51 @@
1
+ """dotenv 文件单次读取;语法预检与原生 value 解码消费同一份文本。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from io import StringIO
7
+ from pathlib import Path
8
+
9
+ from dotenv import dotenv_values
10
+ from dotenv.parser import parse_stream
11
+ from pydantic_settings import DotEnvSettingsSource
12
+ from pydantic_settings.sources.utils import parse_env_vars
13
+
14
+ from ..diagnostics import SourceLocation
15
+ from ..errors import ConfigurationSourceError
16
+ from .contracts import ConfigurationSource
17
+ from .environment_source import _LayeredEnvSource
18
+ from .input_shape import _environment_input_name
19
+
20
+
21
+ class _SnapshotDotEnvSource(DotEnvSettingsSource, _LayeredEnvSource):
22
+ def _read_env_file(self, file_path: Path) -> Mapping[str, str | None]:
23
+ text = file_path.read_text(encoding=self.env_file_encoding or "utf-8")
24
+ seen: set[str] = set()
25
+ for binding in parse_stream(StringIO(text)):
26
+ if binding.error:
27
+ raise ConfigurationSourceError(
28
+ source_id=ConfigurationSource.DOTENV.value,
29
+ location=SourceLocation(
30
+ owner=ConfigurationSource.DOTENV.value,
31
+ file=str(file_path),
32
+ line=binding.original.line,
33
+ symbol=None,
34
+ ),
35
+ original_error=ValueError("invalid dotenv syntax"),
36
+ )
37
+ key = binding.key
38
+ if key is not None and key.casefold().startswith(self.env_prefix.casefold()):
39
+ parts = _environment_input_name(key, self.env_prefix)
40
+ if parts in seen:
41
+ raise ValueError("duplicate dotenv path")
42
+ seen.add(parts)
43
+ return parse_env_vars(
44
+ {
45
+ _environment_input_name(key, self.env_prefix): value
46
+ for key, value in dotenv_values(stream=StringIO(text)).items()
47
+ },
48
+ True,
49
+ self.env_ignore_empty,
50
+ self.env_parse_none_str,
51
+ )
@@ -0,0 +1,38 @@
1
+ """原生环境 source 的读取期分层;保留 JSON 与嵌套变量的覆盖身份。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic.fields import FieldInfo
8
+ from pydantic_settings import EnvSettingsSource
9
+
10
+
11
+ class _LayeredEnvSource(EnvSettingsSource):
12
+ def __call__(self) -> dict[str, Any]:
13
+ self.input_layers: dict[int, dict[str, object]] = {}
14
+ return super().__call__()
15
+
16
+ def prepare_field_value(
17
+ self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool
18
+ ) -> Any:
19
+ snapshot = self.env_vars
20
+ delimiter = self.env_nested_delimiter
21
+ assert delimiter is not None
22
+ depths = sorted({name.count(delimiter) for name in snapshot})
23
+ prepared: object = None
24
+ try:
25
+ for depth in depths:
26
+ self.env_vars = {
27
+ name: raw for name, raw in snapshot.items() if name.count(delimiter) == depth
28
+ }
29
+ layer_value, _, complex_value = self.get_field_value(field, field_name)
30
+ decoded = super().prepare_field_value(field_name, field, layer_value, complex_value)
31
+ if decoded is not None:
32
+ self.input_layers.setdefault(depth, {})[field_name] = decoded
33
+ prepared = decoded
34
+ finally:
35
+ self.env_vars = snapshot
36
+ # __call__ 仍只执行一次;最终合并消费 input_layers,不消费这个原生返回视图。
37
+ # 这里不校验 Options,也不重读 OS 或 dotenv。
38
+ return prepared
@@ -0,0 +1,124 @@
1
+ """Configuration value 所有的 typed snapshot 规则,供 Options 与 Settings 复用。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping, MutableMapping, MutableSequence, MutableSet
6
+ from enum import Enum
7
+ from functools import cached_property
8
+ from types import MappingProxyType
9
+ from typing import Annotated, Any, Literal, cast, get_args, get_origin
10
+
11
+ from pydantic import BaseModel, Secret, SecretBytes, SecretStr
12
+
13
+ from .values import freeze_value, is_frozen_secret, is_supported_scalar
14
+
15
+
16
+ def validate_model_shape(model_type: type[BaseModel]) -> None:
17
+ # frozen 只保护字段赋值,不能保护 extra 和 PrivateAttr。
18
+ if model_type.model_config.get("frozen") is not True:
19
+ raise TypeError("snapshot models must be frozen")
20
+ if model_type.model_config.get("extra") == "allow":
21
+ raise TypeError("snapshot models must not allow mutable extra fields")
22
+ if model_type.__private_attributes__:
23
+ raise TypeError("snapshot models must not declare private attributes")
24
+ if any(
25
+ isinstance(member, cached_property)
26
+ for base in model_type.__mro__
27
+ for member in vars(base).values()
28
+ ):
29
+ # cached_property 可绕过 frozen 在读取时向 __dict__ 写入缓存,不能发布为共享默认值。
30
+ raise TypeError("snapshot models must not declare cached properties")
31
+
32
+
33
+ def validate_immutable_annotation(
34
+ annotation: object, seen: frozenset[object] = frozenset()
35
+ ) -> None:
36
+ if annotation in seen:
37
+ return
38
+ origin = get_origin(annotation)
39
+ if annotation in (Any, object):
40
+ raise TypeError("snapshot annotations must not use Any/object")
41
+ mutable = (list, dict, set, bytearray, MutableMapping, MutableSequence, MutableSet)
42
+ if annotation in mutable or origin in mutable:
43
+ raise TypeError("snapshot annotations must not use mutable containers")
44
+ if isinstance(annotation, type) and issubclass(annotation, BaseModel):
45
+ validate_model_shape(annotation)
46
+ for field in annotation.model_fields.values():
47
+ validate_immutable_annotation(field.annotation, seen | {annotation})
48
+ elif isinstance(annotation, type) and issubclass(annotation, Enum):
49
+ if any(not is_supported_scalar(member) for member in annotation):
50
+ raise TypeError("snapshot enum members must be deeply immutable")
51
+ elif origin is Annotated:
52
+ validate_immutable_annotation(get_args(annotation)[0], seen)
53
+ elif origin is not Literal:
54
+ _validate_annotation_arguments(get_args(annotation), seen)
55
+
56
+
57
+ def _validate_annotation_arguments(arguments: tuple[object, ...], seen: frozenset[object]) -> None:
58
+ for argument in arguments:
59
+ if argument is not Ellipsis:
60
+ validate_immutable_annotation(argument, seen)
61
+
62
+
63
+ def freeze_typed_value(value: object, *, allow_secrets: bool = True) -> object:
64
+ if isinstance(value, (Secret, SecretStr, SecretBytes)):
65
+ if not allow_secrets:
66
+ raise TypeError("runtime settings cannot contain secrets")
67
+ return freeze_value(value)
68
+ if isinstance(value, BaseModel):
69
+ return _freeze_model(value, allow_secrets=allow_secrets)
70
+ if isinstance(value, Mapping):
71
+ return MappingProxyType(
72
+ {
73
+ freeze_typed_value(key, allow_secrets=allow_secrets): freeze_typed_value(
74
+ item, allow_secrets=allow_secrets
75
+ )
76
+ for key, item in value.items()
77
+ }
78
+ )
79
+ if isinstance(value, tuple):
80
+ items = tuple(freeze_typed_value(item, allow_secrets=allow_secrets) for item in value)
81
+ if type(value) is tuple:
82
+ return items
83
+ fields = getattr(type(value), "_fields", None)
84
+ if isinstance(fields, tuple) and not hasattr(value, "__dict__"):
85
+ return cast(Callable[..., tuple[object, ...]], type(value))(*items)
86
+ raise TypeError("unsupported tuple subtype")
87
+ if isinstance(value, frozenset):
88
+ return frozenset(freeze_typed_value(item, allow_secrets=allow_secrets) for item in value)
89
+ if is_supported_scalar(value) or (allow_secrets and is_frozen_secret(value)):
90
+ return value
91
+ raise TypeError(f"validated object graph contains unsupported {type(value).__name__}")
92
+
93
+
94
+ def _freeze_model(value: BaseModel, *, allow_secrets: bool) -> BaseModel:
95
+ validate_model_shape(type(value))
96
+ # model_copy 会保留 __dict__ 中的额外状态,因此复制前必须验证实例边界。
97
+ if set(vars(value)) - type(value).model_fields.keys():
98
+ raise TypeError("snapshot models must not contain undeclared instance state")
99
+ updates = {
100
+ name: freeze_typed_value(getattr(value, name), allow_secrets=allow_secrets)
101
+ for name in type(value).model_fields
102
+ }
103
+ return value.model_copy(update=updates)
104
+
105
+
106
+ def _serialization_value(value: object) -> object:
107
+ """仅在原生 serializer 边界复制冻结容器,不发布这个可变副本。"""
108
+ if isinstance(value, BaseModel):
109
+ return value.model_copy(
110
+ update={
111
+ name: _serialization_value(getattr(value, name))
112
+ for name in type(value).model_fields
113
+ }
114
+ )
115
+ if isinstance(value, Mapping):
116
+ return {key: _serialization_value(item) for key, item in value.items()}
117
+ if isinstance(value, tuple):
118
+ items = tuple(_serialization_value(item) for item in value)
119
+ if type(value) is tuple:
120
+ return items
121
+ return cast(Callable[..., tuple[object, ...]], type(value))(*items)
122
+ if isinstance(value, frozenset):
123
+ return frozenset(_serialization_value(item) for item in value)
124
+ return value
@@ -0,0 +1,58 @@
1
+ """从实际输入路径构造原生 source 的读取形状,不持有 Options schema。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import TypeAlias
7
+
8
+ from pydantic import BaseModel
9
+ from pydantic_settings import BaseSettings
10
+
11
+ from .values import canonical_segment
12
+
13
+ _PathTree: TypeAlias = dict[str, "_PathTree"]
14
+
15
+
16
+ def _environment_input_name(name: str, prefix: str) -> str:
17
+ if not name.casefold().startswith(prefix.casefold()):
18
+ return name
19
+ first, separator, tail = name[len(prefix) :].partition("__")
20
+ # section 是已知字段;后续路径的类型尚未声明,不能提前折叠 Mapping 的数据键。
21
+ return prefix.casefold() + canonical_segment(first) + separator + tail
22
+
23
+
24
+ def _input_sections(names: Iterable[str], prefix: str) -> frozenset[str]:
25
+ return frozenset(
26
+ canonical_segment(name[len(prefix) :].split("__")[0])
27
+ for name in names
28
+ if name.casefold().startswith(prefix.casefold())
29
+ )
30
+
31
+
32
+ def _cli_settings_type(args: tuple[str, ...]) -> type[BaseSettings]:
33
+ paths: _PathTree = {}
34
+ seen: set[tuple[str, ...]] = set()
35
+ for argument in args:
36
+ if argument in ("-h", "--help"):
37
+ # Builder 只接收应用配置参数;Host 处理 help,原生 argparse 不得退出宿主进程。
38
+ raise ValueError("help flags belong to the Host")
39
+ if not argument.startswith("--"):
40
+ continue
41
+ parts = tuple(canonical_segment(part) for part in argument[2:].split("=", 1)[0].split("."))
42
+ # 原生 CLI source 会合并对象或覆盖叶子;平台在解码前拒绝同一路径的重复声明。
43
+ if parts in seen:
44
+ raise ValueError("duplicate canonical CLI path")
45
+ seen.add(parts)
46
+ node = paths
47
+ for part in parts:
48
+ node = node.setdefault(part, {})
49
+ annotations = {key: _shape_annotation(value) for key, value in paths.items()}
50
+ return type("_ApplicationCliSettings", (BaseSettings,), {"__annotations__": annotations})
51
+
52
+
53
+ def _shape_annotation(value: object) -> object:
54
+ if isinstance(value, dict) and value:
55
+ annotations = {key: _shape_annotation(child) for key, child in value.items()}
56
+ return type("_CliSection", (BaseModel,), {"__annotations__": annotations})
57
+ # Union 保留普通字符串,同时让原生 source 解码 JSON mapping/sequence。
58
+ return dict[str, object] | list[object] | str
@@ -0,0 +1,103 @@
1
+ """Configuration precedence、provenance chain 与 immutable root 发布。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+
7
+ from .contracts import (
8
+ ConfigurationContribution,
9
+ ConfigurationProvenance,
10
+ ConfigurationSource,
11
+ ConfigurationSourceDiagnostic,
12
+ _SourcePayload,
13
+ )
14
+ from .root import ConfigurationRoot
15
+ from .values import MISSING, canonical_path, raw_kind
16
+
17
+
18
+ class _ConfigurationMerger:
19
+ """拥有一次 merge 的 mutable working tree;完成后只发布 immutable root。"""
20
+
21
+ __slots__ = ("_chains", "_merged")
22
+
23
+ def __init__(self) -> None:
24
+ self._merged: dict[str, object] = {}
25
+ self._chains: dict[tuple[str, ...], list[ConfigurationContribution]] = {}
26
+
27
+ def merge(
28
+ self,
29
+ *,
30
+ payloads: tuple[_SourcePayload, ...],
31
+ diagnostics: tuple[ConfigurationSourceDiagnostic, ...],
32
+ secret_paths: frozenset[str],
33
+ ) -> ConfigurationRoot:
34
+ # Enum declaration 是唯一 priority metadata;source 调用顺序不参与覆盖裁决。
35
+ for payload in sorted(
36
+ payloads, key=lambda item: (_source_ordinal(item.source), item.input_depth)
37
+ ):
38
+ self._merge_mapping(self._merged, payload.values, prefix=(), payload=payload)
39
+ provenance = {
40
+ path: ConfigurationProvenance(
41
+ path=".".join(path),
42
+ chain=tuple(chain),
43
+ effective_source=chain[-1],
44
+ )
45
+ for path, chain in self._chains.items()
46
+ }
47
+ canonical_secrets = frozenset(".".join(canonical_path(path)) for path in secret_paths)
48
+ return ConfigurationRoot(
49
+ tree=self._merged,
50
+ provenance=provenance,
51
+ secret_paths=canonical_secrets,
52
+ sources=tuple(sorted(diagnostics, key=lambda item: _source_ordinal(item.source))),
53
+ )
54
+
55
+ def _merge_mapping(
56
+ self,
57
+ target: dict[str, object],
58
+ incoming: Mapping[str, object],
59
+ *,
60
+ prefix: tuple[str, ...],
61
+ payload: _SourcePayload,
62
+ ) -> None:
63
+ for key, incoming_value in incoming.items():
64
+ path_parts = (*prefix, key)
65
+ path = path_parts
66
+ contribution = ConfigurationContribution(
67
+ source_id=payload.source,
68
+ source_location=payload.location,
69
+ raw_kind=raw_kind(incoming_value),
70
+ )
71
+ current = target.get(key, MISSING)
72
+ if isinstance(current, dict) and isinstance(incoming_value, Mapping):
73
+ self._chains.setdefault(path, []).append(contribution)
74
+ self._merge_mapping(
75
+ current,
76
+ incoming_value,
77
+ prefix=path_parts,
78
+ payload=payload,
79
+ )
80
+ continue
81
+
82
+ self._remove_descendant_chains(path)
83
+ self._chains.setdefault(path, []).append(contribution)
84
+ if isinstance(incoming_value, Mapping):
85
+ nested: dict[str, object] = {}
86
+ target[key] = nested
87
+ self._merge_mapping(
88
+ nested,
89
+ incoming_value,
90
+ prefix=path_parts,
91
+ payload=payload,
92
+ )
93
+ else:
94
+ target[key] = incoming_value
95
+
96
+ def _remove_descendant_chains(self, path: tuple[str, ...]) -> None:
97
+ for descendant in tuple(self._chains):
98
+ if len(descendant) > len(path) and descendant[: len(path)] == path:
99
+ del self._chains[descendant]
100
+
101
+
102
+ def _source_ordinal(source: ConfigurationSource) -> int:
103
+ return tuple(ConfigurationSource).index(source)