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.
- pz_cli-5.0.0.dist-info/METADATA +51 -0
- pz_cli-5.0.0.dist-info/RECORD +399 -0
- pz_cli-5.0.0.dist-info/WHEEL +4 -0
- pz_cli-5.0.0.dist-info/entry_points.txt +5 -0
- pz_core/__init__.py +64 -0
- pz_core/__main__.py +7 -0
- pz_core/app.py +45 -0
- pz_core/assembly/__init__.py +17 -0
- pz_core/assembly/app_discovery.py +1240 -0
- pz_core/assembly/apps/__init__.py +25 -0
- pz_core/assembly/apps/compiler.py +824 -0
- pz_core/assembly/apps/providers.py +188 -0
- pz_core/assembly/cli/__init__.py +9 -0
- pz_core/assembly/cli/compiler.py +166 -0
- pz_core/assembly/diagnostics/__init__.py +21 -0
- pz_core/assembly/diagnostics/checks.py +257 -0
- pz_core/assembly/diagnostics/dev_sources.py +569 -0
- pz_core/assembly/events/__init__.py +5 -0
- pz_core/assembly/events/compiler.py +219 -0
- pz_core/assembly/extensions/__init__.py +5 -0
- pz_core/assembly/extensions/compiler.py +696 -0
- pz_core/assembly/infra/__init__.py +6 -0
- pz_core/assembly/infra/compiler.py +272 -0
- pz_core/assembly/lifecycle/__init__.py +5 -0
- pz_core/assembly/lifecycle/compiler.py +95 -0
- pz_core/assembly/middlewares/__init__.py +19 -0
- pz_core/assembly/middlewares/compiler.py +508 -0
- pz_core/assembly/project/__init__.py +7 -0
- pz_core/assembly/project/compiler.py +9 -0
- pz_core/assembly/project/context.py +17 -0
- pz_core/assembly/project/discovery.py +227 -0
- pz_core/assembly/resources/__init__.py +19 -0
- pz_core/assembly/resources/compiler.py +310 -0
- pz_core/assembly/resources/settings.py +129 -0
- pz_core/assembly/routes/__init__.py +7 -0
- pz_core/assembly/routes/compiler.py +400 -0
- pz_core/assembly/runtime_manifests/__init__.py +13 -0
- pz_core/assembly/runtime_manifests/compiler.py +159 -0
- pz_core/assembly/settings/__init__.py +17 -0
- pz_core/assembly/settings/compiler.py +786 -0
- pz_core/assembly/settings/host_project_overrides.py +82 -0
- pz_core/assembly/settings/host_runtime_endpoint.py +207 -0
- pz_core/authoring/__init__.py +79 -0
- pz_core/authoring/app_init_service.py +946 -0
- pz_core/authoring/app_versions.py +814 -0
- pz_core/authoring/cli.py +192 -0
- pz_core/authoring/commands.py +1810 -0
- pz_core/authoring/conflicts.py +180 -0
- pz_core/authoring/dev_bindings.py +916 -0
- pz_core/authoring/dev_git.py +1087 -0
- pz_core/authoring/dev_import.py +1061 -0
- pz_core/authoring/dev_manifest.py +522 -0
- pz_core/authoring/dev_sources.py +1638 -0
- pz_core/authoring/env_cli.py +454 -0
- pz_core/authoring/files.py +109 -0
- pz_core/authoring/ide_projection.py +1129 -0
- pz_core/authoring/naming.py +58 -0
- pz_core/authoring/official_dev_sources.py +432 -0
- pz_core/authoring/official_sources.json +156 -0
- pz_core/authoring/orchestration_service.py +558 -0
- pz_core/authoring/project_default.jsonc +53 -0
- pz_core/authoring/project_defaults.py +88 -0
- pz_core/authoring/project_metadata_service.py +199 -0
- pz_core/authoring/project_settings_checks.py +51 -0
- pz_core/authoring/project_settings_cli.py +299 -0
- pz_core/authoring/project_settings_service.py +1700 -0
- pz_core/authoring/project_state.py +399 -0
- pz_core/authoring/publish_git.py +423 -0
- pz_core/authoring/publish_lock.py +188 -0
- pz_core/authoring/publish_source.py +195 -0
- pz_core/authoring/release.py +1382 -0
- pz_core/authoring/repo_keys.py +29 -0
- pz_core/authoring/root_cli.py +28 -0
- pz_core/authoring/scaffold_service.py +325 -0
- pz_core/authoring/sync_service.py +787 -0
- pz_core/authoring/templates/app/__init__.py +1 -0
- pz_core/authoring/templates/app/api.py +25 -0
- pz_core/authoring/templates/app/apps.py +31 -0
- pz_core/authoring/templates/app/auth_package_init.py +14 -0
- pz_core/authoring/templates/app/auth_readme.py +29 -0
- pz_core/authoring/templates/app/configs_package_init.py +14 -0
- pz_core/authoring/templates/app/default_env.py +18 -0
- pz_core/authoring/templates/app/default_jsonc.py +25 -0
- pz_core/authoring/templates/app/locales_readme.py +25 -0
- pz_core/authoring/templates/app/migrations_readme.py +19 -0
- pz_core/authoring/templates/app/models.py +14 -0
- pz_core/authoring/templates/app/package_init.py +8 -0
- pz_core/authoring/templates/app/pages_base.py +161 -0
- pz_core/authoring/templates/app/pages_readme.py +25 -0
- pz_core/authoring/templates/app/public.py +31 -0
- pz_core/authoring/templates/app/pyproject.py +77 -0
- pz_core/authoring/templates/app/readme.py +74 -0
- pz_core/authoring/templates/app/settings.py +46 -0
- pz_core/authoring/templates/app/static_app_js.py +24 -0
- pz_core/authoring/templates/app/static_readme.py +31 -0
- pz_core/authoring/templates/app/template_config_page.py +23 -0
- pz_core/authoring/templates/app/template_index_html.py +28 -0
- pz_core/authoring/templates/app/templates_readme.py +32 -0
- pz_core/authoring/templates/app/urls.py +36 -0
- pz_core/authoring/templates/app/views.py +36 -0
- pz_core/authoring/templates/project/__init__.py +1 -0
- pz_core/authoring/templates/project/agents.py +51 -0
- pz_core/authoring/templates/project/apps_container_readme.py +37 -0
- pz_core/authoring/templates/project/main_py.py +59 -0
- pz_core/authoring/templates/project/project_env.py +19 -0
- pz_core/authoring/templates/project/pyproject.py +26 -0
- pz_core/authoring/templates/project/pz_env_env.py +65 -0
- pz_core/authoring/templates/project/pz_env_jsonc.py +69 -0
- pz_core/authoring/templates/project/pz_jsonc.py +12 -0
- pz_core/authoring/templates/project/readme.py +94 -0
- pz_core/authoring/templates/project/root_gitkeep.py +8 -0
- pz_core/authoring/templates/shared/__init__.py +5 -0
- pz_core/authoring/templates/shared/text.py +10 -0
- pz_core/authoring/venv_service.py +87 -0
- pz_core/authoring/workspace.py +167 -0
- pz_core/cli.py +291 -0
- pz_core/cli_options.py +74 -0
- pz_core/core/__init__.py +3 -0
- pz_core/core/errors/__init__.py +5 -0
- pz_core/core/errors/models.py +117 -0
- pz_core/core/events/__init__.py +5 -0
- pz_core/core/events/registry.py +54 -0
- pz_core/core/extensions/__init__.py +51 -0
- pz_core/core/extensions/registry.py +790 -0
- pz_core/core/health/__init__.py +6 -0
- pz_core/core/health/contracts.py +10 -0
- pz_core/core/middlewares/__init__.py +6 -0
- pz_core/core/middlewares/models.py +24 -0
- pz_core/core/routing/__init__.py +5 -0
- pz_core/core/routing/models.py +21 -0
- pz_core/core/settings_contracts.py +25 -0
- pz_core/docs/README.md +20 -0
- pz_core/docs/distribution-README.md +438 -0
- pz_core/docs/pz/345/221/275/344/273/244/346/200/273/350/247/210.md +1011 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- pz_core/docs/pz/350/247/204/350/214/203/344/270/216/344/275/277/347/224/250/README.md +98 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- pz_core/docs//345/217/221/347/211/210/345/211/215/346/270/205/345/215/225-4.0.11.md +84 -0
- pz_core/docs//346/226/260/346/211/213/346/214/207/345/215/227.md +422 -0
- pz_core/infra/__init__.py +17 -0
- pz_core/infra/db/__init__.py +23 -0
- pz_core/infra/db/contracts.py +73 -0
- pz_core/infra/health/__init__.py +5 -0
- pz_core/infra/health/contracts.py +8 -0
- pz_core/infra/kv/__init__.py +5 -0
- pz_core/infra/kv/contracts.py +17 -0
- pz_core/infra/observability/__init__.py +23 -0
- pz_core/infra/observability/contracts.py +142 -0
- pz_core/infra/resilience/__init__.py +5 -0
- pz_core/infra/resilience/contracts.py +17 -0
- pz_core/infra/resources/__init__.py +5 -0
- pz_core/infra/resources/contracts.py +16 -0
- pz_core/infra/settings/__init__.py +17 -0
- pz_core/infra/settings/models.py +81 -0
- pz_core/infra/storage/__init__.py +5 -0
- pz_core/infra/storage/ports.py +27 -0
- pz_core/project_root_commands.py +282 -0
- pz_core/project_runtime_cli.py +344 -0
- pz_core/project_sources.py +253 -0
- pz_core/pz_launcher.py +209 -0
- pz_core/root_cli.py +70 -0
- pz_core/runtime/__init__.py +229 -0
- pz_core/runtime/access.py +86 -0
- pz_core/runtime/adapters/__init__.py +5 -0
- pz_core/runtime/adapters/base.py +18 -0
- pz_core/runtime/asyncio_bridge.py +18 -0
- pz_core/runtime/bindings/__init__.py +5 -0
- pz_core/runtime/bindings/container.py +50 -0
- pz_core/runtime/cli/__init__.py +5 -0
- pz_core/runtime/cli/_args.py +67 -0
- pz_core/runtime/cli/app_commands.py +417 -0
- pz_core/runtime/cli/check.py +214 -0
- pz_core/runtime/cli/db.py +297 -0
- pz_core/runtime/cli/main.py +89 -0
- pz_core/runtime/cli/resources.py +280 -0
- pz_core/runtime/discovery.py +23 -0
- pz_core/runtime/environment_integrity.py +157 -0
- pz_core/runtime/errors.py +5 -0
- pz_core/runtime/events.py +87 -0
- pz_core/runtime/facades/__init__.py +38 -0
- pz_core/runtime/facades/_capabilities.py +46 -0
- pz_core/runtime/facades/cache.py +103 -0
- pz_core/runtime/facades/database.py +71 -0
- pz_core/runtime/facades/events.py +380 -0
- pz_core/runtime/facades/health.py +101 -0
- pz_core/runtime/facades/mail.py +121 -0
- pz_core/runtime/facades/observability.py +102 -0
- pz_core/runtime/facades/providers.py +75 -0
- pz_core/runtime/facades/resources.py +52 -0
- pz_core/runtime/facades/storage.py +296 -0
- pz_core/runtime/health/__init__.py +5 -0
- pz_core/runtime/health/service.py +102 -0
- pz_core/runtime/host.py +188 -0
- pz_core/runtime/host_runner.py +282 -0
- pz_core/runtime/http/__init__.py +20 -0
- pz_core/runtime/http/app.py +576 -0
- pz_core/runtime/http_timing.py +173 -0
- pz_core/runtime/invocation.py +29 -0
- pz_core/runtime/json_safe.py +47 -0
- pz_core/runtime/lifecycle/__init__.py +25 -0
- pz_core/runtime/lifecycle/manager.py +533 -0
- pz_core/runtime/plan.py +214 -0
- pz_core/runtime/plan_context.py +33 -0
- pz_core/runtime/plan_snapshot.py +177 -0
- pz_core/runtime/registries/__init__.py +27 -0
- pz_core/runtime/registries/capabilities.py +101 -0
- pz_core/runtime/registries/providers.py +396 -0
- pz_core/runtime/resource_assets.py +344 -0
- pz_core/runtime/run_cli.py +211 -0
- pz_core/runtime/settings.py +888 -0
- pz_core/runtime/worker/__init__.py +21 -0
- pz_core/runtime/worker/engine.py +921 -0
- pz_core/runtime/worker/runtime.py +25 -0
- pz_core/testing/__init__.py +15 -0
- pz_core/testing/doc_route_contracts.py +305 -0
- pz_core/utils/__init__.py +44 -0
- pz_core/utils/env.py +65 -0
- pz_core/utils/project_jsonc/__init__.py +29 -0
- pz_core/utils/project_jsonc/document.py +420 -0
- pz_core/utils/project_settings.py +99 -0
- pz_foundation/__init__.py +50 -0
- pz_foundation/_contracts.py +81 -0
- pz_foundation/_errors.py +34 -0
- pz_foundation/api.py +14 -0
- pz_foundation/apps.py +127 -0
- pz_foundation/auth/__init__.py +1 -0
- pz_foundation/cache_control.py +233 -0
- pz_foundation/cli/__init__.py +2 -0
- pz_foundation/cli/workers.py +565 -0
- pz_foundation/configs/__init__.py +0 -0
- pz_foundation/configs/default.env +15 -0
- pz_foundation/configs/default.jsonc +452 -0
- pz_foundation/db/__init__.py +94 -0
- pz_foundation/db/aerich_executor.py +664 -0
- pz_foundation/db/config.py +358 -0
- pz_foundation/db/connection_manager.py +689 -0
- pz_foundation/db/connection_probe.py +204 -0
- pz_foundation/db/database.py +1291 -0
- pz_foundation/db/deps.py +47 -0
- pz_foundation/db/lifecycle_status.py +85 -0
- pz_foundation/db/migrations.py +477 -0
- pz_foundation/db/mysql.py +57 -0
- pz_foundation/db/postgresql.py +409 -0
- pz_foundation/db/production_pool_fallback.py +143 -0
- pz_foundation/db/public.py +45 -0
- pz_foundation/db/session.py +369 -0
- pz_foundation/db/sqlite.py +15 -0
- pz_foundation/db/tortoise_manager.py +724 -0
- pz_foundation/db/tortoise_psycopg.py +148 -0
- pz_foundation/docs/README.md +18 -0
- pz_foundation/docs/distribution-README.md +319 -0
- 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
- 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
- pz_foundation/docs//350/256/276/350/256/241/346/226/207/346/241/243.md +288 -0
- 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
- pz_foundation/health.py +105 -0
- pz_foundation/host/__init__.py +11 -0
- pz_foundation/host/builder.py +34 -0
- pz_foundation/host/cache_runtime.py +44 -0
- pz_foundation/host/db/__init__.py +17 -0
- pz_foundation/host/db/aerich_config.py +36 -0
- pz_foundation/host/db/config.py +106 -0
- pz_foundation/host/db/connection_manager.py +48 -0
- pz_foundation/host/db/deps.py +5 -0
- pz_foundation/host/db/orm_retry_patch.py +566 -0
- pz_foundation/host/db/request_adapter.py +325 -0
- pz_foundation/host/db/router.py +28 -0
- pz_foundation/host/db/session.py +284 -0
- pz_foundation/host/db/tortoise_manager.py +10 -0
- pz_foundation/host/idempotency.py +132 -0
- pz_foundation/host/lifespan.py +247 -0
- pz_foundation/host/redis_runtime.py +100 -0
- pz_foundation/host/web.py +790 -0
- pz_foundation/infra_provider.py +236 -0
- pz_foundation/kv/__init__.py +25 -0
- pz_foundation/kv/cache.py +1166 -0
- pz_foundation/kv/deps.py +84 -0
- pz_foundation/kv/public.py +25 -0
- pz_foundation/kv/redis_manager.py +1531 -0
- pz_foundation/lazy_exports.py +43 -0
- pz_foundation/lazy_imports.py +124 -0
- pz_foundation/lifecycle.py +392 -0
- pz_foundation/managed_workers.py +1117 -0
- pz_foundation/middlewares/__init__.py +26 -0
- pz_foundation/middlewares/_settings.py +250 -0
- pz_foundation/middlewares/analytics.py +124 -0
- pz_foundation/middlewares/cache_control.py +156 -0
- pz_foundation/middlewares/compression.py +202 -0
- pz_foundation/middlewares/cors.py +62 -0
- pz_foundation/middlewares/geo_country.py +44 -0
- pz_foundation/middlewares/inner_router.py +50 -0
- pz_foundation/middlewares/rate_limited/__init__.py +3 -0
- pz_foundation/middlewares/rate_limited/key_limiter.py +115 -0
- pz_foundation/middlewares/request_logging.py +104 -0
- pz_foundation/middlewares/route_owner.py +309 -0
- pz_foundation/middlewares/security_csrf.py +44 -0
- pz_foundation/middlewares/security_headers/__init__.py +4 -0
- pz_foundation/middlewares/security_headers/conflicts.py +44 -0
- pz_foundation/middlewares/security_headers/csp_nonce.py +200 -0
- pz_foundation/middlewares/security_headers/security_headers.py +44 -0
- pz_foundation/migrations/__init__.py +1 -0
- pz_foundation/migrations/pz_foundation/__init__.py +1 -0
- pz_foundation/models.py +22 -0
- pz_foundation/notifications/__init__.py +23 -0
- pz_foundation/notifications/email.py +542 -0
- pz_foundation/notifications/public.py +31 -0
- pz_foundation/observability/__init__.py +71 -0
- pz_foundation/observability/http.py +34 -0
- pz_foundation/observability/logger/__init__.py +49 -0
- pz_foundation/observability/logger/_bot_settings.py +126 -0
- pz_foundation/observability/logger/bot_message.py +592 -0
- pz_foundation/observability/logger/core.py +385 -0
- pz_foundation/observability/logger/delivery_audit.py +84 -0
- pz_foundation/observability/logger/feishu_bot.py +199 -0
- pz_foundation/observability/logger/telegram_bot.py +194 -0
- pz_foundation/observability/metrics.py +365 -0
- pz_foundation/observability/runtime.py +68 -0
- pz_foundation/public.py +405 -0
- pz_foundation/resources/__init__.py +12 -0
- pz_foundation/resources/builder.py +900 -0
- pz_foundation/resources/images.py +363 -0
- pz_foundation/root_cli.py +20 -0
- pz_foundation/routing/__init__.py +203 -0
- pz_foundation/routing/canonical.py +486 -0
- pz_foundation/routing/convertors.py +82 -0
- pz_foundation/routing/declarations.py +843 -0
- pz_foundation/routing/exports.py +148 -0
- pz_foundation/routing/loaders.py +138 -0
- pz_foundation/routing/manifest.py +215 -0
- pz_foundation/routing/manifest_provider.py +47 -0
- pz_foundation/routing/mounts.py +101 -0
- pz_foundation/routing/ordering.py +172 -0
- pz_foundation/routing/registry.py +133 -0
- pz_foundation/routing/utils.py +28 -0
- pz_foundation/runtime.py +234 -0
- pz_foundation/settings.py +253 -0
- pz_foundation/settings_normalization.py +130 -0
- pz_foundation/storage/__init__.py +25 -0
- pz_foundation/storage/local.py +209 -0
- pz_foundation/storage/public.py +106 -0
- pz_foundation/storage/s3.py +116 -0
- pz_foundation/storage/service.py +234 -0
- pz_foundation/urls.py +19 -0
- pz_foundation/utils/__init__.py +6 -0
- pz_foundation/utils/merge.py +160 -0
- pz_foundation/worker_replicas.py +55 -0
- pz_settings_center/__init__.py +5 -0
- pz_settings_center/api/__init__.py +5 -0
- pz_settings_center/api/runtime_overrides.py +360 -0
- pz_settings_center/apps.py +28 -0
- pz_settings_center/configs/__init__.py +0 -0
- pz_settings_center/configs/default.env +2 -0
- pz_settings_center/configs/default.jsonc +1 -0
- pz_settings_center/docs/01-settings_center/350/203/275/345/212/233/350/276/271/347/225/214.md +124 -0
- pz_settings_center/docs/README.md +12 -0
- pz_settings_center/docs/distribution-README.md +150 -0
- pz_settings_center/docs//350/256/276/350/256/241/346/226/207/346/241/243.md +48 -0
- pz_settings_center/integration_payload.py +18 -0
- pz_settings_center/project_helpers.py +405 -0
- pz_settings_center/public.py +139 -0
- pz_settings_center/routing_helpers.py +73 -0
- pz_settings_center/runtime_override_provider.py +61 -0
- pz_settings_center/typed_settings.py +324 -0
- 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"]
|