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,1087 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ from collections.abc import Mapping, Sequence
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Literal, Protocol
11
+
12
+ from pz_core.runtime.errors import PzError
13
+
14
+
15
+ BaseBranch = Literal["develop", "main"]
16
+
17
+ WORKTREE_DIRTY_CODE = "PZ_DEV_WORKTREE_DIRTY"
18
+ BRANCH_UNSAFE_CODE = "PZ_DEV_BRANCH_UNSAFE"
19
+ BASE_BRANCH_MISSING_CODE = "PZ_DEV_BASE_BRANCH_MISSING"
20
+ GIT_FAILED_CODE = "PZ_DEV_GIT_FAILED"
21
+ PLAN_INVALID_CODE = "PZ_DEV_GIT_PLAN_INVALID"
22
+ ROLLBACK_UNSAFE_CODE = "PZ_DEV_ROLLBACK_UNSAFE"
23
+ SHARED_CHECKOUT_READ_ONLY_CODE = "PZ_DEV_SHARED_CHECKOUT_READ_ONLY"
24
+
25
+ _OWNER = "pz_core.authoring.dev_git"
26
+ _REPO_KEY = re.compile(r"^[a-z][a-z0-9_]*$")
27
+ _BRANCH = re.compile(r"^[a-z][a-z0-9_]*$")
28
+ _TRANSACTION_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
29
+ _URL_CREDENTIALS = re.compile(r"(?P<scheme>[A-Za-z][A-Za-z0-9+.-]*://)[^/@\s]+@")
30
+ _URL_SECRET = re.compile(
31
+ r"(?i)(?P<prefix>[?&](?:access_token|api_key|password|secret|token)=)[^&\s]+"
32
+ )
33
+ _SECRET_ASSIGNMENT = re.compile(
34
+ r"(?i)\b(?P<key>api[_-]?key|password|passwd|secret|token)\s*[:=]\s*[^\s]+"
35
+ )
36
+
37
+
38
+ class GitCommandResult(Protocol):
39
+ @property
40
+ def returncode(self) -> int: ...
41
+
42
+ @property
43
+ def stdout(self) -> str | None: ...
44
+
45
+ @property
46
+ def stderr(self) -> str | None: ...
47
+
48
+
49
+ class GitBackend(Protocol):
50
+ def run(
51
+ self,
52
+ argv: Sequence[str],
53
+ *,
54
+ cwd: Path | None,
55
+ environment: Mapping[str, str],
56
+ ) -> GitCommandResult: ...
57
+
58
+
59
+ class SubprocessGitBackend:
60
+ def run(
61
+ self,
62
+ argv: Sequence[str],
63
+ *,
64
+ cwd: Path | None,
65
+ environment: Mapping[str, str],
66
+ ) -> subprocess.CompletedProcess[str]:
67
+ return subprocess.run(
68
+ list(argv),
69
+ cwd=cwd,
70
+ check=False,
71
+ capture_output=True,
72
+ text=True,
73
+ stdin=subprocess.DEVNULL,
74
+ env=dict(environment),
75
+ shell=False,
76
+ )
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class GitInvocation:
81
+ argv: tuple[str, ...]
82
+ cwd: Path | None
83
+
84
+
85
+ @dataclass(frozen=True, slots=True)
86
+ class GitSnapshot:
87
+ head: str
88
+ branch: str | None
89
+ local_branches: frozenset[str]
90
+
91
+
92
+ @dataclass(frozen=True, slots=True)
93
+ class GitCheckoutState:
94
+ branch: str | None
95
+ dirty: bool
96
+
97
+
98
+ @dataclass(frozen=True, slots=True)
99
+ class GitUpdateResult:
100
+ checkout: Path
101
+ branch: str
102
+ upstream: str | None
103
+ before_head: str
104
+ after_head: str
105
+ updated: bool
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class GitDeleteSafety:
110
+ checkout: Path
111
+ branch: str
112
+ upstream: str
113
+ head: str
114
+
115
+
116
+ @dataclass(frozen=True, slots=True)
117
+ class GitBranchPlan:
118
+ project_dir: Path
119
+ repo_key: str
120
+ source: str
121
+ checkout: Path
122
+ business_branch: str
123
+ transaction_id: str
124
+ base_branch: BaseBranch = "develop"
125
+
126
+
127
+ @dataclass(frozen=True, slots=True)
128
+ class PreparedBranch:
129
+ checkout: Path
130
+ business_branch: str
131
+ base_branch: BaseBranch
132
+ original: GitSnapshot | None
133
+ created_local_branch: bool
134
+ new_clone: bool
135
+ prepared_head: str | None = None
136
+ project_dir: Path | None = None
137
+ repo_key: str | None = None
138
+ transaction_id: str | None = None
139
+ clone_device: int | None = None
140
+ clone_inode: int | None = None
141
+ shared_checkout: bool = False
142
+
143
+
144
+ def _error(
145
+ code: str,
146
+ message: str,
147
+ *,
148
+ action: str,
149
+ detail: str | None = None,
150
+ ) -> PzError:
151
+ return PzError(
152
+ code=code,
153
+ message=message,
154
+ stage="dev",
155
+ owner=_OWNER,
156
+ action=action,
157
+ detail=detail,
158
+ )
159
+
160
+
161
+ def _sanitize_backend_detail(
162
+ value: str,
163
+ *,
164
+ redactions: Sequence[str] = (),
165
+ ) -> str:
166
+ detail = _URL_CREDENTIALS.sub(r"\g<scheme>***@", value)
167
+ detail = _URL_SECRET.sub(r"\g<prefix>***", detail)
168
+ detail = _SECRET_ASSIGNMENT.sub(r"\g<key>=***", detail)
169
+ for redaction in sorted(
170
+ (item for item in redactions if item), key=len, reverse=True
171
+ ):
172
+ detail = detail.replace(redaction, "<redacted>")
173
+ detail = " ".join(detail.split())
174
+ if not detail:
175
+ return "git backend failed"
176
+ if len(detail) > 500:
177
+ return detail[:497] + "..."
178
+ return detail
179
+
180
+
181
+ class DevGitService:
182
+ def __init__(
183
+ self,
184
+ *,
185
+ backend: GitBackend | None = None,
186
+ git_binary: str | None = None,
187
+ environment: Mapping[str, str] | None = None,
188
+ ) -> None:
189
+ self._backend = backend or SubprocessGitBackend()
190
+ self._git_binary = git_binary or shutil.which("git") or "git"
191
+ process_environment = dict(os.environ)
192
+ if environment is not None:
193
+ process_environment.update(environment)
194
+ process_environment.update(
195
+ {
196
+ "GIT_TERMINAL_PROMPT": "0",
197
+ "GIT_OPTIONAL_LOCKS": "0",
198
+ "GCM_INTERACTIVE": "Never",
199
+ "GIT_CONFIG_COUNT": "1",
200
+ "GIT_CONFIG_KEY_0": "core.hooksPath",
201
+ "GIT_CONFIG_VALUE_0": os.devnull,
202
+ "LC_ALL": "C",
203
+ }
204
+ )
205
+ self._environment = process_environment
206
+ self._invocations: list[GitInvocation] = []
207
+
208
+ @property
209
+ def invocations(self) -> tuple[GitInvocation, ...]:
210
+ return tuple(self._invocations)
211
+
212
+ def inspect(self, checkout: Path) -> GitCheckoutState:
213
+ action = "inspect_checkout"
214
+ candidate = checkout.expanduser()
215
+ if not candidate.is_dir():
216
+ raise _error(
217
+ PLAN_INVALID_CODE,
218
+ "Git checkout must resolve to a directory.",
219
+ action=action,
220
+ )
221
+ resolved = candidate.resolve(strict=True)
222
+ status = self._output(
223
+ ("status", "--porcelain=v1", "--untracked-files=all"),
224
+ cwd=resolved,
225
+ action=action,
226
+ )
227
+ return GitCheckoutState(
228
+ branch=self._current_branch(resolved, action=action),
229
+ dirty=bool(status),
230
+ )
231
+
232
+ def inspect_shared(
233
+ self,
234
+ checkout: Path,
235
+ *,
236
+ expected_source: str,
237
+ ) -> GitCheckoutState:
238
+ action = "inspect_shared_checkout"
239
+ resolved = self._validate_shared_target(
240
+ checkout,
241
+ expected_source=expected_source,
242
+ repo_key="shared",
243
+ action=action,
244
+ )
245
+ status = self._output(
246
+ ("status", "--porcelain=v1", "--untracked-files=all"),
247
+ cwd=resolved,
248
+ action=action,
249
+ )
250
+ return GitCheckoutState(
251
+ branch=self._current_branch(resolved, action=action),
252
+ dirty=bool(status),
253
+ )
254
+
255
+ def update(self, checkout: Path, *, expected_branch: str) -> GitUpdateResult:
256
+ action = "update_business_branch"
257
+ self._reject_shared_mutation(checkout, action=action)
258
+ resolved = self._managed_checkout(
259
+ checkout,
260
+ expected_branch=expected_branch,
261
+ action=action,
262
+ )
263
+ before_head = self._output(
264
+ ("rev-parse", "--verify", "HEAD"),
265
+ cwd=resolved,
266
+ action=action,
267
+ )
268
+ upstream = self._upstream(resolved, expected_branch, action=action)
269
+ remote = "origin" if upstream is None else upstream.split("/", 1)[0]
270
+ self._output(
271
+ ("fetch", "--prune", "--no-tags", "--", remote),
272
+ cwd=resolved,
273
+ action=action,
274
+ )
275
+ if upstream is None:
276
+ return GitUpdateResult(
277
+ checkout=resolved,
278
+ branch=expected_branch,
279
+ upstream=None,
280
+ before_head=before_head,
281
+ after_head=before_head,
282
+ updated=False,
283
+ )
284
+ ancestor = self._execute(
285
+ ("merge-base", "--is-ancestor", "HEAD", upstream),
286
+ cwd=resolved,
287
+ action=action,
288
+ allowed_returncodes=frozenset({0, 1}),
289
+ )
290
+ if ancestor.returncode != 0:
291
+ raise _error(
292
+ BRANCH_UNSAFE_CODE,
293
+ "Business branch cannot be fast-forwarded from its upstream.",
294
+ action=action,
295
+ detail=f"upstream={upstream}",
296
+ )
297
+ self._output(
298
+ ("merge", "--ff-only", "--", upstream),
299
+ cwd=resolved,
300
+ action=action,
301
+ )
302
+ self._require_clean(resolved, action=action)
303
+ if self._current_branch(resolved, action=action) != expected_branch:
304
+ raise _error(
305
+ BRANCH_UNSAFE_CODE,
306
+ "Business branch changed during update.",
307
+ action=action,
308
+ )
309
+ after_head = self._output(
310
+ ("rev-parse", "--verify", "HEAD"),
311
+ cwd=resolved,
312
+ action=action,
313
+ )
314
+ return GitUpdateResult(
315
+ checkout=resolved,
316
+ branch=expected_branch,
317
+ upstream=upstream,
318
+ before_head=before_head,
319
+ after_head=after_head,
320
+ updated=after_head != before_head,
321
+ )
322
+
323
+ def preflight_delete(
324
+ self,
325
+ checkout: Path,
326
+ *,
327
+ expected_branch: str,
328
+ ) -> GitDeleteSafety:
329
+ action = "preflight_delete_checkout"
330
+ self._reject_shared_mutation(checkout, action=action)
331
+ resolved = self._managed_checkout(
332
+ checkout,
333
+ expected_branch=expected_branch,
334
+ action=action,
335
+ )
336
+ upstream = self._upstream(resolved, expected_branch, action=action)
337
+ if upstream is None:
338
+ raise _error(
339
+ BRANCH_UNSAFE_CODE,
340
+ "Checkout deletion requires a configured business upstream.",
341
+ action=action,
342
+ )
343
+ ahead = self._output(
344
+ ("rev-list", "--count", f"{upstream}..HEAD"),
345
+ cwd=resolved,
346
+ action=action,
347
+ )
348
+ if ahead != "0":
349
+ raise _error(
350
+ BRANCH_UNSAFE_CODE,
351
+ "Checkout deletion would discard commits not present upstream.",
352
+ action=action,
353
+ detail=f"upstream={upstream}; ahead={ahead}",
354
+ )
355
+ head = self._output(
356
+ ("rev-parse", "--verify", "HEAD"),
357
+ cwd=resolved,
358
+ action=action,
359
+ )
360
+ return GitDeleteSafety(
361
+ checkout=resolved,
362
+ branch=expected_branch,
363
+ upstream=upstream,
364
+ head=head,
365
+ )
366
+
367
+ def prepare(self, plan: GitBranchPlan) -> PreparedBranch:
368
+ self._validate_plan(plan)
369
+ if plan.checkout.expanduser().is_symlink():
370
+ self._validate_shared_checkout(
371
+ plan,
372
+ action="prepare_business_branch",
373
+ )
374
+ return PreparedBranch(
375
+ checkout=plan.checkout.expanduser(),
376
+ business_branch=plan.business_branch,
377
+ base_branch=plan.base_branch,
378
+ original=None,
379
+ created_local_branch=False,
380
+ new_clone=False,
381
+ project_dir=plan.project_dir.expanduser().resolve(strict=False),
382
+ repo_key=plan.repo_key,
383
+ transaction_id=plan.transaction_id,
384
+ shared_checkout=True,
385
+ )
386
+ checkout = plan.checkout.expanduser().resolve(strict=False)
387
+ if checkout.exists() or checkout.is_symlink():
388
+ if checkout.is_symlink() or not checkout.is_dir():
389
+ raise _error(
390
+ PLAN_INVALID_CODE,
391
+ "existing Git checkout must be a real directory.",
392
+ action="prepare_business_branch",
393
+ )
394
+ return self._prepare_existing(plan, checkout)
395
+ return self._prepare_new_clone(plan)
396
+
397
+ def preflight_existing(self, plan: GitBranchPlan) -> None:
398
+ """Validate existing checkout state without fetching or mutating refs."""
399
+ self._validate_plan(plan)
400
+ if plan.checkout.expanduser().is_symlink():
401
+ self._validate_shared_checkout(
402
+ plan,
403
+ action="preflight_business_branch",
404
+ )
405
+ return
406
+ checkout = plan.checkout.expanduser().resolve(strict=False)
407
+ if not checkout.exists() and not checkout.is_symlink():
408
+ return
409
+ if checkout.is_symlink() or not checkout.is_dir():
410
+ raise _error(
411
+ PLAN_INVALID_CODE,
412
+ "existing Git checkout must be a real directory.",
413
+ action="preflight_business_branch",
414
+ )
415
+ self._inspect_existing_checkout(
416
+ plan, checkout, action="preflight_business_branch"
417
+ )
418
+
419
+ def rollback(self, prepared: PreparedBranch) -> None:
420
+ if prepared.shared_checkout:
421
+ return
422
+ if prepared.new_clone:
423
+ self._remove_prepared_clone(prepared)
424
+ return
425
+ self._rollback_existing(prepared)
426
+
427
+ def _execute(
428
+ self,
429
+ arguments: Sequence[str],
430
+ *,
431
+ cwd: Path | None,
432
+ action: str,
433
+ allowed_returncodes: frozenset[int] = frozenset({0}),
434
+ redactions: Sequence[str] = (),
435
+ ) -> GitCommandResult:
436
+ argv = (self._git_binary, *arguments)
437
+ self._invocations.append(GitInvocation(argv=argv, cwd=cwd))
438
+ try:
439
+ result = self._backend.run(
440
+ argv,
441
+ cwd=cwd,
442
+ environment=self._environment,
443
+ )
444
+ except OSError as exc:
445
+ raise _error(
446
+ GIT_FAILED_CODE,
447
+ "Git backend could not be executed.",
448
+ action=action,
449
+ detail=exc.__class__.__name__,
450
+ ) from exc
451
+ if result.returncode not in allowed_returncodes:
452
+ raw_detail = result.stderr or result.stdout or "git backend failed"
453
+ automatic_redactions = (*redactions, str(cwd) if cwd is not None else "")
454
+ raise _error(
455
+ GIT_FAILED_CODE,
456
+ "Git command failed.",
457
+ action=action,
458
+ detail=_sanitize_backend_detail(
459
+ raw_detail,
460
+ redactions=automatic_redactions,
461
+ ),
462
+ )
463
+ return result
464
+
465
+ def _output(
466
+ self,
467
+ arguments: Sequence[str],
468
+ *,
469
+ cwd: Path | None,
470
+ action: str,
471
+ allowed_returncodes: frozenset[int] = frozenset({0}),
472
+ redactions: Sequence[str] = (),
473
+ ) -> str:
474
+ result = self._execute(
475
+ arguments,
476
+ cwd=cwd,
477
+ action=action,
478
+ allowed_returncodes=allowed_returncodes,
479
+ redactions=redactions,
480
+ )
481
+ return (result.stdout or "").strip()
482
+
483
+ def _validate_plan(self, plan: GitBranchPlan) -> None:
484
+ action = "prepare_business_branch"
485
+ if not _REPO_KEY.fullmatch(plan.repo_key):
486
+ raise _error(
487
+ PLAN_INVALID_CODE,
488
+ "Git repo key is invalid.",
489
+ action=action,
490
+ )
491
+ if not _BRANCH.fullmatch(plan.business_branch):
492
+ raise _error(
493
+ PLAN_INVALID_CODE,
494
+ "Business branch is invalid.",
495
+ action=action,
496
+ )
497
+ if plan.base_branch not in {"develop", "main"}:
498
+ raise _error(
499
+ PLAN_INVALID_CODE,
500
+ "Base branch must be develop or main.",
501
+ action=action,
502
+ )
503
+ if not isinstance(plan.source, str) or not plan.source.strip():
504
+ raise _error(
505
+ PLAN_INVALID_CODE,
506
+ "Git source is required.",
507
+ action=action,
508
+ )
509
+ if not _TRANSACTION_ID.fullmatch(plan.transaction_id):
510
+ raise _error(
511
+ PLAN_INVALID_CODE,
512
+ "Transaction id is invalid.",
513
+ action=action,
514
+ )
515
+ project_dir = plan.project_dir.expanduser().resolve(strict=False)
516
+ apps_root = project_dir / "apps"
517
+ sources_root = apps_root / ".sources"
518
+ if apps_root.is_symlink() or sources_root.is_symlink():
519
+ raise _error(
520
+ PLAN_INVALID_CODE,
521
+ "Git checkout parents cannot be symbolic links.",
522
+ action=action,
523
+ )
524
+ expected_checkout = Path(os.path.abspath(sources_root / plan.repo_key))
525
+ checkout = Path(os.path.abspath(plan.checkout.expanduser()))
526
+ if checkout != expected_checkout:
527
+ raise _error(
528
+ PLAN_INVALID_CODE,
529
+ "Final checkout must be the repo-key child of apps/.sources.",
530
+ action=action,
531
+ )
532
+
533
+ def _validate_shared_checkout(self, plan: GitBranchPlan, *, action: str) -> Path:
534
+ return self._validate_shared_target(
535
+ plan.checkout,
536
+ expected_source=plan.source,
537
+ repo_key=plan.repo_key,
538
+ action=action,
539
+ )
540
+
541
+ def _validate_shared_target(
542
+ self,
543
+ checkout: Path,
544
+ *,
545
+ expected_source: str,
546
+ repo_key: str,
547
+ action: str,
548
+ ) -> Path:
549
+ candidate = checkout.expanduser()
550
+ if not candidate.is_symlink():
551
+ raise _error(
552
+ PLAN_INVALID_CODE,
553
+ "Shared checkout entry must be a symbolic link.",
554
+ action=action,
555
+ )
556
+ try:
557
+ resolved = candidate.resolve(strict=True)
558
+ except OSError as exc:
559
+ raise _error(
560
+ PLAN_INVALID_CODE,
561
+ "Shared checkout target is unavailable.",
562
+ action=action,
563
+ detail=exc.__class__.__name__,
564
+ ) from exc
565
+ if not resolved.is_dir():
566
+ raise _error(
567
+ PLAN_INVALID_CODE,
568
+ "Shared checkout target must be a directory.",
569
+ action=action,
570
+ )
571
+ inside_worktree = self._output(
572
+ ("rev-parse", "--is-inside-work-tree"),
573
+ cwd=resolved,
574
+ action=action,
575
+ )
576
+ if inside_worktree != "true":
577
+ raise _error(
578
+ PLAN_INVALID_CODE,
579
+ "Shared checkout target must be a Git worktree.",
580
+ action=action,
581
+ )
582
+ remote = self._output(
583
+ ("remote", "get-url", "origin"),
584
+ cwd=resolved,
585
+ action=action,
586
+ )
587
+ if self._normalized_git_source(remote) != self._normalized_git_source(
588
+ expected_source
589
+ ):
590
+ raise _error(
591
+ PLAN_INVALID_CODE,
592
+ "Shared checkout origin does not match the manifest source.",
593
+ action=action,
594
+ detail=f"repo={repo_key}",
595
+ )
596
+ return resolved
597
+
598
+ @staticmethod
599
+ def _normalized_git_source(value: str) -> str:
600
+ source = str(value or "").strip().rstrip("/")
601
+ if source.startswith(("/", "./", "../", "~")):
602
+ return str(Path(source).expanduser().resolve(strict=False)).removesuffix(
603
+ ".git"
604
+ )
605
+ return source.removesuffix(".git")
606
+
607
+ @staticmethod
608
+ def _reject_shared_mutation(checkout: Path, *, action: str) -> None:
609
+ if checkout.expanduser().is_symlink():
610
+ raise _error(
611
+ SHARED_CHECKOUT_READ_ONLY_CODE,
612
+ "Shared checkout is externally owned and read-only to PZ authoring commands.",
613
+ action=action,
614
+ )
615
+
616
+ def _require_clean(self, checkout: Path, *, action: str) -> None:
617
+ status = self._output(
618
+ ("status", "--porcelain=v1", "--untracked-files=all"),
619
+ cwd=checkout,
620
+ action=action,
621
+ )
622
+ if status:
623
+ raise _error(
624
+ WORKTREE_DIRTY_CODE,
625
+ "Git checkout has tracked, staged, or untracked changes.",
626
+ action=action,
627
+ )
628
+
629
+ def _managed_checkout(
630
+ self,
631
+ checkout: Path,
632
+ *,
633
+ expected_branch: str,
634
+ action: str,
635
+ ) -> Path:
636
+ self._reject_shared_mutation(checkout, action=action)
637
+ if not _BRANCH.fullmatch(expected_branch):
638
+ raise _error(
639
+ PLAN_INVALID_CODE,
640
+ "Expected business branch is invalid.",
641
+ action=action,
642
+ )
643
+ candidate = checkout.expanduser()
644
+ if candidate.is_symlink() or not candidate.is_dir():
645
+ raise _error(
646
+ PLAN_INVALID_CODE,
647
+ "Git checkout must be a real directory.",
648
+ action=action,
649
+ )
650
+ resolved = candidate.resolve(strict=True)
651
+ self._require_clean(resolved, action=action)
652
+ current = self._current_branch(resolved, action=action)
653
+ if current != expected_branch:
654
+ raise _error(
655
+ BRANCH_UNSAFE_CODE,
656
+ "Git checkout is not on the manifest business branch.",
657
+ action=action,
658
+ detail=f"expected={expected_branch}; actual={current or 'detached HEAD'}",
659
+ )
660
+ return resolved
661
+
662
+ def _upstream(
663
+ self,
664
+ checkout: Path,
665
+ branch: str,
666
+ *,
667
+ action: str,
668
+ ) -> str | None:
669
+ upstream = self._output(
670
+ (
671
+ "for-each-ref",
672
+ "--format=%(upstream:short)",
673
+ f"refs/heads/{branch}",
674
+ ),
675
+ cwd=checkout,
676
+ action=action,
677
+ )
678
+ if not upstream:
679
+ return None
680
+ if (
681
+ upstream.startswith("-")
682
+ or "/" not in upstream
683
+ or any(character.isspace() for character in upstream)
684
+ ):
685
+ raise _error(
686
+ BRANCH_UNSAFE_CODE,
687
+ "Business upstream is not a safe remote branch.",
688
+ action=action,
689
+ )
690
+ return upstream
691
+
692
+ def _current_branch(self, checkout: Path, *, action: str) -> str | None:
693
+ branch = self._output(
694
+ ("symbolic-ref", "--quiet", "--short", "HEAD"),
695
+ cwd=checkout,
696
+ action=action,
697
+ allowed_returncodes=frozenset({0, 1}),
698
+ )
699
+ return branch or None
700
+
701
+ def _snapshot(self, checkout: Path, *, action: str) -> GitSnapshot:
702
+ head = self._output(
703
+ ("rev-parse", "--verify", "HEAD"),
704
+ cwd=checkout,
705
+ action=action,
706
+ )
707
+ branch = self._current_branch(checkout, action=action)
708
+ branches = self._output(
709
+ (
710
+ "for-each-ref",
711
+ "--format=%(refname:short)",
712
+ "refs/heads",
713
+ ),
714
+ cwd=checkout,
715
+ action=action,
716
+ )
717
+ return GitSnapshot(
718
+ head=head,
719
+ branch=branch,
720
+ local_branches=frozenset(value for value in branches.splitlines() if value),
721
+ )
722
+
723
+ def _ref_exists(self, checkout: Path, ref: str, *, action: str) -> bool:
724
+ result = self._execute(
725
+ ("show-ref", "--verify", "--quiet", ref),
726
+ cwd=checkout,
727
+ action=action,
728
+ allowed_returncodes=frozenset({0, 1}),
729
+ )
730
+ return result.returncode == 0
731
+
732
+ def _prepare_existing(
733
+ self,
734
+ plan: GitBranchPlan,
735
+ checkout: Path,
736
+ ) -> PreparedBranch:
737
+ action = "prepare_business_branch"
738
+ original = self._inspect_existing_checkout(plan, checkout, action=action)
739
+ self._output(
740
+ ("fetch", "--all", "--prune"),
741
+ cwd=checkout,
742
+ action=action,
743
+ )
744
+ created_local_branch = self._checkout_business_branch(
745
+ checkout,
746
+ plan,
747
+ current_branch=original.branch,
748
+ action=action,
749
+ )
750
+ prepared_head = self._output(
751
+ ("rev-parse", "--verify", "HEAD"),
752
+ cwd=checkout,
753
+ action=action,
754
+ )
755
+ return PreparedBranch(
756
+ checkout=checkout,
757
+ business_branch=plan.business_branch,
758
+ base_branch=plan.base_branch,
759
+ original=original,
760
+ created_local_branch=created_local_branch,
761
+ new_clone=False,
762
+ prepared_head=prepared_head,
763
+ project_dir=plan.project_dir.expanduser().resolve(strict=False),
764
+ repo_key=plan.repo_key,
765
+ transaction_id=plan.transaction_id,
766
+ )
767
+
768
+ def _inspect_existing_checkout(
769
+ self,
770
+ plan: GitBranchPlan,
771
+ checkout: Path,
772
+ *,
773
+ action: str,
774
+ ) -> GitSnapshot:
775
+ self._require_clean(checkout, action=action)
776
+ original = self._snapshot(checkout, action=action)
777
+ if original.branch not in {
778
+ plan.business_branch,
779
+ "develop",
780
+ "main",
781
+ }:
782
+ current = original.branch or "detached HEAD"
783
+ raise _error(
784
+ BRANCH_UNSAFE_CODE,
785
+ "Git checkout is not on an allowed branch.",
786
+ action=action,
787
+ detail=f"current={current}",
788
+ )
789
+ return original
790
+
791
+ def _prepare_new_clone(self, plan: GitBranchPlan) -> PreparedBranch:
792
+ action = "prepare_business_branch"
793
+ project_dir = plan.project_dir.expanduser().resolve(strict=False)
794
+ transaction_root = (
795
+ project_dir / "apps" / ".sources" / ".transactions" / plan.transaction_id
796
+ )
797
+ transactions_root = transaction_root.parent
798
+ if transactions_root.is_symlink() or transaction_root.is_symlink():
799
+ raise _error(
800
+ PLAN_INVALID_CODE,
801
+ "Git transaction parents cannot be symbolic links.",
802
+ action=action,
803
+ )
804
+ staged_checkout = transaction_root / plan.repo_key
805
+ if staged_checkout.exists() or staged_checkout.is_symlink():
806
+ raise _error(
807
+ PLAN_INVALID_CODE,
808
+ "Staged Git checkout already exists.",
809
+ action=action,
810
+ )
811
+ transaction_root.mkdir(parents=True, exist_ok=True)
812
+ owns_staged_checkout = True
813
+ clone_stat: os.stat_result | None = None
814
+ try:
815
+ self._output(
816
+ (
817
+ "clone",
818
+ "--origin",
819
+ "origin",
820
+ "--no-hardlinks",
821
+ "--",
822
+ plan.source,
823
+ str(staged_checkout),
824
+ ),
825
+ cwd=None,
826
+ action=action,
827
+ redactions=(plan.source, str(staged_checkout)),
828
+ )
829
+ clone_stat = staged_checkout.stat(follow_symlinks=False)
830
+ self._require_clean(staged_checkout, action=action)
831
+ current_branch = self._current_branch(staged_checkout, action=action)
832
+ created_local_branch = self._checkout_business_branch(
833
+ staged_checkout,
834
+ plan,
835
+ current_branch=current_branch,
836
+ action=action,
837
+ )
838
+ prepared_head = self._output(
839
+ ("rev-parse", "--verify", "HEAD"),
840
+ cwd=staged_checkout,
841
+ action=action,
842
+ )
843
+ return PreparedBranch(
844
+ checkout=staged_checkout,
845
+ business_branch=plan.business_branch,
846
+ base_branch=plan.base_branch,
847
+ original=None,
848
+ created_local_branch=created_local_branch,
849
+ new_clone=True,
850
+ prepared_head=prepared_head,
851
+ project_dir=project_dir,
852
+ repo_key=plan.repo_key,
853
+ transaction_id=plan.transaction_id,
854
+ clone_device=clone_stat.st_dev,
855
+ clone_inode=clone_stat.st_ino,
856
+ )
857
+ except Exception:
858
+ if owns_staged_checkout and (
859
+ staged_checkout.exists() or staged_checkout.is_symlink()
860
+ ):
861
+ cleanup_stat = clone_stat or staged_checkout.stat(follow_symlinks=False)
862
+ failed_clone = PreparedBranch(
863
+ checkout=staged_checkout,
864
+ business_branch=plan.business_branch,
865
+ base_branch=plan.base_branch,
866
+ original=None,
867
+ created_local_branch=False,
868
+ new_clone=True,
869
+ project_dir=project_dir,
870
+ repo_key=plan.repo_key,
871
+ transaction_id=plan.transaction_id,
872
+ clone_device=cleanup_stat.st_dev,
873
+ clone_inode=cleanup_stat.st_ino,
874
+ )
875
+ self._remove_prepared_clone(failed_clone)
876
+ raise
877
+
878
+ def _checkout_business_branch(
879
+ self,
880
+ checkout: Path,
881
+ plan: GitBranchPlan,
882
+ *,
883
+ current_branch: str | None,
884
+ action: str,
885
+ ) -> bool:
886
+ local_ref = f"refs/heads/{plan.business_branch}"
887
+ remote_ref = f"refs/remotes/origin/{plan.business_branch}"
888
+ if self._ref_exists(checkout, local_ref, action=action):
889
+ if current_branch != plan.business_branch:
890
+ self._output(
891
+ ("switch", "--no-guess", "--", plan.business_branch),
892
+ cwd=checkout,
893
+ action=action,
894
+ )
895
+ return False
896
+ if self._ref_exists(checkout, remote_ref, action=action):
897
+ self._output(
898
+ (
899
+ "switch",
900
+ "--no-guess",
901
+ "--track",
902
+ "-c",
903
+ plan.business_branch,
904
+ f"origin/{plan.business_branch}",
905
+ ),
906
+ cwd=checkout,
907
+ action=action,
908
+ )
909
+ return True
910
+ base_ref = f"refs/remotes/origin/{plan.base_branch}"
911
+ if not self._ref_exists(checkout, base_ref, action=action):
912
+ raise _error(
913
+ BASE_BRANCH_MISSING_CODE,
914
+ "Selected remote base branch does not exist.",
915
+ action=action,
916
+ detail=f"base=origin/{plan.base_branch}",
917
+ )
918
+ self._output(
919
+ (
920
+ "switch",
921
+ "--no-guess",
922
+ "--no-track",
923
+ "-c",
924
+ plan.business_branch,
925
+ f"origin/{plan.base_branch}",
926
+ ),
927
+ cwd=checkout,
928
+ action=action,
929
+ )
930
+ return True
931
+
932
+ def _rollback_existing(self, prepared: PreparedBranch) -> None:
933
+ action = "rollback_business_branch"
934
+ if prepared.original is None:
935
+ raise _error(
936
+ ROLLBACK_UNSAFE_CODE,
937
+ "Existing checkout rollback is missing its original snapshot.",
938
+ action=action,
939
+ )
940
+ raw_checkout = prepared.checkout.expanduser()
941
+ if raw_checkout.is_symlink():
942
+ raise _error(
943
+ ROLLBACK_UNSAFE_CODE,
944
+ "Existing checkout rollback path is unsafe.",
945
+ action=action,
946
+ )
947
+ checkout = raw_checkout.resolve(strict=False)
948
+ if not checkout.is_dir():
949
+ raise _error(
950
+ ROLLBACK_UNSAFE_CODE,
951
+ "Existing checkout rollback path is unsafe.",
952
+ action=action,
953
+ )
954
+ self._require_clean(checkout, action=action)
955
+ original = prepared.original
956
+ if original.branch is None:
957
+ self._output(
958
+ ("switch", "--detach", original.head),
959
+ cwd=checkout,
960
+ action=action,
961
+ )
962
+ else:
963
+ self._output(
964
+ ("switch", "--no-guess", "--", original.branch),
965
+ cwd=checkout,
966
+ action=action,
967
+ )
968
+ restored_head = self._output(
969
+ ("rev-parse", "--verify", "HEAD"),
970
+ cwd=checkout,
971
+ action=action,
972
+ )
973
+ if restored_head != original.head:
974
+ raise _error(
975
+ ROLLBACK_UNSAFE_CODE,
976
+ "Original Git ref no longer points to the recorded commit.",
977
+ action=action,
978
+ )
979
+ if (
980
+ not prepared.created_local_branch
981
+ or prepared.business_branch in original.local_branches
982
+ or prepared.prepared_head is None
983
+ ):
984
+ return
985
+ business_ref = f"refs/heads/{prepared.business_branch}"
986
+ if not self._ref_exists(checkout, business_ref, action=action):
987
+ return
988
+ current_business_head = self._output(
989
+ ("rev-parse", "--verify", business_ref),
990
+ cwd=checkout,
991
+ action=action,
992
+ )
993
+ if current_business_head != prepared.prepared_head:
994
+ return
995
+ self._output(
996
+ (
997
+ "update-ref",
998
+ "-d",
999
+ business_ref,
1000
+ prepared.prepared_head,
1001
+ ),
1002
+ cwd=checkout,
1003
+ action=action,
1004
+ )
1005
+
1006
+ def _remove_prepared_clone(self, prepared: PreparedBranch) -> None:
1007
+ action = "rollback_business_branch"
1008
+ if (
1009
+ prepared.project_dir is None
1010
+ or prepared.repo_key is None
1011
+ or prepared.transaction_id is None
1012
+ or not _REPO_KEY.fullmatch(prepared.repo_key)
1013
+ or not _TRANSACTION_ID.fullmatch(prepared.transaction_id)
1014
+ ):
1015
+ raise _error(
1016
+ ROLLBACK_UNSAFE_CODE,
1017
+ "Staged clone rollback proof is incomplete.",
1018
+ action=action,
1019
+ )
1020
+ project_dir = prepared.project_dir.expanduser().resolve(strict=False)
1021
+ components = (
1022
+ project_dir / "apps",
1023
+ project_dir / "apps" / ".sources",
1024
+ project_dir / "apps" / ".sources" / ".transactions",
1025
+ project_dir
1026
+ / "apps"
1027
+ / ".sources"
1028
+ / ".transactions"
1029
+ / prepared.transaction_id,
1030
+ )
1031
+ if any(component.is_symlink() for component in components):
1032
+ raise _error(
1033
+ ROLLBACK_UNSAFE_CODE,
1034
+ "Staged clone rollback path contains a symbolic link.",
1035
+ action=action,
1036
+ )
1037
+ transaction_root = components[-1]
1038
+ expected_checkout = transaction_root / prepared.repo_key
1039
+ checkout = prepared.checkout.expanduser()
1040
+ if not checkout.is_absolute():
1041
+ checkout = Path.cwd() / checkout
1042
+ if checkout != expected_checkout or checkout.parent != transaction_root:
1043
+ raise _error(
1044
+ ROLLBACK_UNSAFE_CODE,
1045
+ "Staged clone is not the proved transaction child.",
1046
+ action=action,
1047
+ )
1048
+ if not checkout.exists() and not checkout.is_symlink():
1049
+ return
1050
+ if checkout.is_symlink() or not checkout.is_dir():
1051
+ raise _error(
1052
+ ROLLBACK_UNSAFE_CODE,
1053
+ "Staged clone rollback target is not a real directory.",
1054
+ action=action,
1055
+ )
1056
+ if prepared.clone_device is None or prepared.clone_inode is None:
1057
+ raise _error(
1058
+ ROLLBACK_UNSAFE_CODE,
1059
+ "Staged clone rollback is missing directory identity proof.",
1060
+ action=action,
1061
+ )
1062
+ clone_stat = checkout.stat(follow_symlinks=False)
1063
+ if (
1064
+ clone_stat.st_dev != prepared.clone_device
1065
+ or clone_stat.st_ino != prepared.clone_inode
1066
+ ):
1067
+ raise _error(
1068
+ ROLLBACK_UNSAFE_CODE,
1069
+ "Staged clone directory identity changed after preparation.",
1070
+ action=action,
1071
+ )
1072
+ shutil.rmtree(checkout)
1073
+
1074
+
1075
+ __all__ = [
1076
+ "BaseBranch",
1077
+ "DevGitService",
1078
+ "GitBackend",
1079
+ "GitBranchPlan",
1080
+ "GitCheckoutState",
1081
+ "GitDeleteSafety",
1082
+ "GitInvocation",
1083
+ "GitSnapshot",
1084
+ "GitUpdateResult",
1085
+ "PreparedBranch",
1086
+ "SubprocessGitBackend",
1087
+ ]