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,1638 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import os
5
+ import shutil
6
+ import tomllib
7
+ import uuid
8
+ from collections.abc import Mapping, Sequence
9
+ from dataclasses import dataclass
10
+ from pathlib import Path, PurePosixPath
11
+ from typing import Literal
12
+
13
+ from pz_core.project_sources import resolve_sources_manifest_path
14
+ from pz_core.runtime.errors import PzError
15
+
16
+ from .dev_bindings import (
17
+ BINDING_CONFLICT_CODE,
18
+ LinkMutation,
19
+ commit_binding_projection,
20
+ discard_staged_projection,
21
+ plan_binding_projection,
22
+ plan_binding_removals,
23
+ rollback_binding_projection,
24
+ stage_binding_projection,
25
+ )
26
+ from .dev_git import BaseBranch, DevGitService, GitBranchPlan, PreparedBranch
27
+ from .dev_manifest import (
28
+ DevBinding,
29
+ DevManifest,
30
+ DevRepo,
31
+ load_dev_manifest,
32
+ manifest_bytes,
33
+ validate_dev_manifest,
34
+ )
35
+ from .naming import derive_default_local_app_slug
36
+ from .official_dev_sources import ResolvedSource, resolve_source_locator
37
+ from .project_state import (
38
+ _normalize_business_app_id,
39
+ _run_git_ref_check,
40
+ _validate_business_app_git_ref,
41
+ )
42
+
43
+
44
+ _OWNER = "pz_core.authoring.dev_sources"
45
+ _TRANSACTION_FAILED = "PZ_DEV_TRANSACTION_FAILED"
46
+ _REQUEST_INVALID = "PZ_DEV_REQUEST_INVALID"
47
+ _MANIFEST_MISSING = "PZ_DEV_MANIFEST_MISSING"
48
+ _SOURCE_CONFLICT = "PZ_DEV_SOURCE_CONFLICT"
49
+ _SOURCE_NOT_GIT = "PZ_DEV_SOURCE_NOT_GIT"
50
+ _SOURCE_LAYOUT_INVALID = "PZ_DEV_SOURCE_LAYOUT_INVALID"
51
+ _OS_REPLACE = os.replace
52
+
53
+ Operation = Literal["add", "link", "branch", "init", "update", "remove", "unlink"]
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class AddDevSourceRequest:
58
+ project_dir: Path
59
+ locators: tuple[str, ...]
60
+ base_branch: BaseBranch = "develop"
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class BranchDevSourceRequest:
65
+ project_dir: Path
66
+ locator: str
67
+ base_branch: BaseBranch = "develop"
68
+
69
+
70
+ @dataclass(frozen=True, slots=True)
71
+ class InitDevSourcesRequest:
72
+ project_dir: Path
73
+ app_name: str | None = None
74
+ locators: tuple[str, ...] = ("pz",)
75
+ base_branch: BaseBranch = "develop"
76
+
77
+
78
+ @dataclass(frozen=True, slots=True)
79
+ class UpdateDevSourceRequest:
80
+ project_dir: Path
81
+ locator: str
82
+
83
+
84
+ @dataclass(frozen=True, slots=True)
85
+ class RemoveDevSourceRequest:
86
+ project_dir: Path
87
+ binding: str
88
+ delete_checkout: bool = False
89
+
90
+
91
+ @dataclass(frozen=True, slots=True)
92
+ class DevTransactionResult:
93
+ operation: Operation
94
+ project_dir: Path
95
+ business_branch: str
96
+ base_branch: BaseBranch
97
+ repo_keys: tuple[str, ...]
98
+ checkouts: tuple[Path, ...]
99
+ bindings: tuple[str, ...]
100
+ links: tuple[Path, ...]
101
+ updated: bool | None = None
102
+ upstream: str | None = None
103
+ checkout_deleted: bool = False
104
+
105
+
106
+ @dataclass(frozen=True, slots=True)
107
+ class _RepoIntent:
108
+ repo_key: str
109
+ source: str
110
+ checkout: Path
111
+
112
+
113
+ @dataclass(frozen=True, slots=True)
114
+ class _PreparedRepo:
115
+ intent: _RepoIntent
116
+ branch: PreparedBranch
117
+
118
+
119
+ def _error(
120
+ code: str,
121
+ message: str,
122
+ *,
123
+ action: str,
124
+ detail: str | None = None,
125
+ ) -> PzError:
126
+ return PzError(
127
+ code=code,
128
+ message=message,
129
+ stage="dev",
130
+ owner=_OWNER,
131
+ action=action,
132
+ detail=detail,
133
+ )
134
+
135
+
136
+ def _new_git_service() -> DevGitService:
137
+ return DevGitService()
138
+
139
+
140
+ def _transaction_phase(_name: str) -> None:
141
+ return None
142
+
143
+
144
+ def _inject_fault(_name: str) -> None:
145
+ return None
146
+
147
+
148
+ def _atomic_replace(source: Path, destination: Path) -> None:
149
+ _OS_REPLACE(source, destination)
150
+
151
+
152
+ def _fsync_directory(path: Path) -> None:
153
+ descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
154
+ try:
155
+ os.fsync(descriptor)
156
+ finally:
157
+ os.close(descriptor)
158
+
159
+
160
+ def _project_root(value: object, *, action: str) -> Path:
161
+ if not isinstance(value, Path):
162
+ raise _error(
163
+ _REQUEST_INVALID,
164
+ "project_dir must be a pathlib.Path.",
165
+ action=action,
166
+ )
167
+ project_dir = value.expanduser().resolve(strict=False)
168
+ if not project_dir.is_dir():
169
+ raise _error(
170
+ _REQUEST_INVALID,
171
+ f"project directory does not exist: {project_dir}",
172
+ action=action,
173
+ )
174
+ apps_root = project_dir / "apps"
175
+ if apps_root.is_symlink() or not apps_root.is_dir():
176
+ raise _error(
177
+ _REQUEST_INVALID,
178
+ f"project apps directory must be a real directory: {apps_root}",
179
+ action=action,
180
+ )
181
+ return project_dir
182
+
183
+
184
+ def _base_branch(value: object, *, action: str) -> BaseBranch:
185
+ if value not in {"develop", "main"}:
186
+ raise _error(
187
+ _REQUEST_INVALID,
188
+ "base branch must be develop or main.",
189
+ action=action,
190
+ )
191
+ return value # type: ignore[return-value]
192
+
193
+
194
+ def _locators(values: object, *, action: str) -> tuple[str, ...]:
195
+ if not isinstance(values, tuple) or not values:
196
+ raise _error(
197
+ _REQUEST_INVALID,
198
+ "at least one source locator is required.",
199
+ action=action,
200
+ )
201
+ normalized: list[str] = []
202
+ for value in values:
203
+ if not isinstance(value, str) or not value.strip():
204
+ raise _error(
205
+ _REQUEST_INVALID,
206
+ "source locators must be non-empty strings.",
207
+ action=action,
208
+ )
209
+ candidate = value.strip()
210
+ if candidate not in normalized:
211
+ normalized.append(candidate)
212
+ return tuple(normalized)
213
+
214
+
215
+ def _business_manifest(
216
+ project_dir: Path,
217
+ manifest: DevManifest | None,
218
+ *,
219
+ app_name: str | None,
220
+ action: str,
221
+ ) -> DevManifest:
222
+ if manifest is not None:
223
+ business_app_id = _normalize_business_app_id(
224
+ manifest.business_app_id,
225
+ action=action,
226
+ )
227
+ if app_name is not None:
228
+ requested = _normalize_business_app_id(app_name, action=action)
229
+ if requested != business_app_id:
230
+ raise _error(
231
+ "PZ_DEV_BUSINESS_APP_ID_INVALID",
232
+ "business app id is already persisted and cannot be changed.",
233
+ action=action,
234
+ )
235
+ else:
236
+ candidate = app_name or derive_default_local_app_slug(project_dir)
237
+ business_app_id = _normalize_business_app_id(candidate, action=action)
238
+ _validate_business_app_git_ref(
239
+ business_app_id,
240
+ action=action,
241
+ git_ref_runner=_run_git_ref_check,
242
+ )
243
+ if manifest is not None:
244
+ return manifest
245
+ return DevManifest(
246
+ schema_version=5,
247
+ business_app_id=business_app_id,
248
+ repos={},
249
+ bindings={},
250
+ )
251
+
252
+
253
+ def _read_manifest_snapshot(path: Path) -> bytes | None:
254
+ if not path.exists() and not path.is_symlink():
255
+ return None
256
+ if path.is_symlink() or not path.is_file():
257
+ raise _error(
258
+ "PZ_DEV_MANIFEST_INVALID",
259
+ "dev manifest must be a real file.",
260
+ action="load_dev_transaction",
261
+ )
262
+ try:
263
+ return path.read_bytes()
264
+ except OSError as exc:
265
+ raise _error(
266
+ "PZ_DEV_MANIFEST_INVALID",
267
+ "dev manifest could not be read.",
268
+ action="load_dev_transaction",
269
+ detail=str(exc),
270
+ ) from exc
271
+
272
+
273
+ def _resolved_binding(source: ResolvedSource) -> tuple[str, DevBinding]:
274
+ kind = source.binding_kind
275
+ subpath = source.subpath
276
+ link_name = source.link_name
277
+ expected_app_id = source.expected_app_id
278
+ if source.pending_layout:
279
+ kind = "directory"
280
+ subpath = PurePosixPath(".")
281
+ expected_app_id = None
282
+ if kind not in {"directory", "app"}:
283
+ raise _error(
284
+ _SOURCE_LAYOUT_INVALID,
285
+ "source locator did not resolve to a bindable layout.",
286
+ action="resolve_dev_transaction",
287
+ detail=source.locator,
288
+ )
289
+ binding_key = expected_app_id if kind == "app" else link_name
290
+ if not binding_key:
291
+ raise _error(
292
+ _SOURCE_LAYOUT_INVALID,
293
+ "source locator did not resolve a binding identity.",
294
+ action="resolve_dev_transaction",
295
+ detail=source.locator,
296
+ )
297
+ if source.source_kind == "git":
298
+ binding_source = source.clone_source
299
+ repo_key = source.repo_key
300
+ else:
301
+ binding_source = str(Path(source.clone_source).expanduser().resolve())
302
+ repo_key = None
303
+ return binding_key, DevBinding(
304
+ kind=kind,
305
+ source_kind=source.source_kind,
306
+ repo_key=repo_key,
307
+ source=binding_source,
308
+ link=PurePosixPath("apps") / link_name,
309
+ subpath=subpath,
310
+ )
311
+
312
+
313
+ def _build_desired_manifest(
314
+ project_dir: Path,
315
+ before: DevManifest,
316
+ resolved_sources: Sequence[ResolvedSource],
317
+ *,
318
+ operation: Operation,
319
+ base_branch: BaseBranch,
320
+ action: str,
321
+ ) -> tuple[DevManifest, tuple[_RepoIntent, ...], tuple[str, ...]]:
322
+ repos = dict(before.repos)
323
+ bindings = dict(before.bindings)
324
+ intents: dict[str, _RepoIntent] = {}
325
+ selected_bindings: list[str] = []
326
+
327
+ for source in resolved_sources:
328
+ if source.source_kind == "git":
329
+ repo_key = source.repo_key
330
+ if not repo_key:
331
+ raise _error(
332
+ _SOURCE_LAYOUT_INVALID,
333
+ "Git source locator did not resolve a repo key.",
334
+ action=action,
335
+ detail=source.locator,
336
+ )
337
+ existing_repo = repos.get(repo_key)
338
+ if (
339
+ existing_repo is not None
340
+ and existing_repo.source != source.clone_source
341
+ ):
342
+ raise _error(
343
+ _SOURCE_CONFLICT,
344
+ f"repo key is already bound to a different source: {repo_key}",
345
+ action=action,
346
+ detail=(
347
+ f"existing={existing_repo.source}; desired={source.clone_source}"
348
+ ),
349
+ )
350
+ previous_intent = intents.get(repo_key)
351
+ if (
352
+ previous_intent is not None
353
+ and previous_intent.source != source.clone_source
354
+ ):
355
+ raise _error(
356
+ _SOURCE_CONFLICT,
357
+ f"source locators disagree for repo key: {repo_key}",
358
+ action=action,
359
+ )
360
+ checkout_posix = PurePosixPath("apps/.sources") / repo_key
361
+ repos[repo_key] = DevRepo(
362
+ source=source.clone_source,
363
+ checkout=checkout_posix,
364
+ base_branch=base_branch,
365
+ branch=before.business_app_id,
366
+ )
367
+ intents.setdefault(
368
+ repo_key,
369
+ _RepoIntent(
370
+ repo_key=repo_key,
371
+ source=source.clone_source,
372
+ checkout=project_dir / Path(*checkout_posix.parts),
373
+ ),
374
+ )
375
+ elif operation == "branch":
376
+ raise _error(
377
+ _SOURCE_NOT_GIT,
378
+ "branch preparation requires a Git-backed source.",
379
+ action=action,
380
+ detail=source.locator,
381
+ )
382
+
383
+ if operation == "branch":
384
+ continue
385
+ if source.bootstrap:
386
+ continue
387
+ binding_key, binding = _resolved_binding(source)
388
+ previous_binding = bindings.get(binding_key)
389
+ if previous_binding is not None and previous_binding != binding:
390
+ previous_link = previous_binding.link
391
+ if previous_link != binding.link:
392
+ raise _error(
393
+ _SOURCE_CONFLICT,
394
+ f"binding identity is already exposed at another link: {binding_key}",
395
+ action=action,
396
+ detail=f"existing={previous_link}; desired={binding.link}",
397
+ )
398
+ bindings[binding_key] = binding
399
+ if binding_key not in selected_bindings:
400
+ selected_bindings.append(binding_key)
401
+
402
+ desired = DevManifest(
403
+ schema_version=5,
404
+ business_app_id=before.business_app_id,
405
+ repos=repos,
406
+ bindings=bindings,
407
+ )
408
+ validate_dev_manifest(project_dir, desired)
409
+ return desired, tuple(intents.values()), tuple(selected_bindings)
410
+
411
+
412
+ def _binding_base(
413
+ project_dir: Path,
414
+ manifest: DevManifest,
415
+ binding: DevBinding,
416
+ ) -> Path:
417
+ if binding.source_kind == "link":
418
+ return Path(binding.source)
419
+ assert binding.repo_key is not None
420
+ repo = manifest.repos[binding.repo_key]
421
+ return project_dir / Path(*repo.checkout.parts)
422
+
423
+
424
+ def _binding_target(
425
+ project_dir: Path,
426
+ manifest: DevManifest,
427
+ binding: DevBinding,
428
+ ) -> Path:
429
+ base = _binding_base(project_dir, manifest, binding)
430
+ if binding.subpath == PurePosixPath("."):
431
+ return base
432
+ return base / Path(*binding.subpath.parts)
433
+
434
+
435
+ def _layout_error(message: str, *, locator: str, detail: str) -> PzError:
436
+ return _error(
437
+ _SOURCE_LAYOUT_INVALID,
438
+ message,
439
+ action="validate_dev_source_layout",
440
+ detail=f"locator={locator}; {detail}",
441
+ )
442
+
443
+
444
+ def _static_apps_py_app_id(path: Path) -> str | None:
445
+ try:
446
+ tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
447
+ except (OSError, UnicodeError, SyntaxError):
448
+ return None
449
+ class_ids: dict[str, str] = {}
450
+ for statement in tree.body:
451
+ if not isinstance(statement, ast.ClassDef):
452
+ continue
453
+ for field in statement.body:
454
+ targets: tuple[ast.expr, ...]
455
+ value: ast.expr | None
456
+ if isinstance(field, ast.Assign):
457
+ targets = tuple(field.targets)
458
+ value = field.value
459
+ elif isinstance(field, ast.AnnAssign):
460
+ targets = (field.target,)
461
+ value = field.value
462
+ else:
463
+ continue
464
+ if not any(
465
+ isinstance(target, ast.Name) and target.id == "id" for target in targets
466
+ ):
467
+ continue
468
+ if (
469
+ isinstance(value, ast.Constant)
470
+ and isinstance(value.value, str)
471
+ and value.value.isidentifier()
472
+ ):
473
+ class_ids[statement.name] = value.value
474
+ break
475
+ for statement in tree.body:
476
+ if not isinstance(statement, ast.Assign):
477
+ continue
478
+ if not any(
479
+ isinstance(target, ast.Name) and target.id == "app"
480
+ for target in statement.targets
481
+ ):
482
+ continue
483
+ value = statement.value
484
+ if (
485
+ isinstance(value, ast.Call)
486
+ and isinstance(value.func, ast.Name)
487
+ and value.func.id in class_ids
488
+ ):
489
+ return class_ids[value.func.id]
490
+ return None
491
+
492
+
493
+ def _formal_leaf(path: Path) -> str | None:
494
+ pyproject = path / "pyproject.toml"
495
+ if not pyproject.is_file():
496
+ return None
497
+ try:
498
+ payload = tomllib.loads(pyproject.read_text(encoding="utf-8"))
499
+ except (OSError, UnicodeError, tomllib.TOMLDecodeError):
500
+ return None
501
+ project = payload.get("project")
502
+ if not isinstance(project, dict):
503
+ return None
504
+ distribution = project.get("name")
505
+ if not isinstance(distribution, str) or not distribution.strip():
506
+ return None
507
+ app_id = distribution.strip().replace("-", "_")
508
+ apps_py = path / app_id / "apps.py"
509
+ if _static_apps_py_app_id(apps_py) != app_id:
510
+ return None
511
+ return app_id
512
+
513
+
514
+ def _has_formal_top_level_leaf(root: Path) -> bool:
515
+ for child in sorted(root.iterdir(), key=lambda item: item.name):
516
+ if child.name.startswith(".") or child.is_symlink() or not child.is_dir():
517
+ continue
518
+ if _formal_leaf(child) is not None:
519
+ return True
520
+ return False
521
+
522
+
523
+ def _validate_source_layout(
524
+ source: ResolvedSource,
525
+ *,
526
+ source_root: Path,
527
+ ) -> None:
528
+ subpath = PurePosixPath(".") if source.pending_layout else source.subpath
529
+ target = (
530
+ source_root
531
+ if subpath == PurePosixPath(".")
532
+ else source_root / Path(*subpath.parts)
533
+ )
534
+ try:
535
+ source_realpath = source_root.resolve(strict=True)
536
+ target_realpath = target.resolve(strict=True)
537
+ except (OSError, RuntimeError) as exc:
538
+ raise _layout_error(
539
+ "source layout target is missing or broken.",
540
+ locator=source.locator,
541
+ detail=str(exc),
542
+ ) from exc
543
+ if not source_realpath.is_dir() or not target_realpath.is_dir():
544
+ raise _layout_error(
545
+ "source layout target must be a directory.",
546
+ locator=source.locator,
547
+ detail=str(target),
548
+ )
549
+ if not target_realpath.is_relative_to(source_realpath):
550
+ raise _layout_error(
551
+ "source layout target escapes its source root.",
552
+ locator=source.locator,
553
+ detail=str(target_realpath),
554
+ )
555
+ if source.pending_layout:
556
+ if not _has_formal_top_level_leaf(target_realpath):
557
+ raise _layout_error(
558
+ "Git source contains no formal top-level minimal app.",
559
+ locator=source.locator,
560
+ detail=str(target_realpath),
561
+ )
562
+ return
563
+ if source.binding_kind != "app":
564
+ return
565
+ expected_app_id = source.expected_app_id
566
+ discovered_app_id = _formal_leaf(target_realpath)
567
+ if not expected_app_id or discovered_app_id != expected_app_id:
568
+ raise _layout_error(
569
+ "app source does not match its declared app id.",
570
+ locator=source.locator,
571
+ detail=(
572
+ f"expected={expected_app_id!r}; discovered={discovered_app_id!r}; "
573
+ f"path={target_realpath}"
574
+ ),
575
+ )
576
+
577
+
578
+ def _preflight_paths(project_dir: Path, manifest_path: Path) -> None:
579
+ sources_root = manifest_path.parent
580
+ if sources_root.is_symlink():
581
+ raise _error(
582
+ _REQUEST_INVALID,
583
+ "managed source root cannot be a symbolic link.",
584
+ action="preflight_dev_transaction",
585
+ detail=str(sources_root),
586
+ )
587
+ if sources_root.exists() and not sources_root.is_dir():
588
+ raise _error(
589
+ _REQUEST_INVALID,
590
+ "managed source root must be a directory.",
591
+ action="preflight_dev_transaction",
592
+ detail=str(sources_root),
593
+ )
594
+ if manifest_path.is_symlink():
595
+ raise _error(
596
+ "PZ_DEV_MANIFEST_INVALID",
597
+ "dev manifest cannot be a symbolic link.",
598
+ action="preflight_dev_transaction",
599
+ )
600
+ project_realpath = project_dir.resolve(strict=True)
601
+ apps_realpath = (project_dir / "apps").resolve(strict=True)
602
+ if not apps_realpath.is_relative_to(project_realpath):
603
+ raise _error(
604
+ _REQUEST_INVALID,
605
+ "apps directory escapes the project root.",
606
+ action="preflight_dev_transaction",
607
+ )
608
+
609
+
610
+ def _preflight_projection(
611
+ project_dir: Path,
612
+ before: DevManifest,
613
+ desired: DevManifest,
614
+ ) -> None:
615
+ plan_binding_projection(project_dir, before, before)
616
+ before_links = {
617
+ project_dir / Path(*binding.link.parts) for binding in before.bindings.values()
618
+ }
619
+ for binding in desired.bindings.values():
620
+ final_path = project_dir / Path(*binding.link.parts)
621
+ if final_path not in before_links and os.path.lexists(final_path):
622
+ raise _error(
623
+ BINDING_CONFLICT_CODE,
624
+ f"binding destination is occupied by an unmanaged path: {final_path}",
625
+ action="preflight_dev_transaction",
626
+ )
627
+
628
+ partial_bindings = dict(before.bindings)
629
+ for key, binding in desired.bindings.items():
630
+ target = _binding_target(project_dir, desired, binding)
631
+ try:
632
+ available = target.resolve(strict=True).is_dir()
633
+ except (OSError, RuntimeError):
634
+ available = False
635
+ if available:
636
+ partial_bindings[key] = binding
637
+ partial = DevManifest(
638
+ schema_version=5,
639
+ business_app_id=desired.business_app_id,
640
+ repos=desired.repos,
641
+ bindings=partial_bindings,
642
+ )
643
+ validate_dev_manifest(project_dir, partial)
644
+ plan_binding_projection(project_dir, before, partial)
645
+
646
+
647
+ def _git_plan(
648
+ project_dir: Path,
649
+ intent: _RepoIntent,
650
+ *,
651
+ business_branch: str,
652
+ base_branch: BaseBranch,
653
+ transaction_id: str,
654
+ ) -> GitBranchPlan:
655
+ return GitBranchPlan(
656
+ project_dir=project_dir,
657
+ repo_key=intent.repo_key,
658
+ source=intent.source,
659
+ checkout=intent.checkout,
660
+ business_branch=business_branch,
661
+ transaction_id=transaction_id,
662
+ base_branch=base_branch,
663
+ )
664
+
665
+
666
+ def _prepared_source_root(
667
+ source: ResolvedSource,
668
+ prepared: Mapping[str, _PreparedRepo],
669
+ ) -> Path:
670
+ if source.source_kind == "link":
671
+ return Path(source.clone_source).expanduser().resolve()
672
+ assert source.repo_key is not None
673
+ return prepared[source.repo_key].branch.checkout
674
+
675
+
676
+ def _shadow_manifest(
677
+ desired: DevManifest,
678
+ prepared: Mapping[str, _PreparedRepo],
679
+ ) -> tuple[DevManifest, frozenset[Path]]:
680
+ bindings: dict[str, DevBinding] = {}
681
+ staged_final_paths: set[Path] = set()
682
+ for key, binding in desired.bindings.items():
683
+ if binding.source_kind != "git" or binding.repo_key is None:
684
+ bindings[key] = binding
685
+ continue
686
+ prepared_repo = prepared.get(binding.repo_key)
687
+ if prepared_repo is None or not prepared_repo.branch.new_clone:
688
+ bindings[key] = binding
689
+ continue
690
+ bindings[key] = DevBinding(
691
+ kind=binding.kind,
692
+ source_kind="link",
693
+ repo_key=None,
694
+ source=str(prepared_repo.branch.checkout.resolve(strict=True)),
695
+ link=binding.link,
696
+ subpath=binding.subpath,
697
+ )
698
+ staged_final_paths.add(Path(*binding.link.parts))
699
+ return (
700
+ DevManifest(
701
+ schema_version=5,
702
+ business_app_id=desired.business_app_id,
703
+ repos=desired.repos,
704
+ bindings=bindings,
705
+ ),
706
+ frozenset(staged_final_paths),
707
+ )
708
+
709
+
710
+ def _final_relative_target(
711
+ project_dir: Path,
712
+ desired: DevManifest,
713
+ mutation: LinkMutation,
714
+ ) -> str:
715
+ binding = next(
716
+ binding
717
+ for binding in desired.bindings.values()
718
+ if project_dir / Path(*binding.link.parts) == mutation.final_path
719
+ )
720
+ target = _binding_target(project_dir, desired, binding)
721
+ return Path(os.path.relpath(target, start=mutation.final_path.parent)).as_posix()
722
+
723
+
724
+ def _final_link_mutations(
725
+ project_dir: Path,
726
+ desired: DevManifest,
727
+ shadow_mutations: Sequence[LinkMutation],
728
+ staged_final_paths: frozenset[Path],
729
+ ) -> tuple[LinkMutation, ...]:
730
+ mutations: list[LinkMutation] = []
731
+ for mutation in shadow_mutations:
732
+ relative_final = mutation.final_path.relative_to(project_dir)
733
+ if relative_final not in staged_final_paths:
734
+ mutations.append(mutation)
735
+ continue
736
+ relative_target = _final_relative_target(project_dir, desired, mutation)
737
+ mutations.append(
738
+ LinkMutation(
739
+ final_path=mutation.final_path,
740
+ staged_path=mutation.staged_path,
741
+ relative_target=relative_target,
742
+ previous_target=mutation.previous_target,
743
+ )
744
+ )
745
+ return tuple(mutations)
746
+
747
+
748
+ def _retarget_staged_links(
749
+ shadow_mutations: Sequence[LinkMutation],
750
+ final_mutations: Sequence[LinkMutation],
751
+ ) -> None:
752
+ pairs = tuple(zip(shadow_mutations, final_mutations, strict=True))
753
+ try:
754
+ for shadow, final in pairs:
755
+ if shadow.relative_target == final.relative_target:
756
+ continue
757
+ if (
758
+ not shadow.staged_path.is_symlink()
759
+ or os.readlink(shadow.staged_path) != shadow.relative_target
760
+ ):
761
+ raise _error(
762
+ "PZ_DEV_BINDING_INVALID_PROJECTION",
763
+ "staged binding changed before final-target conversion.",
764
+ action="stage_dev_transaction",
765
+ detail=str(shadow.staged_path),
766
+ )
767
+ shadow.staged_path.unlink()
768
+ os.symlink(
769
+ final.relative_target,
770
+ final.staged_path,
771
+ target_is_directory=True,
772
+ )
773
+ except BaseException as original:
774
+ cleanup_failures: list[Exception] = []
775
+ for shadow, final in reversed(pairs):
776
+ path = shadow.staged_path
777
+ if not path.is_symlink():
778
+ continue
779
+ try:
780
+ actual = os.readlink(path)
781
+ if actual not in {shadow.relative_target, final.relative_target}:
782
+ raise RuntimeError(f"staged binding changed concurrently: {path}")
783
+ path.unlink()
784
+ except Exception as exc:
785
+ cleanup_failures.append(exc)
786
+ if cleanup_failures:
787
+ raise RuntimeError(
788
+ "staged binding retarget cleanup failed: "
789
+ + "; ".join(str(failure) for failure in cleanup_failures)
790
+ ) from original
791
+ raise
792
+
793
+
794
+ def _write_staged_manifest(path: Path, payload: bytes) -> None:
795
+ path.parent.mkdir(parents=True, exist_ok=True)
796
+ descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
797
+ try:
798
+ with os.fdopen(descriptor, "wb") as handle:
799
+ handle.write(payload)
800
+ handle.flush()
801
+ os.fsync(handle.fileno())
802
+ except BaseException:
803
+ path.unlink(missing_ok=True)
804
+ raise
805
+
806
+
807
+ def _discard_staged_manifest(path: Path, expected: bytes) -> None:
808
+ if not path.exists() and not path.is_symlink():
809
+ return
810
+ if path.is_symlink() or not path.is_file() or path.read_bytes() != expected:
811
+ raise RuntimeError(f"staged manifest identity changed: {path}")
812
+ path.unlink()
813
+
814
+
815
+ def _prepared_identity_matches(path: Path, prepared: PreparedBranch) -> bool:
816
+ if path.is_symlink() or not path.is_dir():
817
+ return False
818
+ if prepared.clone_device is None or prepared.clone_inode is None:
819
+ return False
820
+ stat = path.stat(follow_symlinks=False)
821
+ return stat.st_dev == prepared.clone_device and stat.st_ino == prepared.clone_inode
822
+
823
+
824
+ def _restore_manifest(
825
+ manifest_path: Path,
826
+ *,
827
+ before_bytes: bytes | None,
828
+ desired_bytes: bytes,
829
+ transaction_id: str,
830
+ ) -> None:
831
+ if not manifest_path.exists() or manifest_path.is_symlink():
832
+ if before_bytes is None and not manifest_path.is_symlink():
833
+ return
834
+ raise RuntimeError("manifest disappeared or changed type during rollback")
835
+ current = manifest_path.read_bytes()
836
+ if current == before_bytes:
837
+ return
838
+ if current != desired_bytes:
839
+ raise RuntimeError("manifest bytes changed concurrently before rollback")
840
+ if before_bytes is None:
841
+ manifest_path.unlink()
842
+ _fsync_directory(manifest_path.parent)
843
+ return
844
+ restore_path = manifest_path.with_name(
845
+ f".{manifest_path.name}.{transaction_id}.restore"
846
+ )
847
+ _write_staged_manifest(restore_path, before_bytes)
848
+ try:
849
+ _OS_REPLACE(restore_path, manifest_path)
850
+ _fsync_directory(manifest_path.parent)
851
+ finally:
852
+ _discard_staged_manifest(restore_path, before_bytes)
853
+
854
+
855
+ def _committed_link_mutations(
856
+ mutations: Sequence[LinkMutation],
857
+ ) -> tuple[LinkMutation, ...]:
858
+ committed: list[LinkMutation] = []
859
+ for mutation in mutations:
860
+ if mutation.final_path.is_symlink():
861
+ actual = os.readlink(mutation.final_path)
862
+ if actual == mutation.relative_target:
863
+ committed.append(mutation)
864
+ continue
865
+ if (
866
+ mutation.previous_target is not None
867
+ and actual == mutation.previous_target
868
+ ):
869
+ continue
870
+ raise RuntimeError(f"binding changed concurrently: {mutation.final_path}")
871
+ if (
872
+ not os.path.lexists(mutation.final_path)
873
+ and mutation.previous_target is None
874
+ ):
875
+ continue
876
+ raise RuntimeError(
877
+ f"binding changed type during rollback: {mutation.final_path}"
878
+ )
879
+ return tuple(committed)
880
+
881
+
882
+ def _restore_new_checkout(prepared: _PreparedRepo) -> None:
883
+ branch = prepared.branch
884
+ if not branch.new_clone:
885
+ return
886
+ staged = branch.checkout
887
+ final = prepared.intent.checkout
888
+ staged_matches = _prepared_identity_matches(staged, branch)
889
+ final_matches = _prepared_identity_matches(final, branch)
890
+ if staged_matches and not os.path.lexists(final):
891
+ return
892
+ if final_matches and not os.path.lexists(staged):
893
+ staged.parent.mkdir(parents=True, exist_ok=True)
894
+ _OS_REPLACE(final, staged)
895
+ return
896
+ if not os.path.lexists(staged) and not os.path.lexists(final):
897
+ return
898
+ raise RuntimeError(
899
+ f"prepared checkout identity changed: {prepared.intent.repo_key}"
900
+ )
901
+
902
+
903
+ def _cleanup_transaction_paths(
904
+ project_dir: Path,
905
+ *,
906
+ transaction_id: str,
907
+ staged_manifest: Path | None,
908
+ desired_bytes: bytes,
909
+ mutations: Sequence[LinkMutation],
910
+ sources_root_existed: bool,
911
+ ) -> None:
912
+ discard_staged_projection(mutations)
913
+ if staged_manifest is not None:
914
+ _discard_staged_manifest(staged_manifest, desired_bytes)
915
+ sources_root = project_dir / "apps" / ".sources"
916
+ transaction_root = sources_root / ".transactions" / transaction_id
917
+ transactions_root = transaction_root.parent
918
+ try:
919
+ transaction_root.rmdir()
920
+ except FileNotFoundError:
921
+ pass
922
+ except OSError:
923
+ if transaction_root.exists() and any(transaction_root.iterdir()):
924
+ raise
925
+ try:
926
+ transactions_root.rmdir()
927
+ except (FileNotFoundError, OSError):
928
+ pass
929
+ if not sources_root_existed:
930
+ try:
931
+ sources_root.rmdir()
932
+ except (FileNotFoundError, OSError):
933
+ pass
934
+
935
+
936
+ def _rollback_transaction(
937
+ project_dir: Path,
938
+ *,
939
+ manifest_path: Path,
940
+ before_bytes: bytes | None,
941
+ desired_bytes: bytes,
942
+ prepared_repos: Sequence[_PreparedRepo],
943
+ git_service: DevGitService | None,
944
+ mutations: Sequence[LinkMutation],
945
+ links_commit_attempted: bool,
946
+ staged_manifest: Path | None,
947
+ manifest_replace_attempted: bool,
948
+ transaction_id: str,
949
+ sources_root_existed: bool,
950
+ ) -> tuple[Exception, ...]:
951
+ failures: list[Exception] = []
952
+ if manifest_replace_attempted:
953
+ try:
954
+ _restore_manifest(
955
+ manifest_path,
956
+ before_bytes=before_bytes,
957
+ desired_bytes=desired_bytes,
958
+ transaction_id=transaction_id,
959
+ )
960
+ except Exception as exc:
961
+ failures.append(exc)
962
+ if links_commit_attempted:
963
+ try:
964
+ committed = _committed_link_mutations(mutations)
965
+ if committed:
966
+ rollback_binding_projection(committed)
967
+ except Exception as exc:
968
+ failures.append(exc)
969
+ for prepared in reversed(tuple(prepared_repos)):
970
+ try:
971
+ _restore_new_checkout(prepared)
972
+ except Exception as exc:
973
+ failures.append(exc)
974
+ if git_service is not None:
975
+ for prepared in reversed(tuple(prepared_repos)):
976
+ try:
977
+ git_service.rollback(prepared.branch)
978
+ except Exception as exc:
979
+ failures.append(exc)
980
+ try:
981
+ _cleanup_transaction_paths(
982
+ project_dir,
983
+ transaction_id=transaction_id,
984
+ staged_manifest=staged_manifest,
985
+ desired_bytes=desired_bytes,
986
+ mutations=mutations,
987
+ sources_root_existed=sources_root_existed,
988
+ )
989
+ except Exception as exc:
990
+ failures.append(exc)
991
+ return tuple(failures)
992
+
993
+
994
+ def _failure_detail(
995
+ original: BaseException,
996
+ *,
997
+ phase: str,
998
+ rollback_failures: Sequence[Exception],
999
+ ) -> str:
1000
+ root = f"{original.__class__.__name__}: {original}"
1001
+ if rollback_failures:
1002
+ rollback = "failed: " + "; ".join(
1003
+ f"{failure.__class__.__name__}: {failure}" for failure in rollback_failures
1004
+ )
1005
+ else:
1006
+ rollback = "ok"
1007
+ return f"phase={phase}; cause={root}; rollback={rollback}"
1008
+
1009
+
1010
+ def _verify_manifest_snapshot(
1011
+ path: Path, expected: bytes | None, *, action: str
1012
+ ) -> None:
1013
+ if _read_manifest_snapshot(path) != expected:
1014
+ raise _error(
1015
+ "PZ_DEV_MANIFEST_CHANGED",
1016
+ "dev manifest changed after transaction preflight.",
1017
+ action=action,
1018
+ )
1019
+
1020
+
1021
+ def _execute_transaction(
1022
+ *,
1023
+ operation: Operation,
1024
+ project_dir_value: object,
1025
+ locator_values: object,
1026
+ base_branch_value: object,
1027
+ app_name: str | None,
1028
+ action: str,
1029
+ ) -> DevTransactionResult:
1030
+ project_dir = _project_root(project_dir_value, action=action)
1031
+ base_branch = _base_branch(base_branch_value, action=action)
1032
+ locators = _locators(locator_values, action=action)
1033
+ manifest_path = resolve_sources_manifest_path(project_dir)
1034
+ transaction_id = f"pz-{uuid.uuid4().hex}"
1035
+ sources_root_existed = manifest_path.parent.exists()
1036
+ before_bytes: bytes | None = None
1037
+ desired_bytes = b""
1038
+ git_service: DevGitService | None = None
1039
+ prepared_repos: list[_PreparedRepo] = []
1040
+ mutations: tuple[LinkMutation, ...] = ()
1041
+ links_commit_attempted = False
1042
+ staged_manifest: Path | None = None
1043
+ manifest_replace_attempted = False
1044
+ phase = "load"
1045
+
1046
+ try:
1047
+ _transaction_phase(phase)
1048
+ before_bytes = _read_manifest_snapshot(manifest_path)
1049
+ loaded = load_dev_manifest(project_dir)
1050
+ if loaded is None and operation != "init":
1051
+ raise _error(
1052
+ _MANIFEST_MISSING,
1053
+ "dev manifest is missing; run pz dev init first.",
1054
+ action=action,
1055
+ )
1056
+ before = _business_manifest(
1057
+ project_dir,
1058
+ loaded,
1059
+ app_name=app_name,
1060
+ action=action,
1061
+ )
1062
+ if _read_manifest_snapshot(manifest_path) != before_bytes:
1063
+ raise _error(
1064
+ "PZ_DEV_MANIFEST_INVALID",
1065
+ "dev manifest changed while it was being loaded.",
1066
+ action=action,
1067
+ )
1068
+
1069
+ phase = "resolve"
1070
+ _transaction_phase(phase)
1071
+ resolved_sources = tuple(
1072
+ resolve_source_locator(locator, project_dir=project_dir, action=action)
1073
+ for locator in locators
1074
+ )
1075
+ desired, intents, selected_bindings = _build_desired_manifest(
1076
+ project_dir,
1077
+ before,
1078
+ resolved_sources,
1079
+ operation=operation,
1080
+ base_branch=base_branch,
1081
+ action=action,
1082
+ )
1083
+ desired_bytes = manifest_bytes(desired)
1084
+
1085
+ phase = "preflight"
1086
+ _transaction_phase(phase)
1087
+ _preflight_paths(project_dir, manifest_path)
1088
+ _preflight_projection(project_dir, before, desired)
1089
+ for source in resolved_sources:
1090
+ if source.source_kind == "link":
1091
+ _validate_source_layout(
1092
+ source,
1093
+ source_root=Path(source.clone_source).expanduser().resolve(),
1094
+ )
1095
+ continue
1096
+ assert source.repo_key is not None
1097
+ intent = next(item for item in intents if item.repo_key == source.repo_key)
1098
+ if intent.checkout.exists():
1099
+ _validate_source_layout(source, source_root=intent.checkout)
1100
+ plans = tuple(
1101
+ _git_plan(
1102
+ project_dir,
1103
+ intent,
1104
+ business_branch=before.business_app_id,
1105
+ base_branch=base_branch,
1106
+ transaction_id=transaction_id,
1107
+ )
1108
+ for intent in intents
1109
+ )
1110
+ if plans:
1111
+ git_service = _new_git_service()
1112
+ for plan in plans:
1113
+ git_service.preflight_existing(plan)
1114
+
1115
+ phase = "git_prepare"
1116
+ _transaction_phase(phase)
1117
+ if git_service is not None:
1118
+ for intent, plan in zip(intents, plans, strict=True):
1119
+ prepared = _PreparedRepo(
1120
+ intent=intent, branch=git_service.prepare(plan)
1121
+ )
1122
+ prepared_repos.append(prepared)
1123
+ if prepared.branch.new_clone:
1124
+ _inject_fault("after_clone")
1125
+ _inject_fault("after_branch")
1126
+ prepared_by_key = {
1127
+ prepared.intent.repo_key: prepared for prepared in prepared_repos
1128
+ }
1129
+
1130
+ phase = "layout"
1131
+ _transaction_phase(phase)
1132
+ for source in resolved_sources:
1133
+ _validate_source_layout(
1134
+ source,
1135
+ source_root=_prepared_source_root(source, prepared_by_key),
1136
+ )
1137
+ _inject_fault("layout")
1138
+
1139
+ phase = "stage"
1140
+ _transaction_phase(phase)
1141
+ if desired_bytes != before_bytes:
1142
+ staged_manifest = manifest_path.with_name(
1143
+ f".{manifest_path.name}.{transaction_id}.tmp"
1144
+ )
1145
+ _write_staged_manifest(staged_manifest, desired_bytes)
1146
+ shadow, staged_final_paths = _shadow_manifest(desired, prepared_by_key)
1147
+ shadow_mutations = plan_binding_projection(project_dir, before, shadow)
1148
+ mutations = _final_link_mutations(
1149
+ project_dir,
1150
+ desired,
1151
+ shadow_mutations,
1152
+ staged_final_paths,
1153
+ )
1154
+ stage_binding_projection(shadow_mutations)
1155
+ _retarget_staged_links(
1156
+ shadow_mutations,
1157
+ mutations,
1158
+ )
1159
+ _inject_fault("stage")
1160
+
1161
+ phase = "place"
1162
+ _transaction_phase(phase)
1163
+ _verify_manifest_snapshot(manifest_path, before_bytes, action=action)
1164
+ for prepared in prepared_repos:
1165
+ if not prepared.branch.new_clone:
1166
+ continue
1167
+ final = prepared.intent.checkout
1168
+ if os.path.lexists(final):
1169
+ raise _error(
1170
+ BINDING_CONFLICT_CODE,
1171
+ f"Git checkout destination became occupied: {final}",
1172
+ action=action,
1173
+ )
1174
+ _atomic_replace(prepared.branch.checkout, final)
1175
+ _inject_fault("place")
1176
+
1177
+ phase = "links"
1178
+ _transaction_phase(phase)
1179
+ _verify_manifest_snapshot(manifest_path, before_bytes, action=action)
1180
+ links_commit_attempted = True
1181
+ commit_binding_projection(mutations)
1182
+ _inject_fault("links")
1183
+
1184
+ phase = "manifest"
1185
+ _transaction_phase(phase)
1186
+ if staged_manifest is not None:
1187
+ _verify_manifest_snapshot(manifest_path, before_bytes, action=action)
1188
+ manifest_replace_attempted = True
1189
+ _atomic_replace(staged_manifest, manifest_path)
1190
+ _fsync_directory(manifest_path.parent)
1191
+ _inject_fault("manifest")
1192
+
1193
+ phase = "cleanup"
1194
+ _transaction_phase(phase)
1195
+ _cleanup_transaction_paths(
1196
+ project_dir,
1197
+ transaction_id=transaction_id,
1198
+ staged_manifest=staged_manifest,
1199
+ desired_bytes=desired_bytes,
1200
+ mutations=mutations,
1201
+ sources_root_existed=sources_root_existed,
1202
+ )
1203
+ return DevTransactionResult(
1204
+ operation=operation,
1205
+ project_dir=project_dir,
1206
+ business_branch=before.business_app_id,
1207
+ base_branch=base_branch,
1208
+ repo_keys=tuple(intent.repo_key for intent in intents),
1209
+ checkouts=tuple(intent.checkout for intent in intents),
1210
+ bindings=selected_bindings,
1211
+ links=tuple(mutation.final_path for mutation in mutations),
1212
+ )
1213
+ except BaseException as original:
1214
+ rollback_failures = _rollback_transaction(
1215
+ project_dir,
1216
+ manifest_path=manifest_path,
1217
+ before_bytes=before_bytes,
1218
+ desired_bytes=desired_bytes,
1219
+ prepared_repos=prepared_repos,
1220
+ git_service=git_service,
1221
+ mutations=mutations,
1222
+ links_commit_attempted=links_commit_attempted,
1223
+ staged_manifest=staged_manifest,
1224
+ manifest_replace_attempted=manifest_replace_attempted,
1225
+ transaction_id=transaction_id,
1226
+ sources_root_existed=sources_root_existed,
1227
+ )
1228
+ if isinstance(original, PzError) and not rollback_failures:
1229
+ raise
1230
+ wrapped = _error(
1231
+ _TRANSACTION_FAILED,
1232
+ "dev source transaction failed.",
1233
+ action=action,
1234
+ detail=_failure_detail(
1235
+ original,
1236
+ phase=phase,
1237
+ rollback_failures=rollback_failures,
1238
+ ),
1239
+ )
1240
+ raise wrapped from original
1241
+
1242
+
1243
+ def add_dev_source(request: AddDevSourceRequest) -> DevTransactionResult:
1244
+ if not isinstance(request, AddDevSourceRequest):
1245
+ raise _error(
1246
+ _REQUEST_INVALID,
1247
+ "add_dev_source requires an AddDevSourceRequest.",
1248
+ action="add_dev_source",
1249
+ )
1250
+ return _execute_transaction(
1251
+ operation="add",
1252
+ project_dir_value=request.project_dir,
1253
+ locator_values=request.locators,
1254
+ base_branch_value=request.base_branch,
1255
+ app_name=None,
1256
+ action="add_dev_source",
1257
+ )
1258
+
1259
+
1260
+ def branch_dev_source(request: BranchDevSourceRequest) -> DevTransactionResult:
1261
+ if not isinstance(request, BranchDevSourceRequest):
1262
+ raise _error(
1263
+ _REQUEST_INVALID,
1264
+ "branch_dev_source requires a BranchDevSourceRequest.",
1265
+ action="branch_dev_source",
1266
+ )
1267
+ project_dir = _project_root(request.project_dir, action="branch_dev_source")
1268
+ locator = _locators((request.locator,), action="branch_dev_source")[0]
1269
+ manifest = load_dev_manifest(project_dir)
1270
+ if manifest is not None:
1271
+ repo_key = _repo_key_for_locator(
1272
+ project_dir,
1273
+ manifest,
1274
+ locator,
1275
+ action="branch_dev_source",
1276
+ )
1277
+ checkout = project_dir / Path(*manifest.repos[repo_key].checkout.parts)
1278
+ if checkout.is_symlink():
1279
+ raise _error(
1280
+ "PZ_DEV_SHARED_CHECKOUT_READ_ONLY",
1281
+ "Shared checkout is externally owned and read-only to PZ authoring commands.",
1282
+ action="branch_dev_source",
1283
+ detail=repo_key,
1284
+ )
1285
+ return _execute_transaction(
1286
+ operation="branch",
1287
+ project_dir_value=project_dir,
1288
+ locator_values=(locator,),
1289
+ base_branch_value=request.base_branch,
1290
+ app_name=None,
1291
+ action="branch_dev_source",
1292
+ )
1293
+
1294
+
1295
+ def initialize_dev_sources(request: InitDevSourcesRequest) -> DevTransactionResult:
1296
+ if not isinstance(request, InitDevSourcesRequest):
1297
+ raise _error(
1298
+ _REQUEST_INVALID,
1299
+ "initialize_dev_sources requires an InitDevSourcesRequest.",
1300
+ action="initialize_dev_sources",
1301
+ )
1302
+ return _execute_transaction(
1303
+ operation="init",
1304
+ project_dir_value=request.project_dir,
1305
+ locator_values=request.locators,
1306
+ base_branch_value=request.base_branch,
1307
+ app_name=request.app_name,
1308
+ action="initialize_dev_sources",
1309
+ )
1310
+
1311
+
1312
+ def _repo_key_for_locator(
1313
+ project_dir: Path,
1314
+ manifest: DevManifest,
1315
+ locator: str,
1316
+ *,
1317
+ action: str,
1318
+ ) -> str:
1319
+ if locator in manifest.repos:
1320
+ return locator
1321
+ binding = manifest.bindings.get(locator)
1322
+ if binding is not None:
1323
+ if binding.source_kind != "git" or binding.repo_key is None:
1324
+ raise _error(
1325
+ _SOURCE_NOT_GIT,
1326
+ "development source is not Git-backed.",
1327
+ action=action,
1328
+ detail=locator,
1329
+ )
1330
+ return binding.repo_key
1331
+ resolved = resolve_source_locator(locator, project_dir=project_dir, action=action)
1332
+ if (
1333
+ resolved.source_kind != "git"
1334
+ or resolved.repo_key is None
1335
+ or resolved.repo_key not in manifest.repos
1336
+ ):
1337
+ raise _error(
1338
+ _SOURCE_NOT_GIT,
1339
+ "development source is not a managed Git checkout.",
1340
+ action=action,
1341
+ detail=locator,
1342
+ )
1343
+ return resolved.repo_key
1344
+
1345
+
1346
+ def update_dev_source(request: UpdateDevSourceRequest) -> DevTransactionResult:
1347
+ if not isinstance(request, UpdateDevSourceRequest):
1348
+ raise _error(
1349
+ _REQUEST_INVALID,
1350
+ "update_dev_source requires an UpdateDevSourceRequest.",
1351
+ action="update_dev_source",
1352
+ )
1353
+ action = "update_dev_source"
1354
+ project_dir = _project_root(request.project_dir, action=action)
1355
+ locator = _locators((request.locator,), action=action)[0]
1356
+ manifest = load_dev_manifest(project_dir)
1357
+ if manifest is None:
1358
+ raise _error(
1359
+ _MANIFEST_MISSING,
1360
+ "dev manifest is missing; run pz dev init first.",
1361
+ action=action,
1362
+ )
1363
+ repo_key = _repo_key_for_locator(
1364
+ project_dir,
1365
+ manifest,
1366
+ locator,
1367
+ action=action,
1368
+ )
1369
+ repo = manifest.repos[repo_key]
1370
+ checkout = project_dir / Path(*repo.checkout.parts)
1371
+ if checkout.is_symlink():
1372
+ raise _error(
1373
+ "PZ_DEV_SHARED_CHECKOUT_READ_ONLY",
1374
+ "Shared checkout is externally owned and read-only to PZ authoring commands.",
1375
+ action=action,
1376
+ detail=repo_key,
1377
+ )
1378
+ service = _new_git_service()
1379
+ update = service.update(checkout, expected_branch=manifest.business_app_id)
1380
+ repo_bindings = tuple(
1381
+ key
1382
+ for key, binding in sorted(manifest.bindings.items())
1383
+ if binding.repo_key == repo_key
1384
+ )
1385
+ return DevTransactionResult(
1386
+ operation="update",
1387
+ project_dir=project_dir,
1388
+ business_branch=manifest.business_app_id,
1389
+ base_branch=repo.base_branch,
1390
+ repo_keys=(repo_key,),
1391
+ checkouts=(checkout,),
1392
+ bindings=repo_bindings,
1393
+ links=(),
1394
+ updated=update.updated,
1395
+ upstream=update.upstream,
1396
+ )
1397
+
1398
+
1399
+ def _checkout_delete_safety(
1400
+ project_dir: Path,
1401
+ manifest: DevManifest,
1402
+ *,
1403
+ repo_key: str,
1404
+ action: str,
1405
+ ) -> tuple[DevGitService, Path]:
1406
+ if any(binding.repo_key == repo_key for binding in manifest.bindings.values()):
1407
+ raise _error(
1408
+ "PZ_DEV_CHECKOUT_DELETE_UNSAFE",
1409
+ "checkout still has managed bindings.",
1410
+ action=action,
1411
+ detail=repo_key,
1412
+ )
1413
+ repo = manifest.repos[repo_key]
1414
+ checkout = project_dir / Path(*repo.checkout.parts)
1415
+ service = _new_git_service()
1416
+ if checkout.is_symlink():
1417
+ return service, checkout
1418
+ try:
1419
+ service.preflight_delete(
1420
+ checkout,
1421
+ expected_branch=manifest.business_app_id,
1422
+ )
1423
+ except PzError as exc:
1424
+ raise _error(
1425
+ "PZ_DEV_CHECKOUT_DELETE_UNSAFE",
1426
+ "checkout does not satisfy the deletion safety contract.",
1427
+ action=action,
1428
+ detail=f"repo={repo_key}; reason={exc.code}",
1429
+ ) from exc
1430
+ return service, checkout
1431
+
1432
+
1433
+ def _quarantine_checkout(
1434
+ project_dir: Path,
1435
+ checkout: Path,
1436
+ *,
1437
+ transaction_id: str,
1438
+ repo_key: str,
1439
+ ) -> Path:
1440
+ sources_root = project_dir / "apps" / ".sources"
1441
+ trash_root = sources_root / ".trash" / transaction_id
1442
+ for component in (sources_root, sources_root / ".trash", trash_root):
1443
+ if component.is_symlink():
1444
+ raise RuntimeError("checkout quarantine path contains a symbolic link")
1445
+ trash_root.mkdir(parents=True, exist_ok=False)
1446
+ quarantine = trash_root / repo_key
1447
+ _OS_REPLACE(checkout, quarantine)
1448
+ _fsync_directory(sources_root)
1449
+ return quarantine
1450
+
1451
+
1452
+ def _cleanup_quarantine(path: Path) -> None:
1453
+ try:
1454
+ if path.is_symlink():
1455
+ path.unlink()
1456
+ else:
1457
+ shutil.rmtree(path)
1458
+ path.parent.rmdir()
1459
+ path.parent.parent.rmdir()
1460
+ except OSError:
1461
+ # The checkout is already atomically detached from managed state. A later
1462
+ # cleanup may remove the owner-only hidden quarantine.
1463
+ return
1464
+
1465
+
1466
+ def remove_dev_source(request: RemoveDevSourceRequest) -> DevTransactionResult:
1467
+ if not isinstance(request, RemoveDevSourceRequest):
1468
+ raise _error(
1469
+ _REQUEST_INVALID,
1470
+ "remove_dev_source requires a RemoveDevSourceRequest.",
1471
+ action="remove_dev_source",
1472
+ )
1473
+ action = "remove_dev_source"
1474
+ project_dir = _project_root(request.project_dir, action=action)
1475
+ binding_key = _locators((request.binding,), action=action)[0]
1476
+ manifest_path = resolve_sources_manifest_path(project_dir)
1477
+ before_bytes = _read_manifest_snapshot(manifest_path)
1478
+ before = load_dev_manifest(project_dir)
1479
+ if before is None or before_bytes is None:
1480
+ raise _error(
1481
+ _MANIFEST_MISSING,
1482
+ "dev manifest is missing; run pz dev init first.",
1483
+ action=action,
1484
+ )
1485
+ binding = before.bindings.get(binding_key)
1486
+ if binding is None:
1487
+ raise _error(
1488
+ "PZ_DEV_BINDING_MISSING",
1489
+ f"managed binding does not exist: {binding_key}",
1490
+ action=action,
1491
+ )
1492
+
1493
+ repos = dict(before.repos)
1494
+ bindings = dict(before.bindings)
1495
+ bindings.pop(binding_key)
1496
+ repo_key = binding.repo_key
1497
+ if request.delete_checkout:
1498
+ if binding.source_kind != "git" or repo_key is None:
1499
+ raise _error(
1500
+ "PZ_DEV_CHECKOUT_DELETE_UNSAFE",
1501
+ "direct-link bindings do not own a managed checkout.",
1502
+ action=action,
1503
+ )
1504
+ if any(candidate.repo_key == repo_key for candidate in bindings.values()):
1505
+ raise _error(
1506
+ "PZ_DEV_CHECKOUT_DELETE_UNSAFE",
1507
+ "checkout still has managed bindings.",
1508
+ action=action,
1509
+ detail=repo_key,
1510
+ )
1511
+ repos.pop(repo_key)
1512
+ desired = DevManifest(
1513
+ schema_version=5,
1514
+ business_app_id=before.business_app_id,
1515
+ repos=repos,
1516
+ bindings=bindings,
1517
+ )
1518
+ validate_dev_manifest(project_dir, desired)
1519
+ removals = plan_binding_removals(project_dir, before, desired)
1520
+ desired_bytes = manifest_bytes(desired)
1521
+ transaction_id = f"pz-{uuid.uuid4().hex}"
1522
+ staged_manifest = manifest_path.with_name(
1523
+ f".{manifest_path.name}.{transaction_id}.tmp"
1524
+ )
1525
+ delete_checkout: Path | None = None
1526
+ if request.delete_checkout:
1527
+ assert repo_key is not None
1528
+ safety_manifest = DevManifest(
1529
+ schema_version=5,
1530
+ business_app_id=before.business_app_id,
1531
+ repos=before.repos,
1532
+ bindings=bindings,
1533
+ )
1534
+ _, delete_checkout = _checkout_delete_safety(
1535
+ project_dir,
1536
+ safety_manifest,
1537
+ repo_key=repo_key,
1538
+ action=action,
1539
+ )
1540
+
1541
+ links_committed = False
1542
+ manifest_committed = False
1543
+ quarantine: Path | None = None
1544
+ try:
1545
+ _write_staged_manifest(staged_manifest, desired_bytes)
1546
+ if _read_manifest_snapshot(manifest_path) != before_bytes:
1547
+ raise _error(
1548
+ "PZ_DEV_MANIFEST_INVALID",
1549
+ "dev manifest changed before binding removal.",
1550
+ action=action,
1551
+ )
1552
+ commit_binding_projection((), removals)
1553
+ links_committed = True
1554
+ _atomic_replace(staged_manifest, manifest_path)
1555
+ _fsync_directory(manifest_path.parent)
1556
+ manifest_committed = True
1557
+ if delete_checkout is not None:
1558
+ assert repo_key is not None
1559
+ quarantine = _quarantine_checkout(
1560
+ project_dir,
1561
+ delete_checkout,
1562
+ transaction_id=transaction_id,
1563
+ repo_key=repo_key,
1564
+ )
1565
+ except BaseException as original:
1566
+ failures: list[Exception] = []
1567
+ if quarantine is not None and delete_checkout is not None:
1568
+ try:
1569
+ _OS_REPLACE(quarantine, delete_checkout)
1570
+ except Exception as exc:
1571
+ failures.append(exc)
1572
+ if manifest_committed:
1573
+ try:
1574
+ _restore_manifest(
1575
+ manifest_path,
1576
+ before_bytes=before_bytes,
1577
+ desired_bytes=desired_bytes,
1578
+ transaction_id=transaction_id,
1579
+ )
1580
+ except Exception as exc:
1581
+ failures.append(exc)
1582
+ if links_committed:
1583
+ try:
1584
+ rollback_binding_projection((), removals)
1585
+ except Exception as exc:
1586
+ failures.append(exc)
1587
+ staged_manifest.unlink(missing_ok=True)
1588
+ if isinstance(original, PzError) and not failures:
1589
+ raise
1590
+ raise _error(
1591
+ _TRANSACTION_FAILED,
1592
+ "dev source removal transaction failed.",
1593
+ action=action,
1594
+ detail=_failure_detail(
1595
+ original,
1596
+ phase="remove",
1597
+ rollback_failures=failures,
1598
+ ),
1599
+ ) from original
1600
+ finally:
1601
+ staged_manifest.unlink(missing_ok=True)
1602
+
1603
+ if quarantine is not None:
1604
+ _cleanup_quarantine(quarantine)
1605
+ link_path = project_dir / Path(*binding.link.parts)
1606
+ checkout_paths = (
1607
+ ()
1608
+ if repo_key is None
1609
+ else (project_dir / Path(*before.repos[repo_key].checkout.parts),)
1610
+ )
1611
+ return DevTransactionResult(
1612
+ operation="remove",
1613
+ project_dir=project_dir,
1614
+ business_branch=before.business_app_id,
1615
+ base_branch=(
1616
+ before.repos[repo_key].base_branch if repo_key is not None else "develop"
1617
+ ),
1618
+ repo_keys=() if repo_key is None else (repo_key,),
1619
+ checkouts=checkout_paths,
1620
+ bindings=(binding_key,),
1621
+ links=(link_path,),
1622
+ checkout_deleted=delete_checkout is not None,
1623
+ )
1624
+
1625
+
1626
+ __all__ = [
1627
+ "AddDevSourceRequest",
1628
+ "BranchDevSourceRequest",
1629
+ "DevTransactionResult",
1630
+ "InitDevSourcesRequest",
1631
+ "RemoveDevSourceRequest",
1632
+ "UpdateDevSourceRequest",
1633
+ "add_dev_source",
1634
+ "branch_dev_source",
1635
+ "initialize_dev_sources",
1636
+ "remove_dev_source",
1637
+ "update_dev_source",
1638
+ ]