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,1240 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from functools import lru_cache
6
+ import importlib.util
7
+ from importlib.util import find_spec
8
+ from pathlib import Path, PurePosixPath
9
+ import re
10
+ import sys
11
+ import tomllib
12
+ import types
13
+ from typing import TYPE_CHECKING, Any, Iterable
14
+
15
+ from pz_core.core.errors import PzError
16
+ from pz_core.project_sources import load_official_source_catalog
17
+
18
+ if TYPE_CHECKING:
19
+ from pz_core.authoring.dev_manifest import DevBinding, DevManifest
20
+
21
+
22
+ _SKIP_DISCOVERY_PARTS = {
23
+ ".git",
24
+ ".venv",
25
+ "__pycache__",
26
+ "build",
27
+ "dist",
28
+ "docs",
29
+ "test",
30
+ "tests",
31
+ }
32
+ _DEPENDENCY_NAME_RE = re.compile(r"^\s*([A-Za-z0-9_.-]+)")
33
+ _FORMAL_APP_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$")
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class DiscoveredAppReference:
38
+ module_path: str
39
+ import_root: Path | None = None
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class VisibleAppProjection:
44
+ app_id: str
45
+ module_path: str
46
+ visible_root: Path
47
+ source_root: Path
48
+ import_root: Path
49
+ binding_key: str | None = None
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class StaticAppDeclaration:
54
+ module_path: str
55
+ app_id: str
56
+ name: str
57
+ version: str
58
+ required_pz: str
59
+ priority: object | None = None
60
+ requires_apps: object | None = None
61
+ required_protocols: object | None = None
62
+ method_names: frozenset[str] = frozenset()
63
+
64
+
65
+ _MISSING_MODULE = object()
66
+
67
+
68
+ def _prefer_minimal_package_root(candidate: Path) -> Path:
69
+ """Prefer the inner package root for formal minimal-package containers."""
70
+ nested_package_root = candidate / candidate.name
71
+ if (nested_package_root / "__init__.py").is_file():
72
+ return nested_package_root.resolve()
73
+ return candidate.resolve()
74
+
75
+
76
+ def resolve_package_root(module_path: str) -> Path | None:
77
+ package_name = _validate_declared_app_token(
78
+ str(module_path or "").strip(),
79
+ action="resolve_package_root",
80
+ )
81
+ if not package_name:
82
+ return None
83
+ located = _resolve_package_root_cached(package_name, _sys_path_fingerprint())
84
+ if located is not None:
85
+ return Path(located)
86
+ try:
87
+ spec = find_spec(package_name)
88
+ except Exception:
89
+ return None
90
+ if spec is None:
91
+ return None
92
+ if spec.submodule_search_locations:
93
+ return Path(next(iter(spec.submodule_search_locations))).resolve()
94
+ if spec.origin:
95
+ return Path(spec.origin).resolve().parent
96
+ return None
97
+
98
+
99
+ @lru_cache(maxsize=512)
100
+ def _resolve_package_root_cached(
101
+ package_name: str,
102
+ sys_path_fingerprint: tuple[str, ...],
103
+ ) -> str | None:
104
+ normalized = str(package_name or "").strip()
105
+ if not normalized:
106
+ return None
107
+ relative_path = Path(*normalized.split("."))
108
+ for entry in sys_path_fingerprint:
109
+ base = str(entry or "").strip()
110
+ if not base:
111
+ continue
112
+ candidate = Path(base).expanduser().resolve() / relative_path
113
+ if candidate.is_dir():
114
+ return str(_prefer_minimal_package_root(candidate))
115
+ package_file = candidate.with_suffix(".py")
116
+ if package_file.is_file():
117
+ return str(package_file.resolve().parent)
118
+ return None
119
+
120
+
121
+ def resolve_declared_app_id(module_path: str) -> str:
122
+ package_name = _validate_declared_app_token(
123
+ str(module_path or "").strip(),
124
+ action="resolve_app_id",
125
+ )
126
+ if not package_name:
127
+ return ""
128
+ try:
129
+ return read_static_app_declaration(package_name, action="resolve_app_id").app_id
130
+ except PzError as exc:
131
+ if exc.code == "PZ_APP_DECLARATION_NOT_FOUND":
132
+ raise PzError(
133
+ code="PZ_APP_IMPORT_FAILED",
134
+ message=f"Failed to locate app module '{package_name}.apps'.",
135
+ stage="apps",
136
+ owner=package_name,
137
+ action="resolve_app_id",
138
+ detail=exc.detail,
139
+ ) from exc
140
+ raise
141
+
142
+
143
+ def read_static_app_declaration(
144
+ module_path: str,
145
+ *,
146
+ action: str = "read_static_app_declaration",
147
+ ) -> StaticAppDeclaration:
148
+ package_name = _validate_declared_app_token(
149
+ str(module_path or "").strip(),
150
+ action=action,
151
+ )
152
+ if not package_name:
153
+ raise PzError(
154
+ code="PZ_APP_INVALID_PATH",
155
+ message="App path must declare a directory/package name.",
156
+ stage="apps",
157
+ owner="apps",
158
+ action=action,
159
+ )
160
+ return _read_static_app_declaration_cached(
161
+ package_name,
162
+ _sys_path_fingerprint(),
163
+ action,
164
+ )
165
+
166
+
167
+ @lru_cache(maxsize=1024)
168
+ def _read_static_app_declaration_cached(
169
+ package_name: str,
170
+ sys_path_fingerprint: tuple[str, ...],
171
+ action: str,
172
+ ) -> StaticAppDeclaration:
173
+ app_module_name = f"{package_name}.apps"
174
+ origin_path = _locate_apps_module_path(app_module_name, sys_path_fingerprint)
175
+ if origin_path is None:
176
+ raise PzError(
177
+ code="PZ_APP_DECLARATION_NOT_FOUND",
178
+ message=f"App module '{app_module_name}' was not found.",
179
+ stage="apps",
180
+ owner=package_name,
181
+ action=action,
182
+ )
183
+ if not origin_path.is_file():
184
+ raise PzError(
185
+ code="PZ_APP_ENTRY_MISSING",
186
+ message=f"App module '{app_module_name}' does not expose 'app'.",
187
+ stage="apps",
188
+ owner=package_name,
189
+ action=action,
190
+ )
191
+ try:
192
+ source = origin_path.read_text(encoding="utf-8")
193
+ tree = ast.parse(source, filename=str(origin_path))
194
+ except Exception as exc:
195
+ raise PzError(
196
+ code="PZ_APP_DECLARATION_INVALID",
197
+ message=f"Failed to parse app module '{app_module_name}': {exc}",
198
+ stage="apps",
199
+ owner=package_name,
200
+ action=action,
201
+ ) from exc
202
+
203
+ classes = {node.name: node for node in tree.body if isinstance(node, ast.ClassDef)}
204
+ app_class_name = _read_module_app_class_name(tree)
205
+ if not app_class_name:
206
+ raise PzError(
207
+ code="PZ_APP_ENTRY_MISSING",
208
+ message=f"App module '{app_module_name}' does not expose 'app = <AppClass>()'.",
209
+ stage="apps",
210
+ owner=package_name,
211
+ action=action,
212
+ )
213
+ class_node = classes.get(app_class_name)
214
+ if class_node is None:
215
+ raise PzError(
216
+ code="PZ_APP_ENTRY_MISSING",
217
+ message=f"App module '{app_module_name}' declares app from unknown class '{app_class_name}'.",
218
+ stage="apps",
219
+ owner=package_name,
220
+ action=action,
221
+ )
222
+
223
+ fields = _read_static_class_fields(class_node)
224
+ app_id = _read_static_required_string_field(
225
+ fields,
226
+ "id",
227
+ package_name=package_name,
228
+ code="PZ_APP_ID_MISSING",
229
+ invalid_code="PZ_APP_ID_INVALID",
230
+ action=action,
231
+ )
232
+ if not app_id.isidentifier():
233
+ raise PzError(
234
+ code="PZ_APP_ID_INVALID",
235
+ message=f"App '{package_name}' must declare a valid app.id identifier.",
236
+ stage="apps",
237
+ owner=package_name,
238
+ action=action,
239
+ )
240
+ return StaticAppDeclaration(
241
+ module_path=package_name,
242
+ app_id=app_id,
243
+ name=_read_static_required_string_field(
244
+ fields,
245
+ "name",
246
+ package_name=app_id,
247
+ code="PZ_APP_NAME_MISSING",
248
+ invalid_code="PZ_APP_NAME_MISSING",
249
+ action=action,
250
+ ),
251
+ version=_read_static_required_string_field(
252
+ fields,
253
+ "version",
254
+ package_name=app_id,
255
+ code="PZ_APP_VERSION_MISSING",
256
+ invalid_code="PZ_APP_VERSION_MISSING",
257
+ action=action,
258
+ ),
259
+ required_pz=_read_static_required_string_field(
260
+ fields,
261
+ "required_pz",
262
+ package_name=app_id,
263
+ code="PZ_APP_REQUIRED_PZ_MISSING",
264
+ invalid_code="PZ_APP_REQUIRED_PZ_MISSING",
265
+ action=action,
266
+ ),
267
+ priority=_read_static_optional_field(fields, "priority"),
268
+ requires_apps=_read_static_optional_field(fields, "requires_apps"),
269
+ required_protocols=_read_static_optional_field(fields, "required_protocols"),
270
+ method_names=frozenset(
271
+ statement.name
272
+ for statement in class_node.body
273
+ if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef))
274
+ ),
275
+ )
276
+
277
+
278
+ def _sys_path_fingerprint() -> tuple[str, ...]:
279
+ return tuple(str(entry or "") for entry in sys.path)
280
+
281
+
282
+ def import_app_settings_module(module_path: str, *, action: str) -> Any | None:
283
+ module_name = f"{module_path}.settings"
284
+ module_file = _locate_declared_app_sibling_module_file(module_path, "settings.py")
285
+ if module_file is None:
286
+ return None
287
+ return _import_entrypoint_module_from_file(module_name, module_file, action=action)
288
+
289
+
290
+ def import_app_entrypoint_module_from_decl(module_path: str, *, action: str) -> Any:
291
+ module_name = f"{module_path}.apps"
292
+ module_file = _locate_declared_app_sibling_module_file(module_path, "apps.py")
293
+ if module_file is None:
294
+ raise PzError(
295
+ code="PZ_APP_IMPORT_FAILED",
296
+ message=f"Failed to locate app module '{module_name}'.",
297
+ stage="apps",
298
+ owner=module_path,
299
+ action=action,
300
+ )
301
+ return _import_entrypoint_module_from_file(module_name, module_file, action=action)
302
+
303
+
304
+ def _import_entrypoint_module_from_file(
305
+ module_name: str, module_file: Path, *, action: str
306
+ ) -> Any:
307
+ package_name, _, leaf_name = module_name.rpartition(".")
308
+ package_root = module_file.parent
309
+ restore_names = [module_name]
310
+ package_parts = package_name.split(".") if package_name else []
311
+ package_dirs = (
312
+ [
313
+ *(
314
+ package_root.parents[len(package_parts) - index - 2]
315
+ for index in range(len(package_parts) - 1)
316
+ ),
317
+ package_root,
318
+ ]
319
+ if package_parts
320
+ else []
321
+ )
322
+ for index in range(1, len(package_parts) + 1):
323
+ restore_names.append(".".join(package_parts[:index]))
324
+ saved_modules = {
325
+ name: sys.modules.get(name, _MISSING_MODULE) for name in restore_names
326
+ }
327
+ try:
328
+ for index in range(1, len(package_parts) + 1):
329
+ current_name = ".".join(package_parts[:index])
330
+ current_dir = package_dirs[index - 1]
331
+ package_module = types.ModuleType(current_name)
332
+ package_module.__file__ = str(current_dir / "__init__.py")
333
+ package_module.__path__ = [str(current_dir)]
334
+ package_module.__package__ = current_name
335
+ package_module.__spec__ = importlib.util.spec_from_loader(
336
+ current_name, loader=None, is_package=True
337
+ )
338
+ sys.modules[current_name] = package_module
339
+ spec = importlib.util.spec_from_file_location(
340
+ module_name,
341
+ module_file,
342
+ submodule_search_locations=[str(package_root)]
343
+ if leaf_name == "__init__"
344
+ else None,
345
+ )
346
+ if spec is None or spec.loader is None:
347
+ raise ImportError(f"cannot load module from {module_file}")
348
+ module = importlib.util.module_from_spec(spec)
349
+ sys.modules[module_name] = module
350
+ spec.loader.exec_module(module)
351
+ return module
352
+ except PzError:
353
+ raise
354
+ except Exception as exc:
355
+ raise PzError(
356
+ code="PZ_APP_IMPORT_FAILED",
357
+ message=f"Failed to import app module '{module_name}': {exc}",
358
+ stage="apps",
359
+ owner=module_name,
360
+ action=action,
361
+ ) from exc
362
+ finally:
363
+ for name, saved in saved_modules.items():
364
+ if saved is _MISSING_MODULE:
365
+ sys.modules.pop(name, None)
366
+ else:
367
+ sys.modules[name] = saved
368
+
369
+
370
+ def _locate_declared_app_sibling_module_file(
371
+ module_path: str,
372
+ file_name: str,
373
+ ) -> Path | None:
374
+ apps_module_path = _locate_apps_module_path(
375
+ f"{module_path}.apps", _sys_path_fingerprint()
376
+ )
377
+ if apps_module_path is None:
378
+ return None
379
+ candidate = apps_module_path.with_name(file_name)
380
+ return candidate if candidate.is_file() else None
381
+
382
+
383
+ def _locate_apps_module_path(
384
+ app_module_name: str,
385
+ sys_path_fingerprint: tuple[str, ...],
386
+ ) -> Path | None:
387
+ origin = _locate_apps_module_file_cached(app_module_name, sys_path_fingerprint)
388
+ if origin is not None:
389
+ return Path(origin)
390
+ original_sys_path = sys.path
391
+ sys.path = list(sys_path_fingerprint)
392
+ try:
393
+ spec = find_spec(app_module_name)
394
+ except Exception:
395
+ return None
396
+ finally:
397
+ sys.path = original_sys_path
398
+ if spec is None or not spec.origin:
399
+ return None
400
+ return Path(spec.origin)
401
+
402
+
403
+ def _locate_apps_module_file(app_module_name: str) -> Path | None:
404
+ located = _locate_apps_module_file_cached(app_module_name, _sys_path_fingerprint())
405
+ return Path(located) if located is not None else None
406
+
407
+
408
+ @lru_cache(maxsize=512)
409
+ def _locate_apps_module_file_cached(
410
+ app_module_name: str,
411
+ sys_path_fingerprint: tuple[str, ...],
412
+ ) -> str | None:
413
+ module_name = str(app_module_name or "").strip()
414
+ if not module_name:
415
+ return None
416
+ parts = module_name.split(".")
417
+ relative_path = Path(*parts).with_suffix(".py")
418
+ for entry in sys_path_fingerprint:
419
+ base = str(entry or "").strip()
420
+ if not base:
421
+ continue
422
+ candidate = Path(base).expanduser().resolve() / relative_path
423
+ if candidate.is_file():
424
+ return str(candidate)
425
+ if len(parts) == 2 and parts[-1] == "apps":
426
+ nested_apps = (
427
+ Path(base).expanduser().resolve() / parts[0] / parts[0] / "apps.py"
428
+ )
429
+ if nested_apps.is_file():
430
+ return str(nested_apps.resolve())
431
+ return None
432
+
433
+
434
+ def _read_static_class_app_id(node: ast.ClassDef) -> str:
435
+ fields = _read_static_class_fields(node)
436
+ value = fields.get("id")
437
+ if isinstance(value, str) and value.strip().isidentifier():
438
+ return value.strip()
439
+ return ""
440
+
441
+
442
+ def _read_module_app_class_name(tree: ast.Module) -> str:
443
+ for node in tree.body:
444
+ value: ast.AST | None
445
+ if isinstance(node, ast.Assign):
446
+ value = node.value
447
+ targets = node.targets
448
+ elif isinstance(node, ast.AnnAssign):
449
+ value = node.value
450
+ targets = (node.target,)
451
+ else:
452
+ continue
453
+ if value is None:
454
+ continue
455
+ for target in targets:
456
+ if isinstance(target, ast.Name) and target.id == "app":
457
+ return _read_static_app_instance_class_name(value)
458
+ return ""
459
+
460
+
461
+ def _read_static_app_instance_class_name(node: ast.AST) -> str:
462
+ if isinstance(node, ast.Call):
463
+ return _read_static_app_instance_class_name(node.func)
464
+ if isinstance(node, ast.Name):
465
+ return node.id
466
+ return ""
467
+
468
+
469
+ def _read_static_class_fields(node: ast.ClassDef) -> dict[str, object]:
470
+ fields: dict[str, object] = {}
471
+ for statement in node.body:
472
+ value: ast.AST | None
473
+ targets: Iterable[ast.AST]
474
+ if isinstance(statement, ast.Assign):
475
+ value = statement.value
476
+ targets = statement.targets
477
+ elif isinstance(statement, ast.AnnAssign):
478
+ value = statement.value
479
+ targets = (statement.target,)
480
+ else:
481
+ continue
482
+ if value is None:
483
+ continue
484
+ for target in targets:
485
+ if isinstance(target, ast.Name):
486
+ fields[target.id] = _read_static_literal(value)
487
+ return fields
488
+
489
+
490
+ def _read_static_literal(node: ast.AST) -> object:
491
+ try:
492
+ return ast.literal_eval(node)
493
+ except Exception:
494
+ return _MISSING_MODULE
495
+
496
+
497
+ def _read_static_required_string_field(
498
+ fields: dict[str, object],
499
+ field_name: str,
500
+ *,
501
+ package_name: str,
502
+ code: str,
503
+ invalid_code: str,
504
+ action: str,
505
+ ) -> str:
506
+ value = fields.get(field_name, _MISSING_MODULE)
507
+ if value is _MISSING_MODULE:
508
+ raise PzError(
509
+ code=code,
510
+ message=f"App '{package_name}' must declare static string field '{field_name}'.",
511
+ stage="apps",
512
+ owner=package_name,
513
+ action=action,
514
+ )
515
+ if not isinstance(value, str) or not value.strip():
516
+ raise PzError(
517
+ code=invalid_code,
518
+ message=f"App '{package_name}' must declare non-empty static string field '{field_name}'.",
519
+ stage="apps",
520
+ owner=package_name,
521
+ action=action,
522
+ )
523
+ return value.strip()
524
+
525
+
526
+ def _read_static_optional_field(
527
+ fields: dict[str, object], field_name: str
528
+ ) -> object | None:
529
+ value = fields.get(field_name, _MISSING_MODULE)
530
+ if value is _MISSING_MODULE:
531
+ return None
532
+ return value
533
+
534
+
535
+ def _normalize_package_name(module_path: str) -> str:
536
+ return str(module_path or "").strip()
537
+
538
+
539
+ def _validate_declared_app_token(module_path: str, *, action: str) -> str:
540
+ normalized = str(module_path or "").strip()
541
+ if not normalized:
542
+ return ""
543
+ if ":" in normalized or normalized.endswith(".apps"):
544
+ raise PzError(
545
+ code="PZ_APP_INVALID_PATH",
546
+ message=(
547
+ f"App path '{normalized}' must declare a directory/package name only; "
548
+ "installed_apps does not accept '<pkg>.apps' or '<pkg>.apps:app'."
549
+ ),
550
+ stage="apps",
551
+ owner=normalized,
552
+ action=action,
553
+ )
554
+ return normalized
555
+
556
+
557
+ def _read_project_dir(project_dir: str | Path | None) -> Path | None:
558
+ if project_dir is None:
559
+ return None
560
+ return Path(project_dir).expanduser().resolve()
561
+
562
+
563
+ def _normalize_distribution_name(value: str) -> str:
564
+ return re.sub(r"[-_.]+", "-", str(value or "").strip().lower()).strip("-")
565
+
566
+
567
+ def _is_skipped_discovery_dir(path: Path) -> bool:
568
+ name = path.name
569
+ if name.startswith("."):
570
+ return True
571
+ return name in _SKIP_DISCOVERY_PARTS
572
+
573
+
574
+ def _read_standard_apps_py_app_id(apps_py: Path) -> str:
575
+ if not apps_py.is_file():
576
+ return ""
577
+ try:
578
+ source = apps_py.read_text(encoding="utf-8")
579
+ tree = ast.parse(source, filename=str(apps_py))
580
+ except Exception:
581
+ return ""
582
+
583
+ class_ids: dict[str, str] = {}
584
+ for node in tree.body:
585
+ if not isinstance(node, ast.ClassDef):
586
+ continue
587
+ class_id = _read_static_class_app_id(node)
588
+ if class_id:
589
+ class_ids[node.name] = class_id
590
+
591
+ for node in tree.body:
592
+ if not isinstance(node, ast.Assign):
593
+ continue
594
+ for target in node.targets:
595
+ if isinstance(target, ast.Name) and target.id == "app":
596
+ class_name = _read_static_app_instance_class_name(node.value)
597
+ if class_name and class_name in class_ids:
598
+ return class_ids[class_name]
599
+ return ""
600
+
601
+
602
+ def _has_standard_apps_py(apps_py: Path) -> bool:
603
+ return bool(_read_standard_apps_py_app_id(apps_py))
604
+
605
+
606
+ def _visible_app_error(
607
+ code: str,
608
+ message: str,
609
+ *,
610
+ owner: str,
611
+ ) -> PzError:
612
+ return PzError(
613
+ code=code,
614
+ message=message,
615
+ stage="apps",
616
+ owner=owner,
617
+ action="iter_visible_app_projections",
618
+ )
619
+
620
+
621
+ def _read_visible_leaf_app_id(leaf_root: Path) -> str:
622
+ pyproject_path = leaf_root / "pyproject.toml"
623
+ if not pyproject_path.is_file():
624
+ return _read_bundled_visible_leaf_app_id(leaf_root)
625
+ try:
626
+ payload = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
627
+ except (OSError, UnicodeError, tomllib.TOMLDecodeError):
628
+ return ""
629
+ project = payload.get("project")
630
+ if not isinstance(project, dict):
631
+ return ""
632
+ distribution = project.get("name")
633
+ if not isinstance(distribution, str) or distribution != distribution.strip():
634
+ return ""
635
+ app_id = distribution.replace("-", "_")
636
+ if not _FORMAL_APP_ID_RE.fullmatch(app_id) or leaf_root.name != app_id:
637
+ return ""
638
+ module_root = leaf_root / app_id
639
+ if not (module_root / "__init__.py").is_file():
640
+ return ""
641
+ if _read_standard_apps_py_app_id(module_root / "apps.py") != app_id:
642
+ return ""
643
+ return app_id
644
+
645
+
646
+ def _read_project_distribution(project_root: Path) -> str:
647
+ pyproject_path = project_root / "pyproject.toml"
648
+ if not pyproject_path.is_file():
649
+ return ""
650
+ try:
651
+ payload = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
652
+ except (OSError, UnicodeError, tomllib.TOMLDecodeError):
653
+ return ""
654
+ project = payload.get("project")
655
+ if not isinstance(project, dict):
656
+ return ""
657
+ distribution = project.get("name")
658
+ return distribution if isinstance(distribution, str) else ""
659
+
660
+
661
+ def _read_bundled_visible_leaf_app_id(leaf_root: Path) -> str:
662
+ try:
663
+ resolved_leaf = leaf_root.resolve(strict=True)
664
+ except (OSError, RuntimeError):
665
+ return ""
666
+ for source in load_official_source_catalog().values():
667
+ if not source.bootstrap:
668
+ continue
669
+ project_distributions = {
670
+ distribution
671
+ for distribution, child in source.children.items()
672
+ if child.kind == "project" and child.subpath == PurePosixPath(".")
673
+ }
674
+ for child in source.children.values():
675
+ app_id = child.expected_app_id
676
+ if child.kind != "app" or app_id is None or app_id != leaf_root.name:
677
+ continue
678
+ project_root = resolved_leaf
679
+ for _part in child.subpath.parts:
680
+ project_root = project_root.parent
681
+ if project_root.joinpath(*child.subpath.parts) != resolved_leaf:
682
+ continue
683
+ if _read_project_distribution(project_root) not in project_distributions:
684
+ continue
685
+ module_root = resolved_leaf / app_id
686
+ if not (module_root / "__init__.py").is_file():
687
+ continue
688
+ if _read_standard_apps_py_app_id(module_root / "apps.py") == app_id:
689
+ return app_id
690
+ return ""
691
+
692
+
693
+ def _build_visible_projection(
694
+ leaf_root: Path,
695
+ *,
696
+ binding_key: str | None,
697
+ ) -> VisibleAppProjection | None:
698
+ app_id = _read_visible_leaf_app_id(leaf_root)
699
+ if not app_id:
700
+ return None
701
+ try:
702
+ source_root = leaf_root.resolve(strict=True)
703
+ except (OSError, RuntimeError):
704
+ return None
705
+ return VisibleAppProjection(
706
+ app_id=app_id,
707
+ module_path=app_id,
708
+ visible_root=leaf_root,
709
+ source_root=source_root,
710
+ import_root=source_root,
711
+ binding_key=binding_key,
712
+ )
713
+
714
+
715
+ def _binding_source_root(
716
+ project_dir: Path,
717
+ manifest: DevManifest,
718
+ binding: DevBinding,
719
+ ) -> Path:
720
+ if binding.source_kind == "link":
721
+ root = Path(binding.source)
722
+ else:
723
+ assert binding.repo_key is not None
724
+ root = project_dir / Path(*manifest.repos[binding.repo_key].checkout.parts)
725
+ if binding.subpath == PurePosixPath("."):
726
+ return root
727
+ return root / Path(*binding.subpath.parts)
728
+
729
+
730
+ def _resolve_managed_link_target(
731
+ project_dir: Path,
732
+ manifest: DevManifest,
733
+ *,
734
+ binding_key: str,
735
+ binding: DevBinding,
736
+ ) -> tuple[Path, Path]:
737
+ if len(binding.link.parts) != 2 or binding.link.parts[0] != "apps":
738
+ raise _visible_app_error(
739
+ "PZ_VISIBLE_APP_BINDING_INVALID",
740
+ f"managed binding {binding_key!r} must project directly below apps/.",
741
+ owner=binding_key,
742
+ )
743
+ link_name = binding.link.parts[-1]
744
+ if link_name.startswith(".") or link_name in _SKIP_DISCOVERY_PARTS:
745
+ raise _visible_app_error(
746
+ "PZ_VISIBLE_APP_BINDING_INVALID",
747
+ f"managed binding {binding_key!r} uses a hidden projection path.",
748
+ owner=binding_key,
749
+ )
750
+ link = project_dir / Path(*binding.link.parts)
751
+ if not link.is_symlink():
752
+ raise _visible_app_error(
753
+ "PZ_VISIBLE_APP_BINDING_INVALID",
754
+ f"managed binding {binding_key!r} is missing its symbolic link.",
755
+ owner=binding_key,
756
+ )
757
+ expected = _binding_source_root(project_dir, manifest, binding)
758
+ try:
759
+ actual_root = link.resolve(strict=True)
760
+ expected_root = expected.resolve(strict=True)
761
+ except (OSError, RuntimeError) as exc:
762
+ raise _visible_app_error(
763
+ "PZ_VISIBLE_APP_BINDING_INVALID",
764
+ f"managed binding {binding_key!r} has a broken or looping target.",
765
+ owner=binding_key,
766
+ ) from exc
767
+ if actual_root != expected_root:
768
+ raise _visible_app_error(
769
+ "PZ_VISIBLE_APP_BINDING_INVALID",
770
+ f"managed binding {binding_key!r} does not match its manifest target.",
771
+ owner=binding_key,
772
+ )
773
+ return link, actual_root
774
+
775
+
776
+ def _managed_binding_projections(
777
+ project_dir: Path,
778
+ manifest: DevManifest,
779
+ ) -> tuple[VisibleAppProjection, ...]:
780
+ projections: list[VisibleAppProjection] = []
781
+ for binding_key, binding in sorted(
782
+ manifest.bindings.items(),
783
+ key=lambda item: item[1].link.as_posix(),
784
+ ):
785
+ link, _actual_root = _resolve_managed_link_target(
786
+ project_dir,
787
+ manifest,
788
+ binding_key=binding_key,
789
+ binding=binding,
790
+ )
791
+ if link.name != binding_key:
792
+ raise _visible_app_error(
793
+ "PZ_VISIBLE_APP_BINDING_INVALID",
794
+ f"managed binding {binding_key!r} must use the same link name.",
795
+ owner=binding_key,
796
+ )
797
+ if binding.kind == "app":
798
+ projection = _build_visible_projection(link, binding_key=binding_key)
799
+ if projection is None or projection.app_id != binding_key:
800
+ raise _visible_app_error(
801
+ "PZ_VISIBLE_APP_BINDING_INVALID",
802
+ f"managed app binding {binding_key!r} does not expose its declared leaf.",
803
+ owner=binding_key,
804
+ )
805
+ projections.append(projection)
806
+ continue
807
+ directory_projections: list[VisibleAppProjection] = []
808
+ try:
809
+ children = sorted(link.iterdir(), key=lambda item: item.name)
810
+ except OSError as exc:
811
+ raise _visible_app_error(
812
+ "PZ_VISIBLE_APP_BINDING_INVALID",
813
+ f"managed directory binding {binding_key!r} cannot be read.",
814
+ owner=binding_key,
815
+ ) from exc
816
+ for child in children:
817
+ if _is_skipped_discovery_dir(child) or child.is_symlink():
818
+ continue
819
+ if not child.is_dir():
820
+ continue
821
+ projection = _build_visible_projection(child, binding_key=binding_key)
822
+ if projection is not None:
823
+ directory_projections.append(projection)
824
+ if not directory_projections:
825
+ raise _visible_app_error(
826
+ "PZ_VISIBLE_APP_BINDING_INVALID",
827
+ f"managed directory binding {binding_key!r} exposes no formal top-level leaf.",
828
+ owner=binding_key,
829
+ )
830
+ projections.extend(directory_projections)
831
+ return tuple(projections)
832
+
833
+
834
+ def _is_safe_unmanaged_link(project_dir: Path, source_root: Path) -> bool:
835
+ try:
836
+ relative = source_root.relative_to(project_dir)
837
+ except ValueError:
838
+ return False
839
+ return not any(part.startswith(".") for part in relative.parts)
840
+
841
+
842
+ def _unmanaged_local_projections(
843
+ project_dir: Path,
844
+ managed_links: frozenset[Path],
845
+ ) -> tuple[VisibleAppProjection, ...]:
846
+ apps_root = project_dir / "apps"
847
+ if not apps_root.is_dir():
848
+ return ()
849
+ projections: list[VisibleAppProjection] = []
850
+ try:
851
+ entries = sorted(apps_root.iterdir(), key=lambda item: item.name)
852
+ except OSError:
853
+ return ()
854
+ for entry in entries:
855
+ if _is_skipped_discovery_dir(entry) or entry in managed_links:
856
+ continue
857
+ if entry.is_symlink():
858
+ try:
859
+ source_root = entry.resolve(strict=True)
860
+ except (OSError, RuntimeError):
861
+ continue
862
+ if not _is_safe_unmanaged_link(project_dir, source_root):
863
+ continue
864
+ elif entry.is_dir():
865
+ source_root = entry.resolve()
866
+ else:
867
+ continue
868
+ projection = _build_visible_projection(entry, binding_key=None)
869
+ if projection is None or projection.app_id != entry.name:
870
+ continue
871
+ if entry.is_symlink() and projection.source_root != source_root:
872
+ continue
873
+ projections.append(projection)
874
+ return tuple(projections)
875
+
876
+
877
+ def iter_visible_app_projections(
878
+ project_dir: str | Path,
879
+ ) -> tuple[VisibleAppProjection, ...]:
880
+ from pz_core.authoring.dev_manifest import load_dev_manifest
881
+
882
+ resolved_project_dir = Path(project_dir).expanduser().resolve()
883
+ manifest = load_dev_manifest(resolved_project_dir)
884
+ managed = (
885
+ ()
886
+ if manifest is None
887
+ else _managed_binding_projections(
888
+ resolved_project_dir,
889
+ manifest,
890
+ )
891
+ )
892
+ managed_links = (
893
+ frozenset()
894
+ if manifest is None
895
+ else frozenset(
896
+ resolved_project_dir / Path(*binding.link.parts)
897
+ for binding in manifest.bindings.values()
898
+ )
899
+ )
900
+ unmanaged = _unmanaged_local_projections(resolved_project_dir, managed_links)
901
+ ordered = sorted(
902
+ (*managed, *unmanaged),
903
+ key=lambda item: (item.visible_root.as_posix(), item.app_id),
904
+ )
905
+ by_id: dict[str, VisibleAppProjection] = {}
906
+ for projection in ordered:
907
+ existing = by_id.get(projection.app_id)
908
+ if existing is not None:
909
+ raise _visible_app_error(
910
+ "PZ_VISIBLE_APP_DUPLICATE",
911
+ (
912
+ f"visible app {projection.app_id!r} is exposed by both "
913
+ f"{existing.visible_root} and {projection.visible_root}."
914
+ ),
915
+ owner=projection.app_id,
916
+ )
917
+ by_id[projection.app_id] = projection
918
+ return tuple(ordered)
919
+
920
+
921
+ def _discover_project_local_app_references(
922
+ project_dir: Path,
923
+ module_path: str,
924
+ projections: tuple[VisibleAppProjection, ...],
925
+ ) -> tuple[DiscoveredAppReference, ...]:
926
+ if module_path == "apps":
927
+ return tuple(
928
+ DiscoveredAppReference(
929
+ module_path=item.module_path,
930
+ import_root=item.import_root,
931
+ )
932
+ for item in projections
933
+ )
934
+ return tuple(
935
+ DiscoveredAppReference(
936
+ module_path=item.module_path,
937
+ import_root=item.import_root,
938
+ )
939
+ for item in projections
940
+ if module_path in {item.app_id, item.module_path}
941
+ )
942
+
943
+
944
+ def _has_project_local_app_candidate(
945
+ project_dir: Path,
946
+ module_path: str,
947
+ projections: tuple[VisibleAppProjection, ...],
948
+ ) -> bool:
949
+ if module_path == "apps":
950
+ return (project_dir / "apps").is_dir()
951
+ return any(module_path in {item.app_id, item.module_path} for item in projections)
952
+
953
+
954
+ def _discover_leaf_apps_from_import_root(
955
+ module_path: str,
956
+ package_root: Path,
957
+ *,
958
+ import_root: Path | None = None,
959
+ ) -> tuple[DiscoveredAppReference, ...]:
960
+ if _has_standard_apps_py(package_root / "apps.py"):
961
+ return (
962
+ DiscoveredAppReference(module_path=module_path, import_root=import_root),
963
+ )
964
+
965
+ discovered: list[DiscoveredAppReference] = []
966
+ for child in sorted(package_root.iterdir(), key=lambda item: item.name):
967
+ if not child.is_dir():
968
+ continue
969
+ if child.name.startswith(".") or child.name in _SKIP_DISCOVERY_PARTS:
970
+ continue
971
+ if not (child / "__init__.py").is_file():
972
+ continue
973
+ child_module_path = f"{module_path}.{child.name}"
974
+ if _has_standard_apps_py(child / "apps.py"):
975
+ discovered.append(
976
+ DiscoveredAppReference(
977
+ module_path=child_module_path, import_root=import_root
978
+ )
979
+ )
980
+ continue
981
+ discovered.extend(
982
+ _discover_leaf_apps_from_import_root(
983
+ child_module_path, child, import_root=import_root
984
+ )
985
+ )
986
+ return tuple(discovered)
987
+
988
+
989
+ def _derive_import_root(package_root: Path, module_path: str) -> Path | None:
990
+ parts = [
991
+ segment for segment in str(module_path or "").strip().split(".") if segment
992
+ ]
993
+ if not parts:
994
+ return None
995
+ try:
996
+ return package_root.parents[len(parts) - 1].resolve()
997
+ except IndexError:
998
+ return None
999
+
1000
+
1001
+ def _is_physical_project_source_path(project_dir: Path, package_root: Path) -> bool:
1002
+ try:
1003
+ relative = package_root.resolve().relative_to(project_dir.resolve())
1004
+ except (OSError, ValueError):
1005
+ return False
1006
+ return ".sources" in relative.parts
1007
+
1008
+
1009
+ def discover_declared_app_references(
1010
+ declared_apps: Iterable[str],
1011
+ *,
1012
+ project_dir: str | Path | None = None,
1013
+ ) -> tuple[DiscoveredAppReference, ...]:
1014
+ resolved_project_dir = _read_project_dir(project_dir)
1015
+ visible_projections = (
1016
+ ()
1017
+ if resolved_project_dir is None
1018
+ else iter_visible_app_projections(resolved_project_dir)
1019
+ )
1020
+ discovered: list[DiscoveredAppReference] = []
1021
+ seen_paths: set[str] = set()
1022
+
1023
+ for raw_item in declared_apps:
1024
+ normalized = _validate_declared_app_token(
1025
+ str(raw_item or "").strip(),
1026
+ action="discover_declared_app_references",
1027
+ )
1028
+ if not normalized:
1029
+ continue
1030
+
1031
+ refs: tuple[DiscoveredAppReference, ...] = ()
1032
+ project_local_candidate_exists = False
1033
+ if resolved_project_dir is not None:
1034
+ project_local_candidate_exists = _has_project_local_app_candidate(
1035
+ resolved_project_dir,
1036
+ normalized,
1037
+ visible_projections,
1038
+ )
1039
+ refs = _discover_project_local_app_references(
1040
+ resolved_project_dir,
1041
+ normalized,
1042
+ visible_projections,
1043
+ )
1044
+ if not refs and not project_local_candidate_exists:
1045
+ package_root = resolve_package_root(normalized)
1046
+ if (
1047
+ package_root is not None
1048
+ and resolved_project_dir is not None
1049
+ and _is_physical_project_source_path(
1050
+ resolved_project_dir,
1051
+ package_root,
1052
+ )
1053
+ ):
1054
+ project_local_candidate_exists = True
1055
+ elif package_root is not None:
1056
+ refs = _discover_leaf_apps_from_import_root(
1057
+ normalized,
1058
+ package_root,
1059
+ import_root=_derive_import_root(package_root, normalized),
1060
+ )
1061
+
1062
+ if not refs and not project_local_candidate_exists:
1063
+ refs = (DiscoveredAppReference(module_path=normalized),)
1064
+
1065
+ for ref in refs:
1066
+ if ref.module_path in seen_paths:
1067
+ continue
1068
+ seen_paths.add(ref.module_path)
1069
+ discovered.append(ref)
1070
+
1071
+ return tuple(discovered)
1072
+
1073
+
1074
+ def resolve_import_roots_for_app_references(
1075
+ references: Iterable[DiscoveredAppReference],
1076
+ ) -> tuple[Path, ...]:
1077
+ seen_roots: set[Path] = set()
1078
+ import_roots: list[Path] = []
1079
+ for ref in references:
1080
+ if ref.import_root is None:
1081
+ continue
1082
+ _append_import_root(
1083
+ ref.import_root, import_roots=import_roots, seen_roots=seen_roots
1084
+ )
1085
+ for dependency_root in _resolve_local_path_dependency_import_roots(
1086
+ ref.import_root
1087
+ ):
1088
+ _append_import_root(
1089
+ dependency_root, import_roots=import_roots, seen_roots=seen_roots
1090
+ )
1091
+ return tuple(import_roots)
1092
+
1093
+
1094
+ def resolve_declared_app_import_roots(
1095
+ declared_apps: Iterable[str],
1096
+ *,
1097
+ project_dir: str | Path | None = None,
1098
+ ) -> tuple[Path, ...]:
1099
+ return resolve_import_roots_for_app_references(
1100
+ discover_declared_app_references(declared_apps, project_dir=project_dir)
1101
+ )
1102
+
1103
+
1104
+ def _append_import_root(
1105
+ root: Path, *, import_roots: list[Path], seen_roots: set[Path]
1106
+ ) -> None:
1107
+ resolved = root.resolve()
1108
+ if resolved in seen_roots:
1109
+ return
1110
+ seen_roots.add(resolved)
1111
+ import_roots.append(resolved)
1112
+
1113
+
1114
+ def _resolve_local_path_dependency_import_roots(import_root: Path) -> tuple[Path, ...]:
1115
+ roots: list[Path] = []
1116
+ seen_roots: set[Path] = {import_root.resolve()}
1117
+
1118
+ def visit(package_root: Path) -> None:
1119
+ source_paths = _local_uv_source_paths(package_root)
1120
+ if not source_paths:
1121
+ return
1122
+ for dependency_name in _declared_dependency_names(package_root):
1123
+ dependency_root = source_paths.get(dependency_name)
1124
+ if dependency_root is None:
1125
+ continue
1126
+ resolved = dependency_root.resolve()
1127
+ if resolved in seen_roots:
1128
+ continue
1129
+ seen_roots.add(resolved)
1130
+ roots.append(resolved)
1131
+ visit(resolved)
1132
+
1133
+ visit(import_root.resolve())
1134
+ return tuple(roots)
1135
+
1136
+
1137
+ def _declared_dependency_names(package_root: Path) -> tuple[str, ...]:
1138
+ payload = _read_pyproject_payload(package_root / "pyproject.toml")
1139
+ project = payload.get("project")
1140
+ if not isinstance(project, dict):
1141
+ return ()
1142
+ dependencies = project.get("dependencies")
1143
+ if not isinstance(dependencies, list):
1144
+ return ()
1145
+ names: list[str] = []
1146
+ for raw_dependency in dependencies:
1147
+ if not isinstance(raw_dependency, str):
1148
+ continue
1149
+ name = _dependency_distribution_name(raw_dependency)
1150
+ if name and name not in names:
1151
+ names.append(name)
1152
+ return tuple(names)
1153
+
1154
+
1155
+ def _dependency_distribution_name(value: str) -> str:
1156
+ match = _DEPENDENCY_NAME_RE.match(value)
1157
+ if match is None:
1158
+ return ""
1159
+ return _normalize_distribution_name(match.group(1))
1160
+
1161
+
1162
+ def _local_uv_source_paths(package_root: Path) -> dict[str, Path]:
1163
+ mapped: dict[str, Path] = {}
1164
+ for candidate in (package_root, *package_root.parents):
1165
+ pyproject_path = candidate / "pyproject.toml"
1166
+ payload = _read_pyproject_payload(pyproject_path)
1167
+ if not payload:
1168
+ continue
1169
+ tool = payload.get("tool")
1170
+ if not isinstance(tool, dict):
1171
+ continue
1172
+ uv = tool.get("uv")
1173
+ if not isinstance(uv, dict):
1174
+ continue
1175
+ sources = uv.get("sources")
1176
+ if not isinstance(sources, dict):
1177
+ continue
1178
+ for raw_name, raw_meta in sources.items():
1179
+ name = _normalize_distribution_name(str(raw_name))
1180
+ if not name or name in mapped:
1181
+ continue
1182
+ raw_path = ""
1183
+ if isinstance(raw_meta, dict):
1184
+ raw_path = str(raw_meta.get("path") or "").strip()
1185
+ elif isinstance(raw_meta, str):
1186
+ raw_path = raw_meta.strip()
1187
+ if not raw_path:
1188
+ continue
1189
+ source_path = Path(raw_path).expanduser()
1190
+ if not source_path.is_absolute():
1191
+ source_path = pyproject_path.parent / source_path
1192
+ if source_path.is_dir():
1193
+ mapped[name] = source_path.resolve()
1194
+ return mapped
1195
+
1196
+
1197
+ def _read_pyproject_payload(path: Path) -> dict[str, Any]:
1198
+ if not path.is_file():
1199
+ return {}
1200
+ try:
1201
+ payload = tomllib.loads(path.read_text(encoding="utf-8"))
1202
+ except Exception:
1203
+ return {}
1204
+ return payload if isinstance(payload, dict) else {}
1205
+
1206
+
1207
+ def discover_leaf_app_packages(
1208
+ module_path: str,
1209
+ *,
1210
+ project_dir: str | Path | None = None,
1211
+ ) -> tuple[str, ...]:
1212
+ normalized = _validate_declared_app_token(
1213
+ str(module_path or "").strip(),
1214
+ action="discover_leaf_app_packages",
1215
+ )
1216
+ if not normalized:
1217
+ return ()
1218
+ return tuple(
1219
+ ref.module_path
1220
+ for ref in discover_declared_app_references(
1221
+ (normalized,), project_dir=project_dir
1222
+ )
1223
+ )
1224
+
1225
+
1226
+ __all__ = [
1227
+ "DiscoveredAppReference",
1228
+ "StaticAppDeclaration",
1229
+ "VisibleAppProjection",
1230
+ "discover_declared_app_references",
1231
+ "discover_leaf_app_packages",
1232
+ "import_app_entrypoint_module_from_decl",
1233
+ "import_app_settings_module",
1234
+ "iter_visible_app_projections",
1235
+ "read_static_app_declaration",
1236
+ "resolve_declared_app_import_roots",
1237
+ "resolve_declared_app_id",
1238
+ "resolve_import_roots_for_app_references",
1239
+ "resolve_package_root",
1240
+ ]