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,1166 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import os
5
+ from dataclasses import dataclass, field, replace
6
+ from functools import lru_cache
7
+ from pathlib import Path
8
+ from threading import RLock
9
+ import asyncio
10
+ import json
11
+ import time
12
+ from concurrent.futures import Future
13
+ from threading import Event, Thread
14
+ from typing import Any, Callable, Generic, Iterable, Optional, Sequence, TypeVar
15
+ from urllib.parse import unquote, urlparse
16
+
17
+ try:
18
+ import redis
19
+ except Exception: # pragma: no cover
20
+ redis = None # type: ignore
21
+
22
+ import pz_settings_center.public as settings_center
23
+ from ..observability import get_logger
24
+ from .redis_manager import RedisConfig, RedisManager
25
+
26
+
27
+ logger = get_logger(__name__)
28
+
29
+
30
+ def _resolved_settings(*, project_dir: str | os.PathLike[str] | None = None):
31
+ if project_dir is None:
32
+ return settings_center.settings
33
+ return settings_center.resolve_settings(project_dir=str(Path(project_dir).resolve()))
34
+
35
+
36
+ def _project_settings(*, project_dir: str | os.PathLike[str] | None = None):
37
+ try:
38
+ return _resolved_settings(project_dir=project_dir).project
39
+ except Exception:
40
+ return None
41
+
42
+
43
+ def _foundation_settings_section(
44
+ name: str,
45
+ *,
46
+ project_dir: str | os.PathLike[str] | None = None,
47
+ ):
48
+ section_name = str(name or "").strip()
49
+ try:
50
+ resolved = _resolved_settings(project_dir=project_dir)
51
+ if hasattr(resolved, "app_settings"):
52
+ foundation_payload = resolved.app_settings("pz_foundation")
53
+ if isinstance(foundation_payload, dict):
54
+ section = foundation_payload.get(section_name, {})
55
+ if section not in (None, {}):
56
+ return section
57
+ foundation_settings = getattr(resolved, "pz_foundation", None)
58
+ if isinstance(foundation_settings, dict):
59
+ return foundation_settings.get(section_name, {})
60
+ if foundation_settings is None:
61
+ return {}
62
+ return getattr(foundation_settings, section_name, {})
63
+ except Exception:
64
+ return {}
65
+
66
+
67
+ @dataclass(frozen=True, slots=True)
68
+ class FoundationRedisSettings:
69
+ enabled: bool = False
70
+ host: str = ""
71
+ port: int = 6379
72
+ db: int = 0
73
+ password: str | None = None
74
+ url: str | None = None
75
+ service: str = "mymaster"
76
+ allow_cluster: bool = False
77
+ connect_timeout_seconds: float = 1.0
78
+ socket_timeout_seconds: float = 1.0
79
+ operation_max_attempts: int = 2
80
+ connection_retry_attempts: int = 0
81
+ retry_base_delay_seconds: float = 0.05
82
+ retry_max_delay_seconds: float = 0.2
83
+ acquire_failure_cooldown_seconds: float = 2.0
84
+ health_failure_cooldown_seconds: float = 2.0
85
+
86
+
87
+ class FoundationRedisUnavailable(RuntimeError):
88
+ """Raised when a required foundation Redis capability is disabled, misconfigured, or unavailable."""
89
+
90
+
91
+ def _split_redis_host_entry(raw_entry: str, default_port: int) -> tuple[str, int]:
92
+ entry = str(raw_entry or "").strip()
93
+ if not entry:
94
+ return "", default_port
95
+ if entry.startswith("["):
96
+ closing = entry.find("]")
97
+ if closing > 0:
98
+ host = entry[1:closing].strip()
99
+ remainder = entry[closing + 1 :].strip()
100
+ if remainder.startswith(":"):
101
+ try:
102
+ return host, int(remainder[1:] or default_port)
103
+ except Exception:
104
+ return host, default_port
105
+ return host, default_port
106
+ if entry.count(":") == 1:
107
+ host, raw_port = entry.rsplit(":", 1)
108
+ try:
109
+ return host.strip(), int(raw_port or default_port)
110
+ except Exception:
111
+ return host.strip(), default_port
112
+ return entry, default_port
113
+
114
+
115
+ def _parse_redis_endpoints(raw: str | None, default_port: int) -> tuple[tuple[str, int], ...]:
116
+ endpoints: list[tuple[str, int]] = []
117
+ for part in str(raw or "").split(","):
118
+ host, port = _split_redis_host_entry(part, default_port)
119
+ if not host:
120
+ continue
121
+ endpoints.append((host, port))
122
+ return tuple(endpoints)
123
+
124
+
125
+ def _split_redis_netloc(netloc: str) -> tuple[str, str]:
126
+ text = str(netloc or "").strip()
127
+ if "@" not in text:
128
+ return "", text
129
+ userinfo, hostspec = text.rsplit("@", 1)
130
+ return userinfo, hostspec
131
+
132
+
133
+ def _parse_redis_url_password(userinfo: str) -> str | None:
134
+ text = str(userinfo or "")
135
+ if ":" not in text:
136
+ return None
137
+ _username, _separator, raw_password = text.partition(":")
138
+ if not raw_password:
139
+ return None
140
+ return unquote(raw_password)
141
+
142
+
143
+ def _build_redis_config_from_resolved_values(
144
+ *,
145
+ host: str,
146
+ port: int,
147
+ db: int,
148
+ password: str | None,
149
+ service: str,
150
+ allow_cluster: bool,
151
+ connect_timeout_seconds: float,
152
+ socket_timeout_seconds: float,
153
+ operation_max_attempts: int,
154
+ connection_retry_attempts: int,
155
+ retry_base_delay_seconds: float,
156
+ retry_max_delay_seconds: float,
157
+ acquire_failure_cooldown_seconds: float,
158
+ health_failure_cooldown_seconds: float,
159
+ ) -> RedisConfig:
160
+ endpoints = _parse_redis_endpoints(host, int(port or 6379))
161
+ base_kwargs = {
162
+ "db": db,
163
+ "password": password,
164
+ "sentinel_service": str(service or "mymaster") or "mymaster",
165
+ "allow_cluster": bool(allow_cluster),
166
+ "connect_timeout_seconds": connect_timeout_seconds,
167
+ "socket_timeout_seconds": socket_timeout_seconds,
168
+ "operation_max_attempts": operation_max_attempts,
169
+ "connection_retry_attempts": connection_retry_attempts,
170
+ "retry_base_delay_seconds": retry_base_delay_seconds,
171
+ "retry_max_delay_seconds": retry_max_delay_seconds,
172
+ "acquire_failure_cooldown_seconds": acquire_failure_cooldown_seconds,
173
+ "health_failure_cooldown_seconds": health_failure_cooldown_seconds,
174
+ }
175
+ if len(endpoints) > 1:
176
+ primary_host, primary_port = endpoints[0]
177
+ return RedisConfig(
178
+ host=primary_host,
179
+ port=primary_port,
180
+ sentinel_hosts=endpoints,
181
+ **base_kwargs,
182
+ )
183
+ if len(endpoints) == 1:
184
+ primary_host, primary_port = endpoints[0]
185
+ return RedisConfig(
186
+ host=primary_host,
187
+ port=primary_port,
188
+ **base_kwargs,
189
+ )
190
+ return RedisConfig(
191
+ host="",
192
+ port=int(port or 6379),
193
+ **base_kwargs,
194
+ )
195
+
196
+
197
+ @dataclass(slots=True)
198
+ class InMemoryFoundationCache:
199
+ _lock: RLock = field(default_factory=RLock)
200
+ _strings: dict[str, str] = field(default_factory=dict)
201
+ _lists: dict[str, list[str]] = field(default_factory=dict)
202
+ _sets: dict[str, set[str]] = field(default_factory=dict)
203
+
204
+ def get(self, key: str) -> str | None:
205
+ with self._lock:
206
+ return self._strings.get(key)
207
+
208
+ def mget(self, keys: Iterable[str]) -> list[str | None]:
209
+ return [self.get(str(key)) for key in keys]
210
+
211
+ def set(
212
+ self,
213
+ key: str,
214
+ value: object,
215
+ *,
216
+ ttl_seconds: int | None = None,
217
+ ex: int | None = None,
218
+ nx: bool = False,
219
+ ) -> bool:
220
+ del ttl_seconds, ex
221
+ with self._lock:
222
+ if nx and key in self._strings:
223
+ return False
224
+ self._strings[key] = str(value)
225
+ return True
226
+
227
+ def delete(self, key: str) -> None:
228
+ with self._lock:
229
+ self._strings.pop(key, None)
230
+ self._lists.pop(key, None)
231
+ self._sets.pop(key, None)
232
+
233
+ def delete_if_value_matches(self, key: str, expected_value: object) -> bool:
234
+ with self._lock:
235
+ if self._strings.get(key) != str(expected_value):
236
+ return False
237
+ self._strings.pop(key, None)
238
+ return True
239
+
240
+ def sadd(self, key: str, *values: object) -> int:
241
+ with self._lock:
242
+ payload = self._sets.setdefault(key, set())
243
+ before = len(payload)
244
+ payload.update(str(value) for value in values)
245
+ return len(payload) - before
246
+
247
+ def smembers(self, key: str) -> set[str]:
248
+ with self._lock:
249
+ return set(self._sets.get(key, set()))
250
+
251
+ def rpush(self, key: str, *values: object) -> int:
252
+ with self._lock:
253
+ payload = self._lists.setdefault(key, [])
254
+ payload.extend(str(value) for value in values)
255
+ return len(payload)
256
+
257
+ def lrange(self, key: str, start: int, end: int) -> list[str]:
258
+ with self._lock:
259
+ payload = list(self._lists.get(key, []))
260
+ if end == -1:
261
+ return payload[start:]
262
+ return payload[start : end + 1]
263
+
264
+
265
+ @dataclass(slots=True)
266
+ class RedisFoundationCache:
267
+ settings: FoundationRedisSettings
268
+ _client: Any = field(init=False, repr=False)
269
+
270
+ def __post_init__(self) -> None:
271
+ self._client = self._build_client()
272
+
273
+ def get(self, key: str) -> str | None:
274
+ value = self._client.get(key)
275
+ if value is None:
276
+ return None
277
+ return str(value)
278
+
279
+ def mget(self, keys: Iterable[str]) -> list[str | None]:
280
+ values = self._client.mget(list(keys))
281
+ return [None if value is None else str(value) for value in values]
282
+
283
+ def set(
284
+ self,
285
+ key: str,
286
+ value: object,
287
+ *,
288
+ ttl_seconds: int | None = None,
289
+ ex: int | None = None,
290
+ nx: bool = False,
291
+ ) -> bool:
292
+ ttl = ex if ex is not None else ttl_seconds
293
+ result = self._client.set(key, str(value), ex=ttl, nx=nx)
294
+ return bool(result)
295
+
296
+ def delete(self, key: str) -> None:
297
+ self._client.delete(key)
298
+
299
+ def delete_if_value_matches(self, key: str, expected_value: object) -> bool:
300
+ result = self._client.eval(
301
+ "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
302
+ 1,
303
+ key,
304
+ str(expected_value),
305
+ )
306
+ return bool(result)
307
+
308
+ def sadd(self, key: str, *values: object) -> int:
309
+ return int(self._client.sadd(key, *(str(value) for value in values)))
310
+
311
+ def smembers(self, key: str) -> set[str]:
312
+ return {str(item) for item in self._client.smembers(key)}
313
+
314
+ def rpush(self, key: str, *values: object) -> int:
315
+ return int(self._client.rpush(key, *(str(value) for value in values)))
316
+
317
+ def lrange(self, key: str, start: int, end: int) -> list[str]:
318
+ return [str(item) for item in self._client.lrange(key, start, end)]
319
+
320
+ def _build_client(self) -> Any:
321
+ if redis is None:
322
+ raise RuntimeError("redis package is required for RedisFoundationCache")
323
+ if not self.settings.enabled:
324
+ raise RuntimeError("foundation redis is disabled")
325
+ if self.settings.url:
326
+ return redis.Redis.from_url(
327
+ self.settings.url,
328
+ decode_responses=True,
329
+ socket_timeout=float(self.settings.socket_timeout_seconds or 1.0),
330
+ socket_connect_timeout=float(self.settings.connect_timeout_seconds or 1.0),
331
+ retry_on_timeout=False,
332
+ )
333
+ if not self.settings.host.strip():
334
+ raise RuntimeError("foundation redis host is not configured")
335
+ return redis.Redis(
336
+ host=self.settings.host,
337
+ port=self.settings.port,
338
+ db=self.settings.db,
339
+ password=self.settings.password,
340
+ decode_responses=True,
341
+ socket_timeout=float(self.settings.socket_timeout_seconds or 1.0),
342
+ socket_connect_timeout=float(self.settings.connect_timeout_seconds or 1.0),
343
+ retry_on_timeout=False,
344
+ )
345
+
346
+ def ping(self) -> bool:
347
+ return bool(self._client.ping())
348
+
349
+
350
+ @dataclass
351
+ class _L1Item:
352
+ value: Any
353
+ expires_at: float
354
+
355
+
356
+ @dataclass(frozen=True, slots=True)
357
+ class CacheSnapshot:
358
+ json_values: dict[str, Any | None]
359
+ version_values: dict[str, int]
360
+
361
+ def get_json(self, key: str) -> Any | None:
362
+ return self.json_values.get(key)
363
+
364
+ def get_version(self, name: str) -> int:
365
+ value = self.version_values.get(name, 0)
366
+ try:
367
+ return int(value)
368
+ except Exception:
369
+ return 0
370
+
371
+
372
+ class CacheService:
373
+ def __init__(self, redis_manager: Any) -> None:
374
+ self._redis_manager = redis_manager
375
+ self._version_prefix = "cache:version:"
376
+ self._lock_prefix = "lock:"
377
+ self._l1: dict[str, _L1Item] = {}
378
+ self._l1_lock = asyncio.Lock()
379
+
380
+ def _l1_get(self, key: str) -> Any | None:
381
+ item = self._l1.get(key)
382
+ if not item:
383
+ return None
384
+ if item.expires_at and item.expires_at < time.time():
385
+ self._l1.pop(key, None)
386
+ return None
387
+ return item.value
388
+
389
+ def _l1_set(self, key: str, value: Any, ttl: Optional[int]) -> None:
390
+ expires_at = 0.0 if not ttl else time.time() + int(ttl)
391
+ self._l1[key] = _L1Item(value=value, expires_at=expires_at)
392
+
393
+ def _l1_invalidate(self, keys: Sequence[str]) -> None:
394
+ for key in keys:
395
+ self._l1.pop(key, None)
396
+
397
+ async def get_json(self, key: str) -> Any | None:
398
+ cached = self._l1_get(key)
399
+ if cached is not None:
400
+ return cached
401
+ try:
402
+ client = await self._redis_manager.get_connection()
403
+ raw = await client.get(key)
404
+ if raw is None:
405
+ return None
406
+ parsed = json.loads(raw)
407
+ ttl = await client.ttl(key)
408
+ ttl = 0 if ttl < 0 else int(ttl)
409
+ self._l1_set(key, parsed, ttl or None)
410
+ return parsed
411
+ except Exception as exc:
412
+ logger.debug("cache.get_json(%s) failed: %s", key, exc)
413
+ raise
414
+
415
+ async def read_snapshot(
416
+ self,
417
+ *,
418
+ json_keys: Sequence[str] = (),
419
+ version_names: Sequence[str] = (),
420
+ ) -> CacheSnapshot:
421
+ json_values: dict[str, Any | None] = {str(key): None for key in json_keys}
422
+ version_values: dict[str, int] = {str(name): 0 for name in version_names}
423
+ targets: list[tuple[str, str]] = []
424
+ raw_keys: list[str] = []
425
+
426
+ for key in dict.fromkeys(str(key) for key in json_keys):
427
+ cached = self._l1_get(key)
428
+ if cached is not None:
429
+ json_values[key] = cached
430
+ continue
431
+ targets.append(("json", key))
432
+ raw_keys.append(key)
433
+ for name in dict.fromkeys(str(name) for name in version_names):
434
+ targets.append(("version", name))
435
+ raw_keys.append(self._version_prefix + name)
436
+
437
+ if not raw_keys:
438
+ return CacheSnapshot(json_values=json_values, version_values=version_values)
439
+
440
+ try:
441
+ client = await self._redis_manager.get_connection()
442
+ raw_values = await client.mget(raw_keys)
443
+ for target, raw in zip(targets, raw_values):
444
+ kind, name = target
445
+ if kind == "json":
446
+ json_values[name] = None if raw is None else json.loads(raw)
447
+ else:
448
+ version_values[name] = 0 if raw is None else int(raw)
449
+ return CacheSnapshot(json_values=json_values, version_values=version_values)
450
+ except Exception as exc:
451
+ logger.debug("cache.read_snapshot(json_keys=%s, version_names=%s) failed: %s", json_keys, version_names, exc)
452
+ raise
453
+
454
+ async def set_json(self, key: str, value: Any, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None) -> None:
455
+ try:
456
+ client = await self._redis_manager.get_connection()
457
+ payload = json.dumps(value, ensure_ascii=False)
458
+ if ttl and ttl > 0:
459
+ await client.setex(key, int(ttl), payload)
460
+ else:
461
+ await client.set(key, payload)
462
+ for tag in tags or []:
463
+ tag_key = f"cache:tag:{tag}"
464
+ await client.sadd(tag_key, key)
465
+ if ttl and ttl > 0:
466
+ await client.expire(tag_key, int(ttl))
467
+ self._l1_set(key, value, ttl)
468
+ except Exception as exc:
469
+ logger.debug("cache.set_json(%s) failed: %s", key, exc)
470
+ raise
471
+
472
+ async def invalidate(self, *, keys: Optional[Sequence[str]] = None, tags: Optional[Sequence[str]] = None) -> None:
473
+ keys = list(keys or [])
474
+ try:
475
+ client = await self._redis_manager.get_connection()
476
+ for tag in tags or []:
477
+ tag_key = f"cache:tag:{tag}"
478
+ members = await client.smembers(tag_key)
479
+ if members:
480
+ keys.extend(list(members))
481
+ await client.delete(tag_key)
482
+ if keys:
483
+ await client.delete(*set(keys))
484
+ except Exception as exc:
485
+ logger.debug("cache.invalidate(tags=%s, keys=%s) failed: %s", tags, keys, exc)
486
+ raise
487
+ self._l1_invalidate(set(keys))
488
+
489
+ async def get_version(self, name: str) -> int:
490
+ try:
491
+ client = await self._redis_manager.get_connection()
492
+ raw = await client.get(self._version_prefix + name)
493
+ return 0 if raw is None else int(raw)
494
+ except Exception as exc:
495
+ logger.debug("cache.get_version(%s) failed: %s", name, exc)
496
+ raise
497
+
498
+ async def bump_version(self, name: str) -> int:
499
+ try:
500
+ client = await self._redis_manager.get_connection()
501
+ return int(await client.incr(self._version_prefix + name))
502
+ except Exception as exc:
503
+ logger.debug("cache.bump_version(%s) failed: %s", name, exc)
504
+ raise
505
+
506
+ async def try_acquire_lock(self, name: str, ttl: int = 15) -> bool:
507
+ try:
508
+ client = await self._redis_manager.get_connection()
509
+ return bool(await client.set(self._lock_prefix + name, "1", ex=int(ttl), nx=True))
510
+ except Exception as exc:
511
+ logger.debug("cache.try_acquire_lock(%s) failed: %s", name, exc)
512
+ raise
513
+
514
+
515
+ class SyncRedisAdapter:
516
+ """Run async redis-manager commands on a dedicated event-loop thread."""
517
+
518
+ def __init__(self, redis_manager: Any) -> None:
519
+ if redis_manager is None:
520
+ raise ValueError("SyncRedisAdapter requires an explicit redis manager")
521
+ self._redis_manager = redis_manager
522
+ self._ready = Event()
523
+ self._loop: asyncio.AbstractEventLoop | None = None
524
+ self._thread = Thread(target=self._run_loop, name="foundation-sync-cache", daemon=True)
525
+ self._thread.start()
526
+ self._ready.wait()
527
+
528
+ def _run_loop(self) -> None:
529
+ loop = asyncio.new_event_loop()
530
+ self._loop = loop
531
+ asyncio.set_event_loop(loop)
532
+ self._ready.set()
533
+ loop.run_forever()
534
+ loop.close()
535
+
536
+ async def _invoke(self, method_name: str, *args: Any, **kwargs: Any) -> Any:
537
+ client = await self._redis_manager.get_connection()
538
+ method = getattr(client, method_name)
539
+ return await method(*args, **kwargs)
540
+
541
+ def call(self, method_name: str, *args: Any, **kwargs: Any) -> Any:
542
+ if self._loop is None:
543
+ raise RuntimeError("sync redis adapter loop is not ready")
544
+ future: Future[Any] = asyncio.run_coroutine_threadsafe(
545
+ self._invoke(method_name, *args, **kwargs),
546
+ self._loop,
547
+ )
548
+ return future.result()
549
+
550
+ def __getattr__(self, name: str) -> Any:
551
+ def caller(*args: Any, **kwargs: Any) -> Any:
552
+ return self.call(name, *args, **kwargs)
553
+
554
+ return caller
555
+
556
+ def close(self) -> None:
557
+ loop = self._loop
558
+ if loop is None or loop.is_closed():
559
+ return
560
+ with contextlib.suppress(Exception):
561
+ future = asyncio.run_coroutine_threadsafe(
562
+ self._redis_manager.reset_connection(),
563
+ loop,
564
+ )
565
+ future.result(timeout=1.0)
566
+ loop.call_soon_threadsafe(loop.stop)
567
+ self._thread.join(timeout=1.0)
568
+
569
+
570
+ class SyncCacheService:
571
+ def __init__(self, backend: Any) -> None:
572
+ self._backend = backend
573
+ self._version_prefix = "cache:version:"
574
+ self._lock_prefix = "lock:"
575
+ self._l1: dict[str, _L1Item] = {}
576
+
577
+ def _l1_get(self, key: str) -> Any | None:
578
+ item = self._l1.get(key)
579
+ if not item:
580
+ return None
581
+ if item.expires_at and item.expires_at < time.time():
582
+ self._l1.pop(key, None)
583
+ return None
584
+ return item.value
585
+
586
+ def _l1_set(self, key: str, value: Any, ttl: Optional[int]) -> None:
587
+ expires_at = 0.0 if not ttl else time.time() + int(ttl)
588
+ self._l1[key] = _L1Item(value=value, expires_at=expires_at)
589
+
590
+ def _l1_invalidate(self, keys: Sequence[str]) -> None:
591
+ for key in keys:
592
+ self._l1.pop(key, None)
593
+
594
+ def get_json(self, key: str) -> Any | None:
595
+ cached = self._l1_get(key)
596
+ if cached is not None:
597
+ return cached
598
+ try:
599
+ raw = self._backend.get(key)
600
+ if raw is None:
601
+ return None
602
+ parsed = json.loads(raw)
603
+ ttl_reader = getattr(self._backend, "ttl", None)
604
+ ttl = int(ttl_reader(key)) if callable(ttl_reader) else 0
605
+ ttl = 0 if ttl < 0 else ttl
606
+ self._l1_set(key, parsed, ttl or None)
607
+ return parsed
608
+ except Exception as exc:
609
+ logger.debug("sync cache.get_json(%s) failed: %s", key, exc)
610
+ return None
611
+
612
+ def read_snapshot(
613
+ self,
614
+ *,
615
+ json_keys: Sequence[str] = (),
616
+ version_names: Sequence[str] = (),
617
+ ) -> CacheSnapshot:
618
+ json_values: dict[str, Any | None] = {str(key): None for key in json_keys}
619
+ version_values: dict[str, int] = {str(name): 0 for name in version_names}
620
+ targets: list[tuple[str, str]] = []
621
+ raw_keys: list[str] = []
622
+
623
+ for key in dict.fromkeys(str(key) for key in json_keys):
624
+ cached = self._l1_get(key)
625
+ if cached is not None:
626
+ json_values[key] = cached
627
+ continue
628
+ targets.append(("json", key))
629
+ raw_keys.append(key)
630
+ for name in dict.fromkeys(str(name) for name in version_names):
631
+ targets.append(("version", name))
632
+ raw_keys.append(self._version_prefix + name)
633
+
634
+ if not raw_keys:
635
+ return CacheSnapshot(json_values=json_values, version_values=version_values)
636
+
637
+ try:
638
+ raw_values = self._backend.mget(raw_keys)
639
+ for target, raw in zip(targets, raw_values):
640
+ kind, name = target
641
+ if kind == "json":
642
+ json_values[name] = None if raw is None else json.loads(raw)
643
+ else:
644
+ version_values[name] = 0 if raw is None else int(raw)
645
+ except Exception as exc:
646
+ logger.debug(
647
+ "sync cache.read_snapshot(json_keys=%s, version_names=%s) failed: %s",
648
+ json_keys,
649
+ version_names,
650
+ exc,
651
+ )
652
+ return CacheSnapshot(json_values=json_values, version_values=version_values)
653
+
654
+ def set_json(self, key: str, value: Any, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None) -> None:
655
+ try:
656
+ payload = json.dumps(value, ensure_ascii=False)
657
+ ttl_value = int(ttl) if ttl and ttl > 0 else None
658
+ if ttl_value is not None:
659
+ setex = getattr(self._backend, "setex", None)
660
+ if callable(setex):
661
+ setex(key, ttl_value, payload)
662
+ else:
663
+ self._backend.set(key, payload, ex=ttl_value)
664
+ else:
665
+ self._backend.set(key, payload)
666
+ for tag in tags or ():
667
+ tag_key = f"cache:tag:{tag}"
668
+ self._backend.sadd(tag_key, key)
669
+ expire = getattr(self._backend, "expire", None)
670
+ if ttl_value is not None and callable(expire):
671
+ expire(tag_key, ttl_value)
672
+ self._l1_set(key, value, ttl)
673
+ except Exception as exc:
674
+ logger.debug("sync cache.set_json(%s) failed: %s", key, exc)
675
+ self._l1_set(key, value, ttl)
676
+
677
+ def invalidate(self, *, keys: Optional[Sequence[str]] = None, tags: Optional[Sequence[str]] = None) -> None:
678
+ invalidated_keys = list(keys or ())
679
+ try:
680
+ for tag in tags or ():
681
+ tag_key = f"cache:tag:{tag}"
682
+ members = self._backend.smembers(tag_key)
683
+ if members:
684
+ invalidated_keys.extend(list(members))
685
+ self._backend.delete(tag_key)
686
+ if invalidated_keys:
687
+ self._backend.delete(*tuple(set(invalidated_keys)))
688
+ except Exception as exc:
689
+ logger.debug("sync cache.invalidate(tags=%s, keys=%s) failed: %s", tags, invalidated_keys, exc)
690
+ self._l1_invalidate(set(invalidated_keys))
691
+
692
+ def get_version(self, name: str) -> int:
693
+ try:
694
+ raw = self._backend.get(self._version_prefix + str(name))
695
+ return 0 if raw is None else int(raw)
696
+ except Exception as exc:
697
+ logger.debug("sync cache.get_version(%s) failed: %s", name, exc)
698
+ return 0
699
+
700
+ def bump_version(self, name: str) -> int:
701
+ try:
702
+ return int(self._backend.incr(self._version_prefix + str(name)))
703
+ except Exception as exc:
704
+ logger.debug("sync cache.bump_version(%s) failed: %s", name, exc)
705
+ return 0
706
+
707
+ def close(self) -> None:
708
+ closer = getattr(self._backend, "close", None)
709
+ if callable(closer):
710
+ with contextlib.suppress(Exception):
711
+ closer()
712
+
713
+
714
+ T = TypeVar("T")
715
+
716
+
717
+ class _LazyAccessor(Generic[T]):
718
+ __slots__ = ("_factory", "_instance")
719
+
720
+ def __init__(self, factory: Callable[[], T]) -> None:
721
+ object.__setattr__(self, "_factory", factory)
722
+ object.__setattr__(self, "_instance", None)
723
+
724
+ def _get_instance(self) -> T:
725
+ inst = object.__getattribute__(self, "_instance")
726
+ if inst is None:
727
+ inst = object.__getattribute__(self, "_factory")()
728
+ object.__setattr__(self, "_instance", inst)
729
+ return inst # type: ignore[return-value]
730
+
731
+ def __getattr__(self, name: str) -> Any:
732
+ return getattr(self._get_instance(), name)
733
+
734
+ def __setattr__(self, name: str, value: Any) -> None:
735
+ if name in {"_factory", "_instance"}:
736
+ object.__setattr__(self, name, value)
737
+ return
738
+ setattr(self._get_instance(), name, value)
739
+
740
+ def __repr__(self) -> str:
741
+ inst = object.__getattribute__(self, "_instance")
742
+ if inst is None:
743
+ return "<lazy cache service proxy>"
744
+ return repr(inst)
745
+
746
+
747
+ _CACHE_SINGLETONS: dict[str, RedisFoundationCache] = {}
748
+
749
+
750
+ def load_foundation_redis_settings(
751
+ *,
752
+ project_dir: str | os.PathLike[str] | None = None,
753
+ ) -> FoundationRedisSettings:
754
+ return _resolve_foundation_redis_settings(project_dir=project_dir)
755
+
756
+
757
+ def _redis_binding_configured(settings: FoundationRedisSettings) -> bool:
758
+ if not settings.enabled:
759
+ return False
760
+ return bool(str(settings.url or "").strip() or str(settings.host or "").strip())
761
+
762
+
763
+ def describe_foundation_redis_binding(
764
+ *,
765
+ project_dir: str | os.PathLike[str] | None = None,
766
+ settings: FoundationRedisSettings | None = None,
767
+ ) -> dict[str, Any]:
768
+ resolved = settings or _resolve_foundation_redis_settings(project_dir=project_dir)
769
+ configured = _redis_binding_configured(resolved)
770
+ backend = "redis" if configured else "none"
771
+ return {
772
+ "enabled": bool(resolved.enabled),
773
+ "configured": configured,
774
+ "backend": backend,
775
+ "operational": False,
776
+ "degraded": False,
777
+ "ready": False,
778
+ "error": None,
779
+ "host": resolved.host,
780
+ "port": resolved.port,
781
+ "db": resolved.db,
782
+ "url": resolved.url,
783
+ "probe_deferred": True,
784
+ }
785
+
786
+
787
+ def probe_foundation_redis_binding(
788
+ *,
789
+ project_dir: str | os.PathLike[str] | None = None,
790
+ settings: FoundationRedisSettings | None = None,
791
+ capability_name: str = "foundation redis",
792
+ ) -> dict[str, Any]:
793
+ status = describe_foundation_redis_binding(project_dir=project_dir, settings=settings)
794
+ if not status["enabled"] or not status["configured"]:
795
+ status["probe_deferred"] = False
796
+ return status
797
+ try:
798
+ cache = get_required_foundation_cache(
799
+ project_dir=project_dir,
800
+ settings=settings,
801
+ capability_name=capability_name,
802
+ )
803
+ ready = bool(cache.ping())
804
+ status["ready"] = ready
805
+ status["operational"] = ready
806
+ status["probe_deferred"] = False
807
+ return status
808
+ except Exception as exc:
809
+ status["error"] = str(exc)
810
+ status["probe_deferred"] = False
811
+ return status
812
+
813
+
814
+ def _resolve_foundation_redis_settings(
815
+ *,
816
+ project_dir: str | os.PathLike[str] | None = None,
817
+ ) -> FoundationRedisSettings:
818
+ redis_payload = _redis_snapshot(project_dir=project_dir)
819
+ url = _read_str(redis_payload, "url")
820
+ host = _read_str(redis_payload, "host")
821
+ port = _read_int(redis_payload, "port", 6379)
822
+ db = _read_int(redis_payload, "db", 0)
823
+ password = _read_optional_str(redis_payload, "password")
824
+ enabled = _read_bool_value(redis_payload, "enabled", False)
825
+ service = _read_str(redis_payload, "service") or "mymaster"
826
+ allow_cluster = _read_bool_value(redis_payload, "allow_cluster", False)
827
+ connect_timeout_seconds = _read_float(redis_payload, "connect_timeout_seconds", 1.0)
828
+ socket_timeout_seconds = _read_float(redis_payload, "socket_timeout_seconds", 1.0)
829
+ operation_max_attempts = _read_int(redis_payload, "operation_max_attempts", 2)
830
+ connection_retry_attempts = _read_int(redis_payload, "connection_retry_attempts", 0)
831
+ retry_base_delay_seconds = _read_float(redis_payload, "retry_base_delay_seconds", 0.05)
832
+ retry_max_delay_seconds = _read_float(redis_payload, "retry_max_delay_seconds", 0.2)
833
+ acquire_failure_cooldown_seconds = _read_float(redis_payload, "acquire_failure_cooldown_seconds", 2.0)
834
+ health_failure_cooldown_seconds = _read_float(redis_payload, "health_failure_cooldown_seconds", 2.0)
835
+ return FoundationRedisSettings(
836
+ enabled=enabled,
837
+ host=host,
838
+ port=port,
839
+ db=db,
840
+ password=password,
841
+ url=url,
842
+ service=service,
843
+ allow_cluster=allow_cluster,
844
+ connect_timeout_seconds=connect_timeout_seconds,
845
+ socket_timeout_seconds=socket_timeout_seconds,
846
+ operation_max_attempts=operation_max_attempts,
847
+ connection_retry_attempts=connection_retry_attempts,
848
+ retry_base_delay_seconds=retry_base_delay_seconds,
849
+ retry_max_delay_seconds=retry_max_delay_seconds,
850
+ acquire_failure_cooldown_seconds=acquire_failure_cooldown_seconds,
851
+ health_failure_cooldown_seconds=health_failure_cooldown_seconds,
852
+ )
853
+
854
+
855
+ def get_foundation_cache(
856
+ *,
857
+ project_dir: str | os.PathLike[str] | None = None,
858
+ settings: FoundationRedisSettings | None = None,
859
+ ) -> RedisFoundationCache:
860
+ resolved_settings = settings or _resolve_foundation_redis_settings(project_dir=project_dir)
861
+ key = _cache_key(resolved_settings)
862
+ cache = _CACHE_SINGLETONS.get(key)
863
+ if cache is None:
864
+ cache = RedisFoundationCache(resolved_settings)
865
+ _CACHE_SINGLETONS[key] = cache
866
+ return cache
867
+
868
+
869
+ def get_required_foundation_cache(
870
+ *,
871
+ project_dir: str | os.PathLike[str] | None = None,
872
+ settings: FoundationRedisSettings | None = None,
873
+ capability_name: str = "foundation redis",
874
+ ) -> RedisFoundationCache:
875
+ resolved_settings = settings or _resolve_foundation_redis_settings(project_dir=project_dir)
876
+ capability_label = str(capability_name or "foundation redis").strip() or "foundation redis"
877
+ if not resolved_settings.enabled:
878
+ raise FoundationRedisUnavailable(f"{capability_label} is disabled")
879
+ if not str(resolved_settings.url or resolved_settings.host or "").strip():
880
+ raise FoundationRedisUnavailable(f"{capability_label} is not configured")
881
+ try:
882
+ return get_foundation_cache(settings=resolved_settings)
883
+ except FoundationRedisUnavailable:
884
+ raise
885
+ except Exception as exc:
886
+ raise FoundationRedisUnavailable(f"{capability_label} is unavailable") from exc
887
+
888
+
889
+ def get_foundation_redis_manager(
890
+ *,
891
+ project_dir: str | os.PathLike[str] | None = None,
892
+ settings: FoundationRedisSettings | None = None,
893
+ ) -> RedisManager:
894
+ resolved_settings = settings or _resolve_foundation_redis_settings(project_dir=project_dir)
895
+ return RedisManager(build_foundation_redis_config(resolved_settings))
896
+
897
+
898
+ def build_required_foundation_redis_config(
899
+ *,
900
+ project_dir: str | os.PathLike[str] | None = None,
901
+ settings: FoundationRedisSettings | None = None,
902
+ capability_name: str = "foundation redis",
903
+ decode_responses: bool = True,
904
+ ) -> RedisConfig:
905
+ resolved_settings = settings or _resolve_foundation_redis_settings(project_dir=project_dir)
906
+ capability_label = str(capability_name or "foundation redis").strip() or "foundation redis"
907
+ if not resolved_settings.enabled:
908
+ raise FoundationRedisUnavailable(f"{capability_label} is disabled")
909
+ if not str(resolved_settings.url or resolved_settings.host or "").strip():
910
+ raise FoundationRedisUnavailable(f"{capability_label} is not configured")
911
+ return replace(
912
+ build_foundation_redis_config(resolved_settings),
913
+ decode_responses=bool(decode_responses),
914
+ )
915
+
916
+
917
+ @lru_cache(maxsize=32)
918
+ def _cached_required_foundation_redis_manager(
919
+ project_dir: str,
920
+ capability_name: str,
921
+ decode_responses: bool,
922
+ ) -> RedisManager:
923
+ return RedisManager(
924
+ build_required_foundation_redis_config(
925
+ project_dir=project_dir,
926
+ capability_name=capability_name,
927
+ decode_responses=decode_responses,
928
+ )
929
+ )
930
+
931
+
932
+ def get_required_foundation_redis_manager(
933
+ *,
934
+ project_dir: str | os.PathLike[str] | None = None,
935
+ settings: FoundationRedisSettings | None = None,
936
+ capability_name: str = "foundation redis",
937
+ decode_responses: bool = True,
938
+ ) -> RedisManager:
939
+ if settings is not None:
940
+ return RedisManager(
941
+ build_required_foundation_redis_config(
942
+ settings=settings,
943
+ capability_name=capability_name,
944
+ decode_responses=decode_responses,
945
+ )
946
+ )
947
+ resolved_project_dir = "" if project_dir is None else str(Path(project_dir).resolve())
948
+ return _cached_required_foundation_redis_manager(
949
+ resolved_project_dir,
950
+ str(capability_name or "foundation redis"),
951
+ bool(decode_responses),
952
+ )
953
+
954
+
955
+ async def get_required_foundation_redis_connection(
956
+ *,
957
+ project_dir: str | os.PathLike[str] | None = None,
958
+ settings: FoundationRedisSettings | None = None,
959
+ capability_name: str = "foundation redis",
960
+ decode_responses: bool = True,
961
+ ):
962
+ manager = get_required_foundation_redis_manager(
963
+ project_dir=project_dir,
964
+ settings=settings,
965
+ capability_name=capability_name,
966
+ decode_responses=decode_responses,
967
+ )
968
+ return await manager.get_connection()
969
+
970
+
971
+ def build_foundation_redis_config(settings: FoundationRedisSettings) -> RedisConfig:
972
+ host = settings.host
973
+ port = settings.port
974
+ db = settings.db
975
+ password = settings.password
976
+ if settings.url:
977
+ parsed = urlparse(settings.url)
978
+ userinfo, hostspec = _split_redis_netloc(parsed.netloc)
979
+ if hostspec:
980
+ host = hostspec
981
+ parsed_password = _parse_redis_url_password(userinfo)
982
+ if parsed_password is not None:
983
+ password = parsed_password
984
+ if parsed.path and parsed.path != "/":
985
+ with_value = parsed.path.removeprefix("/")
986
+ if with_value.isdigit():
987
+ db = int(with_value)
988
+ return _build_redis_config_from_resolved_values(
989
+ host=host or "",
990
+ port=port,
991
+ db=db,
992
+ password=password,
993
+ service=settings.service,
994
+ allow_cluster=settings.allow_cluster,
995
+ connect_timeout_seconds=settings.connect_timeout_seconds,
996
+ socket_timeout_seconds=settings.socket_timeout_seconds,
997
+ operation_max_attempts=settings.operation_max_attempts,
998
+ connection_retry_attempts=settings.connection_retry_attempts,
999
+ retry_base_delay_seconds=settings.retry_base_delay_seconds,
1000
+ retry_max_delay_seconds=settings.retry_max_delay_seconds,
1001
+ acquire_failure_cooldown_seconds=settings.acquire_failure_cooldown_seconds,
1002
+ health_failure_cooldown_seconds=settings.health_failure_cooldown_seconds,
1003
+ )
1004
+
1005
+
1006
+ def describe_foundation_redis_binding(
1007
+ *,
1008
+ project_dir: str | os.PathLike[str] | None = None,
1009
+ settings: FoundationRedisSettings | None = None,
1010
+ ) -> dict[str, Any]:
1011
+ resolved = settings or load_foundation_redis_settings(project_dir=project_dir)
1012
+ configured = bool((resolved.url or "").strip() or (resolved.host or "").strip())
1013
+ return {
1014
+ "enabled": bool(resolved.enabled),
1015
+ "configured": configured,
1016
+ "backend": "redis" if resolved.enabled and configured else "disabled",
1017
+ "operational": False,
1018
+ "degraded": False,
1019
+ "ready": False,
1020
+ "error": None,
1021
+ "host": resolved.host,
1022
+ "port": resolved.port,
1023
+ "db": resolved.db,
1024
+ "url": resolved.url,
1025
+ "probe_deferred": True,
1026
+ }
1027
+
1028
+
1029
+ def probe_foundation_redis_binding(
1030
+ *,
1031
+ project_dir: str | os.PathLike[str] | None = None,
1032
+ settings: FoundationRedisSettings | None = None,
1033
+ capability_name: str = "foundation redis",
1034
+ ) -> dict[str, Any]:
1035
+ status = describe_foundation_redis_binding(project_dir=project_dir, settings=settings)
1036
+ if not status["enabled"] or not status["configured"]:
1037
+ status["probe_deferred"] = False
1038
+ return status
1039
+ try:
1040
+ cache = get_required_foundation_cache(
1041
+ project_dir=project_dir,
1042
+ settings=settings,
1043
+ capability_name=capability_name,
1044
+ )
1045
+ ready = bool(cache.ping())
1046
+ status["ready"] = ready
1047
+ status["operational"] = ready
1048
+ status["degraded"] = not ready
1049
+ status["probe_deferred"] = False
1050
+ return status
1051
+ except Exception as exc:
1052
+ status["error"] = str(exc) or exc.__class__.__name__
1053
+ status["degraded"] = True
1054
+ status["probe_deferred"] = False
1055
+ return status
1056
+
1057
+
1058
+ def reset_foundation_cache() -> None:
1059
+ for cache in list(_CACHE_SINGLETONS.values()):
1060
+ with contextlib.suppress(Exception):
1061
+ cache.close()
1062
+ _CACHE_SINGLETONS.clear()
1063
+ _cached_required_foundation_redis_manager.cache_clear()
1064
+
1065
+
1066
+ def _cache_key(settings: FoundationRedisSettings) -> str:
1067
+ return (
1068
+ f"{settings.url or ''}|{settings.host}|{settings.port}|{settings.db}|"
1069
+ f"{settings.connect_timeout_seconds}|{settings.socket_timeout_seconds}|"
1070
+ f"{settings.operation_max_attempts}|{settings.connection_retry_attempts}|"
1071
+ f"{settings.retry_base_delay_seconds}|{settings.retry_max_delay_seconds}|"
1072
+ f"{settings.acquire_failure_cooldown_seconds}|{settings.health_failure_cooldown_seconds}"
1073
+ )
1074
+
1075
+
1076
+ def _redis_snapshot(
1077
+ *,
1078
+ project_dir: str | os.PathLike[str] | None = None,
1079
+ ):
1080
+ try:
1081
+ return _payload_dict(_foundation_settings_section("cache", project_dir=project_dir))
1082
+ except Exception:
1083
+ return {}
1084
+
1085
+
1086
+ def _payload_dict(payload: Any) -> dict[str, Any]:
1087
+ if isinstance(payload, dict):
1088
+ return dict(payload)
1089
+ if hasattr(payload, "to_dict") and callable(payload.to_dict):
1090
+ try:
1091
+ value = payload.to_dict()
1092
+ except Exception:
1093
+ value = None
1094
+ if isinstance(value, dict):
1095
+ return dict(value)
1096
+ return dict(getattr(payload, "__dict__", {}) or {})
1097
+
1098
+
1099
+ def _read_str(payload: Any, key: str) -> str:
1100
+ if isinstance(payload, dict):
1101
+ value = payload.get(key, "")
1102
+ else:
1103
+ value = getattr(payload, key, "")
1104
+ return str(value or "").strip()
1105
+
1106
+
1107
+ def _read_optional_str(payload: Any, key: str) -> str | None:
1108
+ value = _read_str(payload, key)
1109
+ return value or None
1110
+
1111
+
1112
+ def _read_int(payload: Any, key: str, default: int) -> int:
1113
+ if isinstance(payload, dict):
1114
+ value = payload.get(key, default)
1115
+ else:
1116
+ value = getattr(payload, key, default)
1117
+ try:
1118
+ return int(value if value is not None else default)
1119
+ except Exception:
1120
+ return default
1121
+
1122
+
1123
+ def _read_float(payload: Any, key: str, default: float) -> float:
1124
+ if isinstance(payload, dict):
1125
+ value = payload.get(key, default)
1126
+ else:
1127
+ value = getattr(payload, key, default)
1128
+ try:
1129
+ return float(value if value is not None else default)
1130
+ except Exception:
1131
+ return default
1132
+
1133
+
1134
+ def _read_bool_value(payload: Any, key: str, default: bool) -> bool:
1135
+ if isinstance(payload, dict):
1136
+ value = payload.get(key, default)
1137
+ else:
1138
+ value = getattr(payload, key, default)
1139
+ if value is None:
1140
+ return default
1141
+ if isinstance(value, bool):
1142
+ return value
1143
+ return str(value).strip().lower() in {"1", "true", "yes", "on"}
1144
+
1145
+
1146
+ __all__ = [
1147
+ "CacheService",
1148
+ "describe_foundation_redis_binding",
1149
+ "FoundationRedisUnavailable",
1150
+ "FoundationRedisSettings",
1151
+ "InMemoryFoundationCache",
1152
+ "probe_foundation_redis_binding",
1153
+ "RedisFoundationCache",
1154
+ "SyncCacheService",
1155
+ "SyncRedisAdapter",
1156
+ "build_foundation_redis_config",
1157
+ "build_required_foundation_redis_config",
1158
+ "get_foundation_cache",
1159
+ "get_required_foundation_cache",
1160
+ "get_foundation_redis_manager",
1161
+ "get_required_foundation_redis_connection",
1162
+ "get_required_foundation_redis_manager",
1163
+ "load_foundation_redis_settings",
1164
+ "reset_foundation_cache",
1165
+ "_LazyAccessor",
1166
+ ]