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,814 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ import os
6
+ import re
7
+ import shutil
8
+ import stat
9
+ import subprocess
10
+ import sys
11
+ import tempfile
12
+ import tomllib
13
+ import uuid
14
+ from dataclasses import dataclass
15
+ from html.parser import HTMLParser
16
+ from pathlib import Path
17
+ from typing import Callable, Iterable, Mapping, MutableMapping, cast
18
+ from urllib.parse import unquote, urljoin, urlsplit, urlunsplit
19
+ from urllib.request import Request, urlopen
20
+
21
+ import tomlkit # type: ignore[import-not-found]
22
+ from packaging.requirements import Requirement
23
+ from packaging.specifiers import SpecifierSet
24
+ from packaging.utils import canonicalize_name, parse_sdist_filename, parse_wheel_filename
25
+ from packaging.version import InvalidVersion, Version
26
+ from tomlkit.exceptions import TOMLKitError # type: ignore[import-not-found]
27
+
28
+ from pz_core.runtime.errors import PzError
29
+
30
+ from .project_state import (
31
+ add_declared_app,
32
+ ensure_project_state,
33
+ load_installed_apps,
34
+ remove_declared_app,
35
+ )
36
+ from .release import install_env, load_install_target
37
+ from .venv_service import ensure_project_virtualenv, resolve_virtualenv_python
38
+
39
+
40
+ _OWNER = "pz_core.authoring.app_versions"
41
+ _OpenUrl = Callable[..., object]
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class AvailableVersion:
46
+ version: str
47
+ prerelease: bool
48
+ yanked: bool
49
+ compatible: bool
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class AppVersionState:
54
+ app_id: str
55
+ distribution: str
56
+ available: tuple[AvailableVersion, ...]
57
+ latest: str | None
58
+ declared: str | None
59
+ locked: str | None
60
+ installed: str | None
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class AppDeclarationResult:
65
+ app_id: str
66
+ distribution: str
67
+ version: str | None
68
+ installed_apps: tuple[str, ...]
69
+ changed: bool
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class AppInstallResult:
74
+ app_id: str
75
+ distribution: str
76
+ version: str
77
+ python: Path
78
+ lock_path: Path
79
+
80
+
81
+ @dataclass(frozen=True, slots=True)
82
+ class _ReleaseFile:
83
+ version: Version
84
+ yanked: bool
85
+ requires_python: str | None
86
+
87
+
88
+ class _SimpleLinksParser(HTMLParser):
89
+ def __init__(self) -> None:
90
+ super().__init__(convert_charrefs=True)
91
+ self.files: list[tuple[str, bool, str | None]] = []
92
+
93
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
94
+ if tag.lower() != "a":
95
+ return
96
+ values = {key.lower(): value for key, value in attrs}
97
+ href = values.get("href")
98
+ if not href:
99
+ return
100
+ filename = unquote(urlsplit(href).path.rsplit("/", 1)[-1])
101
+ self.files.append(
102
+ (
103
+ filename,
104
+ "data-yanked" in values,
105
+ values.get("data-requires-python"),
106
+ )
107
+ )
108
+
109
+
110
+ def _error(code: str, message: str, *, action: str, detail: str | None = None) -> PzError:
111
+ return PzError(
112
+ code=code,
113
+ message=message,
114
+ stage="cli",
115
+ owner=_OWNER,
116
+ action=action,
117
+ detail=detail,
118
+ )
119
+
120
+
121
+ def distribution_name(app_id: str) -> str:
122
+ return canonicalize_name(app_id.replace("_", "-"))
123
+
124
+
125
+ def _redacted_index(url: str) -> str:
126
+ parsed = urlsplit(url)
127
+ host = parsed.hostname or ""
128
+ if parsed.port is not None:
129
+ host = f"{host}:{parsed.port}"
130
+ return urlunsplit((parsed.scheme, host, parsed.path, "", ""))
131
+
132
+
133
+ def _redact_credentials(text: str) -> str:
134
+ return re.sub(
135
+ r"([A-Za-z][A-Za-z0-9+.-]*://)[^/@\s]+@",
136
+ r"\1<redacted>@",
137
+ text,
138
+ )
139
+
140
+
141
+ def _index_request(url: str) -> Request:
142
+ parsed = urlsplit(url)
143
+ host = parsed.hostname or ""
144
+ if parsed.port is not None:
145
+ host = f"{host}:{parsed.port}"
146
+ clean = urlunsplit((parsed.scheme, host, parsed.path, parsed.query, ""))
147
+ request = Request(
148
+ clean,
149
+ headers={
150
+ "Accept": "application/vnd.pypi.simple.v1+json, text/html;q=0.9",
151
+ "User-Agent": "pz/5 app-versions",
152
+ },
153
+ )
154
+ if parsed.username is not None:
155
+ credentials = f"{unquote(parsed.username)}:{unquote(parsed.password or '')}"
156
+ encoded = base64.b64encode(credentials.encode("utf-8")).decode("ascii")
157
+ request.add_header("Authorization", f"Basic {encoded}")
158
+ return request
159
+
160
+
161
+ def _version_from_filename(filename: str, distribution: str) -> Version | None:
162
+ try:
163
+ if filename.endswith(".whl"):
164
+ name, version, _build, _tags = parse_wheel_filename(filename)
165
+ else:
166
+ name, version = parse_sdist_filename(filename)
167
+ except (InvalidVersion, ValueError):
168
+ return None
169
+ if canonicalize_name(name) != distribution:
170
+ return None
171
+ return version
172
+
173
+
174
+ def _release_files_from_json(payload: object, distribution: str) -> tuple[_ReleaseFile, ...]:
175
+ if not isinstance(payload, Mapping) or not isinstance(payload.get("files"), list):
176
+ raise ValueError("Simple API JSON payload does not contain files")
177
+ files: list[_ReleaseFile] = []
178
+ for raw in cast(list[object], payload["files"]):
179
+ if not isinstance(raw, Mapping):
180
+ continue
181
+ filename = raw.get("filename")
182
+ if not isinstance(filename, str):
183
+ continue
184
+ version = _version_from_filename(filename, distribution)
185
+ if version is None:
186
+ continue
187
+ requires_python = raw.get("requires-python")
188
+ files.append(
189
+ _ReleaseFile(
190
+ version=version,
191
+ yanked=bool(raw.get("yanked", False)),
192
+ requires_python=requires_python if isinstance(requires_python, str) else None,
193
+ )
194
+ )
195
+ return tuple(files)
196
+
197
+
198
+ def _release_files_from_html(text: str, distribution: str) -> tuple[_ReleaseFile, ...]:
199
+ parser = _SimpleLinksParser()
200
+ parser.feed(text)
201
+ files: list[_ReleaseFile] = []
202
+ for filename, yanked, requires_python in parser.files:
203
+ version = _version_from_filename(filename, distribution)
204
+ if version is not None:
205
+ files.append(_ReleaseFile(version, yanked, requires_python))
206
+ return tuple(files)
207
+
208
+
209
+ def _python_compatible(specifier: str | None) -> bool:
210
+ if not specifier:
211
+ return True
212
+ current = Version(".".join(str(value) for value in sys.version_info[:3]))
213
+ try:
214
+ return current in SpecifierSet(specifier)
215
+ except Exception:
216
+ return False
217
+
218
+
219
+ def _available_versions(files: Iterable[_ReleaseFile]) -> tuple[AvailableVersion, ...]:
220
+ grouped: dict[Version, list[_ReleaseFile]] = {}
221
+ for item in files:
222
+ grouped.setdefault(item.version, []).append(item)
223
+ result: list[AvailableVersion] = []
224
+ for version in sorted(grouped, reverse=True):
225
+ release_files = grouped[version]
226
+ compatible_files = [item for item in release_files if _python_compatible(item.requires_python)]
227
+ result.append(
228
+ AvailableVersion(
229
+ version=str(version),
230
+ prerelease=version.is_prerelease,
231
+ yanked=all(item.yanked for item in release_files),
232
+ compatible=any(not item.yanked for item in compatible_files),
233
+ )
234
+ )
235
+ return tuple(result)
236
+
237
+
238
+ def _read_declared_version(project_dir: Path, distribution: str) -> str | None:
239
+ path = project_dir / "pyproject.toml"
240
+ if not path.is_file():
241
+ return None
242
+ try:
243
+ payload = tomllib.loads(path.read_text(encoding="utf-8"))
244
+ except (OSError, UnicodeError, tomllib.TOMLDecodeError):
245
+ return None
246
+ project = payload.get("project")
247
+ dependencies = project.get("dependencies") if isinstance(project, Mapping) else None
248
+ if not isinstance(dependencies, list):
249
+ return None
250
+ for raw in dependencies:
251
+ if not isinstance(raw, str):
252
+ continue
253
+ try:
254
+ requirement = Requirement(raw)
255
+ except Exception:
256
+ continue
257
+ if canonicalize_name(requirement.name) != distribution:
258
+ continue
259
+ specs = list(requirement.specifier)
260
+ if len(specs) == 1 and specs[0].operator == "==" and not specs[0].version.endswith(".*"):
261
+ return str(Version(specs[0].version))
262
+ return None
263
+
264
+
265
+ def _read_locked_version(project_dir: Path, distribution: str) -> str | None:
266
+ path = project_dir / "uv.lock"
267
+ if not path.is_file():
268
+ return None
269
+ try:
270
+ payload = tomllib.loads(path.read_text(encoding="utf-8"))
271
+ except (OSError, UnicodeError, tomllib.TOMLDecodeError):
272
+ return None
273
+ packages = payload.get("package")
274
+ if not isinstance(packages, list):
275
+ return None
276
+ for package in packages:
277
+ if not isinstance(package, Mapping):
278
+ continue
279
+ if canonicalize_name(str(package.get("name") or "")) != distribution:
280
+ continue
281
+ version = package.get("version")
282
+ return str(version) if isinstance(version, str) and version else None
283
+ return None
284
+
285
+
286
+ def _read_installed_version(project_dir: Path, distribution: str, *, action: str) -> str | None:
287
+ venv = project_dir / ".venv"
288
+ if not venv.is_dir():
289
+ return None
290
+ try:
291
+ python = resolve_virtualenv_python(venv, action=action)
292
+ except PzError:
293
+ return None
294
+ code = (
295
+ "import importlib.metadata, sys; "
296
+ "\ntry: print(importlib.metadata.version(sys.argv[1]))"
297
+ "\nexcept importlib.metadata.PackageNotFoundError: raise SystemExit(3)"
298
+ )
299
+ completed = subprocess.run(
300
+ (str(python), "-c", code, distribution),
301
+ check=False,
302
+ capture_output=True,
303
+ text=True,
304
+ )
305
+ if completed.returncode == 3:
306
+ return None
307
+ if completed.returncode != 0:
308
+ raise _error(
309
+ "PZ_APP_VERSION_STATE_MISMATCH",
310
+ f"failed to inspect installed version for {distribution}.",
311
+ action=action,
312
+ detail=(completed.stderr or completed.stdout or "metadata inspection failed").strip(),
313
+ )
314
+ value = completed.stdout.strip()
315
+ return value or None
316
+
317
+
318
+ def query_app_versions(
319
+ app_id: str,
320
+ *,
321
+ project_dir: Path,
322
+ action: str,
323
+ opener: _OpenUrl = urlopen,
324
+ ) -> AppVersionState:
325
+ distribution = distribution_name(app_id)
326
+ target = load_install_target(app_id, project_dir=project_dir, action=action)
327
+ env = install_env(target=target, project_dir=project_dir, action=action)
328
+ base = env.get("UV_DEFAULT_INDEX") or target.index_url or "https://pypi.org/simple/"
329
+ url = urljoin(base.rstrip("/") + "/", distribution + "/")
330
+ try:
331
+ response = opener(_index_request(url), timeout=20)
332
+ content_type = str(getattr(response, "headers", {}).get("Content-Type", ""))
333
+ body = cast(bytes, response.read())
334
+ if "json" in content_type:
335
+ files = _release_files_from_json(json.loads(body.decode("utf-8")), distribution)
336
+ else:
337
+ files = _release_files_from_html(body.decode("utf-8"), distribution)
338
+ except Exception as exc:
339
+ raise _error(
340
+ "PZ_APP_INDEX_QUERY_FAILED",
341
+ f"failed to query versions for {distribution}.",
342
+ action=action,
343
+ detail=f"index={_redacted_index(url)}; error={exc}",
344
+ ) from exc
345
+ available = _available_versions(files)
346
+ latest = next(
347
+ (item.version for item in available if item.compatible and not item.prerelease and not item.yanked),
348
+ None,
349
+ )
350
+ return AppVersionState(
351
+ app_id=app_id,
352
+ distribution=distribution,
353
+ available=available,
354
+ latest=latest,
355
+ declared=_read_declared_version(project_dir, distribution),
356
+ locked=_read_locked_version(project_dir, distribution),
357
+ installed=_read_installed_version(project_dir, distribution, action=action),
358
+ )
359
+
360
+
361
+ def _candidate_resolves(
362
+ state: AppVersionState,
363
+ version: str,
364
+ *,
365
+ project_dir: Path,
366
+ action: str,
367
+ ) -> bool:
368
+ uv_binary = shutil.which("uv")
369
+ if uv_binary is None:
370
+ raise _error(
371
+ "PZ_ENV_UV_MISSING",
372
+ "required dependency backend was not found on PATH.",
373
+ action=action,
374
+ )
375
+ source_pyproject = project_dir / "pyproject.toml"
376
+ if not source_pyproject.is_file():
377
+ raise _error(
378
+ "PZ_PROJECT_METADATA_INVALID",
379
+ "project pyproject.toml is missing.",
380
+ action=action,
381
+ )
382
+ with tempfile.TemporaryDirectory(prefix="pz-app-resolve-") as raw_temp:
383
+ temp = Path(raw_temp)
384
+ shutil.copy2(source_pyproject, temp / "pyproject.toml")
385
+ lock = project_dir / "uv.lock"
386
+ if lock.is_file():
387
+ shutil.copy2(lock, temp / "uv.lock")
388
+ _update_dependency(
389
+ temp,
390
+ state.distribution,
391
+ version,
392
+ action=action,
393
+ )
394
+ _absolutize_temp_source_paths(temp, source_project=project_dir, action=action)
395
+ target = load_install_target(
396
+ state.app_id, project_dir=project_dir, action=action
397
+ )
398
+ env = install_env(target=target, project_dir=project_dir, action=action)
399
+ completed = subprocess.run(
400
+ (
401
+ uv_binary,
402
+ "lock",
403
+ "--dry-run",
404
+ "--project",
405
+ str(temp),
406
+ "--upgrade-package",
407
+ state.distribution,
408
+ "--no-sources-package",
409
+ state.distribution,
410
+ ),
411
+ check=False,
412
+ capture_output=True,
413
+ text=True,
414
+ env=env,
415
+ )
416
+ return completed.returncode == 0
417
+
418
+
419
+ def _absolutize_temp_source_paths(
420
+ temp_project: Path,
421
+ *,
422
+ source_project: Path,
423
+ action: str,
424
+ ) -> None:
425
+ path = temp_project / "pyproject.toml"
426
+ try:
427
+ document = tomlkit.parse(path.read_text(encoding="utf-8"))
428
+ except (OSError, UnicodeError, TOMLKitError) as exc:
429
+ raise _error(
430
+ "PZ_PROJECT_METADATA_INVALID",
431
+ "temporary project metadata is invalid.",
432
+ action=action,
433
+ detail=str(exc),
434
+ ) from exc
435
+ tool = document.get("tool")
436
+ uv = tool.get("uv") if isinstance(tool, MutableMapping) else None
437
+ sources = uv.get("sources") if isinstance(uv, MutableMapping) else None
438
+ if not isinstance(sources, MutableMapping):
439
+ return
440
+ changed = False
441
+ for raw_source in sources.values():
442
+ entries = raw_source if isinstance(raw_source, list) else (raw_source,)
443
+ for entry in entries:
444
+ if not isinstance(entry, MutableMapping):
445
+ continue
446
+ raw_path = entry.get("path")
447
+ if not isinstance(raw_path, str) or Path(raw_path).is_absolute():
448
+ continue
449
+ entry["path"] = str((source_project / raw_path).resolve(strict=False))
450
+ changed = True
451
+ if changed:
452
+ path.write_text(tomlkit.dumps(document), encoding="utf-8")
453
+
454
+
455
+ def resolve_app_version(
456
+ state: AppVersionState,
457
+ requested: str | None,
458
+ *,
459
+ action: str,
460
+ project_dir: Path | None = None,
461
+ ) -> str:
462
+ if requested is None:
463
+ candidates = tuple(
464
+ item.version
465
+ for item in state.available
466
+ if item.compatible and not item.prerelease and not item.yanked
467
+ )
468
+ if not candidates:
469
+ raise _error(
470
+ "PZ_APP_VERSION_NOT_FOUND",
471
+ f"no compatible stable version is available for {state.distribution}.",
472
+ action=action,
473
+ )
474
+ if project_dir is None:
475
+ return candidates[0]
476
+ for candidate in candidates:
477
+ if _candidate_resolves(
478
+ state,
479
+ candidate,
480
+ project_dir=project_dir,
481
+ action=action,
482
+ ):
483
+ return candidate
484
+ raise _error(
485
+ "PZ_APP_VERSION_INCOMPATIBLE",
486
+ f"no stable version of {state.distribution} satisfies the project dependency graph.",
487
+ action=action,
488
+ )
489
+ try:
490
+ version = Version(requested)
491
+ except InvalidVersion as exc:
492
+ raise _error(
493
+ "PZ_APP_VERSION_NOT_FOUND",
494
+ f"requested version is not an exact PEP 440 version: {requested}",
495
+ action=action,
496
+ ) from exc
497
+ match = next((item for item in state.available if Version(item.version) == version), None)
498
+ if match is None:
499
+ raise _error(
500
+ "PZ_APP_VERSION_NOT_FOUND",
501
+ f"version {version} is not available for {state.distribution}.",
502
+ action=action,
503
+ )
504
+ if match.yanked or not match.compatible:
505
+ raise _error(
506
+ "PZ_APP_VERSION_INCOMPATIBLE",
507
+ f"version {version} is not installable for {state.distribution}.",
508
+ action=action,
509
+ )
510
+ normalized = str(version)
511
+ if project_dir is not None and not _candidate_resolves(
512
+ state,
513
+ normalized,
514
+ project_dir=project_dir,
515
+ action=action,
516
+ ):
517
+ raise _error(
518
+ "PZ_APP_VERSION_INCOMPATIBLE",
519
+ f"version {normalized} conflicts with the project dependency graph.",
520
+ action=action,
521
+ )
522
+ return normalized
523
+
524
+
525
+ def _project_table(document: object, *, action: str) -> MutableMapping[str, object]:
526
+ if not isinstance(document, MutableMapping):
527
+ raise _error("PZ_PROJECT_METADATA_INVALID", "pyproject root must be a table.", action=action)
528
+ project = document.get("project")
529
+ if not isinstance(project, MutableMapping):
530
+ raise _error("PZ_PROJECT_METADATA_INVALID", "pyproject project must be a table.", action=action)
531
+ return cast(MutableMapping[str, object], project)
532
+
533
+
534
+ def _write_atomic(path: Path, before: bytes, after: bytes, *, action: str) -> None:
535
+ if before == after:
536
+ return
537
+ mode = stat.S_IMODE(path.stat().st_mode)
538
+ staged = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
539
+ try:
540
+ descriptor = os.open(staged, os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode)
541
+ with os.fdopen(descriptor, "wb") as handle:
542
+ handle.write(after)
543
+ handle.flush()
544
+ os.fsync(handle.fileno())
545
+ if path.read_bytes() != before:
546
+ raise _error("PZ_PROJECT_METADATA_CHANGED", "pyproject.toml changed concurrently.", action=action)
547
+ os.replace(staged, path)
548
+ finally:
549
+ staged.unlink(missing_ok=True)
550
+
551
+
552
+ def _restore_snapshot(
553
+ path: Path,
554
+ *,
555
+ before: bytes,
556
+ expected_current: bytes,
557
+ action: str,
558
+ ) -> None:
559
+ try:
560
+ current = path.read_bytes()
561
+ except OSError as exc:
562
+ raise _error(
563
+ "PZ_PROJECT_METADATA_CHANGED",
564
+ f"cannot restore missing transaction file: {path.name}",
565
+ action=action,
566
+ detail=str(exc),
567
+ ) from exc
568
+ if current == before:
569
+ return
570
+ if current != expected_current:
571
+ raise _error(
572
+ "PZ_PROJECT_METADATA_CHANGED",
573
+ f"cannot restore concurrently changed transaction file: {path.name}",
574
+ action=action,
575
+ )
576
+ _write_atomic(path, current, before, action=action)
577
+
578
+
579
+ def _update_dependency(
580
+ project_dir: Path,
581
+ distribution: str,
582
+ version: str | None,
583
+ *,
584
+ action: str,
585
+ ) -> bool:
586
+ path = project_dir / "pyproject.toml"
587
+ try:
588
+ before = path.read_bytes()
589
+ document = tomlkit.parse(before.decode("utf-8"))
590
+ except (OSError, UnicodeError, TOMLKitError) as exc:
591
+ raise _error("PZ_PROJECT_METADATA_INVALID", "project pyproject.toml is invalid.", action=action, detail=str(exc)) from exc
592
+ project = _project_table(document, action=action)
593
+ dependencies = project.get("dependencies")
594
+ if dependencies is None:
595
+ dependencies = tomlkit.array().multiline(True)
596
+ project["dependencies"] = dependencies
597
+ if not isinstance(dependencies, list):
598
+ raise _error("PZ_PROJECT_METADATA_INVALID", "project.dependencies must be an array.", action=action)
599
+ retained: list[object] = []
600
+ for raw in dependencies:
601
+ if isinstance(raw, str):
602
+ try:
603
+ if canonicalize_name(Requirement(raw).name) == distribution:
604
+ continue
605
+ except Exception:
606
+ pass
607
+ retained.append(raw)
608
+ if version is not None:
609
+ retained.append(f"{distribution}=={version}")
610
+ dependencies.clear()
611
+ dependencies.extend(retained)
612
+ if version is not None:
613
+ tool = document.get("tool")
614
+ uv = tool.get("uv") if isinstance(tool, MutableMapping) else None
615
+ sources = uv.get("sources") if isinstance(uv, MutableMapping) else None
616
+ if isinstance(sources, MutableMapping):
617
+ for key in tuple(sources):
618
+ if canonicalize_name(str(key)) == distribution:
619
+ sources.pop(key, None)
620
+ after = tomlkit.dumps(document).encode("utf-8")
621
+ _write_atomic(path, before, after, action=action)
622
+ return after != before
623
+
624
+
625
+ def declare_app_version(
626
+ app_id: str,
627
+ version: str,
628
+ *,
629
+ project_dir: Path,
630
+ action: str,
631
+ ) -> AppDeclarationResult:
632
+ ensure_project_state(project_dir, action=action)
633
+ normalized = str(Version(version))
634
+ distribution = distribution_name(app_id)
635
+ business_source = project_dir / "apps" / app_id
636
+ if business_source.exists() and not business_source.is_symlink():
637
+ raise _error(
638
+ "PZ_APP_SOURCE_OWNERSHIP_CONFLICT",
639
+ f"{app_id} is already owned as business source under apps/.",
640
+ action=action,
641
+ detail=str(business_source),
642
+ )
643
+ pyproject = project_dir / "pyproject.toml"
644
+ config = project_dir / "pz.jsonc"
645
+ pyproject_before = pyproject.read_bytes()
646
+ config_before = config.read_bytes()
647
+ changed = _update_dependency(project_dir, distribution, normalized, action=action)
648
+ pyproject_after = pyproject.read_bytes()
649
+ try:
650
+ installed_apps = tuple(add_declared_app(project_dir, app_id, action=action))
651
+ except Exception:
652
+ config_after = config.read_bytes()
653
+ _restore_snapshot(
654
+ pyproject,
655
+ before=pyproject_before,
656
+ expected_current=pyproject_after,
657
+ action=action,
658
+ )
659
+ _restore_snapshot(
660
+ config,
661
+ before=config_before,
662
+ expected_current=config_after,
663
+ action=action,
664
+ )
665
+ raise
666
+ return AppDeclarationResult(app_id, distribution, normalized, installed_apps, changed)
667
+
668
+
669
+ def undeclare_app_version(
670
+ app_id: str,
671
+ *,
672
+ project_dir: Path,
673
+ action: str,
674
+ ) -> AppDeclarationResult:
675
+ ensure_project_state(project_dir, action=action)
676
+ if app_id not in load_installed_apps(project_dir, action=action):
677
+ raise _error("PZ_SCAFFOLD_APP_NOT_DECLARED", f"{app_id} is not declared.", action=action)
678
+ distribution = distribution_name(app_id)
679
+ pyproject = project_dir / "pyproject.toml"
680
+ config = project_dir / "pz.jsonc"
681
+ pyproject_before = pyproject.read_bytes()
682
+ config_before = config.read_bytes()
683
+ changed = _update_dependency(project_dir, distribution, None, action=action)
684
+ pyproject_after = pyproject.read_bytes()
685
+ try:
686
+ installed_apps = tuple(remove_declared_app(project_dir, app_id, action=action))
687
+ except Exception:
688
+ config_after = config.read_bytes()
689
+ _restore_snapshot(
690
+ pyproject,
691
+ before=pyproject_before,
692
+ expected_current=pyproject_after,
693
+ action=action,
694
+ )
695
+ _restore_snapshot(
696
+ config,
697
+ before=config_before,
698
+ expected_current=config_after,
699
+ action=action,
700
+ )
701
+ raise
702
+ return AppDeclarationResult(app_id, distribution, None, installed_apps, changed)
703
+
704
+
705
+ def install_declared_app(
706
+ app_id: str,
707
+ version: str,
708
+ *,
709
+ project_dir: Path,
710
+ action: str,
711
+ ) -> AppInstallResult:
712
+ distribution = distribution_name(app_id)
713
+ normalized = str(Version(version))
714
+ declared = _read_declared_version(project_dir, distribution)
715
+ if declared != normalized:
716
+ raise _error(
717
+ "PZ_APP_VERSION_STATE_MISMATCH",
718
+ f"declared version for {distribution} is {declared!r}, expected {normalized}.",
719
+ action=action,
720
+ )
721
+ locked = _read_locked_version(project_dir, distribution)
722
+ actual = _read_installed_version(project_dir, distribution, action=action)
723
+ if locked == normalized and actual == normalized:
724
+ python = resolve_virtualenv_python(project_dir / ".venv", action=action)
725
+ return AppInstallResult(
726
+ app_id,
727
+ distribution,
728
+ normalized,
729
+ python,
730
+ project_dir / "uv.lock",
731
+ )
732
+ venv = ensure_project_virtualenv(project_dir, action=action)
733
+ python = resolve_virtualenv_python(venv.path, action=action)
734
+ uv_binary = shutil.which("uv")
735
+ if uv_binary is None:
736
+ raise _error("PZ_ENV_UV_MISSING", "required dependency backend was not found on PATH.", action=action)
737
+ target = load_install_target(app_id, project_dir=project_dir, action=action)
738
+ env = install_env(target=target, project_dir=project_dir, action=action)
739
+ env["VIRTUAL_ENV"] = str(venv.path)
740
+ completed = subprocess.run(
741
+ (uv_binary, "sync", "--active"),
742
+ cwd=str(project_dir),
743
+ check=False,
744
+ capture_output=True,
745
+ text=True,
746
+ env=env,
747
+ )
748
+ if completed.returncode != 0:
749
+ raise _error(
750
+ "PZ_ENV_SYNC_FAILED",
751
+ f"failed to install {distribution}=={normalized}.",
752
+ action=action,
753
+ detail=_redact_credentials(
754
+ (completed.stderr or completed.stdout or "dependency sync failed").strip()
755
+ ),
756
+ )
757
+ actual = _read_installed_version(project_dir, distribution, action=action)
758
+ locked = _read_locked_version(project_dir, distribution)
759
+ if actual != normalized or locked != normalized:
760
+ raise _error(
761
+ "PZ_APP_VERSION_STATE_MISMATCH",
762
+ f"installed and locked versions for {distribution} did not converge to {normalized}.",
763
+ action=action,
764
+ detail=f"locked={locked!r}; installed={actual!r}",
765
+ )
766
+ return AppInstallResult(app_id, distribution, normalized, python, project_dir / "uv.lock")
767
+
768
+
769
+ def reconcile_project_environment(
770
+ app_id: str,
771
+ *,
772
+ project_dir: Path,
773
+ action: str,
774
+ ) -> Path:
775
+ venv = ensure_project_virtualenv(project_dir, action=action)
776
+ uv_binary = shutil.which("uv")
777
+ if uv_binary is None:
778
+ raise _error("PZ_ENV_UV_MISSING", "required dependency backend was not found on PATH.", action=action)
779
+ target = load_install_target(app_id, project_dir=project_dir, action=action)
780
+ env = install_env(target=target, project_dir=project_dir, action=action)
781
+ env["VIRTUAL_ENV"] = str(venv.path)
782
+ completed = subprocess.run(
783
+ (uv_binary, "sync", "--active"),
784
+ cwd=str(project_dir),
785
+ check=False,
786
+ capture_output=True,
787
+ text=True,
788
+ env=env,
789
+ )
790
+ if completed.returncode != 0:
791
+ raise _error(
792
+ "PZ_ENV_SYNC_FAILED",
793
+ "failed to reconcile the project environment.",
794
+ action=action,
795
+ detail=_redact_credentials(
796
+ (completed.stderr or completed.stdout or "dependency sync failed").strip()
797
+ ),
798
+ )
799
+ return venv.path
800
+
801
+
802
+ __all__ = [
803
+ "AppDeclarationResult",
804
+ "AppInstallResult",
805
+ "AppVersionState",
806
+ "AvailableVersion",
807
+ "declare_app_version",
808
+ "distribution_name",
809
+ "install_declared_app",
810
+ "query_app_versions",
811
+ "reconcile_project_environment",
812
+ "resolve_app_version",
813
+ "undeclare_app_version",
814
+ ]