python-ddd-framework 0.3.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 (355) hide show
  1. python_ddd_framework/__init__.py +449 -0
  2. python_ddd_framework/application/__init__.py +6 -0
  3. python_ddd_framework/application/build_spec.py +37 -0
  4. python_ddd_framework/application/builder.py +260 -0
  5. python_ddd_framework/application/composition.py +219 -0
  6. python_ddd_framework/application/runtime.py +549 -0
  7. python_ddd_framework/application/runtime_hooks.py +327 -0
  8. python_ddd_framework/application/state.py +216 -0
  9. python_ddd_framework/application_services/__init__.py +54 -0
  10. python_ddd_framework/application_services/catalog.py +500 -0
  11. python_ddd_framework/application_services/contracts.py +190 -0
  12. python_ddd_framework/application_services/dispatcher.py +423 -0
  13. python_ddd_framework/application_services/errors.py +89 -0
  14. python_ddd_framework/application_services/execution.py +199 -0
  15. python_ddd_framework/application_services/interceptors.py +44 -0
  16. python_ddd_framework/application_services/invocation.py +431 -0
  17. python_ddd_framework/application_services/policies.py +46 -0
  18. python_ddd_framework/application_services/seeding.py +72 -0
  19. python_ddd_framework/application_services/signature.py +49 -0
  20. python_ddd_framework/application_services/validation.py +84 -0
  21. python_ddd_framework/auditing/__init__.py +12 -0
  22. python_ddd_framework/auditing/contracts.py +39 -0
  23. python_ddd_framework/auditing/control.py +48 -0
  24. python_ddd_framework/auditing/sqlalchemy/__init__.py +6 -0
  25. python_ddd_framework/auditing/sqlalchemy/migrations/0001_auditing.py +45 -0
  26. python_ddd_framework/auditing/sqlalchemy/migrations/__init__.py +1 -0
  27. python_ddd_framework/auditing/sqlalchemy/models.py +31 -0
  28. python_ddd_framework/auditing/sqlalchemy/module.py +32 -0
  29. python_ddd_framework/auditing/sqlalchemy/store.py +63 -0
  30. python_ddd_framework/authorization/__init__.py +38 -0
  31. python_ddd_framework/authorization/catalog.py +63 -0
  32. python_ddd_framework/authorization/contracts.py +241 -0
  33. python_ddd_framework/authorization/definition_discovery.py +81 -0
  34. python_ddd_framework/authorization/definitions.py +43 -0
  35. python_ddd_framework/authorization/errors.py +40 -0
  36. python_ddd_framework/background_execution/__init__.py +17 -0
  37. python_ddd_framework/background_execution/application.py +41 -0
  38. python_ddd_framework/background_execution/child.py +123 -0
  39. python_ddd_framework/background_execution/contracts.py +48 -0
  40. python_ddd_framework/background_execution/lifecycle.py +22 -0
  41. python_ddd_framework/background_execution/local.py +127 -0
  42. python_ddd_framework/background_execution/locks.py +33 -0
  43. python_ddd_framework/background_execution/management.py +17 -0
  44. python_ddd_framework/background_execution/module.py +10 -0
  45. python_ddd_framework/background_execution/permissions.py +14 -0
  46. python_ddd_framework/background_execution/processes.py +331 -0
  47. python_ddd_framework/background_jobs/__init__.py +31 -0
  48. python_ddd_framework/background_jobs/catalog.py +340 -0
  49. python_ddd_framework/background_jobs/contracts.py +208 -0
  50. python_ddd_framework/background_jobs/declaration.py +67 -0
  51. python_ddd_framework/background_jobs/errors.py +28 -0
  52. python_ddd_framework/background_jobs/execution.py +203 -0
  53. python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
  54. python_ddd_framework/background_jobs/pgqueuer/__init__.py +6 -0
  55. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +86 -0
  56. python_ddd_framework/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +95 -0
  57. python_ddd_framework/background_jobs/pgqueuer/migrations/__init__.py +1 -0
  58. python_ddd_framework/background_jobs/pgqueuer/module.py +106 -0
  59. python_ddd_framework/background_jobs/pgqueuer/options.py +82 -0
  60. python_ddd_framework/background_jobs/pgqueuer/runtime.py +479 -0
  61. python_ddd_framework/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
  62. python_ddd_framework/background_jobs/pgqueuer/supervision.py +201 -0
  63. python_ddd_framework/background_workers/__init__.py +22 -0
  64. python_ddd_framework/background_workers/catalog.py +118 -0
  65. python_ddd_framework/background_workers/contracts.py +152 -0
  66. python_ddd_framework/background_workers/errors.py +20 -0
  67. python_ddd_framework/background_workers/execution.py +133 -0
  68. python_ddd_framework/background_workers/runtime.py +219 -0
  69. python_ddd_framework/caching/__init__.py +11 -0
  70. python_ddd_framework/caching/catalog.py +73 -0
  71. python_ddd_framework/caching/contracts.py +73 -0
  72. python_ddd_framework/caching/errors.py +20 -0
  73. python_ddd_framework/cli/__init__.py +105 -0
  74. python_ddd_framework/cli/development.py +85 -0
  75. python_ddd_framework/cli/errors.py +5 -0
  76. python_ddd_framework/cli/inspection.py +96 -0
  77. python_ddd_framework/cli/project.py +69 -0
  78. python_ddd_framework/cli/runtime.py +56 -0
  79. python_ddd_framework/configuration/__init__.py +21 -0
  80. python_ddd_framework/configuration/composition.py +65 -0
  81. python_ddd_framework/configuration/contracts.py +68 -0
  82. python_ddd_framework/configuration/dotenv_source.py +51 -0
  83. python_ddd_framework/configuration/environment_source.py +38 -0
  84. python_ddd_framework/configuration/immutability.py +124 -0
  85. python_ddd_framework/configuration/input_shape.py +58 -0
  86. python_ddd_framework/configuration/merge.py +103 -0
  87. python_ddd_framework/configuration/root.py +215 -0
  88. python_ddd_framework/configuration/sources.py +519 -0
  89. python_ddd_framework/configuration/values.py +427 -0
  90. python_ddd_framework/configuration/yaml_source.py +51 -0
  91. python_ddd_framework/developer_kit/__init__.py +1 -0
  92. python_ddd_framework/developer_kit/generation.py +182 -0
  93. python_ddd_framework/developer_kit/project_metadata.py +45 -0
  94. python_ddd_framework/developer_kit/source.py +53 -0
  95. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -0
  96. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +70 -0
  97. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
  98. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
  99. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
  100. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
  101. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +41 -0
  102. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +20 -0
  103. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +7 -0
  104. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +83 -0
  105. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +84 -0
  106. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
  107. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
  108. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +35 -0
  109. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
  110. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
  111. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +56 -0
  112. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +14 -0
  113. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
  114. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
  115. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
  116. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
  117. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
  118. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
  119. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
  120. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +73 -0
  121. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +25 -0
  122. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +36 -0
  123. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
  124. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
  125. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +7 -0
  126. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/orders.py.jinja +25 -0
  127. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +26 -0
  128. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/__init__.py.jinja +1 -0
  129. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +49 -0
  130. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
  131. python_ddd_framework/developer_kit/templates/project/cookiecutter.json +6 -0
  132. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +9 -0
  133. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
  134. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
  135. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +38 -0
  136. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
  137. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +130 -0
  138. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/app.development.yaml.jinja +30 -0
  139. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
  140. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +20 -0
  141. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +84 -0
  142. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +185 -0
  143. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
  144. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
  145. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +47 -0
  146. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +37 -0
  147. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
  148. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +57 -0
  149. python_ddd_framework/developer_kit/wiring.py +162 -0
  150. python_ddd_framework/diagnostics/__init__.py +23 -0
  151. python_ddd_framework/diagnostics/journal.py +174 -0
  152. python_ddd_framework/diagnostics/model.py +83 -0
  153. python_ddd_framework/diagnostics/source.py +25 -0
  154. python_ddd_framework/distributed_lock/__init__.py +6 -0
  155. python_ddd_framework/distributed_lock/contracts.py +20 -0
  156. python_ddd_framework/distributed_lock/options.py +19 -0
  157. python_ddd_framework/domain/__init__.py +13 -0
  158. python_ddd_framework/domain/aggregates.py +64 -0
  159. python_ddd_framework/domain/entities.py +33 -0
  160. python_ddd_framework/domain/errors.py +12 -0
  161. python_ddd_framework/domain/value_objects.py +31 -0
  162. python_ddd_framework/errors/__init__.py +75 -0
  163. python_ddd_framework/errors/base.py +13 -0
  164. python_ddd_framework/errors/business.py +53 -0
  165. python_ddd_framework/errors/configuration.py +119 -0
  166. python_ddd_framework/errors/diagnostics.py +12 -0
  167. python_ddd_framework/errors/lifecycle.py +164 -0
  168. python_ddd_framework/errors/modularity.py +100 -0
  169. python_ddd_framework/errors/services.py +100 -0
  170. python_ddd_framework/events/__init__.py +25 -0
  171. python_ddd_framework/events/aggregate.py +39 -0
  172. python_ddd_framework/events/catalog.py +136 -0
  173. python_ddd_framework/events/contracts.py +39 -0
  174. python_ddd_framework/events/contribution.py +128 -0
  175. python_ddd_framework/events/discovery.py +105 -0
  176. python_ddd_framework/events/errors.py +48 -0
  177. python_ddd_framework/events/runtime.py +192 -0
  178. python_ddd_framework/fastapi/__init__.py +49 -0
  179. python_ddd_framework/fastapi/action.py +175 -0
  180. python_ddd_framework/fastapi/adapter.py +391 -0
  181. python_ddd_framework/fastapi/application_services.py +654 -0
  182. python_ddd_framework/fastapi/background.py +42 -0
  183. python_ddd_framework/fastapi/contracts.py +275 -0
  184. python_ddd_framework/fastapi/errors.py +32 -0
  185. python_ddd_framework/fastapi/filters.py +111 -0
  186. python_ddd_framework/fastapi/health.py +44 -0
  187. python_ddd_framework/fastapi/http.py +72 -0
  188. python_ddd_framework/fastapi/http_router.py +216 -0
  189. python_ddd_framework/fastapi/manual_action.py +105 -0
  190. python_ddd_framework/fastapi/middleware.py +93 -0
  191. python_ddd_framework/fastapi/parameters.py +97 -0
  192. python_ddd_framework/fastapi/realtime/__init__.py +13 -0
  193. python_ddd_framework/fastapi/realtime/authentication.py +181 -0
  194. python_ddd_framework/fastapi/realtime/connection.py +166 -0
  195. python_ddd_framework/fastapi/realtime/module.py +41 -0
  196. python_ddd_framework/fastapi/realtime/options.py +50 -0
  197. python_ddd_framework/fastapi/realtime/runtime.py +423 -0
  198. python_ddd_framework/fastapi/request_context.py +247 -0
  199. python_ddd_framework/fastapi/route_integrity.py +88 -0
  200. python_ddd_framework/fastapi/routing.py +424 -0
  201. python_ddd_framework/fastapi/server.py +182 -0
  202. python_ddd_framework/fastapi/settings.py +40 -0
  203. python_ddd_framework/fastapi/tracing.py +62 -0
  204. python_ddd_framework/fastapi/transfer.py +128 -0
  205. python_ddd_framework/fastapi/upload_limits.py +63 -0
  206. python_ddd_framework/fastapi/uploads.py +94 -0
  207. python_ddd_framework/hosted_services/__init__.py +29 -0
  208. python_ddd_framework/hosted_services/bridge.py +420 -0
  209. python_ddd_framework/hosted_services/catalog.py +265 -0
  210. python_ddd_framework/hosted_services/contracts.py +55 -0
  211. python_ddd_framework/hosted_services/errors.py +53 -0
  212. python_ddd_framework/hosted_services/options.py +17 -0
  213. python_ddd_framework/hosted_services/runtime.py +267 -0
  214. python_ddd_framework/hosted_services/state.py +40 -0
  215. python_ddd_framework/hosting/__init__.py +4 -0
  216. python_ddd_framework/hosting/instance.py +40 -0
  217. python_ddd_framework/identity/__init__.py +73 -0
  218. python_ddd_framework/identity/application.py +287 -0
  219. python_ddd_framework/identity/contracts.py +283 -0
  220. python_ddd_framework/identity/errors.py +28 -0
  221. python_ddd_framework/identity/http_api.py +90 -0
  222. python_ddd_framework/identity/module.py +49 -0
  223. python_ddd_framework/identity/passwords.py +34 -0
  224. python_ddd_framework/identity/permissions.py +14 -0
  225. python_ddd_framework/identity/services.py +108 -0
  226. python_ddd_framework/identity/sqlalchemy/__init__.py +5 -0
  227. python_ddd_framework/identity/sqlalchemy/migrations/0001_identity.py +143 -0
  228. python_ddd_framework/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
  229. python_ddd_framework/identity/sqlalchemy/migrations/__init__.py +1 -0
  230. python_ddd_framework/identity/sqlalchemy/models.py +85 -0
  231. python_ddd_framework/identity/sqlalchemy/module.py +62 -0
  232. python_ddd_framework/identity/sqlalchemy/stores.py +526 -0
  233. python_ddd_framework/identity/tokens.py +106 -0
  234. python_ddd_framework/invocation/__init__.py +3 -0
  235. python_ddd_framework/invocation/callables.py +171 -0
  236. python_ddd_framework/invocation/contracts.py +33 -0
  237. python_ddd_framework/invocation/entries.py +54 -0
  238. python_ddd_framework/invocation/function_runtime.py +48 -0
  239. python_ddd_framework/invocation/interception.py +193 -0
  240. python_ddd_framework/lifecycle/__init__.py +25 -0
  241. python_ddd_framework/lifecycle/composition.py +35 -0
  242. python_ddd_framework/lifecycle/context.py +31 -0
  243. python_ddd_framework/lifecycle/runtime.py +43 -0
  244. python_ddd_framework/lifecycle/state.py +20 -0
  245. python_ddd_framework/modularity/__init__.py +15 -0
  246. python_ddd_framework/modularity/contracts.py +91 -0
  247. python_ddd_framework/modularity/discovery.py +157 -0
  248. python_ddd_framework/modularity/graph.py +334 -0
  249. python_ddd_framework/modularity/registry.py +100 -0
  250. python_ddd_framework/modularity/selection.py +16 -0
  251. python_ddd_framework/notifications/__init__.py +19 -0
  252. python_ddd_framework/notifications/catalog.py +46 -0
  253. python_ddd_framework/notifications/contracts.py +107 -0
  254. python_ddd_framework/observability/__init__.py +5 -0
  255. python_ddd_framework/observability/context.py +43 -0
  256. python_ddd_framework/observability/export.py +73 -0
  257. python_ddd_framework/observability/formatting.py +89 -0
  258. python_ddd_framework/observability/logging.py +118 -0
  259. python_ddd_framework/observability/options.py +45 -0
  260. python_ddd_framework/observability/tracing.py +80 -0
  261. python_ddd_framework/options/__init__.py +19 -0
  262. python_ddd_framework/options/aliases.py +331 -0
  263. python_ddd_framework/options/contribution.py +62 -0
  264. python_ddd_framework/options/immutability.py +48 -0
  265. python_ddd_framework/options/input_keys.py +214 -0
  266. python_ddd_framework/options/issues.py +335 -0
  267. python_ddd_framework/options/models.py +114 -0
  268. python_ddd_framework/options/registry.py +280 -0
  269. python_ddd_framework/options/schema.py +488 -0
  270. python_ddd_framework/options/validation.py +120 -0
  271. python_ddd_framework/py.typed +0 -0
  272. python_ddd_framework/realtime/__init__.py +12 -0
  273. python_ddd_framework/realtime/contracts.py +28 -0
  274. python_ddd_framework/realtime/diagnostics.py +37 -0
  275. python_ddd_framework/realtime/messages.py +113 -0
  276. python_ddd_framework/redis/__init__.py +6 -0
  277. python_ddd_framework/redis/distributed_lock.py +212 -0
  278. python_ddd_framework/redis/lease_lock.py +36 -0
  279. python_ddd_framework/redis/module.py +50 -0
  280. python_ddd_framework/redis/notification_runtime.py +182 -0
  281. python_ddd_framework/redis/notifications.py +25 -0
  282. python_ddd_framework/redis/options.py +35 -0
  283. python_ddd_framework/redis/runtime.py +116 -0
  284. python_ddd_framework/services/__init__.py +41 -0
  285. python_ddd_framework/services/application_bindings.py +221 -0
  286. python_ddd_framework/services/arbitration.py +381 -0
  287. python_ddd_framework/services/binding.py +102 -0
  288. python_ddd_framework/services/contribution.py +438 -0
  289. python_ddd_framework/services/convention.py +301 -0
  290. python_ddd_framework/services/convention_contracts.py +101 -0
  291. python_ddd_framework/services/exposure.py +23 -0
  292. python_ddd_framework/services/fixed_lifetime.py +52 -0
  293. python_ddd_framework/services/framework_provider.py +178 -0
  294. python_ddd_framework/services/native_graph.py +201 -0
  295. python_ddd_framework/services/provider.py +204 -0
  296. python_ddd_framework/services/registration.py +101 -0
  297. python_ddd_framework/services/repository.py +38 -0
  298. python_ddd_framework/services/runtime.py +302 -0
  299. python_ddd_framework/settings/__init__.py +30 -0
  300. python_ddd_framework/settings/application.py +95 -0
  301. python_ddd_framework/settings/binding.py +33 -0
  302. python_ddd_framework/settings/catalog.py +121 -0
  303. python_ddd_framework/settings/changes.py +14 -0
  304. python_ddd_framework/settings/contracts.py +115 -0
  305. python_ddd_framework/settings/definition_discovery.py +71 -0
  306. python_ddd_framework/settings/definitions.py +41 -0
  307. python_ddd_framework/settings/errors.py +22 -0
  308. python_ddd_framework/settings/handlers.py +43 -0
  309. python_ddd_framework/settings/management.py +57 -0
  310. python_ddd_framework/settings/manager.py +89 -0
  311. python_ddd_framework/settings/module.py +10 -0
  312. python_ddd_framework/settings/notifications.py +39 -0
  313. python_ddd_framework/settings/permissions.py +14 -0
  314. python_ddd_framework/settings/provider.py +74 -0
  315. python_ddd_framework/settings/refresh.py +140 -0
  316. python_ddd_framework/settings/refresh_module.py +45 -0
  317. python_ddd_framework/settings/sqlalchemy/__init__.py +5 -0
  318. python_ddd_framework/settings/sqlalchemy/migrations/0001_settings.py +31 -0
  319. python_ddd_framework/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
  320. python_ddd_framework/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
  321. python_ddd_framework/settings/sqlalchemy/migrations/__init__.py +1 -0
  322. python_ddd_framework/settings/sqlalchemy/models.py +36 -0
  323. python_ddd_framework/settings/sqlalchemy/module.py +36 -0
  324. python_ddd_framework/settings/sqlalchemy/store.py +86 -0
  325. python_ddd_framework/settings/store.py +16 -0
  326. python_ddd_framework/settings/values.py +39 -0
  327. python_ddd_framework/sqlalchemy/__init__.py +54 -0
  328. python_ddd_framework/sqlalchemy/alembic_runtime/__init__.py +1 -0
  329. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +33 -0
  330. python_ddd_framework/sqlalchemy/alembic_runtime/script.py.mako +14 -0
  331. python_ddd_framework/sqlalchemy/auditing.py +48 -0
  332. python_ddd_framework/sqlalchemy/errors.py +85 -0
  333. python_ddd_framework/sqlalchemy/metadata.py +576 -0
  334. python_ddd_framework/sqlalchemy/migration.py +378 -0
  335. python_ddd_framework/sqlalchemy/migration_options.py +56 -0
  336. python_ddd_framework/sqlalchemy/module.py +51 -0
  337. python_ddd_framework/sqlalchemy/module_migration.py +151 -0
  338. python_ddd_framework/sqlalchemy/options.py +66 -0
  339. python_ddd_framework/sqlalchemy/repository.py +33 -0
  340. python_ddd_framework/sqlalchemy/runtime.py +85 -0
  341. python_ddd_framework/sqlalchemy/session_provider.py +45 -0
  342. python_ddd_framework/sqlalchemy/unit_of_work.py +97 -0
  343. python_ddd_framework/testing/__init__.py +5 -0
  344. python_ddd_framework/testing/runtime.py +112 -0
  345. python_ddd_framework/unit_of_work/__init__.py +14 -0
  346. python_ddd_framework/unit_of_work/contracts.py +223 -0
  347. python_ddd_framework/unit_of_work/errors.py +27 -0
  348. python_ddd_framework/unit_of_work/manager.py +210 -0
  349. python_ddd_framework/unit_of_work/options.py +92 -0
  350. python_ddd_framework-0.3.1.dist-info/METADATA +379 -0
  351. python_ddd_framework-0.3.1.dist-info/RECORD +355 -0
  352. python_ddd_framework-0.3.1.dist-info/WHEEL +4 -0
  353. python_ddd_framework-0.3.1.dist-info/entry_points.txt +9 -0
  354. python_ddd_framework-0.3.1.dist-info/licenses/LICENSE +7 -0
  355. python_ddd_framework-0.3.1.dist-info/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
@@ -0,0 +1,96 @@
1
+ """当前 Host 组合快照的只读展示;不解析服务实例或启动运行时。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator, Mapping
6
+ from typing import cast
7
+
8
+ from pydantic import ConfigDict, TypeAdapter
9
+
10
+ from ..application import Application
11
+ from ..configuration import ConfigurationRoot
12
+ from .errors import CommandError
13
+ from .project import Project
14
+
15
+
16
+ async def inspect_application(environment: str) -> None:
17
+ project = Project.current()
18
+ application = project.host("application").load()(environment=environment)
19
+ if not isinstance(application, Application):
20
+ raise CommandError("The application entry point must return Application")
21
+ try:
22
+ report = _report(application)
23
+ output = TypeAdapter(
24
+ dict[str, object], config=ConfigDict(ser_json_bytes="base64")
25
+ ).dump_json(report, indent=2, fallback=_serialize_mapping)
26
+ finally:
27
+ # COMPOSED 也拥有 DI container;读取或序列化失败时同样交还给正式关闭流程。
28
+ await application.stop()
29
+ print(output.decode("utf-8"))
30
+
31
+
32
+ def _report(application: Application) -> dict[str, object]:
33
+ graph = application.modules.snapshot
34
+ return {
35
+ "modules": {
36
+ "startup_order": [str(key) for key in graph.dependency_order],
37
+ "dependencies": [
38
+ {
39
+ "module": str(module.key),
40
+ "dependencies": [str(key) for key in module.dependencies],
41
+ "source": module.declaration_source,
42
+ }
43
+ for module in application.modules.dependency_order
44
+ ],
45
+ },
46
+ "services": [
47
+ {
48
+ "service": str(row.key.type_hint),
49
+ "component": row.key.component,
50
+ "scope": row.scope.name,
51
+ "owner": str(row.owner),
52
+ "source": row.source,
53
+ "kind": row.kind.value,
54
+ "decision": row.decision.value,
55
+ "reason": row.decision_reason,
56
+ }
57
+ for row in application.service_catalog.registrations
58
+ ],
59
+ "configuration": {
60
+ "sources": application.configuration.sources,
61
+ "values": [
62
+ row
63
+ for section in application.configuration
64
+ for row in _configuration_rows(
65
+ application.configuration,
66
+ (section,),
67
+ application.configuration.diagnostic(section).value,
68
+ )
69
+ ],
70
+ },
71
+ }
72
+
73
+
74
+ def _configuration_rows(
75
+ configuration: ConfigurationRoot, path: tuple[str, ...], value: object
76
+ ) -> Iterator[dict[str, object]]:
77
+ # 只遍历正式 diagnostic 已脱敏的值;被整体隐藏的父节点不得再展开原值。
78
+ if isinstance(value, Mapping) and value:
79
+ for key, item in cast(Mapping[str, object], value).items():
80
+ yield from _configuration_rows(configuration, (*path, key), item)
81
+ return
82
+ # Mapping 数据键允许大小写和点号,查询和输出均保留 segment 身份。
83
+ provenance = configuration._provenance_at(path)
84
+ yield {
85
+ "path": path,
86
+ "value": value,
87
+ "sources": provenance.chain,
88
+ "effective_source": provenance.effective_source,
89
+ }
90
+
91
+
92
+ def _serialize_mapping(value: object) -> object:
93
+ # Pydantic 原生 JSON serializer 负责日期、枚举、集合和 bytes;仅补不可变 Mapping。
94
+ if isinstance(value, Mapping):
95
+ return dict(value)
96
+ raise TypeError("Unsupported diagnostic value")
@@ -0,0 +1,69 @@
1
+ """当前项目的标准 pyproject/distribution/entry points,禁止第二份 Host 清单。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.metadata as metadata
6
+ import tomllib
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from ..application import Application
12
+ from ..modularity import AppModule
13
+ from .errors import CommandError
14
+
15
+ HOST_ENTRY_POINT_GROUP = "python_ddd_framework.hosts"
16
+ MODULE_ENTRY_POINT_GROUP = "python_ddd_framework.modules"
17
+
18
+
19
+ def read_project(path: Path) -> dict[str, Any]:
20
+ try:
21
+ with path.open("rb") as stream:
22
+ return tomllib.load(stream)
23
+ except (OSError, ValueError) as error:
24
+ raise CommandError(
25
+ f"Cannot read project metadata at {path}: {type(error).__name__}"
26
+ ) from None
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class Project:
31
+ root: Path
32
+ name: str
33
+
34
+ @classmethod
35
+ def current(cls) -> Project:
36
+ root = Path.cwd().resolve()
37
+ project = read_project(root / "pyproject.toml").get("project", {})
38
+ name = project.get("name") if isinstance(project, dict) else None
39
+ if not isinstance(name, str) or not name:
40
+ raise CommandError("Current pyproject.toml must declare project.name")
41
+ return cls(root, name)
42
+
43
+ def host(self, name: str) -> metadata.EntryPoint:
44
+ try:
45
+ distribution = metadata.distribution(self.name)
46
+ except metadata.PackageNotFoundError:
47
+ raise CommandError(
48
+ "Install the current project with uv sync, then use uv run pddd"
49
+ ) from None
50
+ matches = tuple(
51
+ entry
52
+ for entry in distribution.entry_points
53
+ if entry.group == HOST_ENTRY_POINT_GROUP and entry.name == name
54
+ )
55
+ if len(matches) != 1:
56
+ raise CommandError(f"Project must declare exactly one {HOST_ENTRY_POINT_GROUP}:{name}")
57
+ return matches[0]
58
+
59
+ def module(self, name: str, application: Application) -> type[AppModule]:
60
+ # entry point 只命名已在当前图中的正式 Module;不导入未启用的 optional provider。
61
+ live = {str(item.key): item.module_type for item in application.modules.dependency_order}
62
+ matches = {
63
+ entry.value
64
+ for entry in metadata.entry_points(group=MODULE_ENTRY_POINT_GROUP)
65
+ if entry.name == name and entry.value in live
66
+ }
67
+ if len(matches) != 1:
68
+ raise CommandError(f"Module alias {name!r} must select exactly one live Module")
69
+ return live[matches.pop()]
@@ -0,0 +1,56 @@
1
+ """项目运行命令只编排正式 Host、迁移与 seed,不拥有业务初始化规则。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from . import ENVIRONMENT_VARIABLE
8
+ from .errors import CommandError
9
+ from .project import Project
10
+
11
+
12
+ async def database(operation: str, environment: str, module: str) -> None:
13
+ from ..application import Application
14
+ from ..application_services.seeding import DataSeeder
15
+ from ..sqlalchemy import SqlAlchemyMigrator
16
+
17
+ project = Project.current()
18
+ factory = project.host("application").load()
19
+ application = factory(environment=environment)
20
+ if not isinstance(application, Application):
21
+ raise CommandError("The application entry point must return Application")
22
+ try:
23
+ selected = project.module(module, application)
24
+ migrator = SqlAlchemyMigrator(application)
25
+ if operation == "revision":
26
+ await migrator.revision(selected)
27
+ elif operation == "upgrade":
28
+ await migrator.upgrade_heads(module=selected)
29
+ elif operation == "status":
30
+ await migrator.check_current_heads(module=selected)
31
+ elif operation == "seed":
32
+ await migrator.check_current_heads(module=selected)
33
+ await application._start(run_services=False)
34
+ await DataSeeder(application).seed_all(module=selected)
35
+ else:
36
+ raise CommandError(f"Unknown database operation: {operation}")
37
+ finally:
38
+ await application.stop()
39
+ print(f"Database {operation} completed")
40
+
41
+
42
+ def development_server(host: str, port: int) -> None:
43
+ from ..fastapi.server import run_host
44
+
45
+ project = Project.current()
46
+ entry = project.host("web")
47
+ os.environ[ENVIRONMENT_VARIABLE] = "development"
48
+ # 原生 access handler 不传播到 Host 的安全 formatter,不能记录含凭据的 query。
49
+ run_host(
50
+ entry.value,
51
+ host=host,
52
+ port=port,
53
+ reload=True,
54
+ project=project.root,
55
+ environment="development",
56
+ )
@@ -0,0 +1,21 @@
1
+ """Configuration public contracts and Application-internal composition owners."""
2
+
3
+ from .contracts import (
4
+ ConfigurationContribution,
5
+ ConfigurationProvenance,
6
+ ConfigurationSource,
7
+ ConfigurationSourceDiagnostic,
8
+ ConfigurationSourceStatus,
9
+ ConfigurationValueDiagnostic,
10
+ )
11
+ from .root import ConfigurationRoot
12
+
13
+ __all__ = (
14
+ "ConfigurationContribution",
15
+ "ConfigurationProvenance",
16
+ "ConfigurationRoot",
17
+ "ConfigurationSource",
18
+ "ConfigurationSourceDiagnostic",
19
+ "ConfigurationSourceStatus",
20
+ "ConfigurationValueDiagnostic",
21
+ )
@@ -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