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,689 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import functools
5
+ import inspect
6
+ import time
7
+ import traceback
8
+ from functools import lru_cache
9
+ from typing import Any, Awaitable, Callable, TypeVar, cast
10
+
11
+ from fastapi import HTTPException
12
+ from ..observability import get_logger
13
+
14
+ try:
15
+ from tortoise.exceptions import OperationalError
16
+ except Exception: # pragma: no cover
17
+ OperationalError = Exception # type: ignore[assignment]
18
+
19
+ from .tortoise_manager import tortoise_manager
20
+ from .lifecycle_status import connection_lifecycle_status
21
+ from .connection_probe import DBConnectionReadinessProbeResult, describe_connection_pool, probe_connection_readiness
22
+ from .production_pool_fallback import maybe_apply_production_pool_replenishment_fallback
23
+
24
+
25
+ logger = get_logger(__name__)
26
+
27
+ T = TypeVar("T")
28
+ AsyncFunc = Callable[..., Any]
29
+
30
+ try: # pragma: no cover
31
+ from asyncpg.exceptions import ConnectionDoesNotExistError as _AsyncpgConnDoesNotExistError # type: ignore
32
+ from asyncpg.exceptions import InterfaceError as _AsyncpgInterfaceError # type: ignore
33
+
34
+ _ASYNC_PG_CONN_ERRORS: tuple[type[BaseException], ...] = (
35
+ _AsyncpgConnDoesNotExistError,
36
+ _AsyncpgInterfaceError,
37
+ )
38
+ except Exception: # pragma: no cover
39
+ _ASYNC_PG_CONN_ERRORS = ()
40
+
41
+ try: # pragma: no cover
42
+ from psycopg_pool import PoolTimeout as _PsycopgPoolTimeout # type: ignore
43
+ except Exception: # pragma: no cover
44
+ _PsycopgPoolTimeout = None # type: ignore[assignment]
45
+
46
+ try: # pragma: no cover
47
+ from ..observability.metrics import metrics # type: ignore
48
+ except Exception: # pragma: no cover
49
+ class _M:
50
+ db_reconnects_total = type("N", (), {"inc": staticmethod(lambda *a, **k: None)})()
51
+ db_retries_total = type("N", (), {"inc": staticmethod(lambda *a, **k: None)})()
52
+
53
+ metrics = _M() # type: ignore
54
+
55
+
56
+ def _is_transient_db_error(exc: BaseException) -> bool:
57
+ if _is_capacity_or_query_timeout_error(exc):
58
+ return False
59
+ if _is_writer_role_changed_error(exc):
60
+ return True
61
+ try:
62
+ if _ASYNC_PG_CONN_ERRORS and isinstance(exc, _ASYNC_PG_CONN_ERRORS):
63
+ return True
64
+ except Exception:
65
+ pass
66
+ message = str(exc or "").lower()
67
+ transient_markers = (
68
+ "lost connection",
69
+ "connection reset",
70
+ "connection was closed",
71
+ "connection does not exist",
72
+ "server closed the connection",
73
+ "terminating connection",
74
+ "broken pipe",
75
+ "connection refused",
76
+ "timed out",
77
+ "timeout",
78
+ "pool is closing",
79
+ )
80
+ return any(marker in message for marker in transient_markers) or isinstance(exc, TimeoutError)
81
+
82
+
83
+ def _is_writer_role_changed_error(exc: BaseException) -> bool:
84
+ message = str(exc or "").lower()
85
+ return (
86
+ "read-only transaction" in message
87
+ or "cannot execute insert in a read-only transaction" in message
88
+ or "cannot execute update in a read-only transaction" in message
89
+ or "cannot execute delete in a read-only transaction" in message
90
+ or "default_transaction_read_only" in message
91
+ )
92
+
93
+
94
+ def _is_pool_exhausted_error(exc: BaseException) -> bool:
95
+ if _PsycopgPoolTimeout is not None and isinstance(exc, _PsycopgPoolTimeout):
96
+ return True
97
+ message = str(exc or "").lower()
98
+ return (
99
+ "couldn't get a connection" in message
100
+ or "could not get a connection" in message
101
+ or "pool timeout" in message
102
+ or "connection pool exhausted" in message
103
+ )
104
+
105
+
106
+ def _is_capacity_or_query_timeout_error(exc: BaseException) -> bool:
107
+ if _is_pool_exhausted_error(exc):
108
+ return True
109
+ message = str(exc or "").lower()
110
+ return (
111
+ "statement timeout" in message
112
+ or "canceling statement due to statement timeout" in message
113
+ or "lock timeout" in message
114
+ or "idle-in-transaction timeout" in message
115
+ or "idle in transaction session timeout" in message
116
+ )
117
+
118
+
119
+ def _describe_alias_pool(alias: str) -> dict[str, Any]:
120
+ try:
121
+ has_connection = getattr(tortoise_manager, "has_connection", None)
122
+ if callable(has_connection) and not bool(has_connection(alias)):
123
+ return {"present": False, "status": "not_configured"}
124
+ connection = tortoise_manager.get_connection(alias)
125
+ except Exception as exc: # noqa: BLE001
126
+ return {"present": False, "status": "lookup_failed", "error_type": type(exc).__name__}
127
+ return describe_connection_pool(connection)
128
+
129
+
130
+ def _describe_known_pools() -> dict[str, dict[str, Any]]:
131
+ return {
132
+ alias: status
133
+ for alias in ("default", "read_only")
134
+ if (status := _describe_alias_pool(alias)).get("present") is not False
135
+ or status.get("status") != "not_configured"
136
+ }
137
+
138
+
139
+ def _safe_error_message(exc: BaseException) -> str:
140
+ text = str(exc or "").replace("\n", " ").strip()
141
+ if len(text) > 300:
142
+ return f"{text[:300]}..."
143
+ return text
144
+
145
+
146
+ def _diagnostic_timeout_from_config(config: dict[str, Any] | None, alias: str) -> float:
147
+ credentials = _connection_credentials(config, alias)
148
+ values: list[float] = []
149
+ for key in ("timeout", "connect_timeout"):
150
+ try:
151
+ value = float(credentials.get(key, 0) or 0)
152
+ except Exception:
153
+ value = 0
154
+ if value > 0:
155
+ values.append(value)
156
+ if not values:
157
+ return 2.0
158
+ return max(0.2, min(2.0, *values))
159
+
160
+
161
+ def _effective_config(config: dict[str, Any] | None) -> dict[str, Any] | None:
162
+ if isinstance(config, dict):
163
+ return config
164
+ runtime_config = getattr(tortoise_manager, "_last_config", None)
165
+ return runtime_config if isinstance(runtime_config, dict) else None
166
+
167
+
168
+ def _connection_credentials(config: dict[str, Any] | None, alias: str) -> dict[str, Any]:
169
+ try:
170
+ payload = dict(((_effective_config(config) or {}).get("connections", {}) or {}).get(alias, {}) or {})
171
+ except Exception:
172
+ return {}
173
+ credentials = payload.get("credentials")
174
+ return dict(credentials) if isinstance(credentials, dict) else {}
175
+
176
+
177
+ def _scalar_float(value: Any) -> float | None:
178
+ try:
179
+ number = float(value)
180
+ except Exception:
181
+ return None
182
+ return number if number >= 0 else None
183
+
184
+
185
+ def _pool_config_snapshot(config: dict[str, Any] | None, alias: str) -> dict[str, Any]:
186
+ credentials = _connection_credentials(config, alias)
187
+ payload: dict[str, Any] = {}
188
+ for key in (
189
+ "minsize",
190
+ "maxsize",
191
+ "timeout",
192
+ "connect_timeout",
193
+ "max_lifetime",
194
+ "max_idle",
195
+ "reconnect_timeout",
196
+ ):
197
+ value = credentials.get(key)
198
+ if isinstance(value, (bool, int, float, str)) and value not in ("", None):
199
+ payload[key] = value
200
+ return payload
201
+
202
+
203
+ def _numeric_pool_value(pool_status: dict[str, Any], *keys: str) -> float | None:
204
+ stats = pool_status.get("stats")
205
+ for key in keys:
206
+ value = pool_status.get(key)
207
+ if value is None and isinstance(stats, dict):
208
+ value = stats.get(key)
209
+ number = _scalar_float(value)
210
+ if number is not None:
211
+ return number
212
+ return None
213
+
214
+
215
+ def _diagnose_capacity_failure(
216
+ *,
217
+ config: dict[str, Any] | None,
218
+ alias: str,
219
+ error: BaseException,
220
+ pool_status: dict[str, Any],
221
+ independent_probe: dict[str, Any],
222
+ manager_uptime_seconds: float,
223
+ ) -> dict[str, Any]:
224
+ pool_config = _pool_config_snapshot(config, alias)
225
+ category = "query_timeout_or_lock_wait"
226
+ if _is_pool_exhausted_error(error):
227
+ if independent_probe.get("attempted") and independent_probe.get("reachable") is False:
228
+ category = "database_unreachable_or_auth"
229
+ else:
230
+ available = _numeric_pool_value(pool_status, "available", "pool_available")
231
+ waiting = _numeric_pool_value(pool_status, "waiting", "requests_waiting")
232
+ connections = _numeric_pool_value(pool_status, "connections", "pool_size")
233
+ max_size = _numeric_pool_value(pool_status, "max_size") or _scalar_float(pool_config.get("maxsize"))
234
+ growing = bool(pool_status.get("growing"))
235
+ if connections is not None and max_size is not None and connections < max_size and (growing or available == 0):
236
+ category = "pool_replenishment_stalled"
237
+ elif available == 0 and waiting and waiting > 0:
238
+ category = "application_pool_depleted"
239
+ else:
240
+ category = "application_pool_capacity"
241
+
242
+ max_lifetime = _scalar_float(pool_config.get("max_lifetime"))
243
+ lifetime_window = False
244
+ if max_lifetime and manager_uptime_seconds >= max(0.0, max_lifetime - 120.0):
245
+ lifetime_window = True
246
+
247
+ return {
248
+ "category": category,
249
+ "alias": alias,
250
+ "database_reachable": independent_probe.get("reachable"),
251
+ "independent_probe_status": independent_probe.get("status"),
252
+ "pool_config": pool_config,
253
+ "manager_uptime_seconds": round(manager_uptime_seconds, 3),
254
+ "pool_lifetime_rotation_window": lifetime_window,
255
+ }
256
+
257
+
258
+ async def _probe_independent_database(config: dict[str, Any] | None, alias: str) -> dict[str, Any]:
259
+ config = _effective_config(config)
260
+ try:
261
+ connection_payload = dict(((config or {}).get("connections", {}) or {}).get(alias, {}) or {})
262
+ except Exception as exc: # noqa: BLE001
263
+ return {"attempted": False, "status": "config_unavailable", "error_type": type(exc).__name__}
264
+
265
+ engine = str(connection_payload.get("engine") or "").strip()
266
+ if "postgres" not in engine and "psycopg" not in engine:
267
+ return {"attempted": False, "status": "unsupported_engine", "engine": engine}
268
+
269
+ credentials = _connection_credentials(config, alias)
270
+ required = ("host", "port", "user", "database")
271
+ if any(not str(credentials.get(key, "") or "").strip() for key in required):
272
+ return {"attempted": False, "status": "incomplete_credentials"}
273
+
274
+ try:
275
+ import psycopg # type: ignore
276
+ except Exception as exc: # pragma: no cover
277
+ return {"attempted": False, "status": "psycopg_unavailable", "error_type": type(exc).__name__}
278
+
279
+ timeout = _diagnostic_timeout_from_config(config, alias)
280
+ connect_timeout = int(max(1, timeout))
281
+ conninfo_kwargs: dict[str, Any] = {
282
+ "host": credentials.get("host"),
283
+ "port": credentials.get("port"),
284
+ "user": credentials.get("user"),
285
+ "password": credentials.get("password"),
286
+ "dbname": credentials.get("database"),
287
+ }
288
+ server_options = str(credentials.get("server_options") or "").strip()
289
+ if server_options:
290
+ conninfo_kwargs["options"] = server_options
291
+ for key in ("sslmode", "sslrootcert", "sslcert", "sslkey", "sslpassword"):
292
+ value = credentials.get(key)
293
+ if value not in (None, ""):
294
+ conninfo_kwargs[key] = value
295
+
296
+ async def _run_probe() -> None:
297
+ conn = await psycopg.AsyncConnection.connect(
298
+ psycopg.conninfo.make_conninfo(**conninfo_kwargs),
299
+ autocommit=True,
300
+ connect_timeout=connect_timeout,
301
+ )
302
+ try:
303
+ await conn.execute("SELECT 1")
304
+ finally:
305
+ await conn.close()
306
+
307
+ try:
308
+ await asyncio.wait_for(_run_probe(), timeout=timeout)
309
+ except Exception as exc: # noqa: BLE001
310
+ return {
311
+ "attempted": True,
312
+ "reachable": False,
313
+ "status": "failed",
314
+ "error_type": type(exc).__name__,
315
+ "error": _safe_error_message(exc),
316
+ "timeout": timeout,
317
+ }
318
+ return {"attempted": True, "reachable": True, "status": "ok", "timeout": timeout}
319
+
320
+
321
+ class DBConnectionManager:
322
+ def __init__(self) -> None:
323
+ self._settings: Any = None
324
+ self._last_check_time = 0.0
325
+ self._check_interval = 60.0
326
+ self._connection_healthy = True
327
+ self._last_reconnect_attempt_time = 0.0
328
+ self._reconnect_cooldown = 0.1
329
+ self._last_reconnect_exception: BaseException | None = None
330
+ self._last_independent_diagnostic_time = 0.0
331
+ self._independent_diagnostic_cooldown = 60.0
332
+ self._manager_started_at = time.monotonic()
333
+ self._lock = asyncio.Lock()
334
+ self._config_builder: Callable[[], dict[str, Any]] | None = None
335
+ self._cluster_factory: Callable[[], Any] | None = None
336
+
337
+ def configure(
338
+ self,
339
+ settings: Any = None,
340
+ *,
341
+ config_builder: Callable[[], dict[str, Any]] | None = None,
342
+ cluster_factory: Callable[[], Any] | None = None,
343
+ ) -> None:
344
+ self._settings = settings
345
+ self._config_builder = config_builder
346
+ self._cluster_factory = cluster_factory
347
+
348
+ async def execute_with_retry(self, func: AsyncFunc, *args: Any, **kwargs: Any) -> Any:
349
+ max_retries = 3
350
+ retry_delay = 0.5
351
+ for attempt in range(max_retries):
352
+ try:
353
+ if self._config_builder is not None or self._cluster_factory is not None:
354
+ await tortoise_manager.ensure_initialized(
355
+ config_builder=self._config_builder,
356
+ cluster_factory=self._cluster_factory,
357
+ )
358
+ else:
359
+ await tortoise_manager.ensure_initialized()
360
+ result = await func(*args, **kwargs)
361
+ self._connection_healthy = True
362
+ self._last_reconnect_attempt_time = 0.0
363
+ self._last_reconnect_exception = None
364
+ return result
365
+ except OperationalError as exc: # type: ignore[misc]
366
+ if _is_capacity_or_query_timeout_error(exc):
367
+ logger.error("数据库容量或查询超时,不重建连接池: %s; pools=%s", exc, _describe_known_pools())
368
+ raise
369
+ if not _is_transient_db_error(exc):
370
+ logger.error("数据库操作错误: %s", exc)
371
+ raise
372
+ self._connection_healthy = False
373
+ try:
374
+ metrics.db_retries_total.inc()
375
+ except Exception:
376
+ pass
377
+ if attempt == max_retries - 1:
378
+ logger.error("数据库连接失败,已重试 %d 次: %s", max_retries, exc)
379
+ raise
380
+ logger.warning("数据库连接失败,正在重试 (%d/%d): %s", attempt + 1, max_retries, exc)
381
+ try:
382
+ await self.check_and_reconnect(force=True, rebuild_first=True)
383
+ except Exception as reconnect_exc: # noqa: BLE001
384
+ logger.warning("数据库重连尝试失败,将继续按重试策略执行: %s", reconnect_exc)
385
+ await asyncio.sleep(retry_delay * (2**attempt))
386
+ except HTTPException as exc:
387
+ status = getattr(exc, "status_code", None)
388
+ if isinstance(status, int) and 400 <= status < 500:
389
+ detail = getattr(exc, "detail", "")
390
+ try:
391
+ msg = detail if isinstance(detail, str) else str(detail)
392
+ except Exception:
393
+ msg = ""
394
+ logger.warning("HTTP %s: %s", status, msg)
395
+ else:
396
+ logger.error("执行数据库操作时发生错误: %s", traceback.format_exc())
397
+ raise
398
+ except Exception as exc: # noqa: BLE001
399
+ if _is_capacity_or_query_timeout_error(exc):
400
+ logger.error("数据库容量或查询超时,不重建连接池: %s; pools=%s", exc, _describe_known_pools())
401
+ raise
402
+ if not _is_transient_db_error(exc):
403
+ logger.error("执行数据库操作时发生错误: %s", traceback.format_exc())
404
+ raise
405
+ self._connection_healthy = False
406
+ if attempt == max_retries - 1:
407
+ logger.error("数据库连接失败,已重试 %d 次: %s", max_retries, exc)
408
+ raise
409
+ logger.warning("数据库连接失败,正在重试 (%d/%d): %s", attempt + 1, max_retries, exc)
410
+ try:
411
+ await self.check_and_reconnect(force=True, rebuild_first=True)
412
+ except Exception as reconnect_exc: # noqa: BLE001
413
+ logger.warning("数据库重连尝试失败,将继续按重试策略执行: %s", reconnect_exc)
414
+ await asyncio.sleep(retry_delay * (2**attempt))
415
+
416
+ async def retry(self, func: Callable[..., Awaitable[Any]], *args: Any, **kwargs: Any) -> Any:
417
+ return await self.execute_with_retry(func, *args, **kwargs)
418
+
419
+ async def run(self, factory: Callable[[], Awaitable[Any]]) -> Any:
420
+ async def _call():
421
+ return await factory()
422
+
423
+ return await self.execute_with_retry(_call)
424
+
425
+ def with_retry(self, func: AsyncFunc) -> AsyncFunc:
426
+ @functools.wraps(func)
427
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
428
+ if not self._connection_healthy:
429
+ await self.check_and_reconnect(force=True)
430
+ try:
431
+ return await self.execute_with_retry(func, *args, **kwargs)
432
+ except OperationalError as exc: # type: ignore[misc]
433
+ if _is_capacity_or_query_timeout_error(exc) or not _is_transient_db_error(exc):
434
+ raise
435
+ await self.check_and_reconnect(force=True, rebuild_first=True)
436
+ return await self.execute_with_retry(func, *args, **kwargs)
437
+
438
+ try:
439
+ wrapper.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
440
+ except Exception:
441
+ pass
442
+ try:
443
+ import typing as _typing
444
+
445
+ try:
446
+ resolved = _typing.get_type_hints(func)
447
+ except Exception:
448
+ resolved = getattr(func, "__annotations__", {}) or {}
449
+ wrapper.__annotations__ = resolved # type: ignore[attr-defined]
450
+ except Exception:
451
+ pass
452
+ return cast(AsyncFunc, wrapper)
453
+
454
+ async def check_and_reconnect(self, *, force: bool = False, rebuild_first: bool = False) -> None:
455
+ async with self._lock:
456
+ current_time = asyncio.get_event_loop().time()
457
+ if not force and (current_time - self._last_check_time < self._check_interval and self._connection_healthy):
458
+ return
459
+ self._last_check_time = current_time
460
+ if not rebuild_first:
461
+ try:
462
+ if self._config_builder is not None or self._cluster_factory is not None:
463
+ await tortoise_manager.ensure_initialized(
464
+ config_builder=self._config_builder,
465
+ cluster_factory=self._cluster_factory,
466
+ )
467
+ else:
468
+ await tortoise_manager.ensure_initialized()
469
+ status = connection_lifecycle_status(tortoise_manager)
470
+ if status.default_ready:
471
+ self._connection_healthy = True
472
+ self._last_reconnect_attempt_time = 0.0
473
+ self._last_reconnect_exception = None
474
+ return
475
+ self._connection_healthy = False
476
+ logger.warning("数据库连接生命周期不可用,准备执行安全重连: %s", status)
477
+ except Exception as exc: # noqa: BLE001
478
+ if not _is_transient_db_error(exc):
479
+ raise
480
+ self._connection_healthy = False
481
+ logger.warning("数据库连接检查失败,准备执行安全重连: %s", exc)
482
+ else:
483
+ self._connection_healthy = False
484
+ logger.warning("数据库连接已在业务路径失败,跳过旧连接池探活并准备安全重连")
485
+
486
+ reconnect_started_at = asyncio.get_event_loop().time()
487
+ if (
488
+ self._last_reconnect_attempt_time
489
+ and reconnect_started_at - self._last_reconnect_attempt_time < self._reconnect_cooldown
490
+ and self._last_reconnect_exception is not None
491
+ ):
492
+ logger.debug("跳过重复数据库重连,仍在冷却窗口内")
493
+ raise RuntimeError("database reconnect throttled during cooldown") from self._last_reconnect_exception
494
+
495
+ self._last_reconnect_attempt_time = reconnect_started_at
496
+ try:
497
+ await tortoise_manager.reconnect()
498
+ status = connection_lifecycle_status(tortoise_manager)
499
+ if not status.default_ready:
500
+ raise RuntimeError(f"database lifecycle not ready after reconnect: {status}")
501
+ self._connection_healthy = True
502
+ self._last_reconnect_attempt_time = 0.0
503
+ self._last_reconnect_exception = None
504
+ try:
505
+ metrics.db_reconnects_total.inc()
506
+ except Exception:
507
+ pass
508
+ logger.info("数据库连接已通过 alias 级重建恢复")
509
+ except Exception as reconnect_exc: # noqa: BLE001
510
+ self._connection_healthy = False
511
+ self._last_reconnect_exception = reconnect_exc
512
+ logger.warning("数据库连接重建失败: %s", reconnect_exc)
513
+ raise
514
+
515
+ async def probe_and_reconnect(self, *, force: bool = False, alias: str = "default") -> None:
516
+ capacity_failure: tuple[BaseException, DBConnectionReadinessProbeResult, dict[str, Any] | None] | None = None
517
+ non_transient_failure: tuple[BaseException, DBConnectionReadinessProbeResult] | None = None
518
+ async with self._lock:
519
+ current_time = asyncio.get_event_loop().time()
520
+ if not force and (current_time - self._last_check_time < self._check_interval and self._connection_healthy):
521
+ return
522
+ self._last_check_time = current_time
523
+
524
+ config: dict[str, Any] | None = None
525
+ if self._config_builder is not None or self._cluster_factory is not None:
526
+ config = self._config_builder() if self._config_builder is not None else None
527
+ await tortoise_manager.ensure_initialized(
528
+ config_builder=(lambda: config) if config is not None else self._config_builder,
529
+ cluster_factory=self._cluster_factory,
530
+ )
531
+ else:
532
+ await tortoise_manager.ensure_initialized()
533
+
534
+ result = await probe_connection_readiness(tortoise_manager, alias=alias)
535
+ if result.ready:
536
+ self._connection_healthy = True
537
+ self._last_reconnect_attempt_time = 0.0
538
+ self._last_reconnect_exception = None
539
+ return
540
+
541
+ self._connection_healthy = False
542
+ if result.error is not None and _is_capacity_or_query_timeout_error(result.error):
543
+ capacity_failure = (result.error, result, config)
544
+
545
+ elif result.error is not None and not _is_transient_db_error(result.error):
546
+ non_transient_failure = (result.error, result)
547
+
548
+ if capacity_failure is not None:
549
+ error, result, config = capacity_failure
550
+ diagnostic = await self._run_independent_database_diagnostic(config, alias)
551
+ pool_status = result.pool_status or _describe_alias_pool(alias)
552
+ diagnosis = _diagnose_capacity_failure(
553
+ config=config,
554
+ alias=alias,
555
+ error=error,
556
+ pool_status=pool_status,
557
+ independent_probe=diagnostic,
558
+ manager_uptime_seconds=time.monotonic() - self._manager_started_at,
559
+ )
560
+ fallback_result = await maybe_apply_production_pool_replenishment_fallback(
561
+ alias=alias,
562
+ error=error,
563
+ lifecycle=result.lifecycle,
564
+ pool_status=pool_status,
565
+ independent_probe=diagnostic,
566
+ diagnosis=diagnosis,
567
+ reconnect=tortoise_manager.reconnect,
568
+ lifecycle_reader=lambda: connection_lifecycle_status(tortoise_manager),
569
+ )
570
+ if fallback_result.recovered:
571
+ self._connection_healthy = True
572
+ self._last_reconnect_attempt_time = 0.0
573
+ self._last_reconnect_exception = None
574
+ try:
575
+ metrics.db_reconnects_total.inc()
576
+ except Exception:
577
+ pass
578
+ return
579
+ if fallback_result.attempted and fallback_result.error is not None:
580
+ self._last_reconnect_exception = fallback_result.error
581
+ logger.error(
582
+ "数据库容量或查询超时,不重建连接池: %s; alias=%s lifecycle=%s "
583
+ "probe_status=%s pool_status=%s independent_probe=%s diagnosis=%s fallback=%s",
584
+ error,
585
+ alias,
586
+ result.lifecycle,
587
+ result.status,
588
+ pool_status,
589
+ diagnostic,
590
+ diagnosis,
591
+ fallback_result,
592
+ )
593
+ raise error
594
+
595
+ if non_transient_failure is not None:
596
+ error, result = non_transient_failure
597
+ logger.warning(
598
+ "数据库探活失败但非断连信号,不执行连接池重建: %s; alias=%s lifecycle=%s probe_status=%s pool_status=%s",
599
+ error,
600
+ alias,
601
+ result.lifecycle,
602
+ result.status,
603
+ result.pool_status or _describe_alias_pool(alias),
604
+ )
605
+ raise error
606
+
607
+ await self.check_and_reconnect(force=True, rebuild_first=True)
608
+
609
+ async def _run_independent_database_diagnostic(
610
+ self,
611
+ config: dict[str, Any] | None,
612
+ alias: str,
613
+ ) -> dict[str, Any]:
614
+ now = asyncio.get_event_loop().time()
615
+ if (
616
+ self._last_independent_diagnostic_time
617
+ and now - self._last_independent_diagnostic_time < self._independent_diagnostic_cooldown
618
+ ):
619
+ return {
620
+ "attempted": False,
621
+ "status": "cooldown",
622
+ "cooldown": self._independent_diagnostic_cooldown,
623
+ }
624
+ self._last_independent_diagnostic_time = now
625
+ return await _probe_independent_database(config, alias)
626
+
627
+ async def ensure_ready(self) -> None:
628
+ if not self._connection_healthy:
629
+ await self.check_and_reconnect(force=True)
630
+ else:
631
+ try:
632
+ if self._config_builder is not None or self._cluster_factory is not None:
633
+ await tortoise_manager.ensure_initialized(
634
+ config_builder=self._config_builder,
635
+ cluster_factory=self._cluster_factory,
636
+ )
637
+ else:
638
+ await tortoise_manager.ensure_initialized()
639
+ self._connection_healthy = True
640
+ self._last_reconnect_attempt_time = 0.0
641
+ self._last_reconnect_exception = None
642
+ except Exception:
643
+ self._connection_healthy = False
644
+ await self.check_and_reconnect(force=True)
645
+
646
+ def wrap(self, target: Any) -> Any:
647
+ manager = self
648
+
649
+ class _DBRetryProxy:
650
+ def __getattr__(self, name: str):
651
+ attr = getattr(target, name)
652
+ if inspect.iscoroutinefunction(attr):
653
+ async def _afn(*args, **kwargs):
654
+ return await manager.execute_with_retry(attr, *args, **kwargs)
655
+
656
+ return _afn
657
+ return attr
658
+
659
+ def __call__(self, *args, **kwargs):
660
+ if inspect.iscoroutinefunction(target):
661
+ async def _afn():
662
+ return await target(*args, **kwargs)
663
+
664
+ return manager.execute_with_retry(_afn)
665
+ return target(*args, **kwargs)
666
+
667
+ return _DBRetryProxy()
668
+
669
+
670
+ @lru_cache(maxsize=1)
671
+ def get_db_manager() -> DBConnectionManager:
672
+ return DBConnectionManager()
673
+
674
+
675
+ class _DBManagerProxy:
676
+ def __getattr__(self, name: str):
677
+ return getattr(get_db_manager(), name)
678
+
679
+ def __setattr__(self, name: str, value: Any) -> None:
680
+ setattr(get_db_manager(), name, value)
681
+
682
+ def __delattr__(self, name: str) -> None:
683
+ delattr(get_db_manager(), name)
684
+
685
+
686
+ db_manager = _DBManagerProxy()
687
+
688
+
689
+ __all__ = ["DBConnectionManager", "db_manager", "get_db_manager"]