pz-cli 5.0.0__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 (399) hide show
  1. pz_cli-5.0.0.dist-info/METADATA +51 -0
  2. pz_cli-5.0.0.dist-info/RECORD +399 -0
  3. pz_cli-5.0.0.dist-info/WHEEL +4 -0
  4. pz_cli-5.0.0.dist-info/entry_points.txt +5 -0
  5. pz_core/__init__.py +64 -0
  6. pz_core/__main__.py +7 -0
  7. pz_core/app.py +45 -0
  8. pz_core/assembly/__init__.py +17 -0
  9. pz_core/assembly/app_discovery.py +1240 -0
  10. pz_core/assembly/apps/__init__.py +25 -0
  11. pz_core/assembly/apps/compiler.py +824 -0
  12. pz_core/assembly/apps/providers.py +188 -0
  13. pz_core/assembly/cli/__init__.py +9 -0
  14. pz_core/assembly/cli/compiler.py +166 -0
  15. pz_core/assembly/diagnostics/__init__.py +21 -0
  16. pz_core/assembly/diagnostics/checks.py +257 -0
  17. pz_core/assembly/diagnostics/dev_sources.py +569 -0
  18. pz_core/assembly/events/__init__.py +5 -0
  19. pz_core/assembly/events/compiler.py +219 -0
  20. pz_core/assembly/extensions/__init__.py +5 -0
  21. pz_core/assembly/extensions/compiler.py +696 -0
  22. pz_core/assembly/infra/__init__.py +6 -0
  23. pz_core/assembly/infra/compiler.py +272 -0
  24. pz_core/assembly/lifecycle/__init__.py +5 -0
  25. pz_core/assembly/lifecycle/compiler.py +95 -0
  26. pz_core/assembly/middlewares/__init__.py +19 -0
  27. pz_core/assembly/middlewares/compiler.py +508 -0
  28. pz_core/assembly/project/__init__.py +7 -0
  29. pz_core/assembly/project/compiler.py +9 -0
  30. pz_core/assembly/project/context.py +17 -0
  31. pz_core/assembly/project/discovery.py +227 -0
  32. pz_core/assembly/resources/__init__.py +19 -0
  33. pz_core/assembly/resources/compiler.py +310 -0
  34. pz_core/assembly/resources/settings.py +129 -0
  35. pz_core/assembly/routes/__init__.py +7 -0
  36. pz_core/assembly/routes/compiler.py +400 -0
  37. pz_core/assembly/runtime_manifests/__init__.py +13 -0
  38. pz_core/assembly/runtime_manifests/compiler.py +159 -0
  39. pz_core/assembly/settings/__init__.py +17 -0
  40. pz_core/assembly/settings/compiler.py +786 -0
  41. pz_core/assembly/settings/host_project_overrides.py +82 -0
  42. pz_core/assembly/settings/host_runtime_endpoint.py +207 -0
  43. pz_core/authoring/__init__.py +79 -0
  44. pz_core/authoring/app_init_service.py +946 -0
  45. pz_core/authoring/app_versions.py +814 -0
  46. pz_core/authoring/cli.py +192 -0
  47. pz_core/authoring/commands.py +1810 -0
  48. pz_core/authoring/conflicts.py +180 -0
  49. pz_core/authoring/dev_bindings.py +916 -0
  50. pz_core/authoring/dev_git.py +1087 -0
  51. pz_core/authoring/dev_import.py +1061 -0
  52. pz_core/authoring/dev_manifest.py +522 -0
  53. pz_core/authoring/dev_sources.py +1638 -0
  54. pz_core/authoring/env_cli.py +454 -0
  55. pz_core/authoring/files.py +109 -0
  56. pz_core/authoring/ide_projection.py +1129 -0
  57. pz_core/authoring/naming.py +58 -0
  58. pz_core/authoring/official_dev_sources.py +432 -0
  59. pz_core/authoring/official_sources.json +156 -0
  60. pz_core/authoring/orchestration_service.py +558 -0
  61. pz_core/authoring/project_default.jsonc +53 -0
  62. pz_core/authoring/project_defaults.py +88 -0
  63. pz_core/authoring/project_metadata_service.py +199 -0
  64. pz_core/authoring/project_settings_checks.py +51 -0
  65. pz_core/authoring/project_settings_cli.py +299 -0
  66. pz_core/authoring/project_settings_service.py +1700 -0
  67. pz_core/authoring/project_state.py +399 -0
  68. pz_core/authoring/publish_git.py +423 -0
  69. pz_core/authoring/publish_lock.py +188 -0
  70. pz_core/authoring/publish_source.py +195 -0
  71. pz_core/authoring/release.py +1382 -0
  72. pz_core/authoring/repo_keys.py +29 -0
  73. pz_core/authoring/root_cli.py +28 -0
  74. pz_core/authoring/scaffold_service.py +325 -0
  75. pz_core/authoring/sync_service.py +787 -0
  76. pz_core/authoring/templates/app/__init__.py +1 -0
  77. pz_core/authoring/templates/app/api.py +25 -0
  78. pz_core/authoring/templates/app/apps.py +31 -0
  79. pz_core/authoring/templates/app/auth_package_init.py +14 -0
  80. pz_core/authoring/templates/app/auth_readme.py +29 -0
  81. pz_core/authoring/templates/app/configs_package_init.py +14 -0
  82. pz_core/authoring/templates/app/default_env.py +18 -0
  83. pz_core/authoring/templates/app/default_jsonc.py +25 -0
  84. pz_core/authoring/templates/app/locales_readme.py +25 -0
  85. pz_core/authoring/templates/app/migrations_readme.py +19 -0
  86. pz_core/authoring/templates/app/models.py +14 -0
  87. pz_core/authoring/templates/app/package_init.py +8 -0
  88. pz_core/authoring/templates/app/pages_base.py +161 -0
  89. pz_core/authoring/templates/app/pages_readme.py +25 -0
  90. pz_core/authoring/templates/app/public.py +31 -0
  91. pz_core/authoring/templates/app/pyproject.py +77 -0
  92. pz_core/authoring/templates/app/readme.py +74 -0
  93. pz_core/authoring/templates/app/settings.py +46 -0
  94. pz_core/authoring/templates/app/static_app_js.py +24 -0
  95. pz_core/authoring/templates/app/static_readme.py +31 -0
  96. pz_core/authoring/templates/app/template_config_page.py +23 -0
  97. pz_core/authoring/templates/app/template_index_html.py +28 -0
  98. pz_core/authoring/templates/app/templates_readme.py +32 -0
  99. pz_core/authoring/templates/app/urls.py +36 -0
  100. pz_core/authoring/templates/app/views.py +36 -0
  101. pz_core/authoring/templates/project/__init__.py +1 -0
  102. pz_core/authoring/templates/project/agents.py +51 -0
  103. pz_core/authoring/templates/project/apps_container_readme.py +37 -0
  104. pz_core/authoring/templates/project/main_py.py +59 -0
  105. pz_core/authoring/templates/project/project_env.py +19 -0
  106. pz_core/authoring/templates/project/pyproject.py +26 -0
  107. pz_core/authoring/templates/project/pz_env_env.py +65 -0
  108. pz_core/authoring/templates/project/pz_env_jsonc.py +69 -0
  109. pz_core/authoring/templates/project/pz_jsonc.py +12 -0
  110. pz_core/authoring/templates/project/readme.py +94 -0
  111. pz_core/authoring/templates/project/root_gitkeep.py +8 -0
  112. pz_core/authoring/templates/shared/__init__.py +5 -0
  113. pz_core/authoring/templates/shared/text.py +10 -0
  114. pz_core/authoring/venv_service.py +87 -0
  115. pz_core/authoring/workspace.py +167 -0
  116. pz_core/cli.py +291 -0
  117. pz_core/cli_options.py +74 -0
  118. pz_core/core/__init__.py +3 -0
  119. pz_core/core/errors/__init__.py +5 -0
  120. pz_core/core/errors/models.py +117 -0
  121. pz_core/core/events/__init__.py +5 -0
  122. pz_core/core/events/registry.py +54 -0
  123. pz_core/core/extensions/__init__.py +51 -0
  124. pz_core/core/extensions/registry.py +790 -0
  125. pz_core/core/health/__init__.py +6 -0
  126. pz_core/core/health/contracts.py +10 -0
  127. pz_core/core/middlewares/__init__.py +6 -0
  128. pz_core/core/middlewares/models.py +24 -0
  129. pz_core/core/routing/__init__.py +5 -0
  130. pz_core/core/routing/models.py +21 -0
  131. pz_core/core/settings_contracts.py +25 -0
  132. pz_core/docs/README.md +20 -0
  133. pz_core/docs/distribution-README.md +438 -0
  134. pz_core/docs/pz/345/221/275/344/273/244/346/200/273/350/247/210.md +1011 -0
  135. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/00-pz/346/236/266/346/236/204/346/200/273/347/272/262.md +240 -0
  136. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/01-/347/233/256/345/275/225/344/270/216/345/210/206/345/261/202/350/247/204/350/214/203.md +216 -0
  137. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/02-/345/214/205/344/270/216/345/210/206/345/217/221/350/247/204/350/214/203.md +403 -0
  138. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/03-App/346/234/200/345/260/217/345/255/220/345/214/205/350/247/204/350/214/203.md +571 -0
  139. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/04-Settings/344/270/216/351/205/215/347/275/256/350/247/204/350/214/203.md +1177 -0
  140. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/05-/350/265/204/346/272/220/344/270/216/350/277/220/350/241/214/347/233/256/345/275/225/350/247/204/350/214/203.md +575 -0
  141. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/06-/346/225/260/346/215/256/345/272/223/344/270/216/345/217/257/351/200/211/345/237/272/347/241/200/350/256/276/346/226/275/350/247/204/350/214/203.md +499 -0
  142. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/07-App/346/233/277/346/215/242/350/247/204/350/214/203.md +206 -0
  143. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/08-/345/271/263/345/217/260/347/272/247App/344/270/216/350/277/220/350/241/214/346/227/266/345/215/225/344/276/213/350/247/204/350/214/203.md +445 -0
  144. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/09-CLI/344/270/216Worker/345/205/245/345/217/243/350/247/204/350/214/203.md +950 -0
  145. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/10-/346/265/213/350/257/225/344/270/216/345/245/221/347/272/246/350/247/204/350/214/203.md +1001 -0
  146. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/11-/344/273/223/345/272/223/347/234/237/347/233/270/346/272/220/345/234/260/345/233/276.md +102 -0
  147. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/12-/346/236/204/345/273/272/344/270/216/345/217/221/345/270/203/346/243/200/346/237/245/346/270/205/345/215/225.md +171 -0
  148. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/13-/346/255/243/345/274/217Facade/346/200/273/350/241/250.md +402 -0
  149. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/14-/345/210/206/345/261/202/344/270/216/346/211/251/345/261/225/350/247/204/350/214/203.md +550 -0
  150. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/15-/345/255/220/345/214/205/345/274/200/345/217/221/344/275/277/347/224/250/346/211/213/345/206/214.md +1004 -0
  151. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/16-/346/211/251/345/261/225/347/202/271/346/255/243/345/274/217/345/215/217/350/256/256/346/270/205/345/215/225.md +352 -0
  152. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/17-pz.protocols.v1/346/255/243/345/274/217/345/215/217/350/256/256.md +266 -0
  153. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/18-/346/211/251/345/261/225/347/202/271/345/206/262/347/252/201/350/243/201/345/206/263/346/200/273/350/241/250.md +131 -0
  154. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/19-/351/224/231/350/257/257/347/240/201/344/270/216/350/257/212/346/226/255/345/215/217/350/256/256.md +176 -0
  155. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/20-/347/247/201/346/234/211/345/214/205/344/273/223/344/270/216/347/273/237/344/270/200/350/256/244/350/257/201/351/203/250/347/275/262/350/247/204/350/214/203.md +243 -0
  156. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/21-contrib/347/272/246/345/256/232/344/270/216Provider/346/216/245/345/205/245/350/247/204/350/214/203.md +365 -0
  157. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/22-/345/244/232/345/233/275/350/257/255/350/250/200/350/265/204/346/272/220/346/262/273/347/220/206/344/270/216/346/214/211/351/234/200/345/212/240/350/275/275/350/247/204/350/214/203.md +912 -0
  158. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/23-/351/246/226/345/261/217/345/261/225/347/244/272/344/270/216/351/246/226/345/270/247/347/250/263/345/256/232/350/247/204/350/214/203.md +269 -0
  159. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/24-pz/345/220/257/345/212/250/344/270/216/350/277/220/350/241/214/346/227/266/347/274/226/350/257/221/346/265/201/347/250/213.md +245 -0
  160. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/25-/351/235/242/345/220/221/345/257/271/350/261/241/345/274/200/345/217/221/350/247/204/350/214/203.md +300 -0
  161. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/README.md +98 -0
  162. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/archive//344/270/255/351/227/264/346/200/201/00-legacy_core/346/240/207/345/207/206pip/345/214/205/345/214/226/351/207/215/346/236/204/346/200/273/347/272/262.md +1073 -0
  163. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/archive//344/270/255/351/227/264/346/200/201/01-/347/233/256/345/275/225/344/270/216/345/210/206/345/261/202/351/207/215/346/236/204/346/211/247/350/241/214/346/226/271/346/241/210.md +224 -0
  164. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/archive//344/270/255/351/227/264/346/200/201/02-/345/214/205/344/270/216/345/210/206/345/217/221/350/247/204/350/214/203-/345/220/253/345/216/206/345/217/262/345/217/243/345/276/204.md +671 -0
  165. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/archive//344/270/255/351/227/264/346/200/201/13-/344/270/200/346/254/241/346/200/247/345/256/236/346/226/275/346/270/205/345/215/225.md +387 -0
  166. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/archive//344/270/255/351/227/264/346/200/201/14-/345/255/220/345/214/205/351/200/202/351/205/215/346/224/271/351/200/240/346/270/205/345/215/225.md +592 -0
  167. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/archive//344/270/255/351/227/264/346/200/201/15-infra/350/243/201/345/211/252/344/270/216/350/220/275/344/275/215/346/270/205/345/215/225.md +81 -0
  168. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/archive//344/270/255/351/227/264/346/200/201/19-core/345/215/225/344/270/200/346/255/243/345/274/217/346/226/271/346/241/210/346/224/266/346/225/233/346/211/247/350/241/214/346/270/205/345/215/225.md +161 -0
  169. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/archive//344/270/255/351/227/264/346/200/201/25-/345/215/217/350/256/256/345/210/260/346/272/220/347/240/201/345/267/256/350/267/235/346/270/205/345/215/225.md +377 -0
  170. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/archive//344/270/255/351/227/264/346/200/201/26-cutover/345/256/214/345/267/245/346/243/200/346/237/245/346/270/205/345/215/225.md +117 -0
  171. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/archive//344/270/255/351/227/264/346/200/201/29-/345/244/232/345/233/275/350/257/255/350/250/200/350/265/204/346/272/220/346/262/273/347/220/206/344/270/216/346/214/211/351/234/200/345/212/240/350/275/275/350/247/204/350/214/203-/345/220/253/350/277/201/347/247/273/347/255/226/347/225/245.md +956 -0
  172. pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/archive//344/270/255/351/227/264/346/200/201/99-/350/265/204/346/272/220/346/224/266/345/217/243/345/220/216/347/273/255TODO.md +84 -0
  173. pz_core/docs//345/217/221/347/211/210/345/211/215/346/270/205/345/215/225-4.0.11.md +84 -0
  174. pz_core/docs//346/226/260/346/211/213/346/214/207/345/215/227.md +422 -0
  175. pz_core/infra/__init__.py +17 -0
  176. pz_core/infra/db/__init__.py +23 -0
  177. pz_core/infra/db/contracts.py +73 -0
  178. pz_core/infra/health/__init__.py +5 -0
  179. pz_core/infra/health/contracts.py +8 -0
  180. pz_core/infra/kv/__init__.py +5 -0
  181. pz_core/infra/kv/contracts.py +17 -0
  182. pz_core/infra/observability/__init__.py +23 -0
  183. pz_core/infra/observability/contracts.py +142 -0
  184. pz_core/infra/resilience/__init__.py +5 -0
  185. pz_core/infra/resilience/contracts.py +17 -0
  186. pz_core/infra/resources/__init__.py +5 -0
  187. pz_core/infra/resources/contracts.py +16 -0
  188. pz_core/infra/settings/__init__.py +17 -0
  189. pz_core/infra/settings/models.py +81 -0
  190. pz_core/infra/storage/__init__.py +5 -0
  191. pz_core/infra/storage/ports.py +27 -0
  192. pz_core/project_root_commands.py +282 -0
  193. pz_core/project_runtime_cli.py +344 -0
  194. pz_core/project_sources.py +253 -0
  195. pz_core/pz_launcher.py +209 -0
  196. pz_core/root_cli.py +70 -0
  197. pz_core/runtime/__init__.py +229 -0
  198. pz_core/runtime/access.py +86 -0
  199. pz_core/runtime/adapters/__init__.py +5 -0
  200. pz_core/runtime/adapters/base.py +18 -0
  201. pz_core/runtime/asyncio_bridge.py +18 -0
  202. pz_core/runtime/bindings/__init__.py +5 -0
  203. pz_core/runtime/bindings/container.py +50 -0
  204. pz_core/runtime/cli/__init__.py +5 -0
  205. pz_core/runtime/cli/_args.py +67 -0
  206. pz_core/runtime/cli/app_commands.py +417 -0
  207. pz_core/runtime/cli/check.py +214 -0
  208. pz_core/runtime/cli/db.py +297 -0
  209. pz_core/runtime/cli/main.py +89 -0
  210. pz_core/runtime/cli/resources.py +280 -0
  211. pz_core/runtime/discovery.py +23 -0
  212. pz_core/runtime/environment_integrity.py +157 -0
  213. pz_core/runtime/errors.py +5 -0
  214. pz_core/runtime/events.py +87 -0
  215. pz_core/runtime/facades/__init__.py +38 -0
  216. pz_core/runtime/facades/_capabilities.py +46 -0
  217. pz_core/runtime/facades/cache.py +103 -0
  218. pz_core/runtime/facades/database.py +71 -0
  219. pz_core/runtime/facades/events.py +380 -0
  220. pz_core/runtime/facades/health.py +101 -0
  221. pz_core/runtime/facades/mail.py +121 -0
  222. pz_core/runtime/facades/observability.py +102 -0
  223. pz_core/runtime/facades/providers.py +75 -0
  224. pz_core/runtime/facades/resources.py +52 -0
  225. pz_core/runtime/facades/storage.py +296 -0
  226. pz_core/runtime/health/__init__.py +5 -0
  227. pz_core/runtime/health/service.py +102 -0
  228. pz_core/runtime/host.py +188 -0
  229. pz_core/runtime/host_runner.py +282 -0
  230. pz_core/runtime/http/__init__.py +20 -0
  231. pz_core/runtime/http/app.py +576 -0
  232. pz_core/runtime/http_timing.py +173 -0
  233. pz_core/runtime/invocation.py +29 -0
  234. pz_core/runtime/json_safe.py +47 -0
  235. pz_core/runtime/lifecycle/__init__.py +25 -0
  236. pz_core/runtime/lifecycle/manager.py +533 -0
  237. pz_core/runtime/plan.py +214 -0
  238. pz_core/runtime/plan_context.py +33 -0
  239. pz_core/runtime/plan_snapshot.py +177 -0
  240. pz_core/runtime/registries/__init__.py +27 -0
  241. pz_core/runtime/registries/capabilities.py +101 -0
  242. pz_core/runtime/registries/providers.py +396 -0
  243. pz_core/runtime/resource_assets.py +344 -0
  244. pz_core/runtime/run_cli.py +211 -0
  245. pz_core/runtime/settings.py +888 -0
  246. pz_core/runtime/worker/__init__.py +21 -0
  247. pz_core/runtime/worker/engine.py +921 -0
  248. pz_core/runtime/worker/runtime.py +25 -0
  249. pz_core/testing/__init__.py +15 -0
  250. pz_core/testing/doc_route_contracts.py +305 -0
  251. pz_core/utils/__init__.py +44 -0
  252. pz_core/utils/env.py +65 -0
  253. pz_core/utils/project_jsonc/__init__.py +29 -0
  254. pz_core/utils/project_jsonc/document.py +420 -0
  255. pz_core/utils/project_settings.py +99 -0
  256. pz_foundation/__init__.py +50 -0
  257. pz_foundation/_contracts.py +81 -0
  258. pz_foundation/_errors.py +34 -0
  259. pz_foundation/api.py +14 -0
  260. pz_foundation/apps.py +127 -0
  261. pz_foundation/auth/__init__.py +1 -0
  262. pz_foundation/cache_control.py +233 -0
  263. pz_foundation/cli/__init__.py +2 -0
  264. pz_foundation/cli/workers.py +565 -0
  265. pz_foundation/configs/__init__.py +0 -0
  266. pz_foundation/configs/default.env +15 -0
  267. pz_foundation/configs/default.jsonc +452 -0
  268. pz_foundation/db/__init__.py +94 -0
  269. pz_foundation/db/aerich_executor.py +664 -0
  270. pz_foundation/db/config.py +358 -0
  271. pz_foundation/db/connection_manager.py +689 -0
  272. pz_foundation/db/connection_probe.py +204 -0
  273. pz_foundation/db/database.py +1291 -0
  274. pz_foundation/db/deps.py +47 -0
  275. pz_foundation/db/lifecycle_status.py +85 -0
  276. pz_foundation/db/migrations.py +477 -0
  277. pz_foundation/db/mysql.py +57 -0
  278. pz_foundation/db/postgresql.py +409 -0
  279. pz_foundation/db/production_pool_fallback.py +143 -0
  280. pz_foundation/db/public.py +45 -0
  281. pz_foundation/db/session.py +369 -0
  282. pz_foundation/db/sqlite.py +15 -0
  283. pz_foundation/db/tortoise_manager.py +724 -0
  284. pz_foundation/db/tortoise_psycopg.py +148 -0
  285. pz_foundation/docs/README.md +18 -0
  286. pz_foundation/docs/distribution-README.md +319 -0
  287. pz_foundation/docs/infra/350/243/201/345/211/252/344/270/216/350/220/275/344/275/215/350/257/264/346/230/216.md +39 -0
  288. pz_foundation/docs//345/205/254/345/274/200/345/205/215/350/264/271/346/216/245/345/217/243/351/230/262/346/273/245/347/224/250/344/270/216/344/272/272/346/234/272/346/240/241/351/252/214.md +680 -0
  289. pz_foundation/docs//350/256/276/350/256/241/346/226/207/346/241/243.md +288 -0
  290. pz_foundation/docs//350/267/257/347/224/261/344/270/216URL/345/244/204/347/220/206/350/247/204/350/214/203.md +119 -0
  291. pz_foundation/health.py +105 -0
  292. pz_foundation/host/__init__.py +11 -0
  293. pz_foundation/host/builder.py +34 -0
  294. pz_foundation/host/cache_runtime.py +44 -0
  295. pz_foundation/host/db/__init__.py +17 -0
  296. pz_foundation/host/db/aerich_config.py +36 -0
  297. pz_foundation/host/db/config.py +106 -0
  298. pz_foundation/host/db/connection_manager.py +48 -0
  299. pz_foundation/host/db/deps.py +5 -0
  300. pz_foundation/host/db/orm_retry_patch.py +566 -0
  301. pz_foundation/host/db/request_adapter.py +325 -0
  302. pz_foundation/host/db/router.py +28 -0
  303. pz_foundation/host/db/session.py +284 -0
  304. pz_foundation/host/db/tortoise_manager.py +10 -0
  305. pz_foundation/host/idempotency.py +132 -0
  306. pz_foundation/host/lifespan.py +247 -0
  307. pz_foundation/host/redis_runtime.py +100 -0
  308. pz_foundation/host/web.py +790 -0
  309. pz_foundation/infra_provider.py +236 -0
  310. pz_foundation/kv/__init__.py +25 -0
  311. pz_foundation/kv/cache.py +1166 -0
  312. pz_foundation/kv/deps.py +84 -0
  313. pz_foundation/kv/public.py +25 -0
  314. pz_foundation/kv/redis_manager.py +1531 -0
  315. pz_foundation/lazy_exports.py +43 -0
  316. pz_foundation/lazy_imports.py +124 -0
  317. pz_foundation/lifecycle.py +392 -0
  318. pz_foundation/managed_workers.py +1117 -0
  319. pz_foundation/middlewares/__init__.py +26 -0
  320. pz_foundation/middlewares/_settings.py +250 -0
  321. pz_foundation/middlewares/analytics.py +124 -0
  322. pz_foundation/middlewares/cache_control.py +156 -0
  323. pz_foundation/middlewares/compression.py +202 -0
  324. pz_foundation/middlewares/cors.py +62 -0
  325. pz_foundation/middlewares/geo_country.py +44 -0
  326. pz_foundation/middlewares/inner_router.py +50 -0
  327. pz_foundation/middlewares/rate_limited/__init__.py +3 -0
  328. pz_foundation/middlewares/rate_limited/key_limiter.py +115 -0
  329. pz_foundation/middlewares/request_logging.py +104 -0
  330. pz_foundation/middlewares/route_owner.py +309 -0
  331. pz_foundation/middlewares/security_csrf.py +44 -0
  332. pz_foundation/middlewares/security_headers/__init__.py +4 -0
  333. pz_foundation/middlewares/security_headers/conflicts.py +44 -0
  334. pz_foundation/middlewares/security_headers/csp_nonce.py +200 -0
  335. pz_foundation/middlewares/security_headers/security_headers.py +44 -0
  336. pz_foundation/migrations/__init__.py +1 -0
  337. pz_foundation/migrations/pz_foundation/__init__.py +1 -0
  338. pz_foundation/models.py +22 -0
  339. pz_foundation/notifications/__init__.py +23 -0
  340. pz_foundation/notifications/email.py +542 -0
  341. pz_foundation/notifications/public.py +31 -0
  342. pz_foundation/observability/__init__.py +71 -0
  343. pz_foundation/observability/http.py +34 -0
  344. pz_foundation/observability/logger/__init__.py +49 -0
  345. pz_foundation/observability/logger/_bot_settings.py +126 -0
  346. pz_foundation/observability/logger/bot_message.py +592 -0
  347. pz_foundation/observability/logger/core.py +385 -0
  348. pz_foundation/observability/logger/delivery_audit.py +84 -0
  349. pz_foundation/observability/logger/feishu_bot.py +199 -0
  350. pz_foundation/observability/logger/telegram_bot.py +194 -0
  351. pz_foundation/observability/metrics.py +365 -0
  352. pz_foundation/observability/runtime.py +68 -0
  353. pz_foundation/public.py +405 -0
  354. pz_foundation/resources/__init__.py +12 -0
  355. pz_foundation/resources/builder.py +900 -0
  356. pz_foundation/resources/images.py +363 -0
  357. pz_foundation/root_cli.py +20 -0
  358. pz_foundation/routing/__init__.py +203 -0
  359. pz_foundation/routing/canonical.py +486 -0
  360. pz_foundation/routing/convertors.py +82 -0
  361. pz_foundation/routing/declarations.py +843 -0
  362. pz_foundation/routing/exports.py +148 -0
  363. pz_foundation/routing/loaders.py +138 -0
  364. pz_foundation/routing/manifest.py +215 -0
  365. pz_foundation/routing/manifest_provider.py +47 -0
  366. pz_foundation/routing/mounts.py +101 -0
  367. pz_foundation/routing/ordering.py +172 -0
  368. pz_foundation/routing/registry.py +133 -0
  369. pz_foundation/routing/utils.py +28 -0
  370. pz_foundation/runtime.py +234 -0
  371. pz_foundation/settings.py +253 -0
  372. pz_foundation/settings_normalization.py +130 -0
  373. pz_foundation/storage/__init__.py +25 -0
  374. pz_foundation/storage/local.py +209 -0
  375. pz_foundation/storage/public.py +106 -0
  376. pz_foundation/storage/s3.py +116 -0
  377. pz_foundation/storage/service.py +234 -0
  378. pz_foundation/urls.py +19 -0
  379. pz_foundation/utils/__init__.py +6 -0
  380. pz_foundation/utils/merge.py +160 -0
  381. pz_foundation/worker_replicas.py +55 -0
  382. pz_settings_center/__init__.py +5 -0
  383. pz_settings_center/api/__init__.py +5 -0
  384. pz_settings_center/api/runtime_overrides.py +360 -0
  385. pz_settings_center/apps.py +28 -0
  386. pz_settings_center/configs/__init__.py +0 -0
  387. pz_settings_center/configs/default.env +2 -0
  388. pz_settings_center/configs/default.jsonc +1 -0
  389. pz_settings_center/docs/01-settings_center/350/203/275/345/212/233/350/276/271/347/225/214.md +124 -0
  390. pz_settings_center/docs/README.md +12 -0
  391. pz_settings_center/docs/distribution-README.md +150 -0
  392. pz_settings_center/docs//350/256/276/350/256/241/346/226/207/346/241/243.md +48 -0
  393. pz_settings_center/integration_payload.py +18 -0
  394. pz_settings_center/project_helpers.py +405 -0
  395. pz_settings_center/public.py +139 -0
  396. pz_settings_center/routing_helpers.py +73 -0
  397. pz_settings_center/runtime_override_provider.py +61 -0
  398. pz_settings_center/typed_settings.py +324 -0
  399. pz_settings_center/urls.py +9 -0
@@ -0,0 +1,786 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ from pathlib import Path
6
+ import tomllib
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from pz_core.assembly.app_discovery import (
10
+ discover_declared_app_references,
11
+ resolve_declared_app_import_roots,
12
+ resolve_declared_app_id,
13
+ resolve_package_root,
14
+ )
15
+ from pz_core.assembly.project import active_project_import_root
16
+ from pz_core.assembly.settings.host_project_overrides import (
17
+ apply_host_project_env_overrides,
18
+ )
19
+ from pz_core.core.errors import PzError
20
+ from pz_core.runtime.environment_integrity import (
21
+ validate_installed_app_identities,
22
+ validate_process_environment,
23
+ validate_project_config_identity,
24
+ )
25
+ from pz_core.utils import PROJECT_JSONC_NAME, iter_env_items, load_jsonc_file
26
+ if TYPE_CHECKING:
27
+ from pz_core.assembly.project import ProjectContext
28
+
29
+
30
+ PROJECT_JSONC_FILE_NAME = PROJECT_JSONC_NAME
31
+ DEFAULT_PZ_ENV_NAME = "develop"
32
+ PZ_ENV_SELECTOR_ENV_NAME = "PZ_ENV"
33
+ _LEGACY_ENV_FIELD = "pyz" + "za_env"
34
+
35
+
36
+ def _normalize_env_value(value: Any) -> Any:
37
+ if not isinstance(value, str):
38
+ return value
39
+ text = value.strip()
40
+ if not text:
41
+ return text
42
+ if text[:1] in {"'", '"'} and text[-1:] == text[:1] and len(text) >= 2:
43
+ return text[1:-1]
44
+ return text
45
+
46
+
47
+ def _read_dotenv(path: Path) -> dict[str, str]:
48
+ data: dict[str, str] = {}
49
+ if not path.is_file():
50
+ return data
51
+ for line_number, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
52
+ line = raw_line.strip()
53
+ if not line or line.startswith("#"):
54
+ continue
55
+ if line.startswith("export "):
56
+ line = line[7:].strip()
57
+ if "=" not in line:
58
+ raise PzError(
59
+ code="PZ_DOTENV_INVALID",
60
+ message=f"Invalid .env entry at line {line_number}: missing '=' delimiter.",
61
+ stage="settings",
62
+ owner="runtime.settings",
63
+ action="read_dotenv",
64
+ )
65
+ key, value = line.split("=", 1)
66
+ key = key.strip()
67
+ value = value.strip()
68
+ if not key:
69
+ raise PzError(
70
+ code="PZ_DOTENV_INVALID",
71
+ message=f"Invalid .env entry at line {line_number}: key must be non-empty.",
72
+ stage="settings",
73
+ owner="runtime.settings",
74
+ action="read_dotenv",
75
+ )
76
+ if value[:1] in {"'", '"'}:
77
+ quote = value[0]
78
+ end_index = value.find(quote, 1)
79
+ if end_index == -1:
80
+ raise PzError(
81
+ code="PZ_DOTENV_INVALID",
82
+ message=f"Invalid .env entry at line {line_number}: unterminated quoted value.",
83
+ stage="settings",
84
+ owner="runtime.settings",
85
+ action="read_dotenv",
86
+ )
87
+ trailing = value[end_index + 1 :].strip()
88
+ if trailing and not trailing.startswith("#"):
89
+ raise PzError(
90
+ code="PZ_DOTENV_INVALID",
91
+ message=f"Invalid .env entry at line {line_number}: unexpected trailing characters.",
92
+ stage="settings",
93
+ owner="runtime.settings",
94
+ action="read_dotenv",
95
+ )
96
+ value = value[1:end_index]
97
+ elif " #" in value:
98
+ value = value.split(" #", 1)[0].rstrip()
99
+ if key in data:
100
+ raise PzError(
101
+ code="PZ_DOTENV_INVALID",
102
+ message=f"Invalid .env entry at line {line_number}: duplicate key '{key}'.",
103
+ stage="settings",
104
+ owner="runtime.settings",
105
+ action="read_dotenv",
106
+ )
107
+ data[key] = value
108
+ return data
109
+
110
+
111
+ def _explicit_pz_env_snapshot() -> dict[str, str]:
112
+ snapshot = {
113
+ str(key): str(_normalize_env_value(value))
114
+ for key, value in iter_env_items()
115
+ }
116
+ validate_process_environment(snapshot)
117
+ return snapshot
118
+
119
+
120
+ def _pz_env_env_file_path(project: "ProjectContext", pz_env: str | None) -> Path | None:
121
+ pz_env_name = str(pz_env or "").strip()
122
+ if not pz_env_name:
123
+ return None
124
+ return project.project_dir / f".env.{pz_env_name}"
125
+
126
+
127
+ def load_project_env_file(
128
+ project: "ProjectContext",
129
+ *,
130
+ pz_env: str | None = None,
131
+ ) -> dict[str, str]:
132
+ if pz_env is None:
133
+ pz_env = _resolve_pz_env_name(_explicit_pz_env_snapshot())
134
+ payload = {
135
+ str(key): str(_normalize_env_value(value))
136
+ for key, value in _read_dotenv(project.env_file_path).items()
137
+ }
138
+ env_file_path = _pz_env_env_file_path(project, pz_env)
139
+ if env_file_path is not None:
140
+ for key, value in _read_dotenv(env_file_path).items():
141
+ payload[str(key)] = str(_normalize_env_value(value))
142
+ validate_process_environment(payload)
143
+ return payload
144
+
145
+
146
+ def _deep_merge(base: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]:
147
+ merged = dict(base)
148
+ for key, value in incoming.items():
149
+ if isinstance(value, dict) and isinstance(merged.get(key), dict):
150
+ merged[key] = _deep_merge(merged[key], value)
151
+ continue
152
+ merged[key] = value
153
+ return merged
154
+
155
+
156
+ def _read_required_string(
157
+ value: Any,
158
+ *,
159
+ code: str,
160
+ message: str,
161
+ owner: str,
162
+ action: str,
163
+ ) -> str:
164
+ if not isinstance(value, str) or not value.strip():
165
+ raise PzError(
166
+ code=code,
167
+ message=message,
168
+ stage="settings",
169
+ owner=owner,
170
+ action=action,
171
+ )
172
+ return value.strip()
173
+
174
+
175
+ def _read_app_module_path(
176
+ value: Any,
177
+ *,
178
+ owner: str,
179
+ action: str,
180
+ ) -> str:
181
+ normalized = _read_required_string(
182
+ value,
183
+ code="PZ_APP_PATH_INVALID",
184
+ message="App module path must be declared as a non-empty string.",
185
+ owner=owner,
186
+ action=action,
187
+ )
188
+ if ":" in normalized or normalized.endswith(".apps"):
189
+ raise PzError(
190
+ code="PZ_APP_PATH_INVALID",
191
+ message=(
192
+ f"App path '{normalized}' must declare a directory/package name only; "
193
+ "installed_apps does not accept '<pkg>.apps' or '<pkg>.apps:app'."
194
+ ),
195
+ stage="settings",
196
+ owner=owner,
197
+ action=action,
198
+ )
199
+ return normalized
200
+
201
+
202
+ def _read_optional_pz_env_name(
203
+ value: Any,
204
+ *,
205
+ owner: str,
206
+ action: str,
207
+ ) -> str | None:
208
+ if value is None:
209
+ return None
210
+ if not isinstance(value, str) or not value.strip():
211
+ raise PzError(
212
+ code="PZ_ENV_INVALID",
213
+ message="pz_env must be declared as a non-empty string when configured.",
214
+ stage="settings",
215
+ owner=owner,
216
+ action=action,
217
+ )
218
+ return value.strip()
219
+
220
+
221
+ def _package_root(module_path: str) -> Path | None:
222
+ return resolve_package_root(
223
+ _read_app_module_path(
224
+ module_path,
225
+ owner="settings",
226
+ action="package_root",
227
+ )
228
+ )
229
+
230
+
231
+ def _resolve_pz_env_name(env: dict[str, str]) -> str:
232
+ selected = _read_optional_pz_env_name(
233
+ env.get(PZ_ENV_SELECTOR_ENV_NAME),
234
+ owner=PZ_ENV_SELECTOR_ENV_NAME,
235
+ action="compile_settings_snapshot",
236
+ )
237
+ if selected is None:
238
+ return DEFAULT_PZ_ENV_NAME
239
+ return selected
240
+
241
+
242
+ def _is_non_empty_mapping(payload: dict[str, Any]) -> bool:
243
+ return bool(payload)
244
+
245
+
246
+ def _read_project_root_config(project: "ProjectContext") -> dict[str, Any]:
247
+ if not project.project_config_path.is_file():
248
+ return {}
249
+ return load_jsonc_file(
250
+ project.project_config_path,
251
+ code="PZ_PROJECT_CONFIG_INVALID",
252
+ owner="runtime.settings",
253
+ action="read_project_config",
254
+ )
255
+
256
+
257
+ def _reject_legacy_pz_env_selectors(
258
+ data: dict[str, Any],
259
+ *,
260
+ action: str,
261
+ selected_pz_env: str | None = None,
262
+ ) -> None:
263
+ legacy_paths: list[str] = []
264
+ if "environment" in data:
265
+ legacy_paths.append("environment")
266
+ if _LEGACY_ENV_FIELD in data:
267
+ legacy_paths.append(_LEGACY_ENV_FIELD)
268
+ if "pz_env" in data:
269
+ legacy_paths.append("pz_env")
270
+ project = data.get("project")
271
+ if isinstance(project, dict):
272
+ if "environment" in project:
273
+ legacy_paths.append("project.environment")
274
+ project_pz_env = project.get("pz_env")
275
+ allow_derived_project_pz_env = (
276
+ selected_pz_env is not None
277
+ and str(project_pz_env).strip() == str(selected_pz_env).strip()
278
+ )
279
+ if _LEGACY_ENV_FIELD in project:
280
+ legacy_paths.append(f"project.{_LEGACY_ENV_FIELD}")
281
+ if "pz_env" in project and not allow_derived_project_pz_env:
282
+ legacy_paths.append("project.pz_env")
283
+ if "env" in project:
284
+ legacy_paths.append("project.env")
285
+ if legacy_paths:
286
+ joined = ", ".join(f"`{item}`" for item in legacy_paths)
287
+ raise PzError(
288
+ code="PZ_PROJECT_CONFIG_INVALID",
289
+ message=f"Project environment selector keys are not allowed: {joined}. Use `PZ_ENV` instead.",
290
+ stage="settings",
291
+ owner="runtime.settings",
292
+ action=action,
293
+ )
294
+
295
+
296
+ def _apply_project_http_runtime_settings(project: dict[str, Any]) -> None:
297
+ pz_env = str(project.get("pz_env") or DEFAULT_PZ_ENV_NAME).strip().lower()
298
+ declared_http = project.get("http")
299
+ http_payload = dict(declared_http) if isinstance(declared_http, dict) else {}
300
+ defaults: dict[str, Any]
301
+ if pz_env == "product":
302
+ defaults = {
303
+ "openapi_url": None,
304
+ "docs_url": None,
305
+ "redoc_url": None,
306
+ }
307
+ else:
308
+ defaults = {
309
+ "openapi_url": "/openapi.json",
310
+ "docs_url": "/docs",
311
+ "redoc_url": "/redoc",
312
+ }
313
+ project["http"] = _deep_merge(defaults, http_payload)
314
+
315
+
316
+ def _pz_env_config_candidates(project: "ProjectContext", pz_env: str) -> tuple[Path, ...]:
317
+ pz_env_name = str(pz_env or "").strip()
318
+ if not pz_env_name:
319
+ return tuple()
320
+ return (project.project_dir / f"pz.{pz_env_name}.jsonc",)
321
+
322
+
323
+ def _read_pz_env_override_config(project: "ProjectContext", pz_env: str) -> dict[str, Any]:
324
+ validate_project_config_identity(project.project_dir)
325
+ for path in _pz_env_config_candidates(project, pz_env):
326
+ payload = load_jsonc_file(
327
+ path,
328
+ code="PZ_PROJECT_CONFIG_INVALID",
329
+ owner="runtime.settings",
330
+ action="read_pz_env_config",
331
+ )
332
+ if payload:
333
+ return payload
334
+ return {}
335
+
336
+
337
+ def _parse_env_value(raw: str) -> Any:
338
+ if not isinstance(raw, str):
339
+ raise PzError(
340
+ code="ENV_VALUE_INVALID",
341
+ message="Env override value must be a string.",
342
+ stage="settings",
343
+ owner="runtime.settings",
344
+ action="parse_env_value",
345
+ )
346
+ value = raw.strip()
347
+ lowered = value.lower()
348
+ if lowered == "true":
349
+ return True
350
+ if lowered == "false":
351
+ return False
352
+ if lowered == "null":
353
+ return None
354
+ if value.startswith("[") or value.startswith("{") or value.startswith('"') or value.startswith("'"):
355
+ try:
356
+ return tomllib.loads(f"v = {value}")["v"]
357
+ except Exception as exc:
358
+ if value.startswith("[") or value.startswith("{"):
359
+ try:
360
+ return json.loads(value)
361
+ except Exception:
362
+ pass
363
+ raise PzError(
364
+ code="ENV_VALUE_INVALID",
365
+ message="Env override value looks structured but could not be parsed.",
366
+ stage="settings",
367
+ owner="runtime.settings",
368
+ action="parse_env_value",
369
+ ) from exc
370
+ try:
371
+ return int(value)
372
+ except ValueError:
373
+ pass
374
+ try:
375
+ return float(value)
376
+ except ValueError:
377
+ pass
378
+ return value
379
+
380
+
381
+ def _set_nested(mapping: dict[str, Any], path: tuple[str, ...], value: Any) -> None:
382
+ def _resolve_existing_key(current: dict[str, Any], key: str) -> str:
383
+ if key in current:
384
+ return key
385
+ lowered = str(key).lower()
386
+ for existing in current.keys():
387
+ if isinstance(existing, str) and existing.lower() == lowered:
388
+ return existing
389
+ return key
390
+
391
+ current = mapping
392
+ for key in path[:-1]:
393
+ resolved_key = _resolve_existing_key(current, key)
394
+ child = current.get(resolved_key)
395
+ if not isinstance(child, dict):
396
+ child = {}
397
+ current[resolved_key] = child
398
+ current = child
399
+ current[_resolve_existing_key(current, path[-1])] = value
400
+
401
+
402
+ _CANONICAL_TOP_LEVEL_ENV_FIELDS: dict[str, tuple[str, ...]] = {
403
+ "cache_dir": ("cache_dir",),
404
+ "runtime_dir": ("runtime_dir",),
405
+ "site_theme": ("site_theme",),
406
+ "site_theme_by_lang": ("site_theme_by_lang",),
407
+ "site_theme_by_country": ("site_theme_by_country",),
408
+ }
409
+
410
+ _TOP_LEVEL_SECTION_PREFIXES = (
411
+ "project",
412
+ "routing",
413
+ )
414
+
415
+
416
+ def _read_top_level_env_override_path(key: str, *, owner: str) -> tuple[str, ...]:
417
+ nested_path: list[str] = []
418
+ for index, raw_part in enumerate(key.split("__")):
419
+ if not isinstance(raw_part, str):
420
+ raise PzError(
421
+ code="ENV_KEY_INVALID",
422
+ message=f"Env override path for '{owner}' must use string segments.",
423
+ stage="settings",
424
+ owner=owner,
425
+ action="compile_settings_snapshot",
426
+ )
427
+ part = raw_part.strip().lower()
428
+ if not part:
429
+ raise PzError(
430
+ code="ENV_KEY_INVALID",
431
+ message=f"Env override path for '{owner}' contains an empty segment at index {index}.",
432
+ stage="settings",
433
+ owner=owner,
434
+ action="compile_settings_snapshot",
435
+ )
436
+ nested_path.append(part)
437
+ return tuple(nested_path)
438
+
439
+
440
+ def _apply_top_level_env_overrides(data: dict[str, Any], env: dict[str, str]) -> None:
441
+ for key, path in _CANONICAL_TOP_LEVEL_ENV_FIELDS.items():
442
+ raw_value = env.get(key)
443
+ if raw_value is None:
444
+ continue
445
+ _set_nested(data, path, _parse_env_value(raw_value))
446
+
447
+ for prefix in _TOP_LEVEL_SECTION_PREFIXES:
448
+ token = prefix + "__"
449
+ for key, raw_value in env.items():
450
+ if not key.startswith(token):
451
+ continue
452
+ nested = _read_top_level_env_override_path(key[len(token) :], owner=prefix)
453
+ _set_nested(data, (prefix,) + nested, _parse_env_value(raw_value))
454
+
455
+
456
+ def _read_env_override_path(key: str, *, app_id: str) -> tuple[str, ...]:
457
+ nested_path: list[str] = []
458
+ for index, raw_part in enumerate(key.split("__")):
459
+ if not isinstance(raw_part, str):
460
+ raise PzError(
461
+ code="PZ_APP_ENV_KEY_INVALID",
462
+ message=f"Env override path for app '{app_id}' must use string segments.",
463
+ stage="settings",
464
+ owner=app_id,
465
+ action="compile_settings_snapshot",
466
+ )
467
+ part = raw_part.strip().lower()
468
+ if not part:
469
+ raise PzError(
470
+ code="PZ_APP_ENV_KEY_INVALID",
471
+ message=(
472
+ f"Env override path for app '{app_id}' contains an empty segment "
473
+ f"at index {index}."
474
+ ),
475
+ stage="settings",
476
+ owner=app_id,
477
+ action="compile_settings_snapshot",
478
+ )
479
+ nested_path.append(part)
480
+ return tuple(nested_path)
481
+
482
+
483
+ def _configured_app_settings_map(data: dict[str, Any]) -> dict[str, Any]:
484
+ configured_apps = data.get("apps")
485
+ if configured_apps is None:
486
+ return {}
487
+ if isinstance(configured_apps, dict):
488
+ return dict(configured_apps)
489
+ raise PzError(
490
+ code="PZ_APPS_NAMESPACE_INVALID",
491
+ message="apps must be declared as a table keyed by app_id.",
492
+ stage="settings",
493
+ owner="apps",
494
+ action="compile_settings_snapshot",
495
+ )
496
+
497
+
498
+ def _try_app_id_from_module_path(module_path: str) -> str | None:
499
+ normalized = _read_app_module_path(
500
+ module_path,
501
+ owner="installed_apps",
502
+ action="compile_settings_snapshot",
503
+ )
504
+ package_root = _package_root(normalized)
505
+ try:
506
+ app_id = resolve_declared_app_id(normalized)
507
+ except PzError as exc:
508
+ if exc.code == "PZ_APP_IMPORT_FAILED":
509
+ if package_root is None:
510
+ return None
511
+ raise
512
+ return app_id or None
513
+
514
+
515
+ def _merge_app_settings(
516
+ configured_map: dict[str, Any],
517
+ env: dict[str, str],
518
+ installed_apps: tuple[str, ...],
519
+ ) -> dict[str, dict[str, Any]]:
520
+ app_settings: dict[str, dict[str, Any]] = {}
521
+ candidate_app_ids: list[str] = []
522
+ module_paths_by_app_id: dict[str, list[str]] = {}
523
+
524
+ for module_path in installed_apps:
525
+ normalized = _read_app_module_path(
526
+ module_path,
527
+ owner="installed_apps",
528
+ action="compile_settings_snapshot",
529
+ )
530
+ app_id = _try_app_id_from_module_path(normalized)
531
+ if app_id is None:
532
+ continue
533
+ if app_id not in candidate_app_ids:
534
+ candidate_app_ids.append(app_id)
535
+ bucket = module_paths_by_app_id.setdefault(app_id, [])
536
+ if normalized not in bucket:
537
+ bucket.append(normalized)
538
+ for app_id in configured_map:
539
+ if app_id not in candidate_app_ids:
540
+ candidate_app_ids.append(app_id)
541
+
542
+ for app_id in candidate_app_ids:
543
+ explicit = configured_map.get(app_id)
544
+ if explicit is None:
545
+ app_settings.setdefault(app_id, {})
546
+ elif isinstance(explicit, dict):
547
+ app_settings[app_id] = _deep_merge(app_settings.get(app_id, {}), explicit)
548
+ else:
549
+ raise PzError(
550
+ code="PZ_APP_SETTINGS_ENTRY_INVALID",
551
+ message=f"apps.{app_id} must be declared as a table.",
552
+ stage="settings",
553
+ owner=app_id,
554
+ action="compile_settings_snapshot",
555
+ )
556
+
557
+ for key, raw_value in env.items():
558
+ for app_id in tuple(app_settings.keys()):
559
+ prefix = app_id.replace("-", "_") + "__"
560
+ if not key.startswith(prefix):
561
+ continue
562
+ nested_path = _read_env_override_path(key[len(prefix):], app_id=app_id)
563
+ if nested_path:
564
+ _set_nested(app_settings.setdefault(app_id, {}), nested_path, _parse_env_value(raw_value))
565
+ return {app_id: dict(values) for app_id, values in app_settings.items()}
566
+
567
+
568
+ def _resolve_declared_installed_apps(data: dict[str, Any]) -> tuple[str, ...]:
569
+ raw = data.get("installed_apps")
570
+ if raw in (None, (), []):
571
+ return tuple()
572
+ if not isinstance(raw, (list, tuple)):
573
+ raise PzError(
574
+ code="PZ_INSTALLED_APPS_INVALID",
575
+ message="installed_apps must be declared as an array of app module paths.",
576
+ stage="settings",
577
+ owner="installed_apps",
578
+ action="compile_settings_snapshot",
579
+ )
580
+ declared_apps: list[str] = []
581
+ for index, item in enumerate(raw):
582
+ declared_apps.append(
583
+ _read_required_string(
584
+ item,
585
+ code="PZ_APP_PATH_INVALID",
586
+ message=f"installed_apps[{index}] must declare a non-empty string app module path.",
587
+ owner="installed_apps",
588
+ action="compile_settings_snapshot",
589
+ )
590
+ )
591
+ resolved = tuple(declared_apps)
592
+ validate_installed_app_identities(resolved)
593
+ return resolved
594
+
595
+
596
+ def _resolve_installed_apps(
597
+ declared_apps: tuple[str, ...],
598
+ *,
599
+ project_dir: Path,
600
+ ) -> tuple[str, ...]:
601
+ return tuple(
602
+ item.module_path
603
+ for item in discover_declared_app_references(declared_apps, project_dir=project_dir)
604
+ )
605
+
606
+
607
+ def _resolve_import_roots(
608
+ declared_apps: tuple[str, ...],
609
+ *,
610
+ project_dir: Path,
611
+ ) -> tuple[Path, ...]:
612
+ return resolve_declared_app_import_roots(declared_apps, project_dir=project_dir)
613
+
614
+
615
+ def _resolve_app_replacements(data: dict[str, Any]) -> dict[str, str]:
616
+ raw = data.get("app_replacements")
617
+ if raw is None and isinstance(data.get("core"), dict):
618
+ raw = data["core"].get("app_replacements")
619
+ if raw in (None, {}):
620
+ return {}
621
+ if not isinstance(raw, dict):
622
+ raise PzError(
623
+ code="PZ_APP_REPLACEMENTS_INVALID",
624
+ message="app_replacements must be declared as a table of source to replacement paths.",
625
+ stage="settings",
626
+ owner="app_replacements",
627
+ action="compile_settings_snapshot",
628
+ )
629
+ out: dict[str, str] = {}
630
+ for source, target in raw.items():
631
+ src = _read_required_string(
632
+ source,
633
+ code="PZ_APP_REPLACEMENT_ENTRY_INVALID",
634
+ message="app_replacements entries must declare source as a non-empty string path.",
635
+ owner="app_replacements",
636
+ action="compile_settings_snapshot",
637
+ )
638
+ dst = _read_required_string(
639
+ target,
640
+ code="PZ_APP_REPLACEMENT_ENTRY_INVALID",
641
+ message="app_replacements entries must declare replacement as a non-empty string path.",
642
+ owner="app_replacements",
643
+ action="compile_settings_snapshot",
644
+ )
645
+ out[src] = dst
646
+ return out
647
+
648
+
649
+ @dataclass(frozen=True, slots=True)
650
+ class SettingsSnapshot:
651
+ project_dir: Path
652
+ data: dict[str, Any]
653
+ installed_apps: tuple[str, ...]
654
+ app_replacements: dict[str, str]
655
+ apps: dict[str, dict[str, Any]]
656
+ declared_installed_apps: tuple[str, ...] = ()
657
+ import_roots: tuple[Path, ...] = ()
658
+
659
+ def get(self, *path: str, default: Any = None) -> Any:
660
+ current: Any = self.data
661
+ for key in path:
662
+ if not isinstance(current, dict):
663
+ return default
664
+ current = current.get(key)
665
+ return current if current is not None else default
666
+
667
+ def with_installed_apps(self, installed_apps: tuple[str, ...]) -> "SettingsSnapshot":
668
+ declared_apps: list[str] = []
669
+ for index, item in enumerate(installed_apps):
670
+ normalized = _read_required_string(
671
+ item,
672
+ code="PZ_APP_PATH_INVALID",
673
+ message=f"installed_apps[{index}] must declare a non-empty string app module path.",
674
+ owner="installed_apps",
675
+ action="with_installed_apps",
676
+ )
677
+ declared_apps.append(normalized)
678
+ validate_installed_app_identities(declared_apps)
679
+ normalized_apps = _resolve_installed_apps(tuple(declared_apps), project_dir=self.project_dir)
680
+ validate_installed_app_identities(normalized_apps)
681
+ import_roots = _resolve_import_roots(tuple(declared_apps), project_dir=self.project_dir)
682
+ with active_project_import_root(self.project_dir, extra_import_roots=import_roots):
683
+ app_settings = _merge_app_settings(
684
+ dict(self.apps),
685
+ {},
686
+ tuple(normalized_apps),
687
+ )
688
+ data = dict(self.data)
689
+ data["apps"] = app_settings
690
+ return SettingsSnapshot(
691
+ project_dir=self.project_dir,
692
+ data=data,
693
+ installed_apps=tuple(normalized_apps),
694
+ import_roots=tuple(import_roots),
695
+ app_replacements=dict(self.app_replacements),
696
+ apps=app_settings,
697
+ declared_installed_apps=tuple(declared_apps),
698
+ )
699
+
700
+ def app_settings(self, app_id: str) -> dict[str, Any]:
701
+ return dict(self.apps.get(app_id, {}))
702
+
703
+
704
+ def compile_settings_snapshot_from_payload(
705
+ project_dir: Path | str,
706
+ payload: dict[str, Any],
707
+ *,
708
+ env: dict[str, str] | None = None,
709
+ selected_pz_env: str | None = None,
710
+ ) -> SettingsSnapshot:
711
+ resolved_project_dir = Path(project_dir).expanduser().resolve()
712
+ merged = _deep_merge({}, payload)
713
+ _reject_legacy_pz_env_selectors(
714
+ merged,
715
+ action="compile_settings_snapshot",
716
+ selected_pz_env=selected_pz_env,
717
+ )
718
+
719
+ env_snapshot = {
720
+ str(key): str(_normalize_env_value(value))
721
+ for key, value in dict(env or {}).items()
722
+ if value is not None
723
+ }
724
+ validate_process_environment(env_snapshot)
725
+ _apply_top_level_env_overrides(merged, env_snapshot)
726
+ apply_host_project_env_overrides(merged, env_snapshot)
727
+
728
+ declared_installed_apps = _resolve_declared_installed_apps(merged)
729
+ configured_apps = _configured_app_settings_map(merged)
730
+ installed_apps = _resolve_installed_apps(declared_installed_apps, project_dir=resolved_project_dir)
731
+ validate_installed_app_identities(installed_apps)
732
+ import_roots = _resolve_import_roots(declared_installed_apps, project_dir=resolved_project_dir)
733
+ app_replacements = _resolve_app_replacements(merged)
734
+ if selected_pz_env:
735
+ project_namespace = merged.get("project")
736
+ if not isinstance(project_namespace, dict):
737
+ project_namespace = {}
738
+ merged["project"] = project_namespace
739
+ project_namespace["pz_env"] = selected_pz_env
740
+ project_namespace = merged.get("project")
741
+ if not isinstance(project_namespace, dict):
742
+ project_namespace = {}
743
+ merged["project"] = project_namespace
744
+ _apply_project_http_runtime_settings(project_namespace)
745
+ with active_project_import_root(resolved_project_dir, extra_import_roots=import_roots):
746
+ app_settings = _merge_app_settings(
747
+ configured_apps,
748
+ env_snapshot,
749
+ installed_apps,
750
+ )
751
+ merged["apps"] = app_settings
752
+ return SettingsSnapshot(
753
+ project_dir=resolved_project_dir,
754
+ data=dict(merged),
755
+ installed_apps=installed_apps,
756
+ import_roots=import_roots,
757
+ app_replacements=dict(app_replacements),
758
+ apps=app_settings,
759
+ declared_installed_apps=declared_installed_apps,
760
+ )
761
+
762
+
763
+ def compile_settings_snapshot(project: "ProjectContext") -> SettingsSnapshot:
764
+ explicit_env_snapshot = _explicit_pz_env_snapshot()
765
+ pz_env = _resolve_pz_env_name(explicit_env_snapshot)
766
+
767
+ env_snapshot = load_project_env_file(project, pz_env=pz_env)
768
+ for key, value in explicit_env_snapshot.items():
769
+ env_snapshot[key] = _normalize_env_value(value)
770
+
771
+ merged: dict[str, Any] = _read_project_root_config(project)
772
+ _reject_legacy_pz_env_selectors(merged, action="read_project_config")
773
+
774
+ merged = _deep_merge(merged, _read_pz_env_override_config(project, pz_env))
775
+ project_namespace = merged.get("project")
776
+ if not isinstance(project_namespace, dict):
777
+ project_namespace = {}
778
+ merged["project"] = project_namespace
779
+ project_namespace["pz_env"] = pz_env
780
+
781
+ return compile_settings_snapshot_from_payload(
782
+ project.project_dir,
783
+ merged,
784
+ env={key: str(value) for key, value in env_snapshot.items()},
785
+ selected_pz_env=pz_env,
786
+ )