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,787 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ import csv
5
+ from fnmatch import fnmatchcase
6
+ import os
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ import tomllib
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ from pz_core.project_sources import (
15
+ OFFICIAL_CONTRIB_SOURCE_ROOT,
16
+ OFFICIAL_TRUTH_SOURCE_REPO_NAMES,
17
+ resolve_contrib_source_root,
18
+ resolve_sources_root,
19
+ )
20
+ from pz_core.runtime.errors import PzError
21
+ from pz_core.utils.project_jsonc import PROJECT_JSONC_NAME, load_jsonc_file
22
+
23
+ from .project_state import load_dev_manifest
24
+
25
+
26
+ _OWNER = "pz_core.authoring.env"
27
+ _EDITABLE_IMPORT_RE = re.compile(r"^\s*import\s+([A-Za-z0-9_]+)\s*;")
28
+ _QUOTED_ABS_PATH_RE = re.compile(r"""['"](/[^'"]+)['"]""")
29
+ _PROJECT_ENV_CONFIG_NAMES = (
30
+ PROJECT_JSONC_NAME,
31
+ "pz.develop.jsonc",
32
+ "pz.test.jsonc",
33
+ "pz.product.jsonc",
34
+ )
35
+ _GENERATED_SOURCE_ARTIFACT_DIR_PATTERNS = (
36
+ ".venv",
37
+ ".pytest_cache",
38
+ "__pycache__",
39
+ "*.egg-info",
40
+ "dist",
41
+ )
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class SyncTarget:
46
+ name: str
47
+ kind: str
48
+ path: Path
49
+ virtualenv_path: Path
50
+ exists: bool
51
+ has_pyproject: bool
52
+
53
+
54
+ def build_sync_target(*, name: str, kind: str, path: Path) -> SyncTarget:
55
+ raw_path = _absolute_without_symlink_resolution(path)
56
+ resolved = raw_path.resolve()
57
+ exists = resolved.exists() and resolved.is_dir()
58
+ return SyncTarget(
59
+ name=name,
60
+ kind=kind,
61
+ path=resolved,
62
+ virtualenv_path=raw_path,
63
+ exists=exists,
64
+ has_pyproject=exists and (resolved / "pyproject.toml").is_file(),
65
+ )
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class _EditableSourcePath:
70
+ resolved: Path
71
+ preferred: Path
72
+
73
+
74
+ def _absolute_without_symlink_resolution(path: Path) -> Path:
75
+ expanded = path.expanduser()
76
+ if not expanded.is_absolute():
77
+ expanded = Path.cwd() / expanded
78
+ return expanded.absolute()
79
+
80
+
81
+ def collect_sync_targets(project_dir: Path) -> tuple[SyncTarget, ...]:
82
+ targets: list[SyncTarget] = []
83
+ seen_paths: set[str] = set()
84
+
85
+ def add_target(*, name: str, kind: str, path: Path) -> None:
86
+ target = build_sync_target(name=name, kind=kind, path=path)
87
+ key = str(target.path)
88
+ if key in seen_paths:
89
+ return
90
+ seen_paths.add(key)
91
+ targets.append(target)
92
+
93
+ add_target(name="project", kind="project", path=project_dir)
94
+ manifest = load_dev_manifest(project_dir)
95
+ if manifest is None:
96
+ return tuple(targets)
97
+ for repo_key, repo in sorted(manifest.repos.items()):
98
+ add_target(
99
+ name=repo_key,
100
+ kind="repo",
101
+ path=project_dir / Path(*repo.checkout.parts),
102
+ )
103
+ for binding_key, binding in sorted(manifest.bindings.items()):
104
+ if binding.source_kind != "link":
105
+ continue
106
+ add_target(
107
+ name=binding_key,
108
+ kind=binding.kind,
109
+ path=Path(binding.source).expanduser(),
110
+ )
111
+ return tuple(targets)
112
+
113
+
114
+ def sync_project_targets(
115
+ project_dir: Path,
116
+ *,
117
+ action: str,
118
+ groups: tuple[str, ...] = (),
119
+ extras: tuple[str, ...] = (),
120
+ reinstall_packages: tuple[str, ...] = (),
121
+ project_only: bool = False,
122
+ ) -> tuple[SyncTarget, ...]:
123
+ uv_binary = _require_uv_binary(action=action)
124
+ sync_targets = collect_sync_targets(project_dir)
125
+ inferred_extras = _infer_required_extras_for_project(project_dir, action=action)
126
+ if project_only:
127
+ sync_targets = tuple(item for item in sync_targets if item.kind == "project")
128
+ ordered_targets = [
129
+ *[item for item in sync_targets if item.kind == "project" and item.exists and item.has_pyproject],
130
+ *[item for item in sync_targets if item.kind != "project" and item.exists and item.has_pyproject],
131
+ ]
132
+ synced: list[SyncTarget] = []
133
+ for item in ordered_targets:
134
+ _sync_target(
135
+ item,
136
+ project_dir=project_dir,
137
+ uv_binary=uv_binary,
138
+ action=action,
139
+ groups=groups,
140
+ extras=extras,
141
+ inferred_extras=inferred_extras,
142
+ reinstall_packages=reinstall_packages,
143
+ )
144
+ synced.append(item)
145
+ return tuple(synced)
146
+
147
+
148
+ def _require_uv_binary(*, action: str) -> str:
149
+ uv_binary = shutil.which("uv")
150
+ if uv_binary:
151
+ return uv_binary
152
+ raise PzError(
153
+ code="PZ_ENV_UV_MISSING",
154
+ message="required dependency backend was not found on PATH.",
155
+ stage="cli",
156
+ owner=_OWNER,
157
+ action=action,
158
+ )
159
+
160
+
161
+ def _sync_target(
162
+ target: SyncTarget,
163
+ *,
164
+ project_dir: Path,
165
+ uv_binary: str,
166
+ action: str,
167
+ groups: tuple[str, ...] = (),
168
+ extras: tuple[str, ...] = (),
169
+ inferred_extras: tuple[str, ...] = (),
170
+ reinstall_packages: tuple[str, ...] = (),
171
+ ) -> None:
172
+ venv_path = _target_virtualenv_path(target.virtualenv_path)
173
+ editable_source_paths = _editable_source_paths_for_target(target.path)
174
+ allowed_editable_paths = tuple(item.resolved for item in editable_source_paths)
175
+ _prune_stale_editable_artifacts(venv_path, allowed_editable_paths=allowed_editable_paths)
176
+ reinstall_queue: list[str] = []
177
+ for package_name in (*reinstall_packages, *_find_corrupt_installed_distributions(venv_path)):
178
+ normalized = str(package_name).strip()
179
+ if normalized and normalized not in reinstall_queue:
180
+ reinstall_queue.append(normalized)
181
+ command = [uv_binary, "sync"]
182
+ for group in groups:
183
+ command.extend(["--group", group])
184
+ for extra in _extras_for_target(target, extras=extras, inferred_extras=inferred_extras):
185
+ command.extend(["--extra", extra])
186
+ for package_name in reinstall_queue:
187
+ command.extend(["--reinstall-package", package_name])
188
+ with _prepare_sync_execution(project_dir=project_dir, target=target, command=command, uv_binary=uv_binary, action=action) as prepared:
189
+ completed = subprocess.run(
190
+ prepared.command,
191
+ cwd=str(prepared.cwd),
192
+ check=False,
193
+ capture_output=True,
194
+ text=True,
195
+ env=prepared.env,
196
+ )
197
+ if completed.returncode == 0:
198
+ _canonicalize_editable_artifacts(venv_path, editable_source_paths=editable_source_paths)
199
+ _prune_generated_source_artifacts_for_target(project_dir=project_dir, target=target)
200
+ return
201
+ detail = (completed.stderr or completed.stdout or "dependency sync failed").strip()
202
+ raise PzError(
203
+ code="PZ_ENV_SYNC_FAILED",
204
+ message=f"failed to run dependency sync for {target.kind}:{target.name}.",
205
+ stage="cli",
206
+ owner=_OWNER,
207
+ action=action,
208
+ detail=detail,
209
+ )
210
+
211
+
212
+ def _extras_for_target(
213
+ target: SyncTarget,
214
+ *,
215
+ extras: tuple[str, ...],
216
+ inferred_extras: tuple[str, ...] = (),
217
+ ) -> tuple[str, ...]:
218
+ if target.kind == "project":
219
+ return tuple(extra for extra in extras if extra)
220
+ supported = _declared_optional_dependency_names(target)
221
+ resolved: list[str] = []
222
+ for extra in (*extras, *inferred_extras):
223
+ normalized = str(extra).strip()
224
+ if normalized and normalized in supported and normalized not in resolved:
225
+ resolved.append(normalized)
226
+ if target.kind == "runtime" and _runtime_target_has_contrib_checkout(target) and "contrib" not in resolved:
227
+ resolved.append("contrib")
228
+ return tuple(resolved)
229
+
230
+
231
+ def _infer_required_extras_for_project(project_dir: Path, *, action: str) -> tuple[str, ...]:
232
+ required: list[str] = []
233
+ for config_name in _PROJECT_ENV_CONFIG_NAMES:
234
+ payload = load_jsonc_file(
235
+ project_dir / config_name,
236
+ owner=_OWNER,
237
+ action=action,
238
+ )
239
+ database = _project_database_config(payload)
240
+ if database is None or not _database_config_is_enabled(database):
241
+ continue
242
+ extra = _database_config_required_extra(database)
243
+ if extra is not None and extra not in required:
244
+ required.append(extra)
245
+ return tuple(required)
246
+
247
+
248
+ def _project_database_config(payload: dict[str, object]) -> dict[str, object] | None:
249
+ apps = payload.get("apps")
250
+ if not isinstance(apps, dict):
251
+ return None
252
+ foundation = apps.get("pz_foundation")
253
+ if not isinstance(foundation, dict):
254
+ return None
255
+ database = foundation.get("database")
256
+ return database if isinstance(database, dict) else None
257
+
258
+
259
+ def _database_config_is_enabled(database: dict[str, object]) -> bool:
260
+ enabled = database.get("enabled")
261
+ state = str(database.get("state") or "").strip().lower()
262
+ if enabled is False or state == "disabled":
263
+ return False
264
+ if enabled is True or state == "enabled":
265
+ return True
266
+ return _database_config_required_extra(database) is not None
267
+
268
+
269
+ def _database_config_required_extra(database: dict[str, object]) -> str | None:
270
+ driver = str(database.get("driver") or "").strip().lower()
271
+ urls = database.get("urls")
272
+ url_values = [str(url).strip().lower() for url in urls] if isinstance(urls, list) else []
273
+ if driver in {"postgres", "postgresql"} or any(
274
+ url.startswith(("postgres://", "postgresql://")) for url in url_values
275
+ ):
276
+ return "db-postgres"
277
+ if driver in {"mysql", "mariadb"} or any(
278
+ url.startswith(("mysql://", "mysql+pymysql://", "mariadb://")) for url in url_values
279
+ ):
280
+ return "db-mysql"
281
+ return None
282
+
283
+
284
+ def _declared_optional_dependency_names(target: SyncTarget) -> frozenset[str]:
285
+ pyproject_path = target.path / "pyproject.toml"
286
+ if not pyproject_path.is_file():
287
+ return frozenset()
288
+ try:
289
+ payload = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
290
+ except tomllib.TOMLDecodeError as exc:
291
+ raise PzError(
292
+ code="PZ_ENV_INVALID_PYPROJECT",
293
+ message=f"failed to parse pyproject for {target.kind}:{target.name}.",
294
+ stage="cli",
295
+ owner=_OWNER,
296
+ action="sync",
297
+ detail=str(exc),
298
+ ) from exc
299
+ optional_dependencies = payload.get("project", {}).get("optional-dependencies", {})
300
+ if not isinstance(optional_dependencies, dict):
301
+ return frozenset()
302
+ return frozenset(str(name).strip() for name in optional_dependencies if str(name).strip())
303
+
304
+
305
+ def _runtime_target_has_contrib_checkout(target: SyncTarget) -> bool:
306
+ path = target.path.resolve()
307
+ candidates = (
308
+ resolve_contrib_source_root(path),
309
+ resolve_contrib_source_root(path.parent),
310
+ )
311
+ return any(candidate.is_dir() for candidate in candidates)
312
+
313
+
314
+ @dataclass(frozen=True, slots=True)
315
+ class _PreparedSyncExecution:
316
+ command: tuple[str, ...]
317
+ cwd: Path
318
+ env: dict[str, str]
319
+
320
+
321
+ @contextmanager
322
+ def _prepare_sync_execution(
323
+ *,
324
+ project_dir: Path,
325
+ target: SyncTarget,
326
+ command: list[str],
327
+ uv_binary: str,
328
+ action: str,
329
+ ):
330
+ env = os.environ.copy()
331
+ prepared_command = list(command)
332
+ if target.kind != "project" or _project_virtualenv_exists(project_dir, target):
333
+ venv_path = _ensure_target_virtualenv(target.virtualenv_path, uv_binary=uv_binary, action=action)
334
+ env["VIRTUAL_ENV"] = str(venv_path)
335
+ prepared_command.append("--active")
336
+ yield _PreparedSyncExecution(
337
+ command=tuple(prepared_command),
338
+ cwd=target.path,
339
+ env=env,
340
+ )
341
+
342
+
343
+ def _project_virtualenv_exists(project_dir: Path, target: SyncTarget) -> bool:
344
+ if target.kind != "project":
345
+ return False
346
+ if target.path.resolve() != project_dir.resolve():
347
+ return False
348
+ return _is_valid_virtualenv(project_dir / ".venv")
349
+
350
+
351
+ def _discover_truth_source_root(target_path: Path) -> Path | None:
352
+ for candidate in (target_path.resolve(), *target_path.resolve().parents):
353
+ if candidate.name == ".sources":
354
+ if all(
355
+ _resolve_truth_source_repo_root(candidate, repo_name) is not None
356
+ for repo_name in OFFICIAL_TRUTH_SOURCE_REPO_NAMES
357
+ ):
358
+ return candidate
359
+ return None
360
+
361
+
362
+ def _resolve_truth_source_repo_root(sources_root: Path, repo_name: str) -> Path | None:
363
+ exact = sources_root / repo_name
364
+ if exact.exists():
365
+ return exact.resolve()
366
+ return None
367
+
368
+
369
+ def _prune_generated_source_artifacts_for_target(*, project_dir: Path, target: SyncTarget) -> None:
370
+ cleanup_roots: list[Path] = []
371
+ direct_sources_root = _discover_truth_source_root(target.path)
372
+ if direct_sources_root is not None:
373
+ cleanup_roots.append(target.path.resolve())
374
+ elif target.kind == "project":
375
+ host_sources_root = _discover_truth_source_root(resolve_sources_root(project_dir))
376
+ if host_sources_root is not None:
377
+ for repo_name in OFFICIAL_TRUTH_SOURCE_REPO_NAMES:
378
+ repo_root = _resolve_truth_source_repo_root(host_sources_root, repo_name)
379
+ if repo_root is not None:
380
+ cleanup_roots.append(repo_root.resolve())
381
+ seen: set[Path] = set()
382
+ for root in cleanup_roots:
383
+ resolved = root.resolve()
384
+ if resolved in seen or not resolved.is_dir():
385
+ continue
386
+ seen.add(resolved)
387
+ _prune_generated_source_artifacts(resolved)
388
+
389
+
390
+ def _prune_generated_source_artifacts(root: Path) -> tuple[Path, ...]:
391
+ removed: list[Path] = []
392
+ seen: set[Path] = set()
393
+ for pattern in _GENERATED_SOURCE_ARTIFACT_DIR_PATTERNS:
394
+ for candidate in sorted(root.rglob(pattern)):
395
+ if not _is_generated_source_artifact_dir(candidate):
396
+ continue
397
+ if candidate.is_symlink():
398
+ link_path = candidate.absolute()
399
+ if link_path in seen:
400
+ continue
401
+ seen.add(link_path)
402
+ candidate.unlink(missing_ok=True)
403
+ removed.append(link_path)
404
+ continue
405
+ if not candidate.is_dir():
406
+ continue
407
+ resolved = candidate.resolve()
408
+ if resolved in seen or resolved == root:
409
+ continue
410
+ seen.add(resolved)
411
+ shutil.rmtree(resolved, ignore_errors=True)
412
+ removed.append(resolved)
413
+ return tuple(removed)
414
+
415
+
416
+ def _is_generated_source_artifact_dir(path: Path) -> bool:
417
+ if not any(fnmatchcase(path.name, pattern) for pattern in _GENERATED_SOURCE_ARTIFACT_DIR_PATTERNS):
418
+ return False
419
+ if path.name == "dist":
420
+ return (path.parent / "pyproject.toml").is_file()
421
+ return True
422
+
423
+
424
+ def _prune_stale_editable_artifacts(
425
+ venv_path: Path,
426
+ *,
427
+ allowed_editable_paths: tuple[Path, ...] = (),
428
+ ) -> tuple[Path, ...]:
429
+ site_packages_dirs = _iter_site_packages_dirs(venv_path)
430
+ if not site_packages_dirs:
431
+ return ()
432
+ stale: set[Path] = set()
433
+ stale_finder_modules: set[str] = set()
434
+ finder_files: dict[str, Path] = {}
435
+
436
+ for site_packages in site_packages_dirs:
437
+ for candidate in site_packages.iterdir():
438
+ if not candidate.is_file():
439
+ continue
440
+ if candidate.suffix not in {".pth", ".py"}:
441
+ continue
442
+ text = candidate.read_text(encoding="utf-8", errors="ignore")
443
+ if candidate.name.endswith("_finder.py"):
444
+ finder_files[candidate.stem] = candidate
445
+ if _editable_artifact_is_stale(candidate, text, allowed_editable_paths=allowed_editable_paths):
446
+ stale.add(candidate)
447
+ if candidate.name.endswith("_finder.py"):
448
+ stale_finder_modules.add(candidate.stem)
449
+
450
+ for site_packages in site_packages_dirs:
451
+ for candidate in site_packages.iterdir():
452
+ if not candidate.is_file() or candidate.suffix != ".pth":
453
+ continue
454
+ if candidate in stale:
455
+ continue
456
+ text = candidate.read_text(encoding="utf-8", errors="ignore")
457
+ imported_finder = _imported_editable_finder_module(text)
458
+ if imported_finder is None:
459
+ continue
460
+ finder_path = finder_files.get(imported_finder)
461
+ if imported_finder in stale_finder_modules or finder_path is None or not finder_path.exists():
462
+ stale.add(candidate)
463
+
464
+ for candidate in sorted(stale):
465
+ candidate.unlink(missing_ok=True)
466
+ return tuple(sorted(stale))
467
+
468
+
469
+ def _find_corrupt_installed_distributions(venv_path: Path) -> tuple[str, ...]:
470
+ broken: list[str] = []
471
+ for site_packages in _iter_site_packages_dirs(venv_path):
472
+ for dist_info in sorted(site_packages.glob("*.dist-info")):
473
+ record_path = dist_info / "RECORD"
474
+ if not record_path.is_file():
475
+ continue
476
+ if not _record_has_missing_site_packages_payload(record_path, site_packages):
477
+ continue
478
+ name = _read_distribution_name_from_metadata(dist_info / "METADATA")
479
+ if not name:
480
+ name = _distribution_name_from_dist_info_dir(dist_info)
481
+ if name and name not in broken:
482
+ broken.append(name)
483
+ return tuple(broken)
484
+
485
+
486
+ def _record_has_missing_site_packages_payload(record_path: Path, site_packages: Path) -> bool:
487
+ try:
488
+ rows = csv.reader(record_path.read_text(encoding="utf-8").splitlines())
489
+ except Exception:
490
+ return False
491
+ for row in rows:
492
+ if not row:
493
+ continue
494
+ rel_path = str(row[0]).strip()
495
+ if not rel_path or rel_path.startswith(".."):
496
+ continue
497
+ normalized = rel_path.replace("\\", "/")
498
+ if normalized.endswith((".pyc", ".pyo")) or "/__pycache__/" in normalized:
499
+ continue
500
+ if not (site_packages / rel_path).exists():
501
+ return True
502
+ return False
503
+
504
+
505
+ def _read_distribution_name_from_metadata(metadata_path: Path) -> str | None:
506
+ try:
507
+ for line in metadata_path.read_text(encoding="utf-8").splitlines():
508
+ if line.startswith("Name: "):
509
+ value = line.split(":", 1)[1].strip()
510
+ if value:
511
+ return value
512
+ except Exception:
513
+ return None
514
+ return None
515
+
516
+
517
+ def _distribution_name_from_dist_info_dir(dist_info_dir: Path) -> str | None:
518
+ name = dist_info_dir.name
519
+ if not name.endswith(".dist-info"):
520
+ return None
521
+ stem = name[: -len(".dist-info")]
522
+ if "-" not in stem:
523
+ return stem.replace("_", "-")
524
+ distribution, _version = stem.rsplit("-", 1)
525
+ distribution = distribution.strip().replace("_", "-")
526
+ return distribution or None
527
+
528
+
529
+ def _iter_site_packages_dirs(venv_path: Path) -> tuple[Path, ...]:
530
+ if not venv_path.is_dir():
531
+ return ()
532
+ discovered: list[Path] = []
533
+ for parent in (venv_path / "lib", venv_path / "Lib"):
534
+ if not parent.is_dir():
535
+ continue
536
+ for candidate in sorted(parent.glob("python*/site-packages")):
537
+ if candidate.is_dir():
538
+ discovered.append(candidate)
539
+ scripts_candidate = venv_path / "Lib" / "site-packages"
540
+ if scripts_candidate.is_dir():
541
+ discovered.append(scripts_candidate)
542
+ seen: set[Path] = set()
543
+ ordered: list[Path] = []
544
+ for candidate in discovered:
545
+ resolved = candidate.resolve()
546
+ if resolved in seen:
547
+ continue
548
+ seen.add(resolved)
549
+ ordered.append(resolved)
550
+ return tuple(ordered)
551
+
552
+
553
+ def _editable_artifact_is_stale(
554
+ path: Path,
555
+ text: str,
556
+ *,
557
+ allowed_editable_paths: tuple[Path, ...] = (),
558
+ ) -> bool:
559
+ if ".pz-sync-" in text:
560
+ return True
561
+ for raw_path in _quoted_absolute_paths(text):
562
+ if ".pz-sync-" in raw_path or not Path(raw_path).exists():
563
+ return True
564
+ if allowed_editable_paths and _is_editable_artifact(path) and not _path_is_allowed_editable_source(
565
+ raw_path,
566
+ allowed_editable_paths,
567
+ ):
568
+ return True
569
+ if path.suffix == ".pth":
570
+ stripped = text.strip()
571
+ if stripped.startswith("/") and "\n" not in stripped and not Path(stripped).exists():
572
+ return True
573
+ if (
574
+ stripped.startswith("/")
575
+ and "\n" not in stripped
576
+ and allowed_editable_paths
577
+ and not _path_is_allowed_editable_source(stripped, allowed_editable_paths)
578
+ ):
579
+ return True
580
+ return False
581
+
582
+
583
+ def _is_editable_artifact(path: Path) -> bool:
584
+ return path.suffix == ".pth" or path.name.startswith("__editable__") or path.name.endswith("_finder.py")
585
+
586
+
587
+ def _path_is_allowed_editable_source(raw_path: str, allowed_editable_paths: tuple[Path, ...]) -> bool:
588
+ try:
589
+ candidate = Path(raw_path).expanduser().resolve()
590
+ except OSError:
591
+ candidate = Path(raw_path).expanduser().absolute()
592
+ for allowed in allowed_editable_paths:
593
+ try:
594
+ normalized = allowed.expanduser().resolve()
595
+ except OSError:
596
+ normalized = allowed.expanduser().absolute()
597
+ if candidate == normalized:
598
+ return True
599
+ try:
600
+ if candidate.is_relative_to(normalized):
601
+ return True
602
+ except ValueError:
603
+ continue
604
+ return False
605
+
606
+
607
+ def _canonicalize_editable_artifacts(
608
+ venv_path: Path,
609
+ *,
610
+ editable_source_paths: tuple[_EditableSourcePath, ...],
611
+ ) -> tuple[Path, ...]:
612
+ if not editable_source_paths:
613
+ return ()
614
+ changed: list[Path] = []
615
+ for site_packages in _iter_site_packages_dirs(venv_path):
616
+ for candidate in site_packages.iterdir():
617
+ if not candidate.is_file() or candidate.suffix not in {".pth", ".py"}:
618
+ continue
619
+ if not _is_editable_artifact(candidate):
620
+ continue
621
+ text = candidate.read_text(encoding="utf-8", errors="ignore")
622
+ rewritten = _canonicalize_editable_artifact_text(candidate, text, editable_source_paths)
623
+ if rewritten == text:
624
+ continue
625
+ candidate.write_text(rewritten, encoding="utf-8")
626
+ changed.append(candidate)
627
+ return tuple(changed)
628
+
629
+
630
+ def _canonicalize_editable_artifact_text(
631
+ path: Path,
632
+ text: str,
633
+ editable_source_paths: tuple[_EditableSourcePath, ...],
634
+ ) -> str:
635
+ rewritten = text
636
+ for raw_path in _quoted_absolute_paths(text):
637
+ preferred = _preferred_editable_path(raw_path, editable_source_paths)
638
+ if preferred is None:
639
+ continue
640
+ rewritten = rewritten.replace(raw_path, preferred.as_posix())
641
+ if path.suffix == ".pth":
642
+ stripped = rewritten.strip()
643
+ if stripped.startswith("/") and "\n" not in stripped:
644
+ preferred = _preferred_editable_path(stripped, editable_source_paths)
645
+ if preferred is not None:
646
+ rewritten = preferred.as_posix()
647
+ if text.endswith("\n"):
648
+ rewritten += "\n"
649
+ return rewritten
650
+
651
+
652
+ def _preferred_editable_path(raw_path: str, editable_source_paths: tuple[_EditableSourcePath, ...]) -> Path | None:
653
+ try:
654
+ candidate = Path(raw_path).expanduser().resolve()
655
+ except OSError:
656
+ candidate = Path(raw_path).expanduser().absolute()
657
+ for source in editable_source_paths:
658
+ try:
659
+ if candidate == source.resolved:
660
+ return source.preferred
661
+ if candidate.is_relative_to(source.resolved):
662
+ return source.preferred / candidate.relative_to(source.resolved)
663
+ except ValueError:
664
+ continue
665
+ return None
666
+
667
+
668
+ def _editable_source_paths_for_target(target_path: Path) -> tuple[_EditableSourcePath, ...]:
669
+ target_root = target_path.expanduser().absolute()
670
+ roots: list[Path] = [target_root]
671
+ roots.extend(_collect_local_path_dependency_preferred_roots(target_root))
672
+ seen: set[Path] = set()
673
+ ordered: list[_EditableSourcePath] = []
674
+ for root in roots:
675
+ try:
676
+ resolved = root.resolve()
677
+ except OSError:
678
+ resolved = root.absolute()
679
+ if resolved in seen:
680
+ continue
681
+ seen.add(resolved)
682
+ ordered.append(_EditableSourcePath(resolved=resolved, preferred=root.absolute()))
683
+ return tuple(ordered)
684
+
685
+
686
+ def _quoted_absolute_paths(text: str) -> tuple[str, ...]:
687
+ return tuple(match.group(1) for match in _QUOTED_ABS_PATH_RE.finditer(text or ""))
688
+
689
+
690
+ def _imported_editable_finder_module(text: str) -> str | None:
691
+ match = _EDITABLE_IMPORT_RE.match(text or "")
692
+ if match is None:
693
+ return None
694
+ return str(match.group(1)).strip() or None
695
+
696
+
697
+ def _collect_local_path_dependency_preferred_roots(target_root: Path) -> tuple[Path, ...]:
698
+ pyproject_path = target_root / "pyproject.toml"
699
+ if not pyproject_path.is_file():
700
+ return ()
701
+ try:
702
+ document = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
703
+ except Exception:
704
+ return ()
705
+ tool = document.get("tool")
706
+ if not isinstance(tool, dict):
707
+ return ()
708
+ uv = tool.get("uv")
709
+ if not isinstance(uv, dict):
710
+ return ()
711
+ sources = uv.get("sources")
712
+ if not isinstance(sources, dict):
713
+ return ()
714
+ roots: list[Path] = []
715
+ for source_meta in sources.values():
716
+ if not isinstance(source_meta, dict):
717
+ continue
718
+ raw_path = str(source_meta.get("path") or "").strip()
719
+ if not raw_path:
720
+ continue
721
+ path = Path(raw_path)
722
+ if path.is_absolute():
723
+ preferred = path.absolute()
724
+ else:
725
+ preferred = (target_root / path).absolute()
726
+ if not preferred.exists():
727
+ continue
728
+ roots.append(preferred)
729
+ return tuple(roots)
730
+
731
+
732
+ def _source_project_root(target_path: Path) -> Path | None:
733
+ absolute = target_path.expanduser().absolute()
734
+ for candidate in (absolute, *absolute.parents):
735
+ if candidate.name == ".sources" and candidate.parent.name == "apps":
736
+ return candidate.parent.parent.absolute()
737
+ return None
738
+
739
+
740
+ def _target_virtualenv_path(target_path: Path) -> Path:
741
+ project_root = _source_project_root(target_path)
742
+ absolute = target_path.expanduser().absolute()
743
+ if project_root is None:
744
+ return absolute.resolve() / ".venv"
745
+ try:
746
+ relative = absolute.relative_to(project_root)
747
+ except ValueError:
748
+ return absolute.resolve() / ".venv"
749
+ return project_root / ".runtime" / "source-venvs" / relative / ".venv"
750
+
751
+
752
+ def _ensure_target_virtualenv(target_path: Path, *, uv_binary: str, action: str) -> Path:
753
+ venv_path = _target_virtualenv_path(target_path)
754
+ if _is_valid_virtualenv(venv_path):
755
+ return venv_path.resolve()
756
+ venv_path.parent.mkdir(parents=True, exist_ok=True)
757
+ completed = subprocess.run(
758
+ [uv_binary, "venv", str(venv_path)],
759
+ cwd=str(target_path),
760
+ check=False,
761
+ capture_output=True,
762
+ text=True,
763
+ env=os.environ.copy(),
764
+ )
765
+ if completed.returncode == 0 and _is_valid_virtualenv(venv_path):
766
+ return venv_path.resolve()
767
+ detail = (completed.stderr or completed.stdout or "failed to create virtual environment").strip()
768
+ raise PzError(
769
+ code="PZ_ENV_SYNC_VENV_FAILED",
770
+ message=f"failed to prepare virtual environment for {target_path}.",
771
+ stage="cli",
772
+ owner=_OWNER,
773
+ action=action,
774
+ detail=detail,
775
+ )
776
+
777
+
778
+ def _is_valid_virtualenv(path: Path) -> bool:
779
+ candidates = (
780
+ path / "bin" / "python",
781
+ path / "bin" / "python3",
782
+ path / "Scripts" / "python.exe",
783
+ )
784
+ return any(candidate.is_file() for candidate in candidates)
785
+
786
+
787
+ __all__ = ["SyncTarget", "build_sync_target", "collect_sync_targets", "sync_project_targets"]