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,916 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import tomllib
5
+ import uuid
6
+ from collections.abc import Mapping, Sequence
7
+ from dataclasses import dataclass
8
+ from pathlib import Path, PurePosixPath
9
+
10
+ from pz_core.authoring.dev_manifest import (
11
+ DevBinding,
12
+ DevManifest,
13
+ DevRepo,
14
+ validate_dev_manifest,
15
+ )
16
+ from pz_core.runtime.errors import PzError
17
+
18
+
19
+ BINDING_CONFLICT_CODE = "PZ_DEV_BINDING_CONFLICT"
20
+ BINDING_OVERLAP_CODE = "PZ_DEV_BINDING_OVERLAP"
21
+ BINDING_SOURCE_CONFLICT_CODE = "PZ_DEV_BINDING_SOURCE_CONFLICT"
22
+ BINDING_INVALID_PROJECTION_CODE = "PZ_DEV_BINDING_INVALID_PROJECTION"
23
+ _OWNER = "pz_core.authoring.dev_bindings"
24
+ _RESOLUTION_ERRORS = (OSError, RuntimeError)
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class LinkMutation:
29
+ final_path: Path
30
+ staged_path: Path
31
+ relative_target: str
32
+ previous_target: str | None
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class LinkRemoval:
37
+ final_path: Path
38
+ previous_target: str
39
+ expected_target: Path
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class _Projection:
44
+ key: str
45
+ binding: DevBinding
46
+ final_path: Path
47
+ target_path: Path
48
+ target_realpath: Path
49
+ base_realpath: Path
50
+ relative_target: str
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class _Preflight:
55
+ before: Mapping[Path, _Projection]
56
+ after: Mapping[Path, _Projection]
57
+
58
+
59
+ def _error(
60
+ code: str, message: str, *, action: str, detail: str | None = None
61
+ ) -> PzError:
62
+ return PzError(
63
+ code=code,
64
+ message=message,
65
+ stage="dev",
66
+ owner=_OWNER,
67
+ action=action,
68
+ detail=detail,
69
+ )
70
+
71
+
72
+ def _lexists(path: Path) -> bool:
73
+ return os.path.lexists(path)
74
+
75
+
76
+ def _paths_overlap(first: Path, second: Path) -> bool:
77
+ return first == second or first in second.parents or second in first.parents
78
+
79
+
80
+ def _join_posix(base: Path, child: PurePosixPath) -> Path:
81
+ if child == PurePosixPath("."):
82
+ return base
83
+ return base / Path(*child.parts)
84
+
85
+
86
+ def _declared_source_path(
87
+ project_dir: Path,
88
+ binding: DevBinding,
89
+ repos: Mapping[str, DevRepo],
90
+ ) -> Path | None:
91
+ if binding.source_kind == "git":
92
+ if not binding.repo_key:
93
+ return None
94
+ repo = repos.get(binding.repo_key)
95
+ if repo is None:
96
+ return None
97
+ base = project_dir / Path(*repo.checkout.parts)
98
+ elif binding.source_kind == "link":
99
+ base = Path(binding.source)
100
+ else:
101
+ return None
102
+ return _join_posix(base, binding.subpath).resolve(strict=False)
103
+
104
+
105
+ def _check_granularity_overlap(
106
+ project_dir: Path,
107
+ before: DevManifest,
108
+ after: DevManifest,
109
+ ) -> None:
110
+ desired_bindings = sorted(after.bindings.items())
111
+ for index, (first_key, first) in enumerate(desired_bindings):
112
+ for second_key, second in desired_bindings[index + 1 :]:
113
+ if first.kind == second.kind:
114
+ continue
115
+ first_source = _declared_source_path(project_dir, first, after.repos)
116
+ second_source = _declared_source_path(project_dir, second, after.repos)
117
+ if (
118
+ first_source is None
119
+ or second_source is None
120
+ or not _paths_overlap(first_source, second_source)
121
+ ):
122
+ continue
123
+ raise _error(
124
+ BINDING_OVERLAP_CODE,
125
+ "directory and app bindings cannot expose overlapping sources",
126
+ action="plan_binding_projection",
127
+ detail=(
128
+ f"desired={first_key}:{first_source}; "
129
+ f"desired={second_key}:{second_source}"
130
+ ),
131
+ )
132
+
133
+ for before_key, existing in sorted(before.bindings.items()):
134
+ for after_key, desired in desired_bindings:
135
+ if existing.kind == desired.kind:
136
+ continue
137
+ existing_source = _declared_source_path(project_dir, existing, before.repos)
138
+ desired_source = _declared_source_path(project_dir, desired, after.repos)
139
+ if (
140
+ existing_source is None
141
+ or desired_source is None
142
+ or not _paths_overlap(existing_source, desired_source)
143
+ ):
144
+ continue
145
+ raise _error(
146
+ BINDING_OVERLAP_CODE,
147
+ "directory and app bindings cannot expose overlapping sources",
148
+ action="plan_binding_projection",
149
+ detail=(
150
+ f"before={before_key}:{existing_source}; "
151
+ f"after={after_key}:{desired_source}"
152
+ ),
153
+ )
154
+
155
+
156
+ def _projection(
157
+ project_dir: Path,
158
+ key: str,
159
+ binding: DevBinding,
160
+ repos: Mapping[str, DevRepo],
161
+ *,
162
+ action: str,
163
+ ) -> _Projection:
164
+ final_path = project_dir / Path(*binding.link.parts)
165
+ try:
166
+ parent_realpath = final_path.parent.resolve(strict=True)
167
+ except _RESOLUTION_ERRORS as exc:
168
+ raise _error(
169
+ BINDING_INVALID_PROJECTION_CODE,
170
+ f"binding parent does not exist: {final_path.parent}",
171
+ action=action,
172
+ detail=str(exc),
173
+ ) from exc
174
+ if not final_path.parent.is_dir() or not parent_realpath.is_relative_to(
175
+ project_dir
176
+ ):
177
+ raise _error(
178
+ BINDING_INVALID_PROJECTION_CODE,
179
+ f"binding parent escapes the project or is not a directory: {final_path.parent}",
180
+ action=action,
181
+ )
182
+
183
+ if binding.source_kind == "git":
184
+ assert binding.repo_key is not None
185
+ repo = repos[binding.repo_key]
186
+ base = project_dir / Path(*repo.checkout.parts)
187
+ else:
188
+ base = Path(binding.source)
189
+ target_path = _join_posix(base, binding.subpath)
190
+ try:
191
+ base_realpath = base.resolve(strict=True)
192
+ target_realpath = target_path.resolve(strict=True)
193
+ except _RESOLUTION_ERRORS as exc:
194
+ raise _error(
195
+ BINDING_INVALID_PROJECTION_CODE,
196
+ f"binding source target is missing or broken: {target_path}",
197
+ action=action,
198
+ detail=str(exc),
199
+ ) from exc
200
+ if not base_realpath.is_dir() or not target_realpath.is_dir():
201
+ raise _error(
202
+ BINDING_INVALID_PROJECTION_CODE,
203
+ f"binding source target must be a directory: {target_path}",
204
+ action=action,
205
+ )
206
+ if not target_realpath.is_relative_to(base_realpath):
207
+ raise _error(
208
+ BINDING_INVALID_PROJECTION_CODE,
209
+ f"binding source target escapes its declared source root: {target_path}",
210
+ action=action,
211
+ detail=f"source_root={base_realpath}; resolved_target={target_realpath}",
212
+ )
213
+
214
+ relative_target = Path(
215
+ os.path.relpath(target_path, start=final_path.parent)
216
+ ).as_posix()
217
+ if Path(relative_target).is_absolute() or "\\" in relative_target:
218
+ raise _error(
219
+ BINDING_INVALID_PROJECTION_CODE,
220
+ f"binding target is not a POSIX relative path: {relative_target}",
221
+ action=action,
222
+ )
223
+ return _Projection(
224
+ key=key,
225
+ binding=binding,
226
+ final_path=final_path,
227
+ target_path=target_path,
228
+ target_realpath=target_realpath,
229
+ base_realpath=base_realpath,
230
+ relative_target=relative_target,
231
+ )
232
+
233
+
234
+ def _projections(
235
+ project_dir: Path,
236
+ manifest: DevManifest,
237
+ *,
238
+ action: str,
239
+ ) -> dict[Path, _Projection]:
240
+ result: dict[Path, _Projection] = {}
241
+ for key, binding in sorted(manifest.bindings.items()):
242
+ item = _projection(
243
+ project_dir,
244
+ key,
245
+ binding,
246
+ manifest.repos,
247
+ action=action,
248
+ )
249
+ result[item.final_path] = item
250
+ return result
251
+
252
+
253
+ def _verify_exact_link(
254
+ projection: _Projection,
255
+ *,
256
+ action: str,
257
+ code: str = BINDING_INVALID_PROJECTION_CODE,
258
+ ) -> str:
259
+ path = projection.final_path
260
+ if not path.is_symlink():
261
+ state = "missing" if not _lexists(path) else "not a symbolic link"
262
+ raise _error(
263
+ code,
264
+ f"managed binding is {state}: {path}",
265
+ action=action,
266
+ )
267
+ try:
268
+ stored_target = os.readlink(path)
269
+ except _RESOLUTION_ERRORS as exc:
270
+ raise _error(
271
+ code,
272
+ f"cannot read managed binding: {path}",
273
+ action=action,
274
+ detail=str(exc),
275
+ ) from exc
276
+ if (
277
+ Path(stored_target).is_absolute()
278
+ or "\\" in stored_target
279
+ or stored_target != projection.relative_target
280
+ ):
281
+ raise _error(
282
+ code,
283
+ f"managed binding has an unexpected stored target: {path}",
284
+ action=action,
285
+ detail=(
286
+ f"expected={projection.relative_target!r}; actual={stored_target!r}"
287
+ ),
288
+ )
289
+ try:
290
+ actual_realpath = path.resolve(strict=True)
291
+ except _RESOLUTION_ERRORS as exc:
292
+ raise _error(
293
+ code,
294
+ f"managed binding is broken: {path}",
295
+ action=action,
296
+ detail=str(exc),
297
+ ) from exc
298
+ if actual_realpath != projection.target_realpath:
299
+ raise _error(
300
+ code,
301
+ f"managed binding resolves to the wrong source: {path}",
302
+ action=action,
303
+ detail=(f"expected={projection.target_realpath}; actual={actual_realpath}"),
304
+ )
305
+ return stored_target
306
+
307
+
308
+ def _distribution_name(pyproject: Path) -> str | None:
309
+ try:
310
+ payload = tomllib.loads(pyproject.read_text(encoding="utf-8"))
311
+ except (OSError, UnicodeError, tomllib.TOMLDecodeError):
312
+ return None
313
+ project = payload.get("project")
314
+ if not isinstance(project, dict):
315
+ return None
316
+ name = project.get("name")
317
+ if not isinstance(name, str) or not name.strip():
318
+ return None
319
+ return name.strip().replace("-", "_")
320
+
321
+
322
+ def _record_tree_distributions(
323
+ source_root: Path,
324
+ distributions: dict[str, set[Path]],
325
+ ) -> None:
326
+ for raw_root, dirnames, filenames in os.walk(source_root, followlinks=False):
327
+ dirnames[:] = [name for name in dirnames if not name.startswith(".")]
328
+ if "pyproject.toml" not in filenames:
329
+ continue
330
+ root = Path(raw_root)
331
+ distribution = _distribution_name(root / "pyproject.toml")
332
+ if distribution is not None:
333
+ distributions.setdefault(distribution, set()).add(root.resolve(strict=True))
334
+
335
+
336
+ def _visible_sources(
337
+ project_dir: Path,
338
+ managed_paths: set[Path],
339
+ ) -> tuple[dict[str, set[Path]], set[Path]]:
340
+ apps_root = project_dir / "apps"
341
+ distributions: dict[str, set[Path]] = {}
342
+ link_targets: set[Path] = set()
343
+ if not apps_root.is_dir():
344
+ return distributions, link_targets
345
+
346
+ for raw_root, dirnames, filenames in os.walk(apps_root, followlinks=False):
347
+ root = Path(raw_root)
348
+ if root == apps_root / ".sources" or (apps_root / ".sources") in root.parents:
349
+ dirnames[:] = []
350
+ continue
351
+ if root in managed_paths or any(
352
+ managed in root.parents for managed in managed_paths
353
+ ):
354
+ dirnames[:] = []
355
+ continue
356
+
357
+ retained: list[str] = []
358
+ for dirname in dirnames:
359
+ child = root / dirname
360
+ if dirname.startswith(".") or child in managed_paths:
361
+ continue
362
+ if child.is_symlink():
363
+ try:
364
+ target = child.resolve(strict=True)
365
+ except _RESOLUTION_ERRORS:
366
+ pass
367
+ else:
368
+ link_targets.add(target)
369
+ if target.is_dir():
370
+ _record_tree_distributions(target, distributions)
371
+ continue
372
+ retained.append(dirname)
373
+ dirnames[:] = retained
374
+
375
+ if "pyproject.toml" not in filenames:
376
+ continue
377
+ distribution = _distribution_name(root / "pyproject.toml")
378
+ if distribution is None:
379
+ continue
380
+ distributions.setdefault(distribution, set()).add(root.resolve(strict=True))
381
+ return distributions, link_targets
382
+
383
+
384
+ def _source_identities(projection: _Projection) -> tuple[tuple[str, Path], ...]:
385
+ if projection.binding.kind == "app":
386
+ return ((projection.key, projection.target_realpath),)
387
+ distributions: dict[str, set[Path]] = {}
388
+ _record_tree_distributions(projection.target_realpath, distributions)
389
+ return tuple(
390
+ (app_id, root)
391
+ for app_id, roots in sorted(distributions.items())
392
+ for root in sorted(roots)
393
+ )
394
+
395
+
396
+ def _check_source_conflicts(
397
+ project_dir: Path,
398
+ before: Mapping[Path, _Projection],
399
+ after: Mapping[Path, _Projection],
400
+ *,
401
+ action: str,
402
+ ) -> None:
403
+ distributions, link_targets = _visible_sources(project_dir, set(before))
404
+ for projection in after.values():
405
+ for link_target in link_targets:
406
+ if _paths_overlap(link_target, projection.target_realpath):
407
+ raise _error(
408
+ BINDING_SOURCE_CONFLICT_CODE,
409
+ f"binding source is already exposed by another link: {projection.key}",
410
+ action=action,
411
+ detail=(
412
+ f"source={projection.target_realpath}; "
413
+ f"existing_link_target={link_target}"
414
+ ),
415
+ )
416
+ for app_id, expected_root in _source_identities(projection):
417
+ existing_roots = distributions.get(app_id, set())
418
+ if not existing_roots:
419
+ continue
420
+ raise _error(
421
+ BINDING_SOURCE_CONFLICT_CODE,
422
+ f"app id is already exposed by a local source: {app_id}",
423
+ action=action,
424
+ detail=(
425
+ f"expected={expected_root}; "
426
+ f"existing={', '.join(str(path) for path in sorted(existing_roots))}"
427
+ ),
428
+ )
429
+
430
+
431
+ def _preflight(
432
+ project_dir: Path,
433
+ before: DevManifest,
434
+ after: DevManifest,
435
+ *,
436
+ action: str,
437
+ ) -> _Preflight:
438
+ project_root = project_dir.expanduser().resolve(strict=False)
439
+ try:
440
+ validate_dev_manifest(project_root, before)
441
+ except _RESOLUTION_ERRORS as exc:
442
+ raise _error(
443
+ BINDING_INVALID_PROJECTION_CODE,
444
+ "current manifest resolves through a broken source path",
445
+ action=action,
446
+ detail=str(exc),
447
+ ) from exc
448
+ try:
449
+ _check_granularity_overlap(project_root, before, after)
450
+ except _RESOLUTION_ERRORS as exc:
451
+ raise _error(
452
+ BINDING_INVALID_PROJECTION_CODE,
453
+ "binding overlap check encountered a broken source path",
454
+ action=action,
455
+ detail=str(exc),
456
+ ) from exc
457
+ try:
458
+ validate_dev_manifest(project_root, after)
459
+ except _RESOLUTION_ERRORS as exc:
460
+ raise _error(
461
+ BINDING_INVALID_PROJECTION_CODE,
462
+ "desired manifest resolves through a broken source path",
463
+ action=action,
464
+ detail=str(exc),
465
+ ) from exc
466
+ before_projections = _projections(project_root, before, action=action)
467
+ after_projections = _projections(project_root, after, action=action)
468
+
469
+ for projection in before_projections.values():
470
+ _verify_exact_link(projection, action=action)
471
+
472
+ for final_path, projection in after_projections.items():
473
+ existing = before_projections.get(final_path)
474
+ if existing is not None:
475
+ continue
476
+ if _lexists(final_path):
477
+ raise _error(
478
+ BINDING_CONFLICT_CODE,
479
+ f"binding destination is occupied by an unmanaged path: {final_path}",
480
+ action=action,
481
+ detail=f"desired_source={projection.target_realpath}",
482
+ )
483
+
484
+ _check_source_conflicts(
485
+ project_root,
486
+ before_projections,
487
+ after_projections,
488
+ action=action,
489
+ )
490
+ return _Preflight(before=before_projections, after=after_projections)
491
+
492
+
493
+ def _staged_path(final_path: Path) -> Path:
494
+ while True:
495
+ candidate = final_path.with_name(
496
+ f".{final_path.name}.pz-link-{uuid.uuid4().hex}"
497
+ )
498
+ if not _lexists(candidate):
499
+ return candidate
500
+
501
+
502
+ def plan_binding_projection(
503
+ project_dir: Path,
504
+ before: DevManifest,
505
+ after: DevManifest,
506
+ ) -> tuple[LinkMutation, ...]:
507
+ checked = _preflight(
508
+ project_dir,
509
+ before,
510
+ after,
511
+ action="plan_binding_projection",
512
+ )
513
+ mutations: list[LinkMutation] = []
514
+ for final_path, desired in sorted(checked.after.items()):
515
+ previous = checked.before.get(final_path)
516
+ if previous is not None:
517
+ previous_target = previous.relative_target
518
+ if (
519
+ previous.relative_target == desired.relative_target
520
+ and previous.target_realpath == desired.target_realpath
521
+ ):
522
+ continue
523
+ else:
524
+ previous_target = None
525
+ mutations.append(
526
+ LinkMutation(
527
+ final_path=final_path,
528
+ staged_path=_staged_path(final_path),
529
+ relative_target=desired.relative_target,
530
+ previous_target=previous_target,
531
+ )
532
+ )
533
+ return tuple(mutations)
534
+
535
+
536
+ def plan_binding_removals(
537
+ project_dir: Path,
538
+ before: DevManifest,
539
+ after: DevManifest,
540
+ ) -> tuple[LinkRemoval, ...]:
541
+ checked = _preflight(
542
+ project_dir,
543
+ before,
544
+ after,
545
+ action="plan_binding_removals",
546
+ )
547
+ removals: list[LinkRemoval] = []
548
+ for final_path, existing in sorted(checked.before.items()):
549
+ if final_path in checked.after:
550
+ continue
551
+ removals.append(
552
+ LinkRemoval(
553
+ final_path=final_path,
554
+ previous_target=existing.relative_target,
555
+ expected_target=existing.target_realpath,
556
+ )
557
+ )
558
+ return tuple(removals)
559
+
560
+
561
+ def _verify_mutation_before_stage(mutation: LinkMutation) -> None:
562
+ if mutation.staged_path.parent != mutation.final_path.parent:
563
+ raise _error(
564
+ BINDING_INVALID_PROJECTION_CODE,
565
+ "staged binding must be a sibling of its final path",
566
+ action="stage_binding_projection",
567
+ detail=f"final={mutation.final_path}; staged={mutation.staged_path}",
568
+ )
569
+ if Path(mutation.relative_target).is_absolute() or "\\" in mutation.relative_target:
570
+ raise _error(
571
+ BINDING_INVALID_PROJECTION_CODE,
572
+ "staged binding target must be a POSIX relative path",
573
+ action="stage_binding_projection",
574
+ detail=mutation.relative_target,
575
+ )
576
+ if _lexists(mutation.staged_path):
577
+ raise _error(
578
+ BINDING_CONFLICT_CODE,
579
+ f"staged binding path is occupied: {mutation.staged_path}",
580
+ action="stage_binding_projection",
581
+ )
582
+ if mutation.previous_target is None:
583
+ if _lexists(mutation.final_path):
584
+ raise _error(
585
+ BINDING_CONFLICT_CODE,
586
+ f"binding destination became occupied: {mutation.final_path}",
587
+ action="stage_binding_projection",
588
+ )
589
+ elif (
590
+ not mutation.final_path.is_symlink()
591
+ or os.readlink(mutation.final_path) != mutation.previous_target
592
+ ):
593
+ raise _error(
594
+ BINDING_INVALID_PROJECTION_CODE,
595
+ f"managed binding changed after planning: {mutation.final_path}",
596
+ action="stage_binding_projection",
597
+ )
598
+ target = mutation.final_path.parent / mutation.relative_target
599
+ try:
600
+ target_realpath = target.resolve(strict=True)
601
+ except _RESOLUTION_ERRORS as exc:
602
+ raise _error(
603
+ BINDING_INVALID_PROJECTION_CODE,
604
+ f"binding target changed after planning: {target}",
605
+ action="stage_binding_projection",
606
+ detail=str(exc),
607
+ ) from exc
608
+ if not target_realpath.is_dir():
609
+ raise _error(
610
+ BINDING_INVALID_PROJECTION_CODE,
611
+ f"binding target is not a directory: {target}",
612
+ action="stage_binding_projection",
613
+ )
614
+
615
+
616
+ def _unlink_exact_link(path: Path, relative_target: str) -> None:
617
+ if not path.is_symlink() or os.readlink(path) != relative_target:
618
+ raise _error(
619
+ BINDING_INVALID_PROJECTION_CODE,
620
+ f"refusing to unlink an unverified symbolic link: {path}",
621
+ action="discard_staged_projection",
622
+ )
623
+ path.unlink()
624
+
625
+
626
+ def discard_staged_projection(mutations: Sequence[LinkMutation]) -> None:
627
+ for mutation in reversed(tuple(mutations)):
628
+ if not _lexists(mutation.staged_path):
629
+ continue
630
+ _unlink_exact_link(mutation.staged_path, mutation.relative_target)
631
+
632
+
633
+ def stage_binding_projection(mutations: Sequence[LinkMutation]) -> None:
634
+ ordered = tuple(mutations)
635
+ for mutation in ordered:
636
+ _verify_mutation_before_stage(mutation)
637
+ staged: list[LinkMutation] = []
638
+ try:
639
+ for mutation in ordered:
640
+ os.symlink(
641
+ mutation.relative_target,
642
+ mutation.staged_path,
643
+ target_is_directory=True,
644
+ )
645
+ staged.append(mutation)
646
+ except BaseException:
647
+ discard_staged_projection(staged)
648
+ raise
649
+
650
+
651
+ def _verify_staged(mutation: LinkMutation, *, action: str) -> None:
652
+ if not mutation.staged_path.is_symlink():
653
+ raise _error(
654
+ BINDING_INVALID_PROJECTION_CODE,
655
+ f"staged binding is missing: {mutation.staged_path}",
656
+ action=action,
657
+ )
658
+ if os.readlink(mutation.staged_path) != mutation.relative_target:
659
+ raise _error(
660
+ BINDING_INVALID_PROJECTION_CODE,
661
+ f"staged binding target changed: {mutation.staged_path}",
662
+ action=action,
663
+ )
664
+ try:
665
+ expected = (mutation.final_path.parent / mutation.relative_target).resolve(
666
+ strict=True
667
+ )
668
+ actual = mutation.staged_path.resolve(strict=True)
669
+ except _RESOLUTION_ERRORS as exc:
670
+ raise _error(
671
+ BINDING_INVALID_PROJECTION_CODE,
672
+ f"staged binding is broken: {mutation.staged_path}",
673
+ action=action,
674
+ detail=str(exc),
675
+ ) from exc
676
+ if actual != expected:
677
+ raise _error(
678
+ BINDING_INVALID_PROJECTION_CODE,
679
+ f"staged binding resolves to the wrong target: {mutation.staged_path}",
680
+ action=action,
681
+ )
682
+
683
+
684
+ def _verify_mutation_destination(mutation: LinkMutation, *, action: str) -> None:
685
+ if mutation.previous_target is None:
686
+ if _lexists(mutation.final_path):
687
+ raise _error(
688
+ BINDING_CONFLICT_CODE,
689
+ f"binding destination became occupied: {mutation.final_path}",
690
+ action=action,
691
+ )
692
+ return
693
+ if (
694
+ not mutation.final_path.is_symlink()
695
+ or os.readlink(mutation.final_path) != mutation.previous_target
696
+ ):
697
+ raise _error(
698
+ BINDING_INVALID_PROJECTION_CODE,
699
+ f"managed binding changed after staging: {mutation.final_path}",
700
+ action=action,
701
+ )
702
+ try:
703
+ mutation.final_path.resolve(strict=True)
704
+ except _RESOLUTION_ERRORS as exc:
705
+ raise _error(
706
+ BINDING_INVALID_PROJECTION_CODE,
707
+ f"managed binding became broken: {mutation.final_path}",
708
+ action=action,
709
+ detail=str(exc),
710
+ ) from exc
711
+
712
+
713
+ def _verify_removal(removal: LinkRemoval, *, action: str) -> None:
714
+ if not removal.final_path.is_symlink():
715
+ raise _error(
716
+ BINDING_INVALID_PROJECTION_CODE,
717
+ f"managed binding cannot be safely removed: {removal.final_path}",
718
+ action=action,
719
+ )
720
+ stored = os.readlink(removal.final_path)
721
+ if Path(stored).is_absolute() or stored != removal.previous_target:
722
+ raise _error(
723
+ BINDING_INVALID_PROJECTION_CODE,
724
+ f"managed binding target changed before removal: {removal.final_path}",
725
+ action=action,
726
+ )
727
+ try:
728
+ resolved = removal.final_path.resolve(strict=True)
729
+ except _RESOLUTION_ERRORS as exc:
730
+ raise _error(
731
+ BINDING_INVALID_PROJECTION_CODE,
732
+ f"managed binding became broken before removal: {removal.final_path}",
733
+ action=action,
734
+ detail=str(exc),
735
+ ) from exc
736
+ if resolved != removal.expected_target:
737
+ raise _error(
738
+ BINDING_INVALID_PROJECTION_CODE,
739
+ f"managed binding resolves to the wrong source: {removal.final_path}",
740
+ action=action,
741
+ )
742
+
743
+
744
+ def _replacement_link(path: Path, relative_target: str) -> None:
745
+ staged = _staged_path(path)
746
+ try:
747
+ os.symlink(relative_target, staged, target_is_directory=True)
748
+ os.replace(staged, path)
749
+ finally:
750
+ if _lexists(staged):
751
+ _unlink_exact_link(staged, relative_target)
752
+
753
+
754
+ def _verify_committed_mutation(mutation: LinkMutation, *, action: str) -> None:
755
+ if (
756
+ not mutation.final_path.is_symlink()
757
+ or os.readlink(mutation.final_path) != mutation.relative_target
758
+ ):
759
+ raise _error(
760
+ BINDING_INVALID_PROJECTION_CODE,
761
+ f"committed binding changed before rollback: {mutation.final_path}",
762
+ action=action,
763
+ )
764
+ try:
765
+ expected = (mutation.final_path.parent / mutation.relative_target).resolve(
766
+ strict=True
767
+ )
768
+ actual = mutation.final_path.resolve(strict=True)
769
+ except _RESOLUTION_ERRORS as exc:
770
+ raise _error(
771
+ BINDING_INVALID_PROJECTION_CODE,
772
+ f"committed binding is broken: {mutation.final_path}",
773
+ action=action,
774
+ detail=str(exc),
775
+ ) from exc
776
+ if actual != expected:
777
+ raise _error(
778
+ BINDING_INVALID_PROJECTION_CODE,
779
+ f"committed binding resolves to the wrong source: {mutation.final_path}",
780
+ action=action,
781
+ )
782
+
783
+
784
+ def _restore_mutation(mutation: LinkMutation, *, action: str) -> None:
785
+ _verify_committed_mutation(mutation, action=action)
786
+ if mutation.previous_target is None:
787
+ mutation.final_path.unlink()
788
+ else:
789
+ _replacement_link(mutation.final_path, mutation.previous_target)
790
+
791
+
792
+ def _restore_removal(removal: LinkRemoval, *, action: str) -> None:
793
+ if _lexists(removal.final_path):
794
+ raise _error(
795
+ BINDING_CONFLICT_CODE,
796
+ f"removed binding destination became occupied: {removal.final_path}",
797
+ action=action,
798
+ )
799
+ _replacement_link(removal.final_path, removal.previous_target)
800
+ _verify_removal(removal, action=action)
801
+
802
+
803
+ def _rollback_completed(
804
+ mutations: Sequence[LinkMutation],
805
+ removals: Sequence[LinkRemoval],
806
+ *,
807
+ action: str,
808
+ ) -> None:
809
+ failures: list[Exception] = []
810
+ for mutation in reversed(tuple(mutations)):
811
+ try:
812
+ _restore_mutation(mutation, action=action)
813
+ except Exception as exc: # pragma: no cover - emergency diagnostic path
814
+ failures.append(exc)
815
+ for removal in reversed(tuple(removals)):
816
+ try:
817
+ _restore_removal(removal, action=action)
818
+ except Exception as exc: # pragma: no cover - emergency diagnostic path
819
+ failures.append(exc)
820
+ if failures:
821
+ raise _error(
822
+ BINDING_INVALID_PROJECTION_CODE,
823
+ "binding rollback could not restore the exact previous projection",
824
+ action=action,
825
+ detail="; ".join(str(failure) for failure in failures),
826
+ )
827
+
828
+
829
+ def commit_binding_projection(
830
+ mutations: Sequence[LinkMutation],
831
+ removals: Sequence[LinkRemoval] = (),
832
+ ) -> None:
833
+ ordered_mutations = tuple(mutations)
834
+ ordered_removals = tuple(removals)
835
+ mutation_paths = {mutation.final_path for mutation in ordered_mutations}
836
+ removal_paths = {removal.final_path for removal in ordered_removals}
837
+ if len(mutation_paths) != len(ordered_mutations) or len(removal_paths) != len(
838
+ ordered_removals
839
+ ):
840
+ raise _error(
841
+ BINDING_INVALID_PROJECTION_CODE,
842
+ "binding commit plan contains duplicate final paths",
843
+ action="commit_binding_projection",
844
+ )
845
+ if mutation_paths & removal_paths:
846
+ raise _error(
847
+ BINDING_INVALID_PROJECTION_CODE,
848
+ "binding commit plan cannot mutate and remove the same path",
849
+ action="commit_binding_projection",
850
+ )
851
+
852
+ for removal in ordered_removals:
853
+ _verify_removal(removal, action="commit_binding_projection")
854
+ for mutation in ordered_mutations:
855
+ _verify_staged(mutation, action="commit_binding_projection")
856
+ _verify_mutation_destination(mutation, action="commit_binding_projection")
857
+
858
+ completed_removals: list[LinkRemoval] = []
859
+ completed_mutations: list[LinkMutation] = []
860
+ try:
861
+ for removal in ordered_removals:
862
+ removal.final_path.unlink()
863
+ completed_removals.append(removal)
864
+ for mutation in ordered_mutations:
865
+ os.replace(mutation.staged_path, mutation.final_path)
866
+ completed_mutations.append(mutation)
867
+ except BaseException as original:
868
+ try:
869
+ _rollback_completed(
870
+ completed_mutations,
871
+ completed_removals,
872
+ action="commit_binding_projection",
873
+ )
874
+ except Exception as rollback_error:
875
+ raise rollback_error from original
876
+ finally:
877
+ discard_staged_projection(ordered_mutations)
878
+ raise
879
+
880
+
881
+ def rollback_binding_projection(
882
+ mutations: Sequence[LinkMutation],
883
+ removals: Sequence[LinkRemoval] = (),
884
+ ) -> None:
885
+ ordered_mutations = tuple(mutations)
886
+ ordered_removals = tuple(removals)
887
+ for mutation in ordered_mutations:
888
+ _verify_committed_mutation(mutation, action="rollback_binding_projection")
889
+ for removal in ordered_removals:
890
+ if _lexists(removal.final_path):
891
+ raise _error(
892
+ BINDING_CONFLICT_CODE,
893
+ f"removed binding destination became occupied: {removal.final_path}",
894
+ action="rollback_binding_projection",
895
+ )
896
+ _rollback_completed(
897
+ ordered_mutations,
898
+ ordered_removals,
899
+ action="rollback_binding_projection",
900
+ )
901
+
902
+
903
+ __all__ = [
904
+ "BINDING_CONFLICT_CODE",
905
+ "BINDING_INVALID_PROJECTION_CODE",
906
+ "BINDING_OVERLAP_CODE",
907
+ "BINDING_SOURCE_CONFLICT_CODE",
908
+ "LinkMutation",
909
+ "LinkRemoval",
910
+ "commit_binding_projection",
911
+ "discard_staged_projection",
912
+ "plan_binding_projection",
913
+ "plan_binding_removals",
914
+ "rollback_binding_projection",
915
+ "stage_binding_projection",
916
+ ]