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,519 @@
1
+ """Pydantic Settings native sources 的受控读取与输入边界验证。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Iterable, Mapping
7
+ from pathlib import Path
8
+ from typing import Final, NoReturn, cast, get_origin
9
+
10
+ from pydantic_settings import (
11
+ BaseSettings,
12
+ CliSettingsSource,
13
+ InitSettingsSource,
14
+ PydanticBaseSettingsSource,
15
+ SettingsConfigDict,
16
+ )
17
+ from pydantic_settings.sources.utils import parse_env_vars
18
+
19
+ from ..diagnostics import SourceLocation
20
+ from ..errors import ConfigurationSourceError
21
+ from .contracts import (
22
+ ConfigurationSource,
23
+ ConfigurationSourceDiagnostic,
24
+ ConfigurationSourceStatus,
25
+ _ConfigurationDefaultContribution,
26
+ _SourcePayload,
27
+ )
28
+ from .dotenv_source import _SnapshotDotEnvSource
29
+ from .environment_source import _LayeredEnvSource
30
+ from .input_shape import _cli_settings_type, _environment_input_name, _input_sections
31
+ from .values import _capture_input_mapping, canonical_path, canonical_segment, is_supported_scalar
32
+ from .yaml_source import _YamlConfigurationSource
33
+
34
+ _ENVIRONMENT_NESTED_DELIMITER: Final = "__"
35
+
36
+
37
+ class _NativeConfigurationSettings(BaseSettings):
38
+ # Source 只负责读取/解码;最终 schema validation 归具体 Options model。
39
+ model_config = SettingsConfigDict(extra="ignore")
40
+
41
+
42
+ class _SnapshotEnvSource(_LayeredEnvSource):
43
+ def _load_env_vars(self) -> Mapping[str, str | None]:
44
+ # 必须在原生大小写归一化丢失重复 key 前校验;校验和解码共享同一次原始读取。
45
+ snapshot = dict(os.environ)
46
+ inputs: dict[str, str] = {}
47
+ for name, value in snapshot.items():
48
+ key = _environment_input_name(name, self.env_prefix)
49
+ if key in inputs:
50
+ raise ValueError("duplicate canonical environment path")
51
+ inputs[key] = value
52
+ return parse_env_vars(inputs, True, self.env_ignore_empty, self.env_parse_none_str)
53
+
54
+
55
+ class _ConfigurationSourceLoader:
56
+ """拥有一次 build 的 source 选择、动态 settings shape 与读取顺序。"""
57
+
58
+ __slots__ = (
59
+ "_base_path",
60
+ "_cli_args",
61
+ "_dotenv_path",
62
+ "_env_prefix",
63
+ "_environment",
64
+ "_explicit_overrides",
65
+ )
66
+
67
+ def __init__(
68
+ self,
69
+ *,
70
+ environment: str,
71
+ base_path: Path,
72
+ env_prefix: str,
73
+ dotenv_path: Path | None,
74
+ explicit_overrides: Mapping[str, object],
75
+ cli_args: tuple[str, ...],
76
+ ) -> None:
77
+ self._environment = environment
78
+ self._base_path = base_path
79
+ self._env_prefix = env_prefix
80
+ self._dotenv_path = dotenv_path
81
+ self._explicit_overrides = explicit_overrides
82
+ self._cli_args = cli_args
83
+
84
+ def load(
85
+ self,
86
+ defaults: tuple[_ConfigurationDefaultContribution, ...],
87
+ ) -> tuple[tuple[_SourcePayload, ...], tuple[ConfigurationSourceDiagnostic, ...]]:
88
+ failure: ConfigurationSourceError | None = None
89
+ try:
90
+ return self._load(defaults)
91
+ except ConfigurationSourceError as error:
92
+ failure = error
93
+ # 离开 except 后重建安全异常,断开原生 parser 的 input/context/traceback 引用。
94
+ assert failure is not None
95
+ raise ConfigurationSourceError(
96
+ source_id=failure.source_id,
97
+ location=failure.location,
98
+ original_error=(
99
+ FileNotFoundError("configuration file missing")
100
+ if isinstance(failure.original_error, FileNotFoundError)
101
+ else ValueError("configuration source rejected")
102
+ ),
103
+ )
104
+
105
+ def _load(
106
+ self, defaults: tuple[_ConfigurationDefaultContribution, ...]
107
+ ) -> tuple[tuple[_SourcePayload, ...], tuple[ConfigurationSourceDiagnostic, ...]]:
108
+ payloads = list(self._load_module_defaults(defaults))
109
+ diagnostics = [
110
+ _loaded_diagnostic(
111
+ ConfigurationSource.MODULE_DEFAULTS,
112
+ _source_location(ConfigurationSource.MODULE_DEFAULTS),
113
+ )
114
+ ]
115
+
116
+ self._append_optional_yaml(
117
+ payloads,
118
+ diagnostics,
119
+ ConfigurationSource.YAML_BASE,
120
+ self._base_path / "app.yaml",
121
+ )
122
+ self._append_optional_yaml(
123
+ payloads,
124
+ diagnostics,
125
+ ConfigurationSource.YAML_ENVIRONMENT,
126
+ self._base_path / f"app.{self._environment}.yaml",
127
+ )
128
+
129
+ explicit_preview = _normalize_with_boundary(
130
+ ConfigurationSource.EXPLICIT,
131
+ _source_location(ConfigurationSource.EXPLICIT),
132
+ self._explicit_overrides,
133
+ )
134
+ # 原生来源各读取一次。后续 Module 声明只能绑定此次输入快照。
135
+ try:
136
+ environment_source = _SnapshotEnvSource(
137
+ _NativeConfigurationSettings,
138
+ env_prefix=self._env_prefix,
139
+ env_nested_delimiter=_ENVIRONMENT_NESTED_DELIMITER,
140
+ )
141
+ except Exception as original_error:
142
+ _raise_source_error(
143
+ ConfigurationSource.OS_ENVIRONMENT,
144
+ _source_location(ConfigurationSource.OS_ENVIRONMENT),
145
+ original_error,
146
+ )
147
+ dotenv_source, dotenv_diagnostic = self._prepare_dotenv()
148
+ env_names = tuple(environment_source.env_vars)
149
+ dotenv_names = tuple(dotenv_source.env_vars) if dotenv_source is not None else ()
150
+ try:
151
+ sections = _input_sections((*env_names, *dotenv_names), self._env_prefix)
152
+ except (TypeError, ValueError) as original_error:
153
+ _raise_source_error(
154
+ ConfigurationSource.OS_ENVIRONMENT,
155
+ _source_location(ConfigurationSource.OS_ENVIRONMENT),
156
+ original_error,
157
+ )
158
+ settings_type = _build_settings_type(
159
+ payloads=tuple(payloads),
160
+ explicit_preview=explicit_preview,
161
+ known_sections=sections,
162
+ )
163
+ diagnostics.append(dotenv_diagnostic)
164
+ if dotenv_source is not None:
165
+ dotenv_source.settings_cls = settings_type
166
+ _validate_prefixed_names(
167
+ dotenv_names,
168
+ settings_type=settings_type,
169
+ env_prefix=self._env_prefix,
170
+ source_id=ConfigurationSource.DOTENV,
171
+ location=dotenv_diagnostic.location,
172
+ )
173
+ payloads.extend(
174
+ _environment_payloads(
175
+ ConfigurationSource.DOTENV, dotenv_diagnostic.location, dotenv_source
176
+ )
177
+ )
178
+
179
+ environment_payload = self._load_environment(settings_type, environment_source)
180
+ diagnostics.append(
181
+ _loaded_diagnostic(
182
+ ConfigurationSource.OS_ENVIRONMENT,
183
+ _source_location(ConfigurationSource.OS_ENVIRONMENT),
184
+ )
185
+ )
186
+ payloads.extend(environment_payload)
187
+
188
+ cli_location = _source_location(ConfigurationSource.CLI)
189
+ try:
190
+ cli_source: CliSettingsSource[BaseSettings] = CliSettingsSource(
191
+ _cli_settings_type(self._cli_args),
192
+ cli_parse_args=self._cli_args,
193
+ cli_exit_on_error=False,
194
+ )
195
+ except Exception as original_error:
196
+ _raise_source_error(ConfigurationSource.CLI, cli_location, original_error)
197
+ payloads.append(
198
+ _SourcePayload(
199
+ ConfigurationSource.CLI,
200
+ cli_location,
201
+ _call_native_source(ConfigurationSource.CLI, cli_location, cli_source),
202
+ )
203
+ )
204
+ diagnostics.append(_loaded_diagnostic(ConfigurationSource.CLI, cli_location))
205
+ explicit_payload = self._load_explicit(settings_type)
206
+ diagnostics.append(
207
+ _loaded_diagnostic(ConfigurationSource.EXPLICIT, explicit_payload.location)
208
+ )
209
+ payloads.append(explicit_payload)
210
+ return tuple(payloads), tuple(diagnostics)
211
+
212
+ @staticmethod
213
+ def _load_module_defaults(
214
+ defaults: tuple[_ConfigurationDefaultContribution, ...],
215
+ ) -> tuple[_SourcePayload, ...]:
216
+ payloads: list[_SourcePayload] = []
217
+ for contribution in defaults:
218
+ try:
219
+ _validate_immutable_module_default(contribution.values)
220
+ except (TypeError, ValueError) as original_error:
221
+ _raise_source_error(
222
+ ConfigurationSource.MODULE_DEFAULTS,
223
+ contribution.source_location,
224
+ original_error,
225
+ )
226
+ values = _normalize_with_boundary(
227
+ ConfigurationSource.MODULE_DEFAULTS,
228
+ contribution.source_location,
229
+ contribution.values,
230
+ )
231
+ payloads.append(
232
+ _SourcePayload(
233
+ source=ConfigurationSource.MODULE_DEFAULTS,
234
+ location=contribution.source_location,
235
+ values=values,
236
+ )
237
+ )
238
+ return tuple(payloads)
239
+
240
+ @staticmethod
241
+ def _append_optional_yaml(
242
+ payloads: list[_SourcePayload],
243
+ diagnostics: list[ConfigurationSourceDiagnostic],
244
+ source_id: ConfigurationSource,
245
+ path: Path,
246
+ ) -> None:
247
+ payload, diagnostic = _load_optional_yaml(source_id, path)
248
+ diagnostics.append(diagnostic)
249
+ if payload is not None:
250
+ payloads.append(payload)
251
+
252
+ def _prepare_dotenv(self) -> tuple[_SnapshotDotEnvSource | None, ConfigurationSourceDiagnostic]:
253
+ path, required, skipped_reason = self._select_dotenv()
254
+ source_id = ConfigurationSource.DOTENV
255
+ location = _source_location(source_id, file=path)
256
+ if path is None:
257
+ return None, _skipped_diagnostic(source_id, location, skipped_reason or "disabled")
258
+ if not path.exists():
259
+ if required:
260
+ _raise_source_error(source_id, location, FileNotFoundError())
261
+ return None, _skipped_diagnostic(source_id, location, "optional_missing")
262
+ try:
263
+ if not path.is_file():
264
+ raise ValueError("dotenv path must be a file")
265
+ source = _SnapshotDotEnvSource(
266
+ _NativeConfigurationSettings,
267
+ env_file=path,
268
+ env_file_encoding="utf-8",
269
+ dotenv_filtering="match_prefix",
270
+ env_prefix=self._env_prefix,
271
+ env_nested_delimiter=_ENVIRONMENT_NESTED_DELIMITER,
272
+ )
273
+ except ConfigurationSourceError:
274
+ raise
275
+ except Exception as original_error:
276
+ _raise_source_error(source_id, location, original_error)
277
+ return source, _loaded_diagnostic(source_id, location)
278
+
279
+ def _select_dotenv(self) -> tuple[Path | None, bool, str | None]:
280
+ if self._dotenv_path is not None:
281
+ return self._dotenv_path, True, None
282
+ if self._environment.casefold() == "development":
283
+ return self._base_path / ".env", False, None
284
+ return None, False, "disabled_for_environment"
285
+
286
+ def _load_environment(
287
+ self,
288
+ settings_type: type[BaseSettings],
289
+ source: _LayeredEnvSource,
290
+ ) -> tuple[_SourcePayload, ...]:
291
+ source_id = ConfigurationSource.OS_ENVIRONMENT
292
+ location = _source_location(source_id)
293
+ source.settings_cls = settings_type
294
+ _validate_prefixed_names(
295
+ source.env_vars,
296
+ settings_type=settings_type,
297
+ env_prefix=self._env_prefix,
298
+ source_id=source_id,
299
+ location=location,
300
+ )
301
+ return _environment_payloads(source_id, location, source)
302
+
303
+ def _load_explicit(self, settings_type: type[BaseSettings]) -> _SourcePayload:
304
+ source_id = ConfigurationSource.EXPLICIT
305
+ location = _source_location(source_id)
306
+ try:
307
+ source = InitSettingsSource(settings_type, dict(self._explicit_overrides))
308
+ except Exception as original_error:
309
+ _raise_source_error(source_id, location, original_error)
310
+ return _SourcePayload(
311
+ source_id,
312
+ location,
313
+ _call_native_source(source_id, location, source),
314
+ )
315
+
316
+
317
+ def _validate_immutable_module_default(value: object) -> None:
318
+ if isinstance(value, Mapping):
319
+ from types import MappingProxyType
320
+
321
+ if not isinstance(value, MappingProxyType):
322
+ raise TypeError("configuration_defaults mappings must be immutable MappingProxyType")
323
+ for key, item in value.items():
324
+ if not isinstance(key, str):
325
+ raise TypeError("configuration mapping keys must be strings")
326
+ _validate_immutable_module_default(item)
327
+ return
328
+ if isinstance(value, (tuple, frozenset)):
329
+ for item in value:
330
+ _validate_immutable_module_default(item)
331
+ return
332
+ if is_supported_scalar(value):
333
+ return
334
+ raise TypeError(f"configuration_defaults contains mutable or opaque {type(value).__name__}")
335
+
336
+
337
+ def _load_optional_yaml(
338
+ source_id: ConfigurationSource,
339
+ path: Path,
340
+ ) -> tuple[_SourcePayload | None, ConfigurationSourceDiagnostic]:
341
+ location = _source_location(source_id, file=path)
342
+ if not path.exists():
343
+ return None, _skipped_diagnostic(source_id, location, "optional_missing")
344
+ if not path.is_file():
345
+ _raise_source_error(source_id, location, ValueError("configuration path is not a file"))
346
+ try:
347
+ source = _YamlConfigurationSource(_NativeConfigurationSettings, yaml_file=path)
348
+ except Exception as original_error:
349
+ _raise_source_error(source_id, location, original_error)
350
+ values = _call_native_source(source_id, location, source)
351
+ return _SourcePayload(source_id, location, values), _loaded_diagnostic(source_id, location)
352
+
353
+
354
+ def _environment_payloads(
355
+ source_id: ConfigurationSource,
356
+ location: SourceLocation,
357
+ source: _LayeredEnvSource,
358
+ ) -> tuple[_SourcePayload, ...]:
359
+ # 同一次读取按嵌套深度保留覆盖层。原生 parser 仍负责解码;字段与数据键的
360
+ # 身份由晚声明的 Options 决定,不能在原生 deep_update 里提前覆盖或合并。
361
+ _call_native_source(source_id, location, source)
362
+ return tuple(
363
+ _SourcePayload(
364
+ source_id, location, _normalize_with_boundary(source_id, location, values), depth
365
+ )
366
+ for depth, values in sorted(source.input_layers.items())
367
+ )
368
+
369
+
370
+ def _call_native_source(
371
+ source_id: ConfigurationSource,
372
+ location: SourceLocation,
373
+ source: PydanticBaseSettingsSource,
374
+ ) -> dict[str, object]:
375
+ # 第三方动态 Any 只存在于这个边界,返回前递归收窄为配置对象树。
376
+ try:
377
+ raw_values = source()
378
+ except Exception as original_error:
379
+ _raise_source_error(source_id, location, original_error)
380
+ return _normalize_with_boundary(source_id, location, raw_values)
381
+
382
+
383
+ def _build_settings_type(
384
+ *,
385
+ payloads: tuple[_SourcePayload, ...],
386
+ explicit_preview: Mapping[str, object],
387
+ known_sections: frozenset[str],
388
+ ) -> type[BaseSettings]:
389
+ mapping_fields: set[str] = set()
390
+ scalar_fields: set[str] = set()
391
+ for payload in payloads:
392
+ _collect_field_shapes(payload.values, mapping_fields, scalar_fields)
393
+ _collect_field_shapes(explicit_preview, mapping_fields, scalar_fields)
394
+ for section in known_sections:
395
+ top_level = canonical_path(section)[0]
396
+ if top_level not in scalar_fields:
397
+ mapping_fields.add(top_level)
398
+
399
+ annotations: dict[str, object] = {
400
+ name: dict if name in mapping_fields else object
401
+ for name in sorted(mapping_fields | scalar_fields)
402
+ }
403
+ namespace: dict[str, object] = {
404
+ "__annotations__": annotations,
405
+ # 无 prefix dotenv key 由 native source 作为 extra 返回;ignore 只屏蔽这些变量。
406
+ "model_config": SettingsConfigDict(extra="ignore"),
407
+ }
408
+ return type(
409
+ "_ApplicationConfigurationSettings",
410
+ (_NativeConfigurationSettings,),
411
+ namespace,
412
+ )
413
+
414
+
415
+ def _collect_field_shapes(
416
+ values: Mapping[str, object],
417
+ mapping_fields: set[str],
418
+ scalar_fields: set[str],
419
+ ) -> None:
420
+ for key, value in values.items():
421
+ if isinstance(value, Mapping):
422
+ mapping_fields.add(key)
423
+ scalar_fields.discard(key)
424
+ elif key not in mapping_fields:
425
+ scalar_fields.add(key)
426
+
427
+
428
+ def _validate_prefixed_names(
429
+ env_names: Iterable[str],
430
+ *,
431
+ settings_type: type[BaseSettings],
432
+ env_prefix: str,
433
+ source_id: ConfigurationSource,
434
+ location: SourceLocation,
435
+ ) -> None:
436
+ prefix = env_prefix.casefold()
437
+ for raw_name in env_names:
438
+ normalized_name = raw_name.casefold()
439
+ if not normalized_name.startswith(prefix):
440
+ continue
441
+ relative = normalized_name[len(prefix) :]
442
+ raw_parts = relative.split(_ENVIRONMENT_NESTED_DELIMITER)
443
+ try:
444
+ parts = (canonical_segment(raw_parts[0]), *raw_parts[1:])
445
+ # 字段来自同一次环境输入快照;未知 section 在 Module 声明完成后统一校验。
446
+ field = settings_type.model_fields[parts[0]]
447
+ if (
448
+ len(parts) > 1
449
+ and field.annotation is not dict
450
+ and get_origin(field.annotation) is not dict
451
+ ):
452
+ raise ValueError(
453
+ f"configuration key {parts[0]!r} does not accept nested environment keys"
454
+ )
455
+ except (IndexError, TypeError, ValueError) as original_error:
456
+ _raise_source_error(source_id, location, original_error)
457
+
458
+
459
+ def _normalize_with_boundary(
460
+ source_id: ConfigurationSource,
461
+ location: SourceLocation,
462
+ values: object,
463
+ ) -> dict[str, object]:
464
+ try:
465
+ if not isinstance(values, Mapping):
466
+ raise TypeError("configuration source must return a mapping")
467
+ return _capture_input_mapping(cast(Mapping[str, object], values))
468
+ except (TypeError, ValueError) as original_error:
469
+ _raise_source_error(source_id, location, original_error)
470
+
471
+
472
+ def _source_location(
473
+ source: ConfigurationSource,
474
+ *,
475
+ file: Path | None = None,
476
+ ) -> SourceLocation:
477
+ return SourceLocation(
478
+ owner=source.value,
479
+ file=str(file) if file is not None else None,
480
+ line=None,
481
+ symbol=None,
482
+ )
483
+
484
+
485
+ def _loaded_diagnostic(
486
+ source: ConfigurationSource,
487
+ location: SourceLocation,
488
+ ) -> ConfigurationSourceDiagnostic:
489
+ return ConfigurationSourceDiagnostic(
490
+ source=source,
491
+ status=ConfigurationSourceStatus.LOADED,
492
+ location=location,
493
+ )
494
+
495
+
496
+ def _skipped_diagnostic(
497
+ source: ConfigurationSource,
498
+ location: SourceLocation,
499
+ reason: str,
500
+ ) -> ConfigurationSourceDiagnostic:
501
+ return ConfigurationSourceDiagnostic(
502
+ source=source,
503
+ status=ConfigurationSourceStatus.SKIPPED,
504
+ location=location,
505
+ reason=reason,
506
+ )
507
+
508
+
509
+ def _raise_source_error(
510
+ source: ConfigurationSource,
511
+ location: SourceLocation,
512
+ original_error: BaseException,
513
+ ) -> NoReturn:
514
+ error = ConfigurationSourceError(
515
+ source_id=source.value,
516
+ location=location,
517
+ original_error=original_error,
518
+ )
519
+ raise error from original_error