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,1382 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ from dataclasses import dataclass
5
+ from email.parser import BytesParser
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import re
10
+ import shutil
11
+ import subprocess
12
+ import tempfile
13
+ import tomllib
14
+ import zipfile
15
+ from urllib.error import HTTPError, URLError
16
+ from urllib.request import Request, urlopen
17
+ from urllib.parse import quote, urlsplit, urlunsplit
18
+
19
+ from pz_core.runtime.errors import PzError
20
+ from pz_core.utils import load_jsonc_file
21
+
22
+ from .publish_lock import enforce_publish_lock
23
+ from .publish_git import (
24
+ PublishCommitResult,
25
+ PublishTagResult,
26
+ commit_publish_app,
27
+ preflight_publish_app,
28
+ require_publish_checkout_at_tag,
29
+ require_publish_tag,
30
+ tag_publish_app,
31
+ )
32
+ from .publish_source import resolve_publish_source
33
+ from .project_state import normalize_formal_app_id, resolve_project_config_path
34
+ from .workspace import resolve_workspace_package
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class PublishTarget:
39
+ name: str
40
+ index: str | None = None
41
+ publish_url: str | None = None
42
+ check_url: str | None = None
43
+ trusted_publishing: str | None = None
44
+ token_env: str | None = None
45
+ username_env: str | None = None
46
+ password_env: str | None = None
47
+ install_username_env: str | None = None
48
+ install_password_env: str | None = None
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class InstallTarget:
53
+ name: str
54
+ index_url: str | None = None
55
+ username_env: str | None = None
56
+ password_env: str | None = None
57
+
58
+
59
+ @dataclass(frozen=True, slots=True)
60
+ class PublishResult:
61
+ app_id: str
62
+ version: str
63
+ package_root: Path
64
+ dist_dir: Path
65
+ target: PublishTarget
66
+ dry_run: bool
67
+ commit: str | None = None
68
+ tag: str | None = None
69
+
70
+
71
+ @dataclass(frozen=True, slots=True)
72
+ class UnpublishResult:
73
+ app_id: str
74
+ distribution_name: str
75
+ version: str
76
+ package_root: Path
77
+ target: PublishTarget
78
+ project_url: str
79
+ removed_versions: tuple[str, ...]
80
+ dry_run: bool
81
+ force: bool
82
+
83
+
84
+ def normalize_publish_app_id(
85
+ project_dir: Path,
86
+ app_id: str,
87
+ *,
88
+ action: str,
89
+ ) -> str:
90
+ del project_dir
91
+ workspace_package = resolve_workspace_package(str(app_id))
92
+ if workspace_package is not None:
93
+ return workspace_package.app_id
94
+ if not isinstance(app_id, str) or not app_id.strip():
95
+ raise PzError(
96
+ code="PZ_PUBLISH_APP_ID_INVALID",
97
+ message="publish app id must be a non-empty string.",
98
+ stage="cli",
99
+ owner="pz_core.authoring.release",
100
+ action=action,
101
+ )
102
+ return normalize_formal_app_id(
103
+ app_id.strip().replace("-", "_"),
104
+ action=action,
105
+ )
106
+
107
+
108
+ def resolve_publish_package_root(
109
+ project_dir: Path,
110
+ app_id: str,
111
+ *,
112
+ action: str,
113
+ ) -> Path:
114
+ normalized_app_id = normalize_publish_app_id(
115
+ project_dir,
116
+ app_id,
117
+ action=action,
118
+ )
119
+ workspace_package = resolve_workspace_package(normalized_app_id)
120
+ if workspace_package is not None and workspace_package.repo.is_dir():
121
+ return workspace_package.repo.resolve()
122
+
123
+ from pz_core.assembly.app_discovery import iter_visible_app_projections
124
+
125
+ for projection in iter_visible_app_projections(project_dir):
126
+ if projection.app_id == normalized_app_id:
127
+ return projection.source_root
128
+
129
+ requested_distribution = normalized_app_id.replace("_", "-")
130
+ if _read_project_distribution_name(project_dir) == requested_distribution:
131
+ return project_dir.resolve()
132
+ raise PzError(
133
+ code="PZ_PUBLISH_SOURCE_MISSING",
134
+ message=f"could not resolve package root for {normalized_app_id}.",
135
+ stage="cli",
136
+ owner="pz_core.authoring.release",
137
+ action=action,
138
+ )
139
+
140
+
141
+ def _read_project_distribution_name(package_root: Path) -> str | None:
142
+ pyproject = package_root / "pyproject.toml"
143
+ if not pyproject.is_file():
144
+ return None
145
+ try:
146
+ payload = tomllib.loads(pyproject.read_text(encoding="utf-8"))
147
+ except (OSError, UnicodeError, tomllib.TOMLDecodeError):
148
+ return None
149
+ project = payload.get("project")
150
+ if not isinstance(project, dict):
151
+ return None
152
+ name = project.get("name")
153
+ return name.strip() if isinstance(name, str) and name.strip() else None
154
+
155
+
156
+ def _read_dotenv(path: Path, *, action: str) -> dict[str, str]:
157
+ values: dict[str, str] = {}
158
+ if not path.is_file():
159
+ return values
160
+ try:
161
+ lines = path.read_text(encoding="utf-8").splitlines()
162
+ except Exception as exc:
163
+ raise PzError(
164
+ code="PZ_SCAFFOLD_DOTENV_INVALID",
165
+ message=f"failed to read env file: {path}",
166
+ stage="cli",
167
+ owner="pz_core.authoring",
168
+ action=action,
169
+ detail=str(exc),
170
+ ) from exc
171
+ for line_number, raw_line in enumerate(lines, start=1):
172
+ line = raw_line.strip()
173
+ if not line or line.startswith("#"):
174
+ continue
175
+ if "=" not in raw_line:
176
+ raise PzError(
177
+ code="PZ_SCAFFOLD_DOTENV_INVALID",
178
+ message=f"invalid env entry at line {line_number}: missing '=' delimiter.",
179
+ stage="cli",
180
+ owner="pz_core.authoring",
181
+ action=action,
182
+ detail=str(path),
183
+ )
184
+ key, raw_value = raw_line.split("=", 1)
185
+ normalized_key = key.strip()
186
+ if not normalized_key:
187
+ raise PzError(
188
+ code="PZ_SCAFFOLD_DOTENV_INVALID",
189
+ message=f"invalid env entry at line {line_number}: key must be non-empty.",
190
+ stage="cli",
191
+ owner="pz_core.authoring",
192
+ action=action,
193
+ detail=str(path),
194
+ )
195
+ value = raw_value.strip()
196
+ if value[:1] in {"'", '"'}:
197
+ quote_char = value[0]
198
+ if len(value) < 2 or not value.endswith(quote_char):
199
+ raise PzError(
200
+ code="PZ_SCAFFOLD_DOTENV_INVALID",
201
+ message=f"invalid env entry at line {line_number}: unterminated quoted value.",
202
+ stage="cli",
203
+ owner="pz_core.authoring",
204
+ action=action,
205
+ detail=str(path),
206
+ )
207
+ value = value[1:-1]
208
+ values[normalized_key] = value
209
+ return values
210
+
211
+
212
+ def build_authoring_env(*roots: Path, action: str) -> dict[str, str]:
213
+ env = dict(os.environ)
214
+ seen: set[Path] = set()
215
+ for root in roots:
216
+ candidate_root = Path(root).expanduser().resolve()
217
+ if candidate_root in seen:
218
+ continue
219
+ seen.add(candidate_root)
220
+ for key, value in _read_dotenv(candidate_root / ".env", action=action).items():
221
+ env.setdefault(key, value)
222
+ return env
223
+
224
+
225
+ def _is_devpi_target(target: PublishTarget, *urls: str | None) -> bool:
226
+ target_name = str(target.name or "").strip().lower()
227
+ if target_name.startswith("devpi_") or target_name == "devpi":
228
+ return True
229
+ for url in urls:
230
+ if not isinstance(url, str) or not url.strip():
231
+ continue
232
+ path = str(urlsplit(url.strip()).path or "").strip().lower()
233
+ if path.endswith("/+simple/") or path.endswith("/+simple"):
234
+ return True
235
+ return False
236
+
237
+
238
+ def _inject_basic_auth(url: str, *, username: str, password: str) -> str:
239
+ parsed = urlsplit(url)
240
+ hostname = str(parsed.hostname or "").strip()
241
+ if not hostname:
242
+ return url
243
+ userinfo = f"{quote(username, safe='')}:{quote(password, safe='')}"
244
+ netloc = f"{userinfo}@{hostname}"
245
+ if parsed.port is not None:
246
+ netloc += f":{parsed.port}"
247
+ return urlunsplit(
248
+ (parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment)
249
+ )
250
+
251
+
252
+ def _require_named_env(
253
+ env: dict[str, str], env_name: str, *, code: str, message: str, action: str
254
+ ) -> str:
255
+ value = str(env.get(env_name) or "").strip()
256
+ if value:
257
+ return value
258
+ raise PzError(
259
+ code=code,
260
+ message=message,
261
+ stage="cli",
262
+ owner="pz_core.authoring",
263
+ action=action,
264
+ )
265
+
266
+
267
+ def _clean_build_artifacts(package_root: Path) -> None:
268
+ for relative_path in ("build", "dist"):
269
+ candidate = package_root / relative_path
270
+ if candidate.exists():
271
+ shutil.rmtree(candidate)
272
+ for candidate in package_root.glob("*.egg-info"):
273
+ if candidate.is_dir():
274
+ shutil.rmtree(candidate)
275
+ elif candidate.exists():
276
+ candidate.unlink()
277
+
278
+
279
+ def _validate_optional_string(
280
+ raw_value: object, *, field: str, action: str
281
+ ) -> str | None:
282
+ if raw_value is None:
283
+ return None
284
+ if not isinstance(raw_value, str) or not raw_value.strip():
285
+ raise PzError(
286
+ code="PZ_SCAFFOLD_PUBLISH_CONFIG_INVALID",
287
+ message=f"publish config field `{field}` must be a non-empty string when provided.",
288
+ stage="cli",
289
+ owner="pz_core.authoring",
290
+ action=action,
291
+ )
292
+ return raw_value.strip()
293
+
294
+
295
+ def _load_project_payload(project_dir: Path, *, action: str) -> dict[str, object]:
296
+ if not project_dir.exists() or not project_dir.is_dir():
297
+ raise PzError(
298
+ code="PZ_SCAFFOLD_PROJECT_DIR_INVALID",
299
+ message=f"project directory must exist: {project_dir}",
300
+ stage="cli",
301
+ owner="pz_core.authoring",
302
+ action=action,
303
+ )
304
+ payload = load_jsonc_file(
305
+ resolve_project_config_path(project_dir),
306
+ code="PZ_SCAFFOLD_PROJECT_CONFIG_INVALID",
307
+ owner="pz_core.authoring",
308
+ action=action,
309
+ )
310
+ if payload and not isinstance(payload, dict):
311
+ raise PzError(
312
+ code="PZ_SCAFFOLD_PROJECT_CONFIG_INVALID",
313
+ message="project config must decode to an object.",
314
+ stage="cli",
315
+ owner="pz_core.authoring",
316
+ action=action,
317
+ )
318
+ return payload or {}
319
+
320
+
321
+ def _load_source_root_pyproject_payload(
322
+ source_root: Path, *, action: str
323
+ ) -> dict[str, object]:
324
+ pyproject_path = source_root / "pyproject.toml"
325
+ if not pyproject_path.is_file():
326
+ return {}
327
+ try:
328
+ payload = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
329
+ except Exception as exc:
330
+ raise PzError(
331
+ code="PZ_SCAFFOLD_PUBLISH_CONFIG_INVALID",
332
+ message=f"source-root pyproject.toml is unreadable: {pyproject_path}",
333
+ stage="cli",
334
+ owner="pz_core.authoring",
335
+ action=action,
336
+ detail=str(exc),
337
+ ) from exc
338
+ if not isinstance(payload, dict):
339
+ raise PzError(
340
+ code="PZ_SCAFFOLD_PUBLISH_CONFIG_INVALID",
341
+ message=f"source-root pyproject.toml must decode to an object: {pyproject_path}",
342
+ stage="cli",
343
+ owner="pz_core.authoring",
344
+ action=action,
345
+ )
346
+ return payload
347
+
348
+
349
+ def _resolve_pyproject_publish_config(
350
+ source_root: Path,
351
+ *,
352
+ action: str,
353
+ ) -> tuple[dict[str, object] | None, str, str]:
354
+ pyproject_path = source_root / "pyproject.toml"
355
+ pyproject_payload = _load_source_root_pyproject_payload(source_root, action=action)
356
+ if not pyproject_path.is_file():
357
+ return None, str(pyproject_path), "tool.pz.publish"
358
+ tool = pyproject_payload.get("tool")
359
+ if tool is None:
360
+ return None, str(pyproject_path), "tool.pz.publish"
361
+ if not isinstance(tool, dict):
362
+ raise PzError(
363
+ code="PZ_SCAFFOLD_PUBLISH_CONFIG_INVALID",
364
+ message="`tool` in source-root pyproject.toml must be an object.",
365
+ stage="cli",
366
+ owner="pz_core.authoring",
367
+ action=action,
368
+ )
369
+ pz_tool = tool.get("pz")
370
+ if pz_tool is None:
371
+ return None, str(pyproject_path), "tool.pz.publish"
372
+ if not isinstance(pz_tool, dict):
373
+ raise PzError(
374
+ code="PZ_SCAFFOLD_PUBLISH_CONFIG_INVALID",
375
+ message="`tool.pz` in source-root pyproject.toml must be an object.",
376
+ stage="cli",
377
+ owner="pz_core.authoring",
378
+ action=action,
379
+ )
380
+ return pz_tool.get("publish"), str(pyproject_path), "tool.pz.publish"
381
+
382
+
383
+ def _resolve_publish_config(
384
+ project_dir: Path,
385
+ *,
386
+ package_root: Path | None = None,
387
+ workspace_package_root: Path | None = None,
388
+ action: str,
389
+ ) -> tuple[dict[str, object] | None, str, str]:
390
+ project_config_path = resolve_project_config_path(project_dir)
391
+ if project_config_path.is_file():
392
+ payload = _load_project_payload(project_dir, action=action)
393
+ publish_config = payload.get("publish")
394
+ if publish_config is not None:
395
+ if not isinstance(publish_config, dict):
396
+ raise PzError(
397
+ code="PZ_SCAFFOLD_PUBLISH_CONFIG_INVALID",
398
+ message="`publish` in project config must be an object.",
399
+ stage="cli",
400
+ owner="pz_core.authoring",
401
+ action=action,
402
+ )
403
+ return publish_config, str(project_config_path), "publish"
404
+ seen: set[Path] = set()
405
+ for search_root in (package_root, workspace_package_root):
406
+ if search_root is None:
407
+ continue
408
+ for candidate in (search_root, *search_root.parents):
409
+ resolved_candidate = candidate.resolve()
410
+ if resolved_candidate in seen:
411
+ continue
412
+ seen.add(resolved_candidate)
413
+ publish_config, config_label, field_prefix = (
414
+ _resolve_pyproject_publish_config(
415
+ resolved_candidate,
416
+ action=action,
417
+ )
418
+ )
419
+ if publish_config is not None:
420
+ return publish_config, config_label, field_prefix
421
+ publish_config, config_label, field_prefix = _resolve_pyproject_publish_config(
422
+ project_dir, action=action
423
+ )
424
+ if publish_config is not None:
425
+ return publish_config, config_label, field_prefix
426
+ return None, str(project_config_path), "publish"
427
+
428
+
429
+ def _load_publish_target(
430
+ project_dir: Path,
431
+ *,
432
+ package_root: Path | None = None,
433
+ workspace_package_root: Path | None = None,
434
+ target_name: str | None,
435
+ action: str,
436
+ ) -> PublishTarget:
437
+ publish_config, config_label, field_prefix = _resolve_publish_config(
438
+ project_dir,
439
+ package_root=package_root,
440
+ workspace_package_root=workspace_package_root,
441
+ action=action,
442
+ )
443
+ target = (
444
+ target_name.strip()
445
+ if isinstance(target_name, str) and target_name.strip()
446
+ else None
447
+ )
448
+ if publish_config is None:
449
+ return PublishTarget(name=target or "default")
450
+ if not isinstance(publish_config, dict):
451
+ raise PzError(
452
+ code="PZ_SCAFFOLD_PUBLISH_CONFIG_INVALID",
453
+ message=f"`{field_prefix}` must be an object.",
454
+ stage="cli",
455
+ owner="pz_core.authoring",
456
+ action=action,
457
+ )
458
+ default_target = _validate_optional_string(
459
+ publish_config.get("default_target"),
460
+ field=f"{field_prefix}.default_target",
461
+ action=action,
462
+ )
463
+ resolved_target_name = target or default_target or "default"
464
+ raw_targets = publish_config.get("targets")
465
+ if raw_targets is None:
466
+ return PublishTarget(name=resolved_target_name)
467
+ if not isinstance(raw_targets, dict):
468
+ raise PzError(
469
+ code="PZ_SCAFFOLD_PUBLISH_CONFIG_INVALID",
470
+ message=f"`{field_prefix}.targets` must be an object.",
471
+ stage="cli",
472
+ owner="pz_core.authoring",
473
+ action=action,
474
+ )
475
+ raw_target = raw_targets.get(resolved_target_name)
476
+ if raw_target is None:
477
+ raise PzError(
478
+ code="PZ_SCAFFOLD_PUBLISH_TARGET_UNKNOWN",
479
+ message=(
480
+ f"publish target `{resolved_target_name}` is not defined in "
481
+ f"{config_label}."
482
+ ),
483
+ stage="cli",
484
+ owner="pz_core.authoring",
485
+ action=action,
486
+ )
487
+ if not isinstance(raw_target, dict):
488
+ raise PzError(
489
+ code="PZ_SCAFFOLD_PUBLISH_CONFIG_INVALID",
490
+ message=f"`{field_prefix}.targets.{resolved_target_name}` must be an object.",
491
+ stage="cli",
492
+ owner="pz_core.authoring",
493
+ action=action,
494
+ )
495
+ return PublishTarget(
496
+ name=resolved_target_name,
497
+ index=_validate_optional_string(
498
+ raw_target.get("index"),
499
+ field=f"{field_prefix}.targets.{resolved_target_name}.index",
500
+ action=action,
501
+ ),
502
+ publish_url=_validate_optional_string(
503
+ raw_target.get("publish_url"),
504
+ field=f"{field_prefix}.targets.{resolved_target_name}.publish_url",
505
+ action=action,
506
+ ),
507
+ check_url=_validate_optional_string(
508
+ raw_target.get("check_url"),
509
+ field=f"{field_prefix}.targets.{resolved_target_name}.check_url",
510
+ action=action,
511
+ ),
512
+ trusted_publishing=_validate_optional_string(
513
+ raw_target.get("trusted_publishing"),
514
+ field=f"{field_prefix}.targets.{resolved_target_name}.trusted_publishing",
515
+ action=action,
516
+ ),
517
+ token_env=_validate_optional_string(
518
+ raw_target.get("token_env"),
519
+ field=f"{field_prefix}.targets.{resolved_target_name}.token_env",
520
+ action=action,
521
+ ),
522
+ username_env=_validate_optional_string(
523
+ raw_target.get("username_env"),
524
+ field=f"{field_prefix}.targets.{resolved_target_name}.username_env",
525
+ action=action,
526
+ ),
527
+ password_env=_validate_optional_string(
528
+ raw_target.get("password_env"),
529
+ field=f"{field_prefix}.targets.{resolved_target_name}.password_env",
530
+ action=action,
531
+ ),
532
+ install_username_env=_validate_optional_string(
533
+ raw_target.get("install_username_env"),
534
+ field=f"{field_prefix}.targets.{resolved_target_name}.install_username_env",
535
+ action=action,
536
+ ),
537
+ install_password_env=_validate_optional_string(
538
+ raw_target.get("install_password_env"),
539
+ field=f"{field_prefix}.targets.{resolved_target_name}.install_password_env",
540
+ action=action,
541
+ ),
542
+ )
543
+
544
+
545
+ def load_install_target(
546
+ app_id: str, *, project_dir: Path, action: str
547
+ ) -> InstallTarget:
548
+ workspace_package = resolve_workspace_package(str(app_id), anchors=(project_dir,))
549
+ package_root = workspace_package.repo if workspace_package is not None else None
550
+ return load_install_target_for_package_root(
551
+ package_root, project_dir=project_dir, action=action
552
+ )
553
+
554
+
555
+ def load_install_target_for_package_root(
556
+ package_root: Path | None,
557
+ *,
558
+ project_dir: Path,
559
+ target_name: str | None = None,
560
+ action: str,
561
+ ) -> InstallTarget:
562
+ target = _load_publish_target(
563
+ project_dir,
564
+ package_root=package_root,
565
+ workspace_package_root=package_root,
566
+ target_name=target_name,
567
+ action=action,
568
+ )
569
+ index_url = target.check_url or target.index
570
+ username_env = target.install_username_env
571
+ password_env = target.install_password_env
572
+ if index_url and _is_devpi_target(
573
+ target, index_url, target.check_url, target.publish_url
574
+ ):
575
+ username_env = username_env or "DEVPI_INSTALL_USER"
576
+ password_env = password_env or "DEVPI_INSTALL_PASSWORD"
577
+ return InstallTarget(
578
+ name=target.name,
579
+ index_url=index_url,
580
+ username_env=username_env,
581
+ password_env=password_env,
582
+ )
583
+
584
+
585
+ def install_env(
586
+ *, target: InstallTarget, project_dir: Path, action: str
587
+ ) -> dict[str, str]:
588
+ return install_env_for_targets((target,), project_dir=project_dir, action=action)
589
+
590
+
591
+ def install_env_for_targets(
592
+ targets: tuple[InstallTarget, ...],
593
+ *,
594
+ project_dir: Path,
595
+ action: str,
596
+ ) -> dict[str, str]:
597
+ env = build_authoring_env(project_dir, action=action)
598
+ index_urls = tuple(
599
+ _install_index_url(target=target, env=env, action=action)
600
+ for target in targets
601
+ if target.index_url is not None
602
+ )
603
+ if not index_urls:
604
+ return env
605
+ if len(index_urls) == 1:
606
+ env["UV_DEFAULT_INDEX"] = index_urls[0]
607
+ return env
608
+ env["UV_INDEX"] = " ".join(index_urls[:-1])
609
+ env["UV_DEFAULT_INDEX"] = index_urls[-1]
610
+ return env
611
+
612
+
613
+ def _install_index_url(
614
+ *, target: InstallTarget, env: dict[str, str], action: str
615
+ ) -> str:
616
+ index_url = str(target.index_url or "").strip()
617
+ if target.username_env is not None or target.password_env is not None:
618
+ if target.username_env is None or target.password_env is None:
619
+ raise PzError(
620
+ code="PZ_SCAFFOLD_INSTALL_TARGET_INVALID",
621
+ message=f"install target `{target.name}` must declare both install username and password env vars.",
622
+ stage="cli",
623
+ owner="pz_core.authoring",
624
+ action=action,
625
+ )
626
+ username = _require_named_env(
627
+ env,
628
+ target.username_env,
629
+ code="PZ_SCAFFOLD_INSTALL_AUTH_MISSING",
630
+ message=f"install target `{target.name}` requires env var `{target.username_env}`.",
631
+ action=action,
632
+ )
633
+ password = _require_named_env(
634
+ env,
635
+ target.password_env,
636
+ code="PZ_SCAFFOLD_INSTALL_AUTH_MISSING",
637
+ message=f"install target `{target.name}` requires env var `{target.password_env}`.",
638
+ action=action,
639
+ )
640
+ index_url = _inject_basic_auth(index_url, username=username, password=password)
641
+ return index_url
642
+
643
+
644
+ def _publish_env(
645
+ *, target: PublishTarget, project_dir: Path, action: str
646
+ ) -> dict[str, str]:
647
+ env = build_authoring_env(project_dir, action=action)
648
+ for source_name, destination_name in (
649
+ (target.token_env, "UV_PUBLISH_TOKEN"),
650
+ (target.username_env, "UV_PUBLISH_USERNAME"),
651
+ (target.password_env, "UV_PUBLISH_PASSWORD"),
652
+ ):
653
+ if source_name is None:
654
+ continue
655
+ env[destination_name] = _require_named_env(
656
+ env,
657
+ source_name,
658
+ code="PZ_SCAFFOLD_PUBLISH_AUTH_MISSING",
659
+ message=f"publish target `{target.name}` requires env var `{source_name}`.",
660
+ action=action,
661
+ )
662
+ return env
663
+
664
+
665
+ def _publish_argv(
666
+ *, uv_binary: str, target: PublishTarget, dist_dir: Path, dry_run: bool
667
+ ) -> list[str]:
668
+ argv = [uv_binary, "publish"]
669
+ if target.index:
670
+ argv.extend(["--index", target.index])
671
+ if target.publish_url:
672
+ argv.extend(["--publish-url", target.publish_url])
673
+ if target.check_url:
674
+ argv.extend(["--check-url", target.check_url])
675
+ if target.trusted_publishing:
676
+ argv.extend(["--trusted-publishing", target.trusted_publishing])
677
+ if dry_run:
678
+ argv.append("--dry-run")
679
+ argv.append(str(dist_dir / "*"))
680
+ return argv
681
+
682
+
683
+ def _run_uv(argv: list[str], *, cwd: Path, env: dict[str, str], action: str) -> None:
684
+ completed = subprocess.run(
685
+ argv,
686
+ cwd=str(cwd),
687
+ env=env,
688
+ check=False,
689
+ capture_output=True,
690
+ text=True,
691
+ )
692
+ if completed.returncode == 0:
693
+ return
694
+ detail = (
695
+ completed.stderr or completed.stdout or "dependency backend command failed"
696
+ ).strip()
697
+ if len(argv) > 1 and argv[1] == "build":
698
+ raise PzError(
699
+ code="PZ_SCAFFOLD_PUBLISH_BUILD_FAILED",
700
+ message="publish failed while building release artifacts.",
701
+ stage="cli",
702
+ owner="pz_core.authoring",
703
+ action=action,
704
+ detail=detail,
705
+ )
706
+ raise PzError(
707
+ code="PZ_SCAFFOLD_PUBLISH_UPLOAD_FAILED",
708
+ message="publish failed while uploading release artifacts.",
709
+ stage="cli",
710
+ owner="pz_core.authoring",
711
+ action=action,
712
+ detail=detail,
713
+ )
714
+
715
+
716
+ def _read_project_version(package_root: Path, *, action: str) -> str:
717
+ pyproject_path = package_root / "pyproject.toml"
718
+ if not pyproject_path.is_file():
719
+ raise PzError(
720
+ code="PZ_SCAFFOLD_PUBLISH_LAYOUT_INVALID",
721
+ message=f"package root is missing pyproject.toml: {package_root}",
722
+ stage="cli",
723
+ owner="pz_core.authoring",
724
+ action=action,
725
+ )
726
+ try:
727
+ payload = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
728
+ except Exception as exc:
729
+ raise PzError(
730
+ code="PZ_SCAFFOLD_PUBLISH_LAYOUT_INVALID",
731
+ message=f"package root has an unreadable pyproject.toml: {pyproject_path}",
732
+ stage="cli",
733
+ owner="pz_core.authoring",
734
+ action=action,
735
+ detail=str(exc),
736
+ ) from exc
737
+ project = payload.get("project")
738
+ if not isinstance(project, dict):
739
+ raise PzError(
740
+ code="PZ_SCAFFOLD_PUBLISH_LAYOUT_INVALID",
741
+ message=f"package root is missing [project] metadata: {pyproject_path}",
742
+ stage="cli",
743
+ owner="pz_core.authoring",
744
+ action=action,
745
+ )
746
+ version = project.get("version")
747
+ if not isinstance(version, str) or not version.strip():
748
+ raise PzError(
749
+ code="PZ_SCAFFOLD_PUBLISH_VERSION_MISSING",
750
+ message="publish requires an explicit project.version before building release artifacts.",
751
+ stage="cli",
752
+ owner="pz_core.authoring",
753
+ action=action,
754
+ )
755
+ return version.strip()
756
+
757
+
758
+ def _read_project_name(package_root: Path, *, action: str) -> str:
759
+ pyproject_path = package_root / "pyproject.toml"
760
+ if not pyproject_path.is_file():
761
+ raise PzError(
762
+ code="PZ_SCAFFOLD_PUBLISH_LAYOUT_INVALID",
763
+ message=f"package root is missing pyproject.toml: {package_root}",
764
+ stage="cli",
765
+ owner="pz_core.authoring",
766
+ action=action,
767
+ )
768
+ try:
769
+ payload = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
770
+ except Exception as exc:
771
+ raise PzError(
772
+ code="PZ_SCAFFOLD_PUBLISH_LAYOUT_INVALID",
773
+ message=f"package root has an unreadable pyproject.toml: {pyproject_path}",
774
+ stage="cli",
775
+ owner="pz_core.authoring",
776
+ action=action,
777
+ detail=str(exc),
778
+ ) from exc
779
+ project = payload.get("project")
780
+ if not isinstance(project, dict):
781
+ raise PzError(
782
+ code="PZ_SCAFFOLD_PUBLISH_LAYOUT_INVALID",
783
+ message=f"package root is missing [project] metadata: {pyproject_path}",
784
+ stage="cli",
785
+ owner="pz_core.authoring",
786
+ action=action,
787
+ )
788
+ name = project.get("name")
789
+ if not isinstance(name, str) or not name.strip():
790
+ raise PzError(
791
+ code="PZ_SCAFFOLD_PUBLISH_LAYOUT_INVALID",
792
+ message="publish/unpublish requires an explicit project.name.",
793
+ stage="cli",
794
+ owner="pz_core.authoring",
795
+ action=action,
796
+ )
797
+ return name.strip()
798
+
799
+
800
+ def _derive_devpi_index_url(target: PublishTarget, *, action: str) -> str:
801
+ if isinstance(target.publish_url, str) and target.publish_url.strip():
802
+ return target.publish_url.strip()
803
+ if isinstance(target.index, str) and target.index.strip():
804
+ return target.index.strip()
805
+ check_url = str(target.check_url or "").strip()
806
+ if check_url.endswith("/+simple/"):
807
+ return check_url[: -len("/+simple/")] + "/"
808
+ if check_url.endswith("/+simple"):
809
+ return check_url[: -len("/+simple")] + "/"
810
+ raise PzError(
811
+ code="PZ_SCAFFOLD_UNPUBLISH_TARGET_INVALID",
812
+ message=(
813
+ f"publish target `{target.name}` must define `publish_url`, `index`, "
814
+ "or a devpi `check_url` to support unpublish."
815
+ ),
816
+ stage="cli",
817
+ owner="pz_core.authoring",
818
+ action=action,
819
+ )
820
+
821
+
822
+ def _build_basic_auth_header(*, username: str, password: str) -> str:
823
+ token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii")
824
+ return f"Basic {token}"
825
+
826
+
827
+ def _add_empty_query_flag(url: str, name: str) -> str:
828
+ parsed = urlsplit(url)
829
+ query = parsed.query
830
+ query = f"{query}&{name}=" if query else f"{name}="
831
+ return urlunsplit(
832
+ (parsed.scheme, parsed.netloc, parsed.path, query, parsed.fragment)
833
+ )
834
+
835
+
836
+ def _devpi_request_json(
837
+ url: str,
838
+ *,
839
+ method: str,
840
+ username: str,
841
+ password: str,
842
+ action: str,
843
+ ) -> dict[str, object]:
844
+ headers = {
845
+ "Accept": "application/json",
846
+ "content-type": "application/json",
847
+ "Authorization": _build_basic_auth_header(username=username, password=password),
848
+ }
849
+ request = Request(url, method=method.upper(), headers=headers)
850
+ try:
851
+ with urlopen(request) as response:
852
+ raw = response.read().decode("utf-8")
853
+ except HTTPError as exc:
854
+ detail = exc.read().decode("utf-8", errors="ignore").strip()
855
+ if exc.code == 404:
856
+ raise PzError(
857
+ code="PZ_SCAFFOLD_UNPUBLISH_NOT_FOUND",
858
+ message="publish target does not contain the requested package.",
859
+ stage="cli",
860
+ owner="pz_core.authoring",
861
+ action=action,
862
+ detail=detail or url,
863
+ ) from exc
864
+ raise PzError(
865
+ code="PZ_SCAFFOLD_UNPUBLISH_REQUEST_FAILED",
866
+ message="unpublish request failed.",
867
+ stage="cli",
868
+ owner="pz_core.authoring",
869
+ action=action,
870
+ detail=detail or f"HTTP {exc.code} {exc.reason}",
871
+ ) from exc
872
+ except URLError as exc:
873
+ raise PzError(
874
+ code="PZ_SCAFFOLD_UNPUBLISH_REQUEST_FAILED",
875
+ message="unpublish request failed.",
876
+ stage="cli",
877
+ owner="pz_core.authoring",
878
+ action=action,
879
+ detail=str(exc.reason),
880
+ ) from exc
881
+ if not raw.strip():
882
+ return {}
883
+ try:
884
+ payload = json.loads(raw)
885
+ except Exception as exc:
886
+ raise PzError(
887
+ code="PZ_SCAFFOLD_UNPUBLISH_RESPONSE_INVALID",
888
+ message="unpublish target returned a non-JSON response.",
889
+ stage="cli",
890
+ owner="pz_core.authoring",
891
+ action=action,
892
+ detail=raw[:500],
893
+ ) from exc
894
+ if not isinstance(payload, dict):
895
+ raise PzError(
896
+ code="PZ_SCAFFOLD_UNPUBLISH_RESPONSE_INVALID",
897
+ message="unpublish target returned an invalid JSON payload.",
898
+ stage="cli",
899
+ owner="pz_core.authoring",
900
+ action=action,
901
+ )
902
+ return payload
903
+
904
+
905
+ def _extract_devpi_versions_for_project(
906
+ payload: dict[str, object], *, index_url: str, action: str
907
+ ) -> tuple[str, ...]:
908
+ result = payload.get("result")
909
+ if not isinstance(result, dict):
910
+ raise PzError(
911
+ code="PZ_SCAFFOLD_UNPUBLISH_RESPONSE_INVALID",
912
+ message="unpublish target did not return a projectconfig object.",
913
+ stage="cli",
914
+ owner="pz_core.authoring",
915
+ action=action,
916
+ )
917
+ index_prefix = index_url.rstrip("/")
918
+ versions: list[str] = []
919
+ for version, metadata in result.items():
920
+ if not isinstance(version, str) or not isinstance(metadata, dict):
921
+ continue
922
+ links = metadata.get("+links")
923
+ if not isinstance(links, list):
924
+ continue
925
+ if any(
926
+ isinstance(link, dict)
927
+ and isinstance(link.get("href"), str)
928
+ and str(link.get("href")).startswith(index_prefix)
929
+ for link in links
930
+ ):
931
+ versions.append(version)
932
+ return tuple(sorted(versions))
933
+
934
+
935
+ def _build_and_publish_package(
936
+ *,
937
+ app_id: str,
938
+ project_dir: Path,
939
+ package_root: Path,
940
+ workspace_package_root: Path | None,
941
+ target_name: str | None,
942
+ dry_run: bool,
943
+ action: str,
944
+ commit: str | None = None,
945
+ tag: str | None = None,
946
+ ) -> PublishResult:
947
+ version = _read_project_version(package_root, action=action)
948
+ distribution_name = _read_project_name(package_root, action=action)
949
+ target = _load_publish_target(
950
+ project_dir,
951
+ package_root=package_root,
952
+ workspace_package_root=workspace_package_root,
953
+ target_name=target_name,
954
+ action=action,
955
+ )
956
+ enforce_publish_lock(
957
+ project_dir=project_dir,
958
+ app_id=app_id,
959
+ distribution_name=distribution_name,
960
+ target=target,
961
+ action=action,
962
+ )
963
+ uv_binary = shutil.which("uv")
964
+ if not uv_binary:
965
+ raise PzError(
966
+ code="PZ_SCAFFOLD_PUBLISH_UV_MISSING",
967
+ message="required publish backend was not found on PATH.",
968
+ stage="cli",
969
+ owner="pz_core.authoring",
970
+ action=action,
971
+ )
972
+ env = _publish_env(target=target, project_dir=project_dir, action=action)
973
+ _clean_build_artifacts(package_root)
974
+ _run_uv([uv_binary, "build"], cwd=package_root, env=env, action=action)
975
+ _validate_and_smoke_wheel(
976
+ app_id=app_id,
977
+ distribution_name=distribution_name,
978
+ version=version,
979
+ dist_dir=package_root / "dist",
980
+ uv_binary=uv_binary,
981
+ env=env,
982
+ action=action,
983
+ )
984
+ _run_uv(
985
+ _publish_argv(
986
+ uv_binary=uv_binary,
987
+ target=target,
988
+ dist_dir=package_root / "dist",
989
+ dry_run=dry_run,
990
+ ),
991
+ cwd=package_root,
992
+ env=env,
993
+ action=action,
994
+ )
995
+ return PublishResult(
996
+ app_id=app_id,
997
+ version=version,
998
+ package_root=package_root,
999
+ dist_dir=package_root / "dist",
1000
+ target=target,
1001
+ dry_run=dry_run,
1002
+ commit=commit,
1003
+ tag=tag,
1004
+ )
1005
+
1006
+
1007
+ def _normalized_distribution(value: str) -> str:
1008
+ return re.sub(r"[-_.]+", "-", value).lower()
1009
+
1010
+
1011
+ def _wheel_import_roots(names: tuple[str, ...] | list[str]) -> tuple[str, ...]:
1012
+ roots: set[str] = set()
1013
+ for name in names:
1014
+ parts = name.split("/")
1015
+ if len(parts) >= 2 and parts[1] == "__init__.py":
1016
+ candidate = parts[0]
1017
+ elif len(parts) == 1 and name.endswith(".py"):
1018
+ candidate = name[:-3]
1019
+ else:
1020
+ continue
1021
+ if candidate.isidentifier():
1022
+ roots.add(candidate)
1023
+ return tuple(sorted(roots))
1024
+
1025
+
1026
+ def _validate_and_smoke_wheel(
1027
+ *,
1028
+ app_id: str,
1029
+ distribution_name: str,
1030
+ version: str,
1031
+ dist_dir: Path,
1032
+ uv_binary: str,
1033
+ env: dict[str, str],
1034
+ action: str,
1035
+ ) -> None:
1036
+ wheels = sorted(dist_dir.glob("*.whl"))
1037
+ if len(wheels) != 1:
1038
+ raise PzError(
1039
+ code="PZ_PUB_WHEEL_INVALID",
1040
+ message="构建结果必须且只能包含一个 wheel。",
1041
+ stage="cli",
1042
+ owner="pz_core.authoring.release",
1043
+ action=action,
1044
+ detail=str(dist_dir),
1045
+ )
1046
+ wheel = wheels[0]
1047
+ try:
1048
+ with zipfile.ZipFile(wheel) as archive:
1049
+ metadata_names = [
1050
+ name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
1051
+ ]
1052
+ if len(metadata_names) != 1:
1053
+ raise ValueError("wheel METADATA is missing or ambiguous")
1054
+ metadata = BytesParser().parsebytes(archive.read(metadata_names[0]))
1055
+ import_roots = _wheel_import_roots(archive.namelist())
1056
+ except Exception as exc:
1057
+ raise PzError(
1058
+ code="PZ_PUB_WHEEL_INVALID",
1059
+ message="无法读取发布 wheel 的 metadata。",
1060
+ stage="cli",
1061
+ owner="pz_core.authoring.release",
1062
+ action=action,
1063
+ detail=str(exc),
1064
+ ) from exc
1065
+ actual_name = str(metadata.get("Name") or "")
1066
+ actual_version = str(metadata.get("Version") or "")
1067
+ if (
1068
+ _normalized_distribution(actual_name)
1069
+ != _normalized_distribution(distribution_name)
1070
+ or actual_version != version
1071
+ ):
1072
+ raise PzError(
1073
+ code="PZ_PUB_WHEEL_IDENTITY_MISMATCH",
1074
+ message="wheel metadata 与目标子包的 distribution/version 不一致。",
1075
+ stage="cli",
1076
+ owner="pz_core.authoring.release",
1077
+ action=action,
1078
+ detail=f"expected={distribution_name}=={version}; actual={actual_name}=={actual_version}",
1079
+ )
1080
+ if not import_roots:
1081
+ raise PzError(
1082
+ code="PZ_PUB_WHEEL_INVALID",
1083
+ message="发布 wheel 中没有可验证的顶层 Python import package。",
1084
+ stage="cli",
1085
+ owner="pz_core.authoring.release",
1086
+ action=action,
1087
+ detail=str(wheel),
1088
+ )
1089
+ with tempfile.TemporaryDirectory(prefix="pz-pub-smoke-") as raw_temp:
1090
+ venv = Path(raw_temp) / "venv"
1091
+ _run_uv([uv_binary, "venv", str(venv)], cwd=dist_dir, env=env, action=action)
1092
+ python = venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
1093
+ _run_uv(
1094
+ [uv_binary, "pip", "install", "--python", str(python), "--no-deps", str(wheel)],
1095
+ cwd=dist_dir,
1096
+ env=env,
1097
+ action=action,
1098
+ )
1099
+ smoke = subprocess.run(
1100
+ (
1101
+ str(python),
1102
+ "-I",
1103
+ "-c",
1104
+ "import importlib,sys; [importlib.import_module(name) for name in sys.argv[1:]]",
1105
+ *import_roots,
1106
+ ),
1107
+ cwd=str(dist_dir),
1108
+ env=env,
1109
+ check=False,
1110
+ capture_output=True,
1111
+ text=True,
1112
+ )
1113
+ if smoke.returncode != 0:
1114
+ raise PzError(
1115
+ code="PZ_PUB_WHEEL_SMOKE_FAILED",
1116
+ message="wheel 在干净环境中的安装/import smoke 失败。",
1117
+ stage="cli",
1118
+ owner="pz_core.authoring.release",
1119
+ action=action,
1120
+ detail=(smoke.stderr or smoke.stdout).strip()[:500],
1121
+ )
1122
+
1123
+
1124
+ def publish_commit(
1125
+ app_id: str,
1126
+ *,
1127
+ project_dir: Path,
1128
+ action: str = "publish_commit_main",
1129
+ ) -> PublishCommitResult:
1130
+ source = resolve_publish_source(project_dir, app_id, action=action)
1131
+ version = _read_project_version(source.package_root, action=action)
1132
+ return commit_publish_app(
1133
+ source.app_id,
1134
+ project_dir=project_dir,
1135
+ version=version,
1136
+ action=action,
1137
+ )
1138
+
1139
+
1140
+ def publish_tag(
1141
+ app_id: str,
1142
+ *,
1143
+ project_dir: Path,
1144
+ commit: str | None = None,
1145
+ action: str = "publish_tag_main",
1146
+ ) -> PublishTagResult:
1147
+ source = resolve_publish_source(project_dir, app_id, action=action)
1148
+ version = _read_project_version(source.package_root, action=action)
1149
+ return tag_publish_app(
1150
+ source.app_id,
1151
+ project_dir=project_dir,
1152
+ version=version,
1153
+ commit=commit,
1154
+ action=action,
1155
+ )
1156
+
1157
+
1158
+ def publish_upload(
1159
+ app_id: str,
1160
+ *,
1161
+ project_dir: Path,
1162
+ target_name: str | None = None,
1163
+ dry_run: bool = False,
1164
+ version: str | None = None,
1165
+ tag_result: PublishTagResult | None = None,
1166
+ action: str = "publish_upload_main",
1167
+ ) -> PublishResult:
1168
+ source = resolve_publish_source(project_dir, app_id, action=action)
1169
+ resolved_version = version or _read_project_version(source.package_root, action=action)
1170
+ resolved_tag = tag_result or require_publish_tag(
1171
+ source.app_id,
1172
+ project_dir=project_dir,
1173
+ version=resolved_version,
1174
+ action=action,
1175
+ )
1176
+ package_root = require_publish_checkout_at_tag(resolved_tag, action=action)
1177
+ current_version = _read_project_version(package_root, action=action)
1178
+ if current_version != resolved_version:
1179
+ raise PzError(
1180
+ code="PZ_PUB_RELEASE_STATE_INVALID",
1181
+ message="当前 project.version 与发布版本不一致。",
1182
+ stage="cli",
1183
+ owner="pz_core.authoring.release",
1184
+ action=action,
1185
+ )
1186
+ result = _build_and_publish_package(
1187
+ app_id=source.app_id,
1188
+ project_dir=project_dir,
1189
+ package_root=package_root,
1190
+ workspace_package_root=source.git_root,
1191
+ target_name=target_name,
1192
+ dry_run=dry_run,
1193
+ action=action,
1194
+ commit=resolved_tag.commit_result.commit,
1195
+ tag=resolved_tag.tag,
1196
+ )
1197
+ return PublishResult(
1198
+ app_id=result.app_id,
1199
+ version=result.version,
1200
+ package_root=source.package_root,
1201
+ dist_dir=source.package_root / "dist",
1202
+ target=result.target,
1203
+ dry_run=result.dry_run,
1204
+ commit=result.commit,
1205
+ tag=result.tag,
1206
+ )
1207
+
1208
+
1209
+ def publish_app(
1210
+ app_id: str,
1211
+ *,
1212
+ project_dir: Path,
1213
+ target_name: str | None = None,
1214
+ dry_run: bool = False,
1215
+ action: str = "publish_main",
1216
+ ) -> PublishResult:
1217
+ source = resolve_publish_source(project_dir, app_id, action=action)
1218
+ if dry_run:
1219
+ source = preflight_publish_app(
1220
+ source.app_id,
1221
+ project_dir=project_dir,
1222
+ action=action,
1223
+ source=source,
1224
+ )
1225
+ return _build_and_publish_package(
1226
+ app_id=source.app_id,
1227
+ project_dir=project_dir,
1228
+ package_root=source.package_root,
1229
+ workspace_package_root=source.git_root,
1230
+ target_name=target_name,
1231
+ dry_run=True,
1232
+ action=action,
1233
+ )
1234
+ source = preflight_publish_app(
1235
+ source.app_id,
1236
+ project_dir=project_dir,
1237
+ action=action,
1238
+ source=source,
1239
+ )
1240
+ # The index/hash conflict check performed by `uv publish --dry-run` must
1241
+ # happen before release history is mutated.
1242
+ try:
1243
+ _build_and_publish_package(
1244
+ app_id=source.app_id,
1245
+ project_dir=project_dir,
1246
+ package_root=source.package_root,
1247
+ workspace_package_root=source.git_root,
1248
+ target_name=target_name,
1249
+ dry_run=True,
1250
+ action=action,
1251
+ )
1252
+ finally:
1253
+ _clean_build_artifacts(source.package_root)
1254
+ commit_result = publish_commit(source.app_id, project_dir=project_dir, action=action)
1255
+ tag_result = publish_tag(
1256
+ source.app_id,
1257
+ project_dir=project_dir,
1258
+ commit=commit_result.commit,
1259
+ action=action,
1260
+ )
1261
+ return publish_upload(
1262
+ source.app_id,
1263
+ project_dir=project_dir,
1264
+ target_name=target_name,
1265
+ tag_result=tag_result,
1266
+ action=action,
1267
+ )
1268
+
1269
+
1270
+ def unpublish_app(
1271
+ app_id: str,
1272
+ *,
1273
+ project_dir: Path,
1274
+ target_name: str | None = None,
1275
+ dry_run: bool = False,
1276
+ force: bool = False,
1277
+ action: str = "publish_rm_main",
1278
+ ) -> UnpublishResult:
1279
+ workspace_package = resolve_workspace_package(str(app_id), anchors=(project_dir,))
1280
+ normalized_app_id = (
1281
+ workspace_package.app_id
1282
+ if workspace_package is not None
1283
+ else normalize_publish_app_id(project_dir, str(app_id), action=action)
1284
+ )
1285
+ package_root = resolve_publish_package_root(
1286
+ project_dir, normalized_app_id, action=action
1287
+ )
1288
+ version = _read_project_version(package_root, action=action)
1289
+ distribution_name = _read_project_name(package_root, action=action)
1290
+ target = _load_publish_target(
1291
+ project_dir,
1292
+ package_root=package_root,
1293
+ workspace_package_root=workspace_package.repo
1294
+ if workspace_package is not None
1295
+ else None,
1296
+ target_name=target_name,
1297
+ action=action,
1298
+ )
1299
+ index_url = _derive_devpi_index_url(target, action=action)
1300
+ if not _is_devpi_target(target, index_url, target.publish_url, target.check_url):
1301
+ raise PzError(
1302
+ code="PZ_SCAFFOLD_UNPUBLISH_TARGET_UNSUPPORTED",
1303
+ message="unpublish currently supports formal devpi targets only.",
1304
+ stage="cli",
1305
+ owner="pz_core.authoring",
1306
+ action=action,
1307
+ )
1308
+ username_env = target.username_env or "DEVPI_OWNER"
1309
+ password_env = target.password_env or "DEVPI_OWNER_PASSWORD"
1310
+ env = build_authoring_env(project_dir, action=action)
1311
+ username = _require_named_env(
1312
+ env,
1313
+ username_env,
1314
+ code="PZ_SCAFFOLD_PUBLISH_AUTH_MISSING",
1315
+ message=f"publish target `{target.name}` requires env var `{username_env}`.",
1316
+ action=action,
1317
+ )
1318
+ password = _require_named_env(
1319
+ env,
1320
+ password_env,
1321
+ code="PZ_SCAFFOLD_PUBLISH_AUTH_MISSING",
1322
+ message=f"publish target `{target.name}` requires env var `{password_env}`.",
1323
+ action=action,
1324
+ )
1325
+ project_url = f"{index_url.rstrip('/')}/{quote(distribution_name, safe='')}"
1326
+ inspect_url = _add_empty_query_flag(project_url, "ignore_bases")
1327
+ payload = _devpi_request_json(
1328
+ inspect_url,
1329
+ method="GET",
1330
+ username=username,
1331
+ password=password,
1332
+ action=action,
1333
+ )
1334
+ removed_versions = _extract_devpi_versions_for_project(
1335
+ payload, index_url=index_url, action=action
1336
+ )
1337
+ if not removed_versions:
1338
+ raise PzError(
1339
+ code="PZ_SCAFFOLD_UNPUBLISH_NOT_FOUND",
1340
+ message="publish target does not contain the requested package.",
1341
+ stage="cli",
1342
+ owner="pz_core.authoring",
1343
+ action=action,
1344
+ detail=distribution_name,
1345
+ )
1346
+ delete_url = _add_empty_query_flag(project_url, "force") if force else project_url
1347
+ if not dry_run:
1348
+ _devpi_request_json(
1349
+ delete_url,
1350
+ method="DELETE",
1351
+ username=username,
1352
+ password=password,
1353
+ action=action,
1354
+ )
1355
+ return UnpublishResult(
1356
+ app_id=normalized_app_id,
1357
+ distribution_name=distribution_name,
1358
+ version=version,
1359
+ package_root=package_root,
1360
+ target=target,
1361
+ project_url=project_url,
1362
+ removed_versions=removed_versions,
1363
+ dry_run=dry_run,
1364
+ force=force,
1365
+ )
1366
+
1367
+
1368
+ __all__ = [
1369
+ "InstallTarget",
1370
+ "PublishResult",
1371
+ "PublishTarget",
1372
+ "UnpublishResult",
1373
+ "build_authoring_env",
1374
+ "install_env",
1375
+ "install_env_for_targets",
1376
+ "load_install_target",
1377
+ "publish_app",
1378
+ "publish_commit",
1379
+ "publish_tag",
1380
+ "publish_upload",
1381
+ "unpublish_app",
1382
+ ]