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,1291 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
import importlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from os import PathLike
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
import sqlite3
|
|
12
|
+
from threading import RLock
|
|
13
|
+
from typing import Any, Protocol
|
|
14
|
+
from urllib.parse import parse_qs, quote, unquote, urlencode, urlparse
|
|
15
|
+
|
|
16
|
+
import pz_settings_center.public as settings_center
|
|
17
|
+
from pz_core.assembly.project import resolve_project_context
|
|
18
|
+
|
|
19
|
+
from .lifecycle_status import DBConnectionLifecycleStatus, connection_lifecycle_status
|
|
20
|
+
from .tortoise_manager import tortoise_manager
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class FoundationDatabaseSettings:
|
|
25
|
+
enabled: bool = False
|
|
26
|
+
driver: str = "sqlite"
|
|
27
|
+
path: str = ""
|
|
28
|
+
urls: tuple[str, ...] = ()
|
|
29
|
+
host: str = ""
|
|
30
|
+
port: int = 0
|
|
31
|
+
hosts: tuple[str, ...] = ()
|
|
32
|
+
ports: tuple[int, ...] = ()
|
|
33
|
+
user: str = ""
|
|
34
|
+
password: str = ""
|
|
35
|
+
database: str = ""
|
|
36
|
+
min_size: int = 1
|
|
37
|
+
max_size: int = 10
|
|
38
|
+
connect_timeout: float = 5.0
|
|
39
|
+
pool_acquire_timeout: float = 5.0
|
|
40
|
+
statement_timeout_ms: int = 30000
|
|
41
|
+
idle_in_transaction_session_timeout_ms: int = 30000
|
|
42
|
+
pool_max_lifetime: float = 3600.0
|
|
43
|
+
pool_max_idle: float = 600.0
|
|
44
|
+
reconnect_timeout: float = 30.0
|
|
45
|
+
health_check_interval: float = 60.0
|
|
46
|
+
ssl_mode: str = ""
|
|
47
|
+
ssl_root_cert: str = ""
|
|
48
|
+
ssl_cert: str = ""
|
|
49
|
+
ssl_key: str = ""
|
|
50
|
+
ssl_password: str = ""
|
|
51
|
+
target_session_attrs: str = ""
|
|
52
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FoundationDatabase(Protocol):
|
|
56
|
+
settings: FoundationDatabaseSettings
|
|
57
|
+
driver: str
|
|
58
|
+
|
|
59
|
+
def create_session(self): ...
|
|
60
|
+
|
|
61
|
+
def readiness_probe(self): ...
|
|
62
|
+
|
|
63
|
+
def executescript(self, script: str) -> None: ...
|
|
64
|
+
|
|
65
|
+
def close(self) -> None: ...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _resolved_settings(*, project_dir: str | PathLike[str] | None = None):
|
|
69
|
+
if project_dir is None:
|
|
70
|
+
return settings_center.settings
|
|
71
|
+
return settings_center.resolve_settings(project_dir=str(Path(project_dir).resolve()))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _project_settings(*, project_dir: str | PathLike[str] | None = None):
|
|
75
|
+
try:
|
|
76
|
+
return _resolved_settings(project_dir=project_dir).project
|
|
77
|
+
except Exception:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _foundation_settings_section(
|
|
82
|
+
name: str,
|
|
83
|
+
*,
|
|
84
|
+
project_dir: str | PathLike[str] | None = None,
|
|
85
|
+
):
|
|
86
|
+
section_name = str(name or "").strip()
|
|
87
|
+
try:
|
|
88
|
+
resolved = _resolved_settings(project_dir=project_dir)
|
|
89
|
+
if hasattr(resolved, "app_settings"):
|
|
90
|
+
foundation_payload = resolved.app_settings("pz_foundation")
|
|
91
|
+
if isinstance(foundation_payload, dict):
|
|
92
|
+
section = foundation_payload.get(section_name, {})
|
|
93
|
+
if section not in (None, {}):
|
|
94
|
+
return section
|
|
95
|
+
foundation_settings = getattr(resolved, "pz_foundation", None)
|
|
96
|
+
if isinstance(foundation_settings, dict):
|
|
97
|
+
return foundation_settings.get(section_name, {})
|
|
98
|
+
if foundation_settings is None:
|
|
99
|
+
return {}
|
|
100
|
+
return getattr(foundation_settings, section_name, {})
|
|
101
|
+
except Exception:
|
|
102
|
+
return {}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(slots=True)
|
|
106
|
+
class SQLiteFoundationSession:
|
|
107
|
+
connection: Any
|
|
108
|
+
driver: str = "sqlite"
|
|
109
|
+
_transaction_active: bool = False
|
|
110
|
+
_lock: RLock | None = None
|
|
111
|
+
|
|
112
|
+
def begin(self) -> SQLiteFoundationTransaction:
|
|
113
|
+
if self._transaction_active:
|
|
114
|
+
raise RuntimeError("nested foundation transactions are not supported")
|
|
115
|
+
if self._lock is not None:
|
|
116
|
+
self._lock.acquire()
|
|
117
|
+
try:
|
|
118
|
+
executor = getattr(self.connection, "execute", None)
|
|
119
|
+
if callable(executor):
|
|
120
|
+
executor("BEGIN")
|
|
121
|
+
else:
|
|
122
|
+
self.connection.cursor().execute("BEGIN")
|
|
123
|
+
self._transaction_active = True
|
|
124
|
+
except Exception:
|
|
125
|
+
if self._lock is not None:
|
|
126
|
+
self._lock.release()
|
|
127
|
+
raise
|
|
128
|
+
return SQLiteFoundationTransaction(self, self.connection, lock=self._lock)
|
|
129
|
+
|
|
130
|
+
def execute(self, statement: str, *args: Any, **kwargs: Any) -> Any:
|
|
131
|
+
acquired_lock = False
|
|
132
|
+
if not self._transaction_active and self._lock is not None:
|
|
133
|
+
self._lock.acquire()
|
|
134
|
+
acquired_lock = True
|
|
135
|
+
try:
|
|
136
|
+
cursor = self.connection.cursor()
|
|
137
|
+
sql, params = _prepare_statement(self.driver, statement, args, kwargs)
|
|
138
|
+
if params is not None:
|
|
139
|
+
cursor.execute(sql, params)
|
|
140
|
+
else:
|
|
141
|
+
cursor.execute(sql)
|
|
142
|
+
description = getattr(cursor, "description", None)
|
|
143
|
+
if description:
|
|
144
|
+
rows = cursor.fetchall()
|
|
145
|
+
if rows and isinstance(rows[0], sqlite3.Row):
|
|
146
|
+
if not self._transaction_active:
|
|
147
|
+
self.connection.commit()
|
|
148
|
+
return rows
|
|
149
|
+
if not self._transaction_active:
|
|
150
|
+
self.connection.commit()
|
|
151
|
+
return [_coerce_dbapi_row(description, row) for row in rows]
|
|
152
|
+
if not self._transaction_active:
|
|
153
|
+
self.connection.commit()
|
|
154
|
+
return int(getattr(cursor, "rowcount", 0) or 0)
|
|
155
|
+
except Exception:
|
|
156
|
+
if not self._transaction_active:
|
|
157
|
+
with contextlib.suppress(Exception):
|
|
158
|
+
self.connection.rollback()
|
|
159
|
+
raise
|
|
160
|
+
finally:
|
|
161
|
+
if acquired_lock and self._lock is not None:
|
|
162
|
+
self._lock.release()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(slots=True)
|
|
166
|
+
class SQLiteFoundationTransaction:
|
|
167
|
+
session: SQLiteFoundationSession
|
|
168
|
+
connection: Any
|
|
169
|
+
lock: RLock | None = None
|
|
170
|
+
_active: bool = True
|
|
171
|
+
|
|
172
|
+
def commit(self) -> None:
|
|
173
|
+
if not self._active:
|
|
174
|
+
return
|
|
175
|
+
self.connection.commit()
|
|
176
|
+
self._active = False
|
|
177
|
+
self.session._transaction_active = False
|
|
178
|
+
if self.lock is not None:
|
|
179
|
+
self.lock.release()
|
|
180
|
+
|
|
181
|
+
def rollback(self) -> None:
|
|
182
|
+
if not self._active:
|
|
183
|
+
return
|
|
184
|
+
self.connection.rollback()
|
|
185
|
+
self._active = False
|
|
186
|
+
self.session._transaction_active = False
|
|
187
|
+
if self.lock is not None:
|
|
188
|
+
self.lock.release()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(slots=True)
|
|
192
|
+
class FoundationDBAPINonTransactionalReadinessProbe:
|
|
193
|
+
connection: Any
|
|
194
|
+
|
|
195
|
+
def check_ready(self) -> bool:
|
|
196
|
+
try:
|
|
197
|
+
executor = getattr(self.connection, "execute", None)
|
|
198
|
+
if callable(executor):
|
|
199
|
+
executor("SELECT 1")
|
|
200
|
+
else:
|
|
201
|
+
self.connection.cursor().execute("SELECT 1")
|
|
202
|
+
except Exception:
|
|
203
|
+
return False
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
SQLiteFoundationReadinessProbe = FoundationDBAPINonTransactionalReadinessProbe
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass(slots=True)
|
|
211
|
+
class SQLiteFoundationDatabase:
|
|
212
|
+
settings: FoundationDatabaseSettings
|
|
213
|
+
_connection: Any = field(init=False, repr=False)
|
|
214
|
+
_lock: RLock = field(default_factory=RLock, init=False, repr=False)
|
|
215
|
+
|
|
216
|
+
def __post_init__(self) -> None:
|
|
217
|
+
if not self.settings.enabled:
|
|
218
|
+
raise RuntimeError("foundation database is disabled")
|
|
219
|
+
driver = _normalize_driver(self.settings.driver, url=_primary_database_url(self.settings))
|
|
220
|
+
if driver != "sqlite":
|
|
221
|
+
raise RuntimeError(f"unsupported foundation database driver: {self.settings.driver!r}")
|
|
222
|
+
db_path = Path(self.settings.path).expanduser()
|
|
223
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
224
|
+
self._connection = sqlite3.connect(db_path, check_same_thread=False)
|
|
225
|
+
self._connection.row_factory = sqlite3.Row
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def driver(self) -> str:
|
|
229
|
+
return "sqlite"
|
|
230
|
+
|
|
231
|
+
def create_session(self) -> SQLiteFoundationSession:
|
|
232
|
+
return SQLiteFoundationSession(self._connection, driver="sqlite", _lock=self._lock)
|
|
233
|
+
|
|
234
|
+
def readiness_probe(self) -> FoundationDBAPINonTransactionalReadinessProbe:
|
|
235
|
+
return FoundationDBAPINonTransactionalReadinessProbe(self._connection)
|
|
236
|
+
|
|
237
|
+
def executescript(self, script: str) -> None:
|
|
238
|
+
self._connection.executescript(script)
|
|
239
|
+
self._connection.commit()
|
|
240
|
+
|
|
241
|
+
def close(self) -> None:
|
|
242
|
+
with contextlib.suppress(Exception):
|
|
243
|
+
self._connection.close()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass(slots=True)
|
|
247
|
+
class _DBAPIFoundationDatabase:
|
|
248
|
+
settings: FoundationDatabaseSettings
|
|
249
|
+
_connection: Any = field(init=False, repr=False)
|
|
250
|
+
_lock: RLock = field(default_factory=RLock, init=False, repr=False)
|
|
251
|
+
|
|
252
|
+
def __post_init__(self) -> None:
|
|
253
|
+
if not self.settings.enabled:
|
|
254
|
+
raise RuntimeError("foundation database is disabled")
|
|
255
|
+
self._connection = self._open_connection()
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def driver(self) -> str:
|
|
259
|
+
return _normalize_driver(self.settings.driver, url=_primary_database_url(self.settings))
|
|
260
|
+
|
|
261
|
+
def create_session(self) -> SQLiteFoundationSession:
|
|
262
|
+
return SQLiteFoundationSession(
|
|
263
|
+
self._connection,
|
|
264
|
+
driver=_normalize_driver(self.settings.driver, url=_primary_database_url(self.settings)),
|
|
265
|
+
_lock=self._lock,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def readiness_probe(self) -> FoundationDBAPINonTransactionalReadinessProbe:
|
|
269
|
+
return FoundationDBAPINonTransactionalReadinessProbe(self._connection)
|
|
270
|
+
|
|
271
|
+
def executescript(self, script: str) -> None:
|
|
272
|
+
for statement in _split_sql_statements(script):
|
|
273
|
+
if not statement:
|
|
274
|
+
continue
|
|
275
|
+
self.create_session().execute(statement)
|
|
276
|
+
|
|
277
|
+
def close(self) -> None:
|
|
278
|
+
with contextlib.suppress(Exception):
|
|
279
|
+
self._connection.close()
|
|
280
|
+
|
|
281
|
+
def _open_connection(self) -> Any:
|
|
282
|
+
raise NotImplementedError
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass(slots=True)
|
|
286
|
+
class MySQLFoundationDatabase(_DBAPIFoundationDatabase):
|
|
287
|
+
def _open_connection(self) -> Any:
|
|
288
|
+
settings = _resolve_database_settings(self.settings)
|
|
289
|
+
host, port = _primary_endpoint(settings, default_port=3306)
|
|
290
|
+
if not host:
|
|
291
|
+
raise RuntimeError("foundation mysql host is not configured")
|
|
292
|
+
try:
|
|
293
|
+
module = importlib.import_module("pymysql")
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
raise RuntimeError("pymysql is required for foundation mysql driver") from exc
|
|
296
|
+
kwargs: dict[str, Any] = {
|
|
297
|
+
"host": host,
|
|
298
|
+
"port": port,
|
|
299
|
+
"user": settings.user,
|
|
300
|
+
"password": settings.password,
|
|
301
|
+
"database": settings.database,
|
|
302
|
+
"charset": "utf8mb4",
|
|
303
|
+
}
|
|
304
|
+
cursor_cls = getattr(getattr(module, "cursors", None), "DictCursor", None)
|
|
305
|
+
if cursor_cls is not None:
|
|
306
|
+
kwargs["cursorclass"] = cursor_cls
|
|
307
|
+
if settings.connect_timeout > 0:
|
|
308
|
+
kwargs["connect_timeout"] = int(max(1, settings.connect_timeout))
|
|
309
|
+
kwargs.update(dict(settings.options or {}))
|
|
310
|
+
kwargs["autocommit"] = True
|
|
311
|
+
return module.connect(**kwargs)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@dataclass(slots=True)
|
|
315
|
+
class PostgreSQLFoundationDatabase(_DBAPIFoundationDatabase):
|
|
316
|
+
def _open_connection(self) -> Any:
|
|
317
|
+
settings = _resolve_database_settings(self.settings)
|
|
318
|
+
try:
|
|
319
|
+
module = importlib.import_module("psycopg")
|
|
320
|
+
except Exception as exc:
|
|
321
|
+
raise RuntimeError("psycopg is required for foundation postgresql driver") from exc
|
|
322
|
+
kwargs: dict[str, Any] = {}
|
|
323
|
+
with contextlib.suppress(Exception):
|
|
324
|
+
rows_module = importlib.import_module("psycopg.rows")
|
|
325
|
+
kwargs["row_factory"] = rows_module.dict_row
|
|
326
|
+
if settings.connect_timeout > 0:
|
|
327
|
+
kwargs["connect_timeout"] = int(max(1, settings.connect_timeout))
|
|
328
|
+
kwargs.update(_build_postgresql_ssl_kwargs(settings))
|
|
329
|
+
kwargs.update(dict(settings.options or {}))
|
|
330
|
+
kwargs["autocommit"] = True
|
|
331
|
+
return module.connect(_build_postgresql_dsn(settings), **kwargs)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
_DATABASE_SINGLETONS: dict[str, FoundationDatabase] = {}
|
|
335
|
+
_NAMED_PARAM_PATTERN = re.compile(r"(?<!:):([A-Za-z_][A-Za-z0-9_]*)")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def load_foundation_database_settings(
|
|
339
|
+
*,
|
|
340
|
+
project_dir: str | PathLike[str] | None = None,
|
|
341
|
+
) -> FoundationDatabaseSettings:
|
|
342
|
+
return _resolve_foundation_database_settings(project_dir=project_dir)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _database_binding_configured(settings: FoundationDatabaseSettings) -> bool:
|
|
346
|
+
if not settings.enabled:
|
|
347
|
+
return False
|
|
348
|
+
if tuple(getattr(settings, "urls", ()) or ()):
|
|
349
|
+
return True
|
|
350
|
+
driver = _normalize_driver(settings.driver, url=_primary_database_url(settings))
|
|
351
|
+
if driver == "sqlite":
|
|
352
|
+
return bool(str(settings.path or "").strip())
|
|
353
|
+
if driver in {"mysql", "postgresql"}:
|
|
354
|
+
return bool(str(settings.host or "").strip() or settings.hosts) and bool(
|
|
355
|
+
str(settings.database or "").strip()
|
|
356
|
+
)
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def describe_foundation_database_binding(
|
|
361
|
+
*,
|
|
362
|
+
project_dir: str | PathLike[str] | None = None,
|
|
363
|
+
settings: FoundationDatabaseSettings | None = None,
|
|
364
|
+
) -> dict[str, Any]:
|
|
365
|
+
resolved = settings or _resolve_foundation_database_settings(project_dir=project_dir)
|
|
366
|
+
configured = _database_binding_configured(resolved)
|
|
367
|
+
return {
|
|
368
|
+
"enabled": bool(resolved.enabled),
|
|
369
|
+
"configured": configured,
|
|
370
|
+
"driver": resolved.driver,
|
|
371
|
+
"path": resolved.path,
|
|
372
|
+
"urls": tuple(resolved.urls),
|
|
373
|
+
"host": resolved.host,
|
|
374
|
+
"port": resolved.port,
|
|
375
|
+
"hosts": tuple(resolved.hosts),
|
|
376
|
+
"ports": tuple(resolved.ports),
|
|
377
|
+
"database": resolved.database,
|
|
378
|
+
"user": resolved.user,
|
|
379
|
+
"target_session_attrs": resolved.target_session_attrs,
|
|
380
|
+
"operational": False,
|
|
381
|
+
"degraded": False,
|
|
382
|
+
"ready": False,
|
|
383
|
+
"error": None,
|
|
384
|
+
"probe_deferred": True,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def probe_foundation_database_binding(
|
|
389
|
+
*,
|
|
390
|
+
project_dir: str | PathLike[str] | None = None,
|
|
391
|
+
settings: FoundationDatabaseSettings | None = None,
|
|
392
|
+
) -> dict[str, Any]:
|
|
393
|
+
status = describe_foundation_database_binding(project_dir=project_dir, settings=settings)
|
|
394
|
+
if not status["enabled"] or not status["configured"]:
|
|
395
|
+
status["probe_deferred"] = False
|
|
396
|
+
return status
|
|
397
|
+
try:
|
|
398
|
+
lifecycle = connection_lifecycle_status(tortoise_manager)
|
|
399
|
+
status["lifecycle"] = _lifecycle_status_payload(lifecycle)
|
|
400
|
+
if lifecycle.default_ready:
|
|
401
|
+
status["ready"] = True
|
|
402
|
+
status["operational"] = True
|
|
403
|
+
status["probe_deferred"] = False
|
|
404
|
+
else:
|
|
405
|
+
status["probe_deferred"] = True
|
|
406
|
+
return status
|
|
407
|
+
except Exception as exc:
|
|
408
|
+
status["error"] = str(exc)
|
|
409
|
+
status["probe_deferred"] = False
|
|
410
|
+
return status
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _lifecycle_status_payload(lifecycle: DBConnectionLifecycleStatus) -> dict[str, Any]:
|
|
414
|
+
return {
|
|
415
|
+
"initialized": lifecycle.initialized,
|
|
416
|
+
"loop_matches": lifecycle.loop_matches,
|
|
417
|
+
"default_configured": lifecycle.default_configured,
|
|
418
|
+
"default_open": lifecycle.default_open,
|
|
419
|
+
"read_only_configured": lifecycle.read_only_configured,
|
|
420
|
+
"read_only_open": lifecycle.read_only_open,
|
|
421
|
+
"default_status": lifecycle.alias_status("default"),
|
|
422
|
+
"read_only_status": lifecycle.alias_status("read_only"),
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def normalize_foundation_database_settings(settings: FoundationDatabaseSettings) -> FoundationDatabaseSettings:
|
|
427
|
+
return _resolve_database_settings(settings)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _resolve_foundation_database_settings(
|
|
431
|
+
*,
|
|
432
|
+
project_dir: str | PathLike[str] | None = None,
|
|
433
|
+
) -> FoundationDatabaseSettings:
|
|
434
|
+
database_payload = _database_snapshot(project_dir=project_dir)
|
|
435
|
+
urls = _read_str_sequence(database_payload, "urls")
|
|
436
|
+
raw_driver = _read_str_value(database_payload, "driver")
|
|
437
|
+
driver = _normalize_driver(raw_driver or "", url="" if raw_driver else (urls[0] if urls else ""))
|
|
438
|
+
enabled = _read_bool_value(database_payload, "enabled", False)
|
|
439
|
+
explicit_path = _read_str_value(database_payload, "path")
|
|
440
|
+
sqlite_path = _sqlite_path_from_url(urls[0] if urls else "")
|
|
441
|
+
default_sqlite_path = str(
|
|
442
|
+
resolve_project_context(_resolve_project_dir(project_dir)).asset_dir / "foundation" / "foundation.sqlite3"
|
|
443
|
+
)
|
|
444
|
+
path = explicit_path or sqlite_path
|
|
445
|
+
if driver == "sqlite" and not path:
|
|
446
|
+
path = default_sqlite_path
|
|
447
|
+
host = _read_str_value(database_payload, "host")
|
|
448
|
+
raw_port = _read_str_value(database_payload, "port")
|
|
449
|
+
ports = _coerce_port_values(raw_port)
|
|
450
|
+
port = ports[0] if ports else 0
|
|
451
|
+
user = _read_str_value(database_payload, "user")
|
|
452
|
+
password = _read_str_value(database_payload, "password")
|
|
453
|
+
database_name = _read_str_value(database_payload, "database")
|
|
454
|
+
min_size = _read_alias_int(database_payload, ("min_size",), 1)
|
|
455
|
+
max_size = _read_alias_int(database_payload, ("max_size",), 10)
|
|
456
|
+
connect_timeout = _read_alias_float(database_payload, ("connect_timeout",), 5.0)
|
|
457
|
+
pool_acquire_timeout = _read_alias_float(database_payload, ("pool_acquire_timeout", "pool_timeout"), 5.0)
|
|
458
|
+
statement_timeout_ms = _read_alias_int(database_payload, ("statement_timeout_ms",), 30000)
|
|
459
|
+
idle_in_transaction_session_timeout_ms = _read_alias_int(
|
|
460
|
+
database_payload,
|
|
461
|
+
("idle_in_transaction_session_timeout_ms",),
|
|
462
|
+
30000,
|
|
463
|
+
)
|
|
464
|
+
pool_max_lifetime = _read_alias_float(database_payload, ("pool_max_lifetime",), 3600.0)
|
|
465
|
+
pool_max_idle = _read_alias_float(database_payload, ("pool_max_idle",), 600.0)
|
|
466
|
+
reconnect_timeout = _read_alias_float(database_payload, ("reconnect_timeout",), 30.0)
|
|
467
|
+
health_check_interval = _read_alias_float(database_payload, ("health_check_interval",), 60.0)
|
|
468
|
+
ssl_mode = _read_str_value(database_payload, "ssl_mode")
|
|
469
|
+
ssl_root_cert = _read_str_value(database_payload, "ssl_root_cert")
|
|
470
|
+
ssl_cert = _read_str_value(database_payload, "ssl_cert")
|
|
471
|
+
ssl_key = _read_str_value(database_payload, "ssl_key")
|
|
472
|
+
ssl_password = _read_str_value(database_payload, "ssl_password")
|
|
473
|
+
target_session_attrs = _read_str_value(database_payload, "target_session_attrs")
|
|
474
|
+
settings = FoundationDatabaseSettings(
|
|
475
|
+
enabled=enabled,
|
|
476
|
+
driver=driver,
|
|
477
|
+
path=path,
|
|
478
|
+
urls=urls,
|
|
479
|
+
host=host,
|
|
480
|
+
port=port,
|
|
481
|
+
ports=ports,
|
|
482
|
+
user=user,
|
|
483
|
+
password=password,
|
|
484
|
+
database=database_name,
|
|
485
|
+
min_size=min_size,
|
|
486
|
+
max_size=max_size,
|
|
487
|
+
connect_timeout=connect_timeout,
|
|
488
|
+
pool_acquire_timeout=pool_acquire_timeout,
|
|
489
|
+
statement_timeout_ms=statement_timeout_ms,
|
|
490
|
+
idle_in_transaction_session_timeout_ms=idle_in_transaction_session_timeout_ms,
|
|
491
|
+
pool_max_lifetime=pool_max_lifetime,
|
|
492
|
+
pool_max_idle=pool_max_idle,
|
|
493
|
+
reconnect_timeout=reconnect_timeout,
|
|
494
|
+
health_check_interval=health_check_interval,
|
|
495
|
+
ssl_mode=ssl_mode,
|
|
496
|
+
ssl_root_cert=ssl_root_cert,
|
|
497
|
+
ssl_cert=ssl_cert,
|
|
498
|
+
ssl_key=ssl_key,
|
|
499
|
+
ssl_password=ssl_password,
|
|
500
|
+
target_session_attrs=target_session_attrs,
|
|
501
|
+
)
|
|
502
|
+
return _resolve_database_settings(settings)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def get_foundation_database(
|
|
506
|
+
*,
|
|
507
|
+
project_dir: str | PathLike[str] | None = None,
|
|
508
|
+
settings: FoundationDatabaseSettings | None = None,
|
|
509
|
+
) -> FoundationDatabase:
|
|
510
|
+
resolved_settings = _resolve_database_settings(
|
|
511
|
+
settings or _resolve_foundation_database_settings(project_dir=project_dir)
|
|
512
|
+
)
|
|
513
|
+
key = _database_singleton_key(resolved_settings)
|
|
514
|
+
database = _DATABASE_SINGLETONS.get(key)
|
|
515
|
+
if database is None:
|
|
516
|
+
if resolved_settings.driver == "sqlite":
|
|
517
|
+
database = SQLiteFoundationDatabase(resolved_settings)
|
|
518
|
+
elif resolved_settings.driver == "mysql":
|
|
519
|
+
database = MySQLFoundationDatabase(resolved_settings)
|
|
520
|
+
elif resolved_settings.driver == "postgresql":
|
|
521
|
+
database = PostgreSQLFoundationDatabase(resolved_settings)
|
|
522
|
+
else:
|
|
523
|
+
raise RuntimeError(f"unsupported foundation database driver: {resolved_settings.driver!r}")
|
|
524
|
+
_DATABASE_SINGLETONS[key] = database
|
|
525
|
+
return database
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def get_foundation_database_if_enabled(
|
|
529
|
+
*,
|
|
530
|
+
project_dir: str | PathLike[str] | None = None,
|
|
531
|
+
settings: FoundationDatabaseSettings | None = None,
|
|
532
|
+
) -> FoundationDatabase | None:
|
|
533
|
+
resolved_settings = _resolve_database_settings(
|
|
534
|
+
settings or _resolve_foundation_database_settings(project_dir=project_dir)
|
|
535
|
+
)
|
|
536
|
+
if not resolved_settings.enabled:
|
|
537
|
+
return None
|
|
538
|
+
return get_foundation_database(project_dir=project_dir, settings=resolved_settings)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def reset_foundation_databases() -> None:
|
|
542
|
+
for database in list(_DATABASE_SINGLETONS.values()):
|
|
543
|
+
with contextlib.suppress(Exception):
|
|
544
|
+
database.close()
|
|
545
|
+
_DATABASE_SINGLETONS.clear()
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _database_snapshot(
|
|
549
|
+
*,
|
|
550
|
+
project_dir: str | PathLike[str] | None = None,
|
|
551
|
+
):
|
|
552
|
+
try:
|
|
553
|
+
payload = _payload_dict(_foundation_settings_section("database", project_dir=project_dir))
|
|
554
|
+
except Exception:
|
|
555
|
+
payload = {}
|
|
556
|
+
return payload
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _resolve_project_dir(project_dir: str | PathLike[str] | None) -> Path:
|
|
560
|
+
if project_dir is not None:
|
|
561
|
+
return resolve_project_context(project_dir).project_dir.resolve()
|
|
562
|
+
try:
|
|
563
|
+
project = _project_settings()
|
|
564
|
+
return resolve_project_context(str(getattr(project, "project_dir", ".") or ".")).project_dir.resolve()
|
|
565
|
+
except Exception:
|
|
566
|
+
return resolve_project_context().project_dir.resolve()
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _primary_database_url(settings: FoundationDatabaseSettings) -> str:
|
|
570
|
+
urls = tuple(getattr(settings, "urls", ()) or ())
|
|
571
|
+
return str(urls[0] or "").strip() if urls else ""
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _payload_dict(payload: Any) -> dict[str, Any]:
|
|
575
|
+
if isinstance(payload, dict):
|
|
576
|
+
return dict(payload)
|
|
577
|
+
if hasattr(payload, "to_dict") and callable(payload.to_dict):
|
|
578
|
+
try:
|
|
579
|
+
value = payload.to_dict()
|
|
580
|
+
except Exception:
|
|
581
|
+
value = None
|
|
582
|
+
if isinstance(value, dict):
|
|
583
|
+
return dict(value)
|
|
584
|
+
return dict(getattr(payload, "__dict__", {}) or {})
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _read_bool_value(payload: Any, key: str, default: bool) -> bool:
|
|
588
|
+
value = payload.get(key, default) if isinstance(payload, dict) else getattr(payload, key, default)
|
|
589
|
+
if value is None:
|
|
590
|
+
return default
|
|
591
|
+
if isinstance(value, bool):
|
|
592
|
+
return value
|
|
593
|
+
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _read_str_value(payload: Any, key: str) -> str:
|
|
597
|
+
value = payload.get(key) if isinstance(payload, dict) else getattr(payload, key, None)
|
|
598
|
+
text = str(value or "").strip()
|
|
599
|
+
if text in {'""', "''"}:
|
|
600
|
+
return ""
|
|
601
|
+
return text
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _read_alias_int(payload: Any, keys: tuple[str, ...], default: int) -> int:
|
|
605
|
+
for key in keys:
|
|
606
|
+
value = payload.get(key) if isinstance(payload, dict) else getattr(payload, key, None)
|
|
607
|
+
if value is None or str(value).strip() == "":
|
|
608
|
+
continue
|
|
609
|
+
try:
|
|
610
|
+
return int(value)
|
|
611
|
+
except Exception:
|
|
612
|
+
continue
|
|
613
|
+
return int(default)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _resolve_database_settings(settings: FoundationDatabaseSettings) -> FoundationDatabaseSettings:
|
|
617
|
+
raw_driver = str(settings.driver or "").strip()
|
|
618
|
+
urls = _coerce_database_url_values(getattr(settings, "urls", ()))
|
|
619
|
+
driver = _normalize_driver(raw_driver, url="" if raw_driver else (urls[0] if urls else ""))
|
|
620
|
+
host = settings.host
|
|
621
|
+
port = int(settings.port or 0)
|
|
622
|
+
hosts = _coerce_host_values(getattr(settings, "hosts", ()))
|
|
623
|
+
ports = _coerce_port_values(getattr(settings, "ports", ()))
|
|
624
|
+
user = settings.user
|
|
625
|
+
password = settings.password
|
|
626
|
+
database_name = settings.database
|
|
627
|
+
min_size = int(getattr(settings, "min_size", 1) or 1)
|
|
628
|
+
max_size = int(getattr(settings, "max_size", 10) or 10)
|
|
629
|
+
pool_acquire_timeout = float(getattr(settings, "pool_acquire_timeout", 5.0) or 0.0)
|
|
630
|
+
statement_timeout_ms = int(getattr(settings, "statement_timeout_ms", 30000) or 0)
|
|
631
|
+
idle_in_transaction_session_timeout_ms = int(
|
|
632
|
+
getattr(settings, "idle_in_transaction_session_timeout_ms", 30000) or 0
|
|
633
|
+
)
|
|
634
|
+
pool_max_lifetime = float(getattr(settings, "pool_max_lifetime", 3600.0) or 0.0)
|
|
635
|
+
pool_max_idle = float(getattr(settings, "pool_max_idle", 600.0) or 0.0)
|
|
636
|
+
reconnect_timeout = float(getattr(settings, "reconnect_timeout", 30.0) or 0.0)
|
|
637
|
+
health_check_interval = float(getattr(settings, "health_check_interval", 60.0) or 0.0)
|
|
638
|
+
path = settings.path
|
|
639
|
+
ssl_mode = settings.ssl_mode
|
|
640
|
+
ssl_root_cert = settings.ssl_root_cert
|
|
641
|
+
ssl_cert = settings.ssl_cert
|
|
642
|
+
ssl_key = settings.ssl_key
|
|
643
|
+
ssl_password = settings.ssl_password
|
|
644
|
+
target_session_attrs = str(getattr(settings, "target_session_attrs", "") or "").strip()
|
|
645
|
+
options = dict(settings.options or {})
|
|
646
|
+
url_sources = urls
|
|
647
|
+
parsed_url_payloads: list[dict[str, Any]] = []
|
|
648
|
+
for url_value in url_sources:
|
|
649
|
+
try:
|
|
650
|
+
parsed_url_payloads.append(_parse_database_url(url_value))
|
|
651
|
+
except Exception:
|
|
652
|
+
raise
|
|
653
|
+
if parsed_url_payloads:
|
|
654
|
+
first = parsed_url_payloads[0]
|
|
655
|
+
parsed_driver = _normalize_driver(first["driver"])
|
|
656
|
+
if raw_driver and parsed_driver != driver:
|
|
657
|
+
urls = ()
|
|
658
|
+
parsed_url_payloads = []
|
|
659
|
+
else:
|
|
660
|
+
parsed_hosts: list[str] = []
|
|
661
|
+
parsed_ports: list[int] = []
|
|
662
|
+
for parsed_payload in parsed_url_payloads:
|
|
663
|
+
if _normalize_driver(parsed_payload["driver"]) != parsed_driver:
|
|
664
|
+
continue
|
|
665
|
+
parsed_hosts.extend(parsed_payload["hosts"])
|
|
666
|
+
parsed_ports.extend(parsed_payload["ports"])
|
|
667
|
+
for key, value in dict(parsed_payload.get("options", {}) or {}).items():
|
|
668
|
+
if key not in options:
|
|
669
|
+
options[key] = value
|
|
670
|
+
if not host:
|
|
671
|
+
host = ",".join(parsed_hosts) or first["host"]
|
|
672
|
+
if not port:
|
|
673
|
+
port = parsed_ports[0] if parsed_ports else first["port"]
|
|
674
|
+
if not hosts:
|
|
675
|
+
hosts = tuple(parsed_hosts) or first["hosts"]
|
|
676
|
+
if not ports:
|
|
677
|
+
ports = tuple(parsed_ports) or first["ports"]
|
|
678
|
+
if not user:
|
|
679
|
+
user = first["user"]
|
|
680
|
+
if not password:
|
|
681
|
+
password = first["password"]
|
|
682
|
+
if not database_name:
|
|
683
|
+
database_name = first["database"]
|
|
684
|
+
if not path:
|
|
685
|
+
path = first["path"]
|
|
686
|
+
if not ssl_mode:
|
|
687
|
+
ssl_mode = first["ssl_mode"]
|
|
688
|
+
if not ssl_root_cert:
|
|
689
|
+
ssl_root_cert = first["ssl_root_cert"]
|
|
690
|
+
if not ssl_cert:
|
|
691
|
+
ssl_cert = first["ssl_cert"]
|
|
692
|
+
if not ssl_key:
|
|
693
|
+
ssl_key = first["ssl_key"]
|
|
694
|
+
if not ssl_password:
|
|
695
|
+
ssl_password = first["ssl_password"]
|
|
696
|
+
if not target_session_attrs:
|
|
697
|
+
target_session_attrs = first["target_session_attrs"]
|
|
698
|
+
if not raw_driver:
|
|
699
|
+
driver = parsed_driver
|
|
700
|
+
host, port, hosts, ports = _normalize_network_settings(
|
|
701
|
+
driver=driver,
|
|
702
|
+
host=host,
|
|
703
|
+
port=port,
|
|
704
|
+
hosts=hosts,
|
|
705
|
+
ports=ports,
|
|
706
|
+
)
|
|
707
|
+
if driver == "sqlite":
|
|
708
|
+
port = 0
|
|
709
|
+
host = ""
|
|
710
|
+
hosts = ()
|
|
711
|
+
ports = ()
|
|
712
|
+
user = ""
|
|
713
|
+
password = ""
|
|
714
|
+
database_name = database_name or Path(path or "").name
|
|
715
|
+
ssl_mode = ""
|
|
716
|
+
ssl_root_cert = ""
|
|
717
|
+
ssl_cert = ""
|
|
718
|
+
ssl_key = ""
|
|
719
|
+
ssl_password = ""
|
|
720
|
+
target_session_attrs = ""
|
|
721
|
+
urls = ()
|
|
722
|
+
elif driver == "mysql":
|
|
723
|
+
port = port or 3306
|
|
724
|
+
elif driver == "postgresql":
|
|
725
|
+
port = port or 5432
|
|
726
|
+
min_size = max(1, min_size)
|
|
727
|
+
max_size = max(min_size, max_size)
|
|
728
|
+
pool_acquire_timeout = max(0.1, pool_acquire_timeout or 5.0)
|
|
729
|
+
statement_timeout_ms = max(0, statement_timeout_ms)
|
|
730
|
+
idle_in_transaction_session_timeout_ms = max(0, idle_in_transaction_session_timeout_ms)
|
|
731
|
+
pool_max_lifetime = max(0.0, pool_max_lifetime)
|
|
732
|
+
pool_max_idle = max(0.0, pool_max_idle)
|
|
733
|
+
reconnect_timeout = max(0.0, reconnect_timeout)
|
|
734
|
+
health_check_interval = max(1.0, health_check_interval or 60.0)
|
|
735
|
+
return FoundationDatabaseSettings(
|
|
736
|
+
enabled=settings.enabled,
|
|
737
|
+
driver=driver,
|
|
738
|
+
path=path,
|
|
739
|
+
urls=urls,
|
|
740
|
+
host=host,
|
|
741
|
+
port=port,
|
|
742
|
+
hosts=hosts,
|
|
743
|
+
ports=ports,
|
|
744
|
+
user=user,
|
|
745
|
+
password=password,
|
|
746
|
+
database=database_name,
|
|
747
|
+
min_size=min_size,
|
|
748
|
+
max_size=max_size,
|
|
749
|
+
connect_timeout=float(settings.connect_timeout or 0.0),
|
|
750
|
+
pool_acquire_timeout=pool_acquire_timeout,
|
|
751
|
+
statement_timeout_ms=statement_timeout_ms,
|
|
752
|
+
idle_in_transaction_session_timeout_ms=idle_in_transaction_session_timeout_ms,
|
|
753
|
+
pool_max_lifetime=pool_max_lifetime,
|
|
754
|
+
pool_max_idle=pool_max_idle,
|
|
755
|
+
reconnect_timeout=reconnect_timeout,
|
|
756
|
+
health_check_interval=health_check_interval,
|
|
757
|
+
ssl_mode=ssl_mode,
|
|
758
|
+
ssl_root_cert=ssl_root_cert,
|
|
759
|
+
ssl_cert=ssl_cert,
|
|
760
|
+
ssl_key=ssl_key,
|
|
761
|
+
ssl_password=ssl_password,
|
|
762
|
+
target_session_attrs=target_session_attrs,
|
|
763
|
+
options=options,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _parse_database_url(url: str) -> dict[str, Any]:
|
|
768
|
+
parsed = urlparse(url)
|
|
769
|
+
driver = _normalize_driver(parsed.scheme, url=url)
|
|
770
|
+
query = parse_qs(parsed.query or "", keep_blank_values=False)
|
|
771
|
+
if driver == "sqlite":
|
|
772
|
+
return {
|
|
773
|
+
"driver": "sqlite",
|
|
774
|
+
"host": "",
|
|
775
|
+
"port": 0,
|
|
776
|
+
"hosts": (),
|
|
777
|
+
"ports": (),
|
|
778
|
+
"user": "",
|
|
779
|
+
"password": "",
|
|
780
|
+
"database": Path(_sqlite_path_from_url(url) or "").name,
|
|
781
|
+
"path": _sqlite_path_from_url(url),
|
|
782
|
+
"ssl_mode": "",
|
|
783
|
+
"ssl_root_cert": "",
|
|
784
|
+
"ssl_cert": "",
|
|
785
|
+
"ssl_key": "",
|
|
786
|
+
"ssl_password": "",
|
|
787
|
+
"target_session_attrs": "",
|
|
788
|
+
"options": {},
|
|
789
|
+
}
|
|
790
|
+
userinfo, hostspec = _split_database_netloc(parsed.netloc or "")
|
|
791
|
+
user, password = _parse_database_userinfo(userinfo)
|
|
792
|
+
hosts, ports = _split_hosts_and_ports(hostspec, _default_port_for_driver(driver))
|
|
793
|
+
return {
|
|
794
|
+
"driver": driver,
|
|
795
|
+
"host": ",".join(hosts),
|
|
796
|
+
"port": ports[0] if ports else 0,
|
|
797
|
+
"hosts": hosts,
|
|
798
|
+
"ports": ports,
|
|
799
|
+
"user": user,
|
|
800
|
+
"password": password,
|
|
801
|
+
"database": unquote((parsed.path or "").lstrip("/")),
|
|
802
|
+
"path": "",
|
|
803
|
+
"ssl_mode": _first_query_value(query, "sslmode"),
|
|
804
|
+
"ssl_root_cert": _first_query_value(query, "sslrootcert"),
|
|
805
|
+
"ssl_cert": _first_query_value(query, "sslcert"),
|
|
806
|
+
"ssl_key": _first_query_value(query, "sslkey"),
|
|
807
|
+
"ssl_password": _first_query_value(query, "sslpassword"),
|
|
808
|
+
"target_session_attrs": _first_query_value(query, "target_session_attrs"),
|
|
809
|
+
"options": _extra_query_options(query),
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _extra_query_options(query: dict[str, list[str]]) -> dict[str, str]:
|
|
814
|
+
reserved = {"sslmode", "sslrootcert", "sslcert", "sslkey", "sslpassword", "target_session_attrs"}
|
|
815
|
+
options: dict[str, str] = {}
|
|
816
|
+
for key in query:
|
|
817
|
+
if key in reserved:
|
|
818
|
+
continue
|
|
819
|
+
value = _first_query_value(query, key)
|
|
820
|
+
if value:
|
|
821
|
+
options[key] = value
|
|
822
|
+
return options
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _sqlite_path_from_url(url: str) -> str:
|
|
826
|
+
raw = str(url or "").strip()
|
|
827
|
+
if not raw.lower().startswith("sqlite:"):
|
|
828
|
+
return ""
|
|
829
|
+
if raw.lower().startswith("sqlite:///:memory:"):
|
|
830
|
+
return ":memory:"
|
|
831
|
+
if raw.lower().startswith("sqlite:////"):
|
|
832
|
+
return raw[len("sqlite:///") :]
|
|
833
|
+
if raw.lower().startswith("sqlite:///"):
|
|
834
|
+
return raw[len("sqlite:///") :]
|
|
835
|
+
if raw.lower().startswith("sqlite://"):
|
|
836
|
+
return raw[len("sqlite://") :]
|
|
837
|
+
return ""
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _default_port_for_driver(driver: str) -> int:
|
|
841
|
+
normalized = _normalize_driver(driver)
|
|
842
|
+
if normalized == "postgresql":
|
|
843
|
+
return 5432
|
|
844
|
+
if normalized == "mysql":
|
|
845
|
+
return 3306
|
|
846
|
+
return 0
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _coerce_host_values(values: Any) -> tuple[str, ...]:
|
|
850
|
+
if isinstance(values, str):
|
|
851
|
+
items = values.split(",")
|
|
852
|
+
else:
|
|
853
|
+
items = values or ()
|
|
854
|
+
out: list[str] = []
|
|
855
|
+
for item in items:
|
|
856
|
+
value = str(item or "").strip()
|
|
857
|
+
if value:
|
|
858
|
+
out.append(value)
|
|
859
|
+
return tuple(out)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _coerce_port_values(values: Any) -> tuple[int, ...]:
|
|
863
|
+
if isinstance(values, str):
|
|
864
|
+
items = values.split(",")
|
|
865
|
+
else:
|
|
866
|
+
items = values or ()
|
|
867
|
+
out: list[int] = []
|
|
868
|
+
for item in items:
|
|
869
|
+
value = str(item or "").strip()
|
|
870
|
+
if not value:
|
|
871
|
+
continue
|
|
872
|
+
try:
|
|
873
|
+
parsed = int(value)
|
|
874
|
+
except Exception:
|
|
875
|
+
continue
|
|
876
|
+
if parsed > 0:
|
|
877
|
+
out.append(parsed)
|
|
878
|
+
return tuple(out)
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _coerce_database_url_values(values: Any) -> tuple[str, ...]:
|
|
882
|
+
if values in (None, ""):
|
|
883
|
+
return ()
|
|
884
|
+
if isinstance(values, str):
|
|
885
|
+
return _split_database_url_list_string(values)
|
|
886
|
+
if isinstance(values, (list, tuple, set)):
|
|
887
|
+
out: list[str] = []
|
|
888
|
+
for item in values:
|
|
889
|
+
if isinstance(item, str):
|
|
890
|
+
out.extend(_split_database_url_list_string(item))
|
|
891
|
+
continue
|
|
892
|
+
value = str(item or "").strip()
|
|
893
|
+
if value:
|
|
894
|
+
out.append(value)
|
|
895
|
+
return tuple(out)
|
|
896
|
+
value = str(values or "").strip()
|
|
897
|
+
return (value,) if value else ()
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def _split_database_url_list_string(value: str) -> tuple[str, ...]:
|
|
901
|
+
text = str(value or "").strip()
|
|
902
|
+
if not text:
|
|
903
|
+
return ()
|
|
904
|
+
if text.startswith("["):
|
|
905
|
+
try:
|
|
906
|
+
decoded = json.loads(text)
|
|
907
|
+
except Exception:
|
|
908
|
+
decoded = None
|
|
909
|
+
if isinstance(decoded, list):
|
|
910
|
+
return _coerce_database_url_values(decoded)
|
|
911
|
+
if "\n" in text or ";" in text or "|" in text:
|
|
912
|
+
parts = re.split(r"[\n;|]+", text)
|
|
913
|
+
else:
|
|
914
|
+
parts = re.split(r"\s*,\s*(?=[A-Za-z][A-Za-z0-9+.-]*://)", text)
|
|
915
|
+
return tuple(part.strip() for part in parts if part.strip())
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _split_database_netloc(netloc: str) -> tuple[str, str]:
|
|
919
|
+
text = str(netloc or "").strip()
|
|
920
|
+
if "@" not in text:
|
|
921
|
+
return "", text
|
|
922
|
+
userinfo, hostspec = text.rsplit("@", 1)
|
|
923
|
+
return userinfo, hostspec
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def _parse_database_userinfo(userinfo: str) -> tuple[str, str]:
|
|
927
|
+
if not userinfo:
|
|
928
|
+
return "", ""
|
|
929
|
+
user, _, password = str(userinfo).partition(":")
|
|
930
|
+
return unquote(user or ""), unquote(password or "")
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _split_hosts_and_ports(hostspec: str, default_port: int) -> tuple[tuple[str, ...], tuple[int, ...]]:
|
|
934
|
+
hosts: list[str] = []
|
|
935
|
+
ports: list[int] = []
|
|
936
|
+
for raw_entry in str(hostspec or "").split(","):
|
|
937
|
+
host, port = _split_host_entry(raw_entry, default_port)
|
|
938
|
+
if not host:
|
|
939
|
+
continue
|
|
940
|
+
hosts.append(host)
|
|
941
|
+
ports.append(port)
|
|
942
|
+
return tuple(hosts), tuple(ports)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _split_host_entry(raw_entry: str, default_port: int) -> tuple[str, int]:
|
|
946
|
+
entry = str(raw_entry or "").strip()
|
|
947
|
+
if not entry:
|
|
948
|
+
return "", default_port
|
|
949
|
+
if entry.startswith("["):
|
|
950
|
+
closing = entry.find("]")
|
|
951
|
+
if closing > 0:
|
|
952
|
+
host = entry[1:closing].strip()
|
|
953
|
+
remainder = entry[closing + 1 :].strip()
|
|
954
|
+
if remainder.startswith(":"):
|
|
955
|
+
try:
|
|
956
|
+
return host, int(remainder[1:] or default_port)
|
|
957
|
+
except Exception:
|
|
958
|
+
return host, default_port
|
|
959
|
+
return host, default_port
|
|
960
|
+
if entry.count(":") == 1:
|
|
961
|
+
host, raw_port = entry.rsplit(":", 1)
|
|
962
|
+
try:
|
|
963
|
+
return host.strip(), int(raw_port or default_port)
|
|
964
|
+
except Exception:
|
|
965
|
+
return host.strip(), default_port
|
|
966
|
+
return entry, default_port
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _format_host_for_uri(host: str) -> str:
|
|
970
|
+
value = str(host or "").strip()
|
|
971
|
+
if ":" in value and not (value.startswith("[") and value.endswith("]")):
|
|
972
|
+
return f"[{value}]"
|
|
973
|
+
return value
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _expand_ports(ports: tuple[int, ...], *, count: int, fallback: int) -> tuple[int, ...]:
|
|
977
|
+
if count <= 0:
|
|
978
|
+
return ()
|
|
979
|
+
valid = tuple(int(item) for item in ports if int(item) > 0)
|
|
980
|
+
if len(valid) >= count:
|
|
981
|
+
return valid[:count]
|
|
982
|
+
if len(valid) == 1:
|
|
983
|
+
return tuple(valid[0] for _ in range(count))
|
|
984
|
+
if valid:
|
|
985
|
+
fill = valid[0]
|
|
986
|
+
return (*valid, *(fill for _ in range(count - len(valid))))
|
|
987
|
+
return tuple(int(fallback) for _ in range(count))
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _normalize_network_settings(
|
|
991
|
+
*,
|
|
992
|
+
driver: str,
|
|
993
|
+
host: str,
|
|
994
|
+
port: int,
|
|
995
|
+
hosts: tuple[str, ...],
|
|
996
|
+
ports: tuple[int, ...],
|
|
997
|
+
) -> tuple[str, int, tuple[str, ...], tuple[int, ...]]:
|
|
998
|
+
normalized_driver = _normalize_driver(driver)
|
|
999
|
+
if normalized_driver not in {"mysql", "postgresql"}:
|
|
1000
|
+
return str(host or "").strip(), int(port or 0), (), ()
|
|
1001
|
+
default_port = _default_port_for_driver(normalized_driver)
|
|
1002
|
+
parsed_hosts = _coerce_host_values(hosts)
|
|
1003
|
+
embedded_ports: tuple[int, ...] = ()
|
|
1004
|
+
if parsed_hosts:
|
|
1005
|
+
parsed_hosts, embedded_ports = _split_hosts_and_ports(",".join(parsed_hosts), default_port)
|
|
1006
|
+
elif str(host or "").strip():
|
|
1007
|
+
parsed_hosts, embedded_ports = _split_hosts_and_ports(str(host or ""), default_port)
|
|
1008
|
+
if not parsed_hosts:
|
|
1009
|
+
return "", int(port or default_port), (), ()
|
|
1010
|
+
explicit_ports = _coerce_port_values(ports)
|
|
1011
|
+
if explicit_ports:
|
|
1012
|
+
port_values = explicit_ports
|
|
1013
|
+
elif port:
|
|
1014
|
+
port_values = (int(port),)
|
|
1015
|
+
else:
|
|
1016
|
+
port_values = embedded_ports
|
|
1017
|
+
normalized_ports = _expand_ports(port_values, count=len(parsed_hosts), fallback=default_port)
|
|
1018
|
+
return ",".join(parsed_hosts), normalized_ports[0], parsed_hosts, normalized_ports
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def _primary_endpoint(settings: FoundationDatabaseSettings, *, default_port: int) -> tuple[str, int]:
|
|
1022
|
+
host = str(settings.host or "").strip()
|
|
1023
|
+
port = int(settings.port or 0)
|
|
1024
|
+
hosts, ports = _coerce_host_values(getattr(settings, "hosts", ())), _coerce_port_values(getattr(settings, "ports", ()))
|
|
1025
|
+
normalized_host, normalized_port, normalized_hosts, normalized_ports = _normalize_network_settings(
|
|
1026
|
+
driver=settings.driver,
|
|
1027
|
+
host=host,
|
|
1028
|
+
port=port,
|
|
1029
|
+
hosts=hosts,
|
|
1030
|
+
ports=ports,
|
|
1031
|
+
)
|
|
1032
|
+
if normalized_hosts:
|
|
1033
|
+
return normalized_hosts[0], normalized_ports[0]
|
|
1034
|
+
return normalized_host, normalized_port or default_port
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def _build_postgresql_dsn(settings: FoundationDatabaseSettings) -> str:
|
|
1038
|
+
hosts = _coerce_host_values(getattr(settings, "hosts", ()))
|
|
1039
|
+
ports = _coerce_port_values(getattr(settings, "ports", ()))
|
|
1040
|
+
if not hosts:
|
|
1041
|
+
normalized_host, _normalized_port, hosts, ports = _normalize_network_settings(
|
|
1042
|
+
driver="postgresql",
|
|
1043
|
+
host=settings.host,
|
|
1044
|
+
port=settings.port,
|
|
1045
|
+
hosts=hosts,
|
|
1046
|
+
ports=ports,
|
|
1047
|
+
)
|
|
1048
|
+
if not hosts and normalized_host:
|
|
1049
|
+
hosts = _coerce_host_values((normalized_host,))
|
|
1050
|
+
if not hosts:
|
|
1051
|
+
raise RuntimeError("foundation postgresql host is not configured")
|
|
1052
|
+
user = settings.user or ""
|
|
1053
|
+
password = settings.password or ""
|
|
1054
|
+
auth = user
|
|
1055
|
+
if password:
|
|
1056
|
+
auth = f"{auth}:{password}" if auth else f":{password}"
|
|
1057
|
+
if auth:
|
|
1058
|
+
if ":" in auth:
|
|
1059
|
+
raw_user, raw_password = auth.split(":", 1)
|
|
1060
|
+
auth = f"{quote(raw_user, safe='')}:{quote(raw_password, safe='')}@"
|
|
1061
|
+
else:
|
|
1062
|
+
auth = f"{quote(auth, safe='')}@"
|
|
1063
|
+
port_list = _expand_ports(
|
|
1064
|
+
ports,
|
|
1065
|
+
count=len(hosts),
|
|
1066
|
+
fallback=_default_port_for_driver("postgresql"),
|
|
1067
|
+
)
|
|
1068
|
+
host = ",".join(f"{_format_host_for_uri(host_item)}:{port_item}" for host_item, port_item in zip(hosts, port_list))
|
|
1069
|
+
name = quote(settings.database or "", safe="")
|
|
1070
|
+
query: dict[str, str] = {}
|
|
1071
|
+
if settings.ssl_mode:
|
|
1072
|
+
query["sslmode"] = settings.ssl_mode
|
|
1073
|
+
if settings.ssl_root_cert:
|
|
1074
|
+
query["sslrootcert"] = settings.ssl_root_cert
|
|
1075
|
+
if settings.ssl_cert:
|
|
1076
|
+
query["sslcert"] = settings.ssl_cert
|
|
1077
|
+
if settings.ssl_key:
|
|
1078
|
+
query["sslkey"] = settings.ssl_key
|
|
1079
|
+
if settings.ssl_password:
|
|
1080
|
+
query["sslpassword"] = settings.ssl_password
|
|
1081
|
+
target_session_attrs = str(getattr(settings, "target_session_attrs", "") or "").strip()
|
|
1082
|
+
if not target_session_attrs and len(hosts) > 1:
|
|
1083
|
+
target_session_attrs = "read-write"
|
|
1084
|
+
if target_session_attrs:
|
|
1085
|
+
query["target_session_attrs"] = target_session_attrs
|
|
1086
|
+
for key, value in dict(settings.options or {}).items():
|
|
1087
|
+
if key == "options" or key in query:
|
|
1088
|
+
continue
|
|
1089
|
+
if isinstance(value, (str, int, float, bool)) and str(value).strip():
|
|
1090
|
+
query[key] = str(value)
|
|
1091
|
+
dsn = f"postgresql://{auth}{host}/{name}"
|
|
1092
|
+
if query:
|
|
1093
|
+
dsn = f"{dsn}?{urlencode(query)}"
|
|
1094
|
+
return dsn
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _database_singleton_key(settings: FoundationDatabaseSettings) -> str:
|
|
1098
|
+
return "|".join(
|
|
1099
|
+
(
|
|
1100
|
+
settings.driver,
|
|
1101
|
+
settings.path,
|
|
1102
|
+
",".join(getattr(settings, "urls", ()) or ()),
|
|
1103
|
+
settings.host,
|
|
1104
|
+
str(settings.port),
|
|
1105
|
+
",".join(getattr(settings, "hosts", ()) or ()),
|
|
1106
|
+
",".join(str(item) for item in (getattr(settings, "ports", ()) or ())),
|
|
1107
|
+
settings.user,
|
|
1108
|
+
settings.database,
|
|
1109
|
+
str(settings.connect_timeout),
|
|
1110
|
+
str(settings.min_size),
|
|
1111
|
+
str(settings.max_size),
|
|
1112
|
+
str(settings.pool_acquire_timeout),
|
|
1113
|
+
str(settings.statement_timeout_ms),
|
|
1114
|
+
str(settings.idle_in_transaction_session_timeout_ms),
|
|
1115
|
+
str(settings.pool_max_lifetime),
|
|
1116
|
+
str(settings.pool_max_idle),
|
|
1117
|
+
str(settings.reconnect_timeout),
|
|
1118
|
+
str(settings.health_check_interval),
|
|
1119
|
+
settings.ssl_mode,
|
|
1120
|
+
settings.ssl_root_cert,
|
|
1121
|
+
settings.ssl_cert,
|
|
1122
|
+
settings.ssl_key,
|
|
1123
|
+
settings.ssl_password,
|
|
1124
|
+
settings.target_session_attrs,
|
|
1125
|
+
repr(sorted(dict(settings.options or {}).items())),
|
|
1126
|
+
)
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def _build_postgresql_ssl_kwargs(settings: FoundationDatabaseSettings) -> dict[str, Any]:
|
|
1131
|
+
kwargs: dict[str, Any] = {}
|
|
1132
|
+
if settings.ssl_mode:
|
|
1133
|
+
kwargs["sslmode"] = settings.ssl_mode
|
|
1134
|
+
if settings.ssl_root_cert:
|
|
1135
|
+
kwargs["sslrootcert"] = settings.ssl_root_cert
|
|
1136
|
+
if settings.ssl_cert:
|
|
1137
|
+
kwargs["sslcert"] = settings.ssl_cert
|
|
1138
|
+
if settings.ssl_key:
|
|
1139
|
+
kwargs["sslkey"] = settings.ssl_key
|
|
1140
|
+
if settings.ssl_password:
|
|
1141
|
+
kwargs["sslpassword"] = settings.ssl_password
|
|
1142
|
+
return kwargs
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _split_sql_statements(script: str) -> list[str]:
|
|
1146
|
+
statements: list[str] = []
|
|
1147
|
+
buffer: list[str] = []
|
|
1148
|
+
in_single = False
|
|
1149
|
+
in_double = False
|
|
1150
|
+
escaped = False
|
|
1151
|
+
for char in str(script or ""):
|
|
1152
|
+
buffer.append(char)
|
|
1153
|
+
if escaped:
|
|
1154
|
+
escaped = False
|
|
1155
|
+
continue
|
|
1156
|
+
if char == "\\":
|
|
1157
|
+
escaped = True
|
|
1158
|
+
continue
|
|
1159
|
+
if char == "'" and not in_double:
|
|
1160
|
+
in_single = not in_single
|
|
1161
|
+
continue
|
|
1162
|
+
if char == '"' and not in_single:
|
|
1163
|
+
in_double = not in_double
|
|
1164
|
+
continue
|
|
1165
|
+
if char == ";" and not in_single and not in_double:
|
|
1166
|
+
statement = "".join(buffer).strip().rstrip(";").strip()
|
|
1167
|
+
if statement:
|
|
1168
|
+
statements.append(statement)
|
|
1169
|
+
buffer = []
|
|
1170
|
+
tail = "".join(buffer).strip()
|
|
1171
|
+
if tail:
|
|
1172
|
+
statements.append(tail.rstrip(";").strip())
|
|
1173
|
+
return [item for item in statements if item]
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def _coerce_dbapi_row(description: Any, row: Any) -> dict[str, Any]:
|
|
1177
|
+
if isinstance(row, dict):
|
|
1178
|
+
return dict(row)
|
|
1179
|
+
names = [str(item[0]) for item in description or ()]
|
|
1180
|
+
if hasattr(row, "keys") and callable(row.keys):
|
|
1181
|
+
return {str(key): row[key] for key in row.keys()}
|
|
1182
|
+
return {name: row[index] for index, name in enumerate(names)}
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _prepare_statement(driver: str, statement: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> tuple[str, Any]:
|
|
1186
|
+
params: Any
|
|
1187
|
+
if kwargs:
|
|
1188
|
+
params = kwargs
|
|
1189
|
+
elif len(args) == 1 and isinstance(args[0], (tuple, list, dict)):
|
|
1190
|
+
params = args[0]
|
|
1191
|
+
elif args:
|
|
1192
|
+
params = args
|
|
1193
|
+
else:
|
|
1194
|
+
params = None
|
|
1195
|
+
normalized = _normalize_driver(driver)
|
|
1196
|
+
if params is None or normalized == "sqlite":
|
|
1197
|
+
return statement, params
|
|
1198
|
+
if isinstance(params, dict):
|
|
1199
|
+
return _NAMED_PARAM_PATTERN.sub(r"%(\1)s", statement), params
|
|
1200
|
+
return statement, params
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def _normalize_driver(driver: str, *, url: str = "") -> str:
|
|
1204
|
+
raw = str(driver or "").strip().lower()
|
|
1205
|
+
if not raw and url:
|
|
1206
|
+
raw = urlparse(url).scheme.lower()
|
|
1207
|
+
if "+" in raw:
|
|
1208
|
+
raw = raw.split("+", 1)[0]
|
|
1209
|
+
if raw in {"postgres", "postgresql", "psql"}:
|
|
1210
|
+
return "postgresql"
|
|
1211
|
+
if raw in {"mysql", "mariadb"}:
|
|
1212
|
+
return "mysql"
|
|
1213
|
+
if raw in {"sqlite", "sqlite3"}:
|
|
1214
|
+
return "sqlite"
|
|
1215
|
+
return raw or "sqlite"
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
def _read_str(payload: Any, key: str) -> str:
|
|
1219
|
+
def _normalize(raw: Any) -> str:
|
|
1220
|
+
value = str(raw or "").strip()
|
|
1221
|
+
if value in {'""', "''"}:
|
|
1222
|
+
return ""
|
|
1223
|
+
return value
|
|
1224
|
+
|
|
1225
|
+
if isinstance(payload, dict):
|
|
1226
|
+
return _normalize(payload.get(key))
|
|
1227
|
+
return _normalize(getattr(payload, key, ""))
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def _read_alias_str(payload: Any, *keys: str) -> str:
|
|
1231
|
+
for key in keys:
|
|
1232
|
+
value = _read_str(payload, key)
|
|
1233
|
+
if value:
|
|
1234
|
+
return value
|
|
1235
|
+
return ""
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def _read_str_sequence(payload: Any, key: str) -> tuple[str, ...]:
|
|
1239
|
+
value = payload.get(key) if isinstance(payload, dict) else getattr(payload, key, None)
|
|
1240
|
+
return _coerce_database_url_values(value)
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def _read_alias_int(payload: Any, keys: tuple[str, ...], default: int) -> int:
|
|
1244
|
+
for key in keys:
|
|
1245
|
+
value = payload.get(key) if isinstance(payload, dict) else getattr(payload, key, None)
|
|
1246
|
+
if value in {None, ""}:
|
|
1247
|
+
continue
|
|
1248
|
+
try:
|
|
1249
|
+
return int(value)
|
|
1250
|
+
except Exception:
|
|
1251
|
+
return default
|
|
1252
|
+
return default
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _read_alias_float(payload: Any, keys: tuple[str, ...], default: float) -> float:
|
|
1256
|
+
for key in keys:
|
|
1257
|
+
value = payload.get(key) if isinstance(payload, dict) else getattr(payload, key, None)
|
|
1258
|
+
if value in {None, ""}:
|
|
1259
|
+
continue
|
|
1260
|
+
try:
|
|
1261
|
+
return float(value)
|
|
1262
|
+
except Exception:
|
|
1263
|
+
return default
|
|
1264
|
+
return default
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
def _first_query_value(query: dict[str, list[str]], key: str) -> str:
|
|
1268
|
+
values = query.get(key) or []
|
|
1269
|
+
if not values:
|
|
1270
|
+
return ""
|
|
1271
|
+
return str(values[0] or "").strip()
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
__all__ = [
|
|
1277
|
+
"describe_foundation_database_binding",
|
|
1278
|
+
"FoundationDatabase",
|
|
1279
|
+
"FoundationDatabaseSettings",
|
|
1280
|
+
"MySQLFoundationDatabase",
|
|
1281
|
+
"normalize_foundation_database_settings",
|
|
1282
|
+
"PostgreSQLFoundationDatabase",
|
|
1283
|
+
"SQLiteFoundationDatabase",
|
|
1284
|
+
"SQLiteFoundationReadinessProbe",
|
|
1285
|
+
"SQLiteFoundationSession",
|
|
1286
|
+
"SQLiteFoundationTransaction",
|
|
1287
|
+
"get_foundation_database",
|
|
1288
|
+
"load_foundation_database_settings",
|
|
1289
|
+
"probe_foundation_database_binding",
|
|
1290
|
+
"reset_foundation_databases",
|
|
1291
|
+
]
|