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,194 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import sys
5
+ from concurrent.futures import ThreadPoolExecutor
6
+ from threading import BoundedSemaphore
7
+ from typing import Callable
8
+
9
+ try:
10
+ import requests
11
+ except Exception: # pragma: no cover
12
+ requests = None # type: ignore
13
+
14
+ from ._bot_settings import resolve_bot_message_char_limit, resolve_bot_pending_limit, resolve_bot_worker_count
15
+ from .bot_message import BotMessage, MessageLevel, MessageType, TelegramMessageFormatter
16
+ from .delivery_audit import record_bot_delivery_drop, record_bot_delivery_failure, record_bot_delivery_success
17
+
18
+
19
+ class TelegramBotHandler(logging.Handler):
20
+ _executor: ThreadPoolExecutor | None = None
21
+ _instances: dict[str, "TelegramBotHandler"] = {}
22
+ _pending_limit: int | None = None
23
+ _pending_semaphore: BoundedSemaphore | None = None
24
+
25
+ @classmethod
26
+ def get_instance(cls, bot_token: str | None = None, chat_id: str | None = None, handler_name: str = "default"):
27
+ if handler_name not in cls._instances and bot_token and chat_id:
28
+ cls._instances[handler_name] = cls(bot_token, chat_id, handler_name)
29
+ return cls._instances.get(handler_name)
30
+
31
+ def __init__(self, bot_token: str, chat_id: str, handler_name: str = "default") -> None:
32
+ super().__init__()
33
+ self.bot_token = bot_token
34
+ self.chat_id = chat_id
35
+ self.handler_name = handler_name
36
+ self.base_url = f"https://api.telegram.org/bot{bot_token}"
37
+
38
+ @classmethod
39
+ def reset(cls) -> None:
40
+ cls._instances.clear()
41
+ cls._pending_limit = None
42
+ cls._pending_semaphore = None
43
+ executor = cls._executor
44
+ cls._executor = None
45
+ if executor is not None:
46
+ try:
47
+ executor.shutdown(wait=False, cancel_futures=True)
48
+ except TypeError:
49
+ executor.shutdown(wait=False)
50
+
51
+ @classmethod
52
+ def _get_executor(cls) -> ThreadPoolExecutor:
53
+ executor = cls._executor
54
+ if executor is None:
55
+ executor = ThreadPoolExecutor(
56
+ max_workers=resolve_bot_worker_count("telegram"),
57
+ thread_name_prefix="foundation_telegram_bot",
58
+ )
59
+ cls._executor = executor
60
+ return executor
61
+
62
+ @classmethod
63
+ def _get_pending_semaphore(cls) -> BoundedSemaphore:
64
+ limit = resolve_bot_pending_limit("telegram")
65
+ semaphore = cls._pending_semaphore
66
+ if semaphore is None or cls._pending_limit != limit:
67
+ semaphore = BoundedSemaphore(limit)
68
+ cls._pending_semaphore = semaphore
69
+ cls._pending_limit = limit
70
+ return semaphore
71
+
72
+ def _submit(self, operation: str, fn: Callable[..., None], *args: object) -> None:
73
+ semaphore = self._get_pending_semaphore()
74
+ if not semaphore.acquire(blocking=False):
75
+ record_bot_delivery_drop(
76
+ channel="telegram",
77
+ handler_name=self.handler_name,
78
+ operation=operation,
79
+ reason="backpressure",
80
+ )
81
+ return
82
+ try:
83
+ future = self._get_executor().submit(fn, *args)
84
+ except Exception:
85
+ try:
86
+ semaphore.release()
87
+ except ValueError:
88
+ pass
89
+ raise
90
+ if future is None:
91
+ try:
92
+ semaphore.release()
93
+ except ValueError:
94
+ pass
95
+ return
96
+
97
+ def _release(_future) -> None: # noqa: ANN001
98
+ try:
99
+ semaphore.release()
100
+ except ValueError:
101
+ pass
102
+
103
+ future.add_done_callback(_release)
104
+
105
+ def emit(self, record: logging.LogRecord) -> None:
106
+ requested_bot_type = getattr(record, "bot_type", None)
107
+ if requested_bot_type not in (None, "both", "telegram"):
108
+ return
109
+ self.send_bot_message(
110
+ BotMessage(
111
+ content=self.format(record),
112
+ level=_message_level_from_record(record),
113
+ title=None,
114
+ )
115
+ )
116
+
117
+ def send_bot_message(self, message: BotMessage) -> None:
118
+ if not self.bot_token or not self.chat_id or requests is None:
119
+ return
120
+ if message.message_type == MessageType.IMAGE and message.image_url:
121
+ governed = TelegramMessageFormatter.prepare_message_for_channel(
122
+ message,
123
+ max_chars=resolve_bot_message_char_limit("telegram", kind="caption"),
124
+ )
125
+ payload = {
126
+ "chat_id": self.chat_id,
127
+ "photo": governed.image_url,
128
+ "caption": TelegramMessageFormatter.format_message(governed, None),
129
+ "parse_mode": "HTML",
130
+ }
131
+ self._submit("sendPhoto", self._post_photo, payload)
132
+ return
133
+ governed = TelegramMessageFormatter.prepare_message_for_channel(
134
+ message,
135
+ max_chars=resolve_bot_message_char_limit("telegram", kind="text"),
136
+ )
137
+ payload = {
138
+ "chat_id": self.chat_id,
139
+ "text": TelegramMessageFormatter.format_message(governed, None),
140
+ "parse_mode": "HTML",
141
+ "disable_web_page_preview": True,
142
+ }
143
+ self._submit("sendMessage", self._post_text, payload)
144
+
145
+ def _post_text(self, payload: dict[str, object]) -> None:
146
+ try:
147
+ response = requests.post(f"{self.base_url}/sendMessage", json=payload, timeout=10)
148
+ response.raise_for_status()
149
+ record_bot_delivery_success(
150
+ channel="telegram",
151
+ handler_name=self.handler_name,
152
+ operation="sendMessage",
153
+ )
154
+ except Exception as exc: # pragma: no cover
155
+ record_bot_delivery_failure(
156
+ channel="telegram",
157
+ handler_name=self.handler_name,
158
+ operation="sendMessage",
159
+ exc=exc,
160
+ )
161
+ print(f"Error sending Telegram message ({self.handler_name}): {exc}", file=sys.stderr)
162
+
163
+ def _post_photo(self, payload: dict[str, object]) -> None:
164
+ try:
165
+ response = requests.post(f"{self.base_url}/sendPhoto", json=payload, timeout=10)
166
+ response.raise_for_status()
167
+ record_bot_delivery_success(
168
+ channel="telegram",
169
+ handler_name=self.handler_name,
170
+ operation="sendPhoto",
171
+ )
172
+ except Exception as exc: # pragma: no cover
173
+ record_bot_delivery_failure(
174
+ channel="telegram",
175
+ handler_name=self.handler_name,
176
+ operation="sendPhoto",
177
+ exc=exc,
178
+ )
179
+ print(f"Error sending Telegram photo ({self.handler_name}): {exc}", file=sys.stderr)
180
+
181
+
182
+ def _message_level_from_record(record: logging.LogRecord) -> MessageLevel:
183
+ if record.levelname == "CUSTOM_EVENT":
184
+ return MessageLevel.CUSTOM_EVENT
185
+ if record.levelname == "ERROR":
186
+ return MessageLevel.ERROR
187
+ if record.levelname == "WARNING":
188
+ return MessageLevel.WARNING
189
+ if record.levelname == "DEBUG":
190
+ return MessageLevel.DEBUG
191
+ return MessageLevel.INFO
192
+
193
+
194
+ __all__ = ["TelegramBotHandler"]
@@ -0,0 +1,365 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterable, Mapping
4
+ from dataclasses import dataclass, field
5
+ from threading import RLock
6
+ from typing import Any, Literal
7
+
8
+
9
+ PrometheusMetricType = Literal["counter", "gauge", "histogram", "summary", "untyped"]
10
+ PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"
11
+
12
+
13
+ def _label_key(labels: dict[str, Any]) -> tuple[tuple[str, str], ...]:
14
+ return tuple(sorted((str(key), str(value)) for key, value in labels.items()))
15
+
16
+
17
+ @dataclass(slots=True)
18
+ class _CounterMetric:
19
+ name: str
20
+ samples: dict[tuple[tuple[str, str], ...], float] = field(default_factory=dict)
21
+ _lock: RLock = field(default_factory=RLock, repr=False)
22
+
23
+ def labels(self, **labels: Any):
24
+ return _BoundCounter(self, _label_key(labels))
25
+
26
+ def inc(self, amount: float = 1.0) -> None:
27
+ with self._lock:
28
+ self.samples[()] = float(self.samples.get((), 0.0) + amount)
29
+
30
+
31
+ @dataclass(slots=True)
32
+ class _BoundCounter:
33
+ metric: _CounterMetric
34
+ labels_key: tuple[tuple[str, str], ...]
35
+
36
+ def inc(self, amount: float = 1.0) -> None:
37
+ with self.metric._lock:
38
+ self.metric.samples[self.labels_key] = float(self.metric.samples.get(self.labels_key, 0.0) + amount)
39
+
40
+
41
+ @dataclass(slots=True)
42
+ class _GaugeMetric:
43
+ name: str
44
+ samples: dict[tuple[tuple[str, str], ...], float] = field(default_factory=dict)
45
+ _lock: RLock = field(default_factory=RLock, repr=False)
46
+
47
+ def labels(self, **labels: Any):
48
+ return _BoundGauge(self, _label_key(labels))
49
+
50
+ def set(self, value: float) -> None:
51
+ with self._lock:
52
+ self.samples[()] = float(value)
53
+
54
+
55
+ @dataclass(slots=True)
56
+ class _BoundGauge:
57
+ metric: _GaugeMetric
58
+ labels_key: tuple[tuple[str, str], ...]
59
+
60
+ def set(self, value: float) -> None:
61
+ with self.metric._lock:
62
+ self.metric.samples[self.labels_key] = float(value)
63
+
64
+
65
+ @dataclass(slots=True)
66
+ class _HistogramMetric:
67
+ name: str
68
+ samples: dict[tuple[tuple[str, str], ...], list[float]] = field(default_factory=dict)
69
+ _lock: RLock = field(default_factory=RLock, repr=False)
70
+
71
+ def labels(self, **labels: Any):
72
+ return _BoundHistogram(self, _label_key(labels))
73
+
74
+ def observe(self, value: float) -> None:
75
+ with self._lock:
76
+ self.samples.setdefault((), []).append(float(value))
77
+
78
+
79
+ @dataclass(slots=True)
80
+ class _BoundHistogram:
81
+ metric: _HistogramMetric
82
+ labels_key: tuple[tuple[str, str], ...]
83
+
84
+ def observe(self, value: float) -> None:
85
+ with self.metric._lock:
86
+ self.metric.samples.setdefault(self.labels_key, []).append(float(value))
87
+
88
+
89
+ class MetricsRegistry:
90
+ def __init__(self) -> None:
91
+ self.http_requests_total = _CounterMetric("http_requests_total")
92
+ self.http_request_duration_seconds = _HistogramMetric("http_request_duration_seconds")
93
+ self.db_reconnects_total = _CounterMetric("db_reconnects_total")
94
+ self.db_retries_total = _CounterMetric("db_retries_total")
95
+ self.foundation_bot_delivery_total = _CounterMetric("foundation_bot_delivery_total")
96
+ self.foundation_storage_probes_total = _CounterMetric("foundation_storage_probes_total")
97
+ self.foundation_mail_probes_total = _CounterMetric("foundation_mail_probes_total")
98
+ self.foundation_health_checks_total = _CounterMetric("foundation_health_checks_total")
99
+ self.foundation_runtime_ready = _GaugeMetric("foundation_runtime_ready")
100
+
101
+ @property
102
+ def enabled(self) -> bool:
103
+ return True
104
+
105
+ def snapshot(self) -> dict[str, Any]:
106
+ return {
107
+ "http_requests_total": dict(self.http_requests_total.samples),
108
+ "http_request_duration_seconds": {key: list(values) for key, values in self.http_request_duration_seconds.samples.items()},
109
+ "db_reconnects_total": dict(self.db_reconnects_total.samples),
110
+ "db_retries_total": dict(self.db_retries_total.samples),
111
+ "foundation_bot_delivery_total": dict(self.foundation_bot_delivery_total.samples),
112
+ "foundation_storage_probes_total": dict(self.foundation_storage_probes_total.samples),
113
+ "foundation_mail_probes_total": dict(self.foundation_mail_probes_total.samples),
114
+ "foundation_health_checks_total": dict(self.foundation_health_checks_total.samples),
115
+ "foundation_runtime_ready": dict(self.foundation_runtime_ready.samples),
116
+ }
117
+
118
+
119
+ @dataclass(frozen=True, slots=True)
120
+ class PrometheusSample:
121
+ value: float
122
+ labels: Mapping[str, str] = field(default_factory=dict)
123
+ suffix: str = ""
124
+
125
+
126
+ @dataclass(frozen=True, slots=True)
127
+ class PrometheusMetricFamily:
128
+ name: str
129
+ metric_type: PrometheusMetricType
130
+ samples: tuple[PrometheusSample, ...]
131
+ help: str = ""
132
+
133
+
134
+ PrometheusCollector = Callable[[], Iterable[PrometheusMetricFamily]]
135
+
136
+
137
+ _prometheus_collectors: dict[str, PrometheusCollector] = {}
138
+ _prometheus_collector_errors: dict[str, float] = {}
139
+ _prometheus_collectors_lock = RLock()
140
+
141
+
142
+ def register_prometheus_collector(name: str, collector: PrometheusCollector) -> None:
143
+ normalized = _normalize_collector_name(name)
144
+ if not callable(collector):
145
+ raise TypeError("prometheus collector must be callable")
146
+ with _prometheus_collectors_lock:
147
+ _prometheus_collectors[normalized] = collector
148
+
149
+
150
+ def unregister_prometheus_collector(name: str) -> None:
151
+ normalized = _normalize_collector_name(name)
152
+ with _prometheus_collectors_lock:
153
+ _prometheus_collectors.pop(normalized, None)
154
+ _prometheus_collector_errors.pop(normalized, None)
155
+
156
+
157
+ def render_prometheus_text(*, namespace: str = "pz") -> str:
158
+ families = [
159
+ *_registry_prometheus_families(get_metrics_registry()),
160
+ *_collector_prometheus_families(),
161
+ ]
162
+ rendered = _render_prometheus_families(families, namespace=namespace)
163
+ return rendered if rendered.endswith("\n") else f"{rendered}\n"
164
+
165
+
166
+ def _normalize_collector_name(name: str) -> str:
167
+ normalized = str(name or "").strip()
168
+ if not normalized:
169
+ raise ValueError("prometheus collector name is required")
170
+ return normalized
171
+
172
+
173
+ def _registry_prometheus_families(registry: MetricsRegistry) -> tuple[PrometheusMetricFamily, ...]:
174
+ families: list[PrometheusMetricFamily] = []
175
+ for value in vars(registry).values():
176
+ if isinstance(value, _CounterMetric):
177
+ families.append(
178
+ PrometheusMetricFamily(
179
+ name=value.name,
180
+ metric_type="counter",
181
+ help=f"{value.name} counter",
182
+ samples=tuple(
183
+ PrometheusSample(value=sample_value, labels=dict(label_key))
184
+ for label_key, sample_value in value.samples.items()
185
+ ),
186
+ )
187
+ )
188
+ elif isinstance(value, _GaugeMetric):
189
+ families.append(
190
+ PrometheusMetricFamily(
191
+ name=value.name,
192
+ metric_type="gauge",
193
+ help=f"{value.name} gauge",
194
+ samples=tuple(
195
+ PrometheusSample(value=sample_value, labels=dict(label_key))
196
+ for label_key, sample_value in value.samples.items()
197
+ ),
198
+ )
199
+ )
200
+ elif isinstance(value, _HistogramMetric):
201
+ samples: list[PrometheusSample] = []
202
+ for label_key, observed_values in value.samples.items():
203
+ labels = dict(label_key)
204
+ samples.append(PrometheusSample(value=float(len(observed_values)), labels=labels, suffix="_count"))
205
+ samples.append(PrometheusSample(value=float(sum(observed_values)), labels=labels, suffix="_sum"))
206
+ families.append(
207
+ PrometheusMetricFamily(
208
+ name=value.name,
209
+ metric_type="summary",
210
+ help=f"{value.name} summary",
211
+ samples=tuple(samples),
212
+ )
213
+ )
214
+ return tuple(families)
215
+
216
+
217
+ def _collector_prometheus_families() -> tuple[PrometheusMetricFamily, ...]:
218
+ with _prometheus_collectors_lock:
219
+ collectors = tuple(_prometheus_collectors.items())
220
+ families: list[PrometheusMetricFamily] = []
221
+ for collector_name, collector in collectors:
222
+ try:
223
+ families.extend(tuple(collector()))
224
+ except Exception:
225
+ with _prometheus_collectors_lock:
226
+ _prometheus_collector_errors[collector_name] = (
227
+ _prometheus_collector_errors.get(collector_name, 0.0) + 1.0
228
+ )
229
+ error_count = _prometheus_collector_errors[collector_name]
230
+ families.append(
231
+ PrometheusMetricFamily(
232
+ name="metrics_collector_errors_total",
233
+ metric_type="counter",
234
+ help="Metrics collector failures during scrape.",
235
+ samples=(PrometheusSample(value=error_count, labels={"collector": collector_name}),),
236
+ )
237
+ )
238
+ return tuple(families)
239
+
240
+
241
+ def _render_prometheus_families(
242
+ families: Iterable[PrometheusMetricFamily],
243
+ *,
244
+ namespace: str,
245
+ ) -> str:
246
+ normalized_namespace = _normalize_metric_part(namespace)
247
+ lines: list[str] = []
248
+ emitted_meta: set[str] = set()
249
+ for family in sorted(tuple(families), key=lambda item: item.name):
250
+ if not family.samples:
251
+ continue
252
+ metric_name = _namespaced_metric_name(family.name, normalized_namespace)
253
+ if metric_name not in emitted_meta:
254
+ lines.append(f"# HELP {metric_name} {_escape_help(family.help or family.name)}")
255
+ lines.append(f"# TYPE {metric_name} {family.metric_type}")
256
+ emitted_meta.add(metric_name)
257
+ for sample in family.samples:
258
+ lines.append(
259
+ _render_sample(
260
+ f"{metric_name}{sample.suffix}",
261
+ value=sample.value,
262
+ labels=sample.labels,
263
+ )
264
+ )
265
+ return "\n".join(lines)
266
+
267
+
268
+ def _render_sample(name: str, *, value: float, labels: Mapping[str, str]) -> str:
269
+ if labels:
270
+ label_text = ",".join(
271
+ f'{_normalize_metric_part(key)}="{_escape_label_value(value)}"'
272
+ for key, value in sorted(labels.items())
273
+ )
274
+ return f"{name}{{{label_text}}} {_format_prometheus_float(float(value))}"
275
+ return f"{name} {_format_prometheus_float(float(value))}"
276
+
277
+
278
+ def _namespaced_metric_name(name: str, namespace: str) -> str:
279
+ normalized = _normalize_metric_part(name)
280
+ if not namespace or normalized.startswith(f"{namespace}_"):
281
+ return normalized
282
+ return f"{namespace}_{normalized}"
283
+
284
+
285
+ def _normalize_metric_part(value: object) -> str:
286
+ text = str(value or "").strip()
287
+ chars = [ch if ch.isalnum() or ch == "_" else "_" for ch in text]
288
+ normalized = "".join(chars).strip("_")
289
+ if not normalized:
290
+ return "metric"
291
+ if normalized[0].isdigit():
292
+ normalized = f"_{normalized}"
293
+ return normalized
294
+
295
+
296
+ def _escape_help(value: object) -> str:
297
+ return str(value or "").replace("\\", "\\\\").replace("\n", "\\n")
298
+
299
+
300
+ def _escape_label_value(value: object) -> str:
301
+ return (
302
+ str(value or "")
303
+ .replace("\\", "\\\\")
304
+ .replace("\n", "\\n")
305
+ .replace('"', '\\"')
306
+ )
307
+
308
+
309
+ def _format_prometheus_float(value: float) -> str:
310
+ if value == float("inf"):
311
+ return "+Inf"
312
+ if value == float("-inf"):
313
+ return "-Inf"
314
+ if value != value:
315
+ return "NaN"
316
+ return format(value, ".15g")
317
+
318
+
319
+ @dataclass(slots=True)
320
+ class _LazyAccessor:
321
+ factory: Callable[[], MetricsRegistry]
322
+ _instance: MetricsRegistry | None = None
323
+
324
+ def _get_instance(self) -> MetricsRegistry:
325
+ if self._instance is None:
326
+ self._instance = self.factory()
327
+ return self._instance
328
+
329
+ def __getattr__(self, name: str) -> Any:
330
+ return getattr(self._get_instance(), name)
331
+
332
+
333
+ _metrics_registry_instance: MetricsRegistry | None = None
334
+
335
+
336
+ def get_metrics_registry() -> MetricsRegistry:
337
+ global _metrics_registry_instance
338
+ if _metrics_registry_instance is None:
339
+ _metrics_registry_instance = MetricsRegistry()
340
+ return _metrics_registry_instance
341
+
342
+
343
+ def reset_metrics_registry() -> None:
344
+ global _metrics_registry_instance
345
+ _metrics_registry_instance = None
346
+ metrics._instance = None
347
+
348
+
349
+ metrics = _LazyAccessor(get_metrics_registry)
350
+
351
+
352
+ __all__ = [
353
+ "MetricsRegistry",
354
+ "PROMETHEUS_CONTENT_TYPE",
355
+ "PrometheusCollector",
356
+ "PrometheusMetricFamily",
357
+ "PrometheusMetricType",
358
+ "PrometheusSample",
359
+ "get_metrics_registry",
360
+ "metrics",
361
+ "register_prometheus_collector",
362
+ "render_prometheus_text",
363
+ "reset_metrics_registry",
364
+ "unregister_prometheus_collector",
365
+ ]
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import pz_settings_center.public as settings_center
7
+ from .logger.core import configure_logging
8
+ from .metrics import get_metrics_registry
9
+
10
+
11
+ def _resolved_settings(*, project_dir: str | os.PathLike[str] | None = None):
12
+ if project_dir is None:
13
+ return settings_center.settings
14
+ return settings_center.resolve_settings(project_dir=str(Path(project_dir).resolve()))
15
+
16
+
17
+ def _project_settings(*, project_dir: str | os.PathLike[str] | None = None):
18
+ try:
19
+ return _resolved_settings(project_dir=project_dir).project
20
+ except Exception:
21
+ return None
22
+
23
+
24
+ def _foundation_settings_section(
25
+ name: str,
26
+ *,
27
+ project_dir: str | os.PathLike[str] | None = None,
28
+ ):
29
+ try:
30
+ return getattr(getattr(_resolved_settings(project_dir=project_dir), "pz_foundation"), str(name or "").strip())
31
+ except Exception:
32
+ return {}
33
+
34
+
35
+ def probe_observability(
36
+ *,
37
+ project_dir: str | os.PathLike[str] | None = None,
38
+ ) -> dict[str, Any]:
39
+ try:
40
+ project = _project_settings(project_dir=project_dir)
41
+ observability = _foundation_settings_section("observability", project_dir=project_dir)
42
+ except Exception:
43
+ project = None
44
+ observability = {}
45
+ metrics_cfg = getattr(observability, "metrics", {})
46
+ project_root = Path(str(getattr(project, "project_dir", ".") or ".")).resolve()
47
+ log_dir = project_root / ".runtime" / "logs" / "foundation"
48
+ status: dict[str, Any] = {
49
+ "enabled": True,
50
+ "configured": True,
51
+ "ready": False,
52
+ "error": None,
53
+ "log_dir": str(log_dir),
54
+ "pz_env": str(settings_center.current_pz_env(project_dir=project_dir, default="develop") or "develop"),
55
+ "metrics_enabled": bool(metrics_cfg.get("enabled", False)) if isinstance(metrics_cfg, dict) else bool(getattr(metrics_cfg, "enabled", False)),
56
+ "metrics_registry_enabled": bool(get_metrics_registry().enabled),
57
+ }
58
+ try:
59
+ log_dir.mkdir(parents=True, exist_ok=True)
60
+ configure_logging(log_dir=log_dir, pz_env=status["pz_env"])
61
+ probe = log_dir / ".probe"
62
+ probe.write_text("ok", encoding="utf-8")
63
+ probe.unlink()
64
+ status["ready"] = True
65
+ except Exception as exc:
66
+ status["error"] = f"{type(exc).__name__}: {exc}"
67
+ return status
68
+ __all__ = ["probe_observability"]