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,786 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import tomllib
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from pz_core.assembly.app_discovery import (
|
|
10
|
+
discover_declared_app_references,
|
|
11
|
+
resolve_declared_app_import_roots,
|
|
12
|
+
resolve_declared_app_id,
|
|
13
|
+
resolve_package_root,
|
|
14
|
+
)
|
|
15
|
+
from pz_core.assembly.project import active_project_import_root
|
|
16
|
+
from pz_core.assembly.settings.host_project_overrides import (
|
|
17
|
+
apply_host_project_env_overrides,
|
|
18
|
+
)
|
|
19
|
+
from pz_core.core.errors import PzError
|
|
20
|
+
from pz_core.runtime.environment_integrity import (
|
|
21
|
+
validate_installed_app_identities,
|
|
22
|
+
validate_process_environment,
|
|
23
|
+
validate_project_config_identity,
|
|
24
|
+
)
|
|
25
|
+
from pz_core.utils import PROJECT_JSONC_NAME, iter_env_items, load_jsonc_file
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from pz_core.assembly.project import ProjectContext
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
PROJECT_JSONC_FILE_NAME = PROJECT_JSONC_NAME
|
|
31
|
+
DEFAULT_PZ_ENV_NAME = "develop"
|
|
32
|
+
PZ_ENV_SELECTOR_ENV_NAME = "PZ_ENV"
|
|
33
|
+
_LEGACY_ENV_FIELD = "pyz" + "za_env"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _normalize_env_value(value: Any) -> Any:
|
|
37
|
+
if not isinstance(value, str):
|
|
38
|
+
return value
|
|
39
|
+
text = value.strip()
|
|
40
|
+
if not text:
|
|
41
|
+
return text
|
|
42
|
+
if text[:1] in {"'", '"'} and text[-1:] == text[:1] and len(text) >= 2:
|
|
43
|
+
return text[1:-1]
|
|
44
|
+
return text
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _read_dotenv(path: Path) -> dict[str, str]:
|
|
48
|
+
data: dict[str, str] = {}
|
|
49
|
+
if not path.is_file():
|
|
50
|
+
return data
|
|
51
|
+
for line_number, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
|
52
|
+
line = raw_line.strip()
|
|
53
|
+
if not line or line.startswith("#"):
|
|
54
|
+
continue
|
|
55
|
+
if line.startswith("export "):
|
|
56
|
+
line = line[7:].strip()
|
|
57
|
+
if "=" not in line:
|
|
58
|
+
raise PzError(
|
|
59
|
+
code="PZ_DOTENV_INVALID",
|
|
60
|
+
message=f"Invalid .env entry at line {line_number}: missing '=' delimiter.",
|
|
61
|
+
stage="settings",
|
|
62
|
+
owner="runtime.settings",
|
|
63
|
+
action="read_dotenv",
|
|
64
|
+
)
|
|
65
|
+
key, value = line.split("=", 1)
|
|
66
|
+
key = key.strip()
|
|
67
|
+
value = value.strip()
|
|
68
|
+
if not key:
|
|
69
|
+
raise PzError(
|
|
70
|
+
code="PZ_DOTENV_INVALID",
|
|
71
|
+
message=f"Invalid .env entry at line {line_number}: key must be non-empty.",
|
|
72
|
+
stage="settings",
|
|
73
|
+
owner="runtime.settings",
|
|
74
|
+
action="read_dotenv",
|
|
75
|
+
)
|
|
76
|
+
if value[:1] in {"'", '"'}:
|
|
77
|
+
quote = value[0]
|
|
78
|
+
end_index = value.find(quote, 1)
|
|
79
|
+
if end_index == -1:
|
|
80
|
+
raise PzError(
|
|
81
|
+
code="PZ_DOTENV_INVALID",
|
|
82
|
+
message=f"Invalid .env entry at line {line_number}: unterminated quoted value.",
|
|
83
|
+
stage="settings",
|
|
84
|
+
owner="runtime.settings",
|
|
85
|
+
action="read_dotenv",
|
|
86
|
+
)
|
|
87
|
+
trailing = value[end_index + 1 :].strip()
|
|
88
|
+
if trailing and not trailing.startswith("#"):
|
|
89
|
+
raise PzError(
|
|
90
|
+
code="PZ_DOTENV_INVALID",
|
|
91
|
+
message=f"Invalid .env entry at line {line_number}: unexpected trailing characters.",
|
|
92
|
+
stage="settings",
|
|
93
|
+
owner="runtime.settings",
|
|
94
|
+
action="read_dotenv",
|
|
95
|
+
)
|
|
96
|
+
value = value[1:end_index]
|
|
97
|
+
elif " #" in value:
|
|
98
|
+
value = value.split(" #", 1)[0].rstrip()
|
|
99
|
+
if key in data:
|
|
100
|
+
raise PzError(
|
|
101
|
+
code="PZ_DOTENV_INVALID",
|
|
102
|
+
message=f"Invalid .env entry at line {line_number}: duplicate key '{key}'.",
|
|
103
|
+
stage="settings",
|
|
104
|
+
owner="runtime.settings",
|
|
105
|
+
action="read_dotenv",
|
|
106
|
+
)
|
|
107
|
+
data[key] = value
|
|
108
|
+
return data
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _explicit_pz_env_snapshot() -> dict[str, str]:
|
|
112
|
+
snapshot = {
|
|
113
|
+
str(key): str(_normalize_env_value(value))
|
|
114
|
+
for key, value in iter_env_items()
|
|
115
|
+
}
|
|
116
|
+
validate_process_environment(snapshot)
|
|
117
|
+
return snapshot
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _pz_env_env_file_path(project: "ProjectContext", pz_env: str | None) -> Path | None:
|
|
121
|
+
pz_env_name = str(pz_env or "").strip()
|
|
122
|
+
if not pz_env_name:
|
|
123
|
+
return None
|
|
124
|
+
return project.project_dir / f".env.{pz_env_name}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def load_project_env_file(
|
|
128
|
+
project: "ProjectContext",
|
|
129
|
+
*,
|
|
130
|
+
pz_env: str | None = None,
|
|
131
|
+
) -> dict[str, str]:
|
|
132
|
+
if pz_env is None:
|
|
133
|
+
pz_env = _resolve_pz_env_name(_explicit_pz_env_snapshot())
|
|
134
|
+
payload = {
|
|
135
|
+
str(key): str(_normalize_env_value(value))
|
|
136
|
+
for key, value in _read_dotenv(project.env_file_path).items()
|
|
137
|
+
}
|
|
138
|
+
env_file_path = _pz_env_env_file_path(project, pz_env)
|
|
139
|
+
if env_file_path is not None:
|
|
140
|
+
for key, value in _read_dotenv(env_file_path).items():
|
|
141
|
+
payload[str(key)] = str(_normalize_env_value(value))
|
|
142
|
+
validate_process_environment(payload)
|
|
143
|
+
return payload
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _deep_merge(base: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]:
|
|
147
|
+
merged = dict(base)
|
|
148
|
+
for key, value in incoming.items():
|
|
149
|
+
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
150
|
+
merged[key] = _deep_merge(merged[key], value)
|
|
151
|
+
continue
|
|
152
|
+
merged[key] = value
|
|
153
|
+
return merged
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _read_required_string(
|
|
157
|
+
value: Any,
|
|
158
|
+
*,
|
|
159
|
+
code: str,
|
|
160
|
+
message: str,
|
|
161
|
+
owner: str,
|
|
162
|
+
action: str,
|
|
163
|
+
) -> str:
|
|
164
|
+
if not isinstance(value, str) or not value.strip():
|
|
165
|
+
raise PzError(
|
|
166
|
+
code=code,
|
|
167
|
+
message=message,
|
|
168
|
+
stage="settings",
|
|
169
|
+
owner=owner,
|
|
170
|
+
action=action,
|
|
171
|
+
)
|
|
172
|
+
return value.strip()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _read_app_module_path(
|
|
176
|
+
value: Any,
|
|
177
|
+
*,
|
|
178
|
+
owner: str,
|
|
179
|
+
action: str,
|
|
180
|
+
) -> str:
|
|
181
|
+
normalized = _read_required_string(
|
|
182
|
+
value,
|
|
183
|
+
code="PZ_APP_PATH_INVALID",
|
|
184
|
+
message="App module path must be declared as a non-empty string.",
|
|
185
|
+
owner=owner,
|
|
186
|
+
action=action,
|
|
187
|
+
)
|
|
188
|
+
if ":" in normalized or normalized.endswith(".apps"):
|
|
189
|
+
raise PzError(
|
|
190
|
+
code="PZ_APP_PATH_INVALID",
|
|
191
|
+
message=(
|
|
192
|
+
f"App path '{normalized}' must declare a directory/package name only; "
|
|
193
|
+
"installed_apps does not accept '<pkg>.apps' or '<pkg>.apps:app'."
|
|
194
|
+
),
|
|
195
|
+
stage="settings",
|
|
196
|
+
owner=owner,
|
|
197
|
+
action=action,
|
|
198
|
+
)
|
|
199
|
+
return normalized
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _read_optional_pz_env_name(
|
|
203
|
+
value: Any,
|
|
204
|
+
*,
|
|
205
|
+
owner: str,
|
|
206
|
+
action: str,
|
|
207
|
+
) -> str | None:
|
|
208
|
+
if value is None:
|
|
209
|
+
return None
|
|
210
|
+
if not isinstance(value, str) or not value.strip():
|
|
211
|
+
raise PzError(
|
|
212
|
+
code="PZ_ENV_INVALID",
|
|
213
|
+
message="pz_env must be declared as a non-empty string when configured.",
|
|
214
|
+
stage="settings",
|
|
215
|
+
owner=owner,
|
|
216
|
+
action=action,
|
|
217
|
+
)
|
|
218
|
+
return value.strip()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _package_root(module_path: str) -> Path | None:
|
|
222
|
+
return resolve_package_root(
|
|
223
|
+
_read_app_module_path(
|
|
224
|
+
module_path,
|
|
225
|
+
owner="settings",
|
|
226
|
+
action="package_root",
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _resolve_pz_env_name(env: dict[str, str]) -> str:
|
|
232
|
+
selected = _read_optional_pz_env_name(
|
|
233
|
+
env.get(PZ_ENV_SELECTOR_ENV_NAME),
|
|
234
|
+
owner=PZ_ENV_SELECTOR_ENV_NAME,
|
|
235
|
+
action="compile_settings_snapshot",
|
|
236
|
+
)
|
|
237
|
+
if selected is None:
|
|
238
|
+
return DEFAULT_PZ_ENV_NAME
|
|
239
|
+
return selected
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _is_non_empty_mapping(payload: dict[str, Any]) -> bool:
|
|
243
|
+
return bool(payload)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _read_project_root_config(project: "ProjectContext") -> dict[str, Any]:
|
|
247
|
+
if not project.project_config_path.is_file():
|
|
248
|
+
return {}
|
|
249
|
+
return load_jsonc_file(
|
|
250
|
+
project.project_config_path,
|
|
251
|
+
code="PZ_PROJECT_CONFIG_INVALID",
|
|
252
|
+
owner="runtime.settings",
|
|
253
|
+
action="read_project_config",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _reject_legacy_pz_env_selectors(
|
|
258
|
+
data: dict[str, Any],
|
|
259
|
+
*,
|
|
260
|
+
action: str,
|
|
261
|
+
selected_pz_env: str | None = None,
|
|
262
|
+
) -> None:
|
|
263
|
+
legacy_paths: list[str] = []
|
|
264
|
+
if "environment" in data:
|
|
265
|
+
legacy_paths.append("environment")
|
|
266
|
+
if _LEGACY_ENV_FIELD in data:
|
|
267
|
+
legacy_paths.append(_LEGACY_ENV_FIELD)
|
|
268
|
+
if "pz_env" in data:
|
|
269
|
+
legacy_paths.append("pz_env")
|
|
270
|
+
project = data.get("project")
|
|
271
|
+
if isinstance(project, dict):
|
|
272
|
+
if "environment" in project:
|
|
273
|
+
legacy_paths.append("project.environment")
|
|
274
|
+
project_pz_env = project.get("pz_env")
|
|
275
|
+
allow_derived_project_pz_env = (
|
|
276
|
+
selected_pz_env is not None
|
|
277
|
+
and str(project_pz_env).strip() == str(selected_pz_env).strip()
|
|
278
|
+
)
|
|
279
|
+
if _LEGACY_ENV_FIELD in project:
|
|
280
|
+
legacy_paths.append(f"project.{_LEGACY_ENV_FIELD}")
|
|
281
|
+
if "pz_env" in project and not allow_derived_project_pz_env:
|
|
282
|
+
legacy_paths.append("project.pz_env")
|
|
283
|
+
if "env" in project:
|
|
284
|
+
legacy_paths.append("project.env")
|
|
285
|
+
if legacy_paths:
|
|
286
|
+
joined = ", ".join(f"`{item}`" for item in legacy_paths)
|
|
287
|
+
raise PzError(
|
|
288
|
+
code="PZ_PROJECT_CONFIG_INVALID",
|
|
289
|
+
message=f"Project environment selector keys are not allowed: {joined}. Use `PZ_ENV` instead.",
|
|
290
|
+
stage="settings",
|
|
291
|
+
owner="runtime.settings",
|
|
292
|
+
action=action,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _apply_project_http_runtime_settings(project: dict[str, Any]) -> None:
|
|
297
|
+
pz_env = str(project.get("pz_env") or DEFAULT_PZ_ENV_NAME).strip().lower()
|
|
298
|
+
declared_http = project.get("http")
|
|
299
|
+
http_payload = dict(declared_http) if isinstance(declared_http, dict) else {}
|
|
300
|
+
defaults: dict[str, Any]
|
|
301
|
+
if pz_env == "product":
|
|
302
|
+
defaults = {
|
|
303
|
+
"openapi_url": None,
|
|
304
|
+
"docs_url": None,
|
|
305
|
+
"redoc_url": None,
|
|
306
|
+
}
|
|
307
|
+
else:
|
|
308
|
+
defaults = {
|
|
309
|
+
"openapi_url": "/openapi.json",
|
|
310
|
+
"docs_url": "/docs",
|
|
311
|
+
"redoc_url": "/redoc",
|
|
312
|
+
}
|
|
313
|
+
project["http"] = _deep_merge(defaults, http_payload)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _pz_env_config_candidates(project: "ProjectContext", pz_env: str) -> tuple[Path, ...]:
|
|
317
|
+
pz_env_name = str(pz_env or "").strip()
|
|
318
|
+
if not pz_env_name:
|
|
319
|
+
return tuple()
|
|
320
|
+
return (project.project_dir / f"pz.{pz_env_name}.jsonc",)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _read_pz_env_override_config(project: "ProjectContext", pz_env: str) -> dict[str, Any]:
|
|
324
|
+
validate_project_config_identity(project.project_dir)
|
|
325
|
+
for path in _pz_env_config_candidates(project, pz_env):
|
|
326
|
+
payload = load_jsonc_file(
|
|
327
|
+
path,
|
|
328
|
+
code="PZ_PROJECT_CONFIG_INVALID",
|
|
329
|
+
owner="runtime.settings",
|
|
330
|
+
action="read_pz_env_config",
|
|
331
|
+
)
|
|
332
|
+
if payload:
|
|
333
|
+
return payload
|
|
334
|
+
return {}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _parse_env_value(raw: str) -> Any:
|
|
338
|
+
if not isinstance(raw, str):
|
|
339
|
+
raise PzError(
|
|
340
|
+
code="ENV_VALUE_INVALID",
|
|
341
|
+
message="Env override value must be a string.",
|
|
342
|
+
stage="settings",
|
|
343
|
+
owner="runtime.settings",
|
|
344
|
+
action="parse_env_value",
|
|
345
|
+
)
|
|
346
|
+
value = raw.strip()
|
|
347
|
+
lowered = value.lower()
|
|
348
|
+
if lowered == "true":
|
|
349
|
+
return True
|
|
350
|
+
if lowered == "false":
|
|
351
|
+
return False
|
|
352
|
+
if lowered == "null":
|
|
353
|
+
return None
|
|
354
|
+
if value.startswith("[") or value.startswith("{") or value.startswith('"') or value.startswith("'"):
|
|
355
|
+
try:
|
|
356
|
+
return tomllib.loads(f"v = {value}")["v"]
|
|
357
|
+
except Exception as exc:
|
|
358
|
+
if value.startswith("[") or value.startswith("{"):
|
|
359
|
+
try:
|
|
360
|
+
return json.loads(value)
|
|
361
|
+
except Exception:
|
|
362
|
+
pass
|
|
363
|
+
raise PzError(
|
|
364
|
+
code="ENV_VALUE_INVALID",
|
|
365
|
+
message="Env override value looks structured but could not be parsed.",
|
|
366
|
+
stage="settings",
|
|
367
|
+
owner="runtime.settings",
|
|
368
|
+
action="parse_env_value",
|
|
369
|
+
) from exc
|
|
370
|
+
try:
|
|
371
|
+
return int(value)
|
|
372
|
+
except ValueError:
|
|
373
|
+
pass
|
|
374
|
+
try:
|
|
375
|
+
return float(value)
|
|
376
|
+
except ValueError:
|
|
377
|
+
pass
|
|
378
|
+
return value
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _set_nested(mapping: dict[str, Any], path: tuple[str, ...], value: Any) -> None:
|
|
382
|
+
def _resolve_existing_key(current: dict[str, Any], key: str) -> str:
|
|
383
|
+
if key in current:
|
|
384
|
+
return key
|
|
385
|
+
lowered = str(key).lower()
|
|
386
|
+
for existing in current.keys():
|
|
387
|
+
if isinstance(existing, str) and existing.lower() == lowered:
|
|
388
|
+
return existing
|
|
389
|
+
return key
|
|
390
|
+
|
|
391
|
+
current = mapping
|
|
392
|
+
for key in path[:-1]:
|
|
393
|
+
resolved_key = _resolve_existing_key(current, key)
|
|
394
|
+
child = current.get(resolved_key)
|
|
395
|
+
if not isinstance(child, dict):
|
|
396
|
+
child = {}
|
|
397
|
+
current[resolved_key] = child
|
|
398
|
+
current = child
|
|
399
|
+
current[_resolve_existing_key(current, path[-1])] = value
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
_CANONICAL_TOP_LEVEL_ENV_FIELDS: dict[str, tuple[str, ...]] = {
|
|
403
|
+
"cache_dir": ("cache_dir",),
|
|
404
|
+
"runtime_dir": ("runtime_dir",),
|
|
405
|
+
"site_theme": ("site_theme",),
|
|
406
|
+
"site_theme_by_lang": ("site_theme_by_lang",),
|
|
407
|
+
"site_theme_by_country": ("site_theme_by_country",),
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
_TOP_LEVEL_SECTION_PREFIXES = (
|
|
411
|
+
"project",
|
|
412
|
+
"routing",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _read_top_level_env_override_path(key: str, *, owner: str) -> tuple[str, ...]:
|
|
417
|
+
nested_path: list[str] = []
|
|
418
|
+
for index, raw_part in enumerate(key.split("__")):
|
|
419
|
+
if not isinstance(raw_part, str):
|
|
420
|
+
raise PzError(
|
|
421
|
+
code="ENV_KEY_INVALID",
|
|
422
|
+
message=f"Env override path for '{owner}' must use string segments.",
|
|
423
|
+
stage="settings",
|
|
424
|
+
owner=owner,
|
|
425
|
+
action="compile_settings_snapshot",
|
|
426
|
+
)
|
|
427
|
+
part = raw_part.strip().lower()
|
|
428
|
+
if not part:
|
|
429
|
+
raise PzError(
|
|
430
|
+
code="ENV_KEY_INVALID",
|
|
431
|
+
message=f"Env override path for '{owner}' contains an empty segment at index {index}.",
|
|
432
|
+
stage="settings",
|
|
433
|
+
owner=owner,
|
|
434
|
+
action="compile_settings_snapshot",
|
|
435
|
+
)
|
|
436
|
+
nested_path.append(part)
|
|
437
|
+
return tuple(nested_path)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _apply_top_level_env_overrides(data: dict[str, Any], env: dict[str, str]) -> None:
|
|
441
|
+
for key, path in _CANONICAL_TOP_LEVEL_ENV_FIELDS.items():
|
|
442
|
+
raw_value = env.get(key)
|
|
443
|
+
if raw_value is None:
|
|
444
|
+
continue
|
|
445
|
+
_set_nested(data, path, _parse_env_value(raw_value))
|
|
446
|
+
|
|
447
|
+
for prefix in _TOP_LEVEL_SECTION_PREFIXES:
|
|
448
|
+
token = prefix + "__"
|
|
449
|
+
for key, raw_value in env.items():
|
|
450
|
+
if not key.startswith(token):
|
|
451
|
+
continue
|
|
452
|
+
nested = _read_top_level_env_override_path(key[len(token) :], owner=prefix)
|
|
453
|
+
_set_nested(data, (prefix,) + nested, _parse_env_value(raw_value))
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _read_env_override_path(key: str, *, app_id: str) -> tuple[str, ...]:
|
|
457
|
+
nested_path: list[str] = []
|
|
458
|
+
for index, raw_part in enumerate(key.split("__")):
|
|
459
|
+
if not isinstance(raw_part, str):
|
|
460
|
+
raise PzError(
|
|
461
|
+
code="PZ_APP_ENV_KEY_INVALID",
|
|
462
|
+
message=f"Env override path for app '{app_id}' must use string segments.",
|
|
463
|
+
stage="settings",
|
|
464
|
+
owner=app_id,
|
|
465
|
+
action="compile_settings_snapshot",
|
|
466
|
+
)
|
|
467
|
+
part = raw_part.strip().lower()
|
|
468
|
+
if not part:
|
|
469
|
+
raise PzError(
|
|
470
|
+
code="PZ_APP_ENV_KEY_INVALID",
|
|
471
|
+
message=(
|
|
472
|
+
f"Env override path for app '{app_id}' contains an empty segment "
|
|
473
|
+
f"at index {index}."
|
|
474
|
+
),
|
|
475
|
+
stage="settings",
|
|
476
|
+
owner=app_id,
|
|
477
|
+
action="compile_settings_snapshot",
|
|
478
|
+
)
|
|
479
|
+
nested_path.append(part)
|
|
480
|
+
return tuple(nested_path)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _configured_app_settings_map(data: dict[str, Any]) -> dict[str, Any]:
|
|
484
|
+
configured_apps = data.get("apps")
|
|
485
|
+
if configured_apps is None:
|
|
486
|
+
return {}
|
|
487
|
+
if isinstance(configured_apps, dict):
|
|
488
|
+
return dict(configured_apps)
|
|
489
|
+
raise PzError(
|
|
490
|
+
code="PZ_APPS_NAMESPACE_INVALID",
|
|
491
|
+
message="apps must be declared as a table keyed by app_id.",
|
|
492
|
+
stage="settings",
|
|
493
|
+
owner="apps",
|
|
494
|
+
action="compile_settings_snapshot",
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _try_app_id_from_module_path(module_path: str) -> str | None:
|
|
499
|
+
normalized = _read_app_module_path(
|
|
500
|
+
module_path,
|
|
501
|
+
owner="installed_apps",
|
|
502
|
+
action="compile_settings_snapshot",
|
|
503
|
+
)
|
|
504
|
+
package_root = _package_root(normalized)
|
|
505
|
+
try:
|
|
506
|
+
app_id = resolve_declared_app_id(normalized)
|
|
507
|
+
except PzError as exc:
|
|
508
|
+
if exc.code == "PZ_APP_IMPORT_FAILED":
|
|
509
|
+
if package_root is None:
|
|
510
|
+
return None
|
|
511
|
+
raise
|
|
512
|
+
return app_id or None
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _merge_app_settings(
|
|
516
|
+
configured_map: dict[str, Any],
|
|
517
|
+
env: dict[str, str],
|
|
518
|
+
installed_apps: tuple[str, ...],
|
|
519
|
+
) -> dict[str, dict[str, Any]]:
|
|
520
|
+
app_settings: dict[str, dict[str, Any]] = {}
|
|
521
|
+
candidate_app_ids: list[str] = []
|
|
522
|
+
module_paths_by_app_id: dict[str, list[str]] = {}
|
|
523
|
+
|
|
524
|
+
for module_path in installed_apps:
|
|
525
|
+
normalized = _read_app_module_path(
|
|
526
|
+
module_path,
|
|
527
|
+
owner="installed_apps",
|
|
528
|
+
action="compile_settings_snapshot",
|
|
529
|
+
)
|
|
530
|
+
app_id = _try_app_id_from_module_path(normalized)
|
|
531
|
+
if app_id is None:
|
|
532
|
+
continue
|
|
533
|
+
if app_id not in candidate_app_ids:
|
|
534
|
+
candidate_app_ids.append(app_id)
|
|
535
|
+
bucket = module_paths_by_app_id.setdefault(app_id, [])
|
|
536
|
+
if normalized not in bucket:
|
|
537
|
+
bucket.append(normalized)
|
|
538
|
+
for app_id in configured_map:
|
|
539
|
+
if app_id not in candidate_app_ids:
|
|
540
|
+
candidate_app_ids.append(app_id)
|
|
541
|
+
|
|
542
|
+
for app_id in candidate_app_ids:
|
|
543
|
+
explicit = configured_map.get(app_id)
|
|
544
|
+
if explicit is None:
|
|
545
|
+
app_settings.setdefault(app_id, {})
|
|
546
|
+
elif isinstance(explicit, dict):
|
|
547
|
+
app_settings[app_id] = _deep_merge(app_settings.get(app_id, {}), explicit)
|
|
548
|
+
else:
|
|
549
|
+
raise PzError(
|
|
550
|
+
code="PZ_APP_SETTINGS_ENTRY_INVALID",
|
|
551
|
+
message=f"apps.{app_id} must be declared as a table.",
|
|
552
|
+
stage="settings",
|
|
553
|
+
owner=app_id,
|
|
554
|
+
action="compile_settings_snapshot",
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
for key, raw_value in env.items():
|
|
558
|
+
for app_id in tuple(app_settings.keys()):
|
|
559
|
+
prefix = app_id.replace("-", "_") + "__"
|
|
560
|
+
if not key.startswith(prefix):
|
|
561
|
+
continue
|
|
562
|
+
nested_path = _read_env_override_path(key[len(prefix):], app_id=app_id)
|
|
563
|
+
if nested_path:
|
|
564
|
+
_set_nested(app_settings.setdefault(app_id, {}), nested_path, _parse_env_value(raw_value))
|
|
565
|
+
return {app_id: dict(values) for app_id, values in app_settings.items()}
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _resolve_declared_installed_apps(data: dict[str, Any]) -> tuple[str, ...]:
|
|
569
|
+
raw = data.get("installed_apps")
|
|
570
|
+
if raw in (None, (), []):
|
|
571
|
+
return tuple()
|
|
572
|
+
if not isinstance(raw, (list, tuple)):
|
|
573
|
+
raise PzError(
|
|
574
|
+
code="PZ_INSTALLED_APPS_INVALID",
|
|
575
|
+
message="installed_apps must be declared as an array of app module paths.",
|
|
576
|
+
stage="settings",
|
|
577
|
+
owner="installed_apps",
|
|
578
|
+
action="compile_settings_snapshot",
|
|
579
|
+
)
|
|
580
|
+
declared_apps: list[str] = []
|
|
581
|
+
for index, item in enumerate(raw):
|
|
582
|
+
declared_apps.append(
|
|
583
|
+
_read_required_string(
|
|
584
|
+
item,
|
|
585
|
+
code="PZ_APP_PATH_INVALID",
|
|
586
|
+
message=f"installed_apps[{index}] must declare a non-empty string app module path.",
|
|
587
|
+
owner="installed_apps",
|
|
588
|
+
action="compile_settings_snapshot",
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
resolved = tuple(declared_apps)
|
|
592
|
+
validate_installed_app_identities(resolved)
|
|
593
|
+
return resolved
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _resolve_installed_apps(
|
|
597
|
+
declared_apps: tuple[str, ...],
|
|
598
|
+
*,
|
|
599
|
+
project_dir: Path,
|
|
600
|
+
) -> tuple[str, ...]:
|
|
601
|
+
return tuple(
|
|
602
|
+
item.module_path
|
|
603
|
+
for item in discover_declared_app_references(declared_apps, project_dir=project_dir)
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _resolve_import_roots(
|
|
608
|
+
declared_apps: tuple[str, ...],
|
|
609
|
+
*,
|
|
610
|
+
project_dir: Path,
|
|
611
|
+
) -> tuple[Path, ...]:
|
|
612
|
+
return resolve_declared_app_import_roots(declared_apps, project_dir=project_dir)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _resolve_app_replacements(data: dict[str, Any]) -> dict[str, str]:
|
|
616
|
+
raw = data.get("app_replacements")
|
|
617
|
+
if raw is None and isinstance(data.get("core"), dict):
|
|
618
|
+
raw = data["core"].get("app_replacements")
|
|
619
|
+
if raw in (None, {}):
|
|
620
|
+
return {}
|
|
621
|
+
if not isinstance(raw, dict):
|
|
622
|
+
raise PzError(
|
|
623
|
+
code="PZ_APP_REPLACEMENTS_INVALID",
|
|
624
|
+
message="app_replacements must be declared as a table of source to replacement paths.",
|
|
625
|
+
stage="settings",
|
|
626
|
+
owner="app_replacements",
|
|
627
|
+
action="compile_settings_snapshot",
|
|
628
|
+
)
|
|
629
|
+
out: dict[str, str] = {}
|
|
630
|
+
for source, target in raw.items():
|
|
631
|
+
src = _read_required_string(
|
|
632
|
+
source,
|
|
633
|
+
code="PZ_APP_REPLACEMENT_ENTRY_INVALID",
|
|
634
|
+
message="app_replacements entries must declare source as a non-empty string path.",
|
|
635
|
+
owner="app_replacements",
|
|
636
|
+
action="compile_settings_snapshot",
|
|
637
|
+
)
|
|
638
|
+
dst = _read_required_string(
|
|
639
|
+
target,
|
|
640
|
+
code="PZ_APP_REPLACEMENT_ENTRY_INVALID",
|
|
641
|
+
message="app_replacements entries must declare replacement as a non-empty string path.",
|
|
642
|
+
owner="app_replacements",
|
|
643
|
+
action="compile_settings_snapshot",
|
|
644
|
+
)
|
|
645
|
+
out[src] = dst
|
|
646
|
+
return out
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@dataclass(frozen=True, slots=True)
|
|
650
|
+
class SettingsSnapshot:
|
|
651
|
+
project_dir: Path
|
|
652
|
+
data: dict[str, Any]
|
|
653
|
+
installed_apps: tuple[str, ...]
|
|
654
|
+
app_replacements: dict[str, str]
|
|
655
|
+
apps: dict[str, dict[str, Any]]
|
|
656
|
+
declared_installed_apps: tuple[str, ...] = ()
|
|
657
|
+
import_roots: tuple[Path, ...] = ()
|
|
658
|
+
|
|
659
|
+
def get(self, *path: str, default: Any = None) -> Any:
|
|
660
|
+
current: Any = self.data
|
|
661
|
+
for key in path:
|
|
662
|
+
if not isinstance(current, dict):
|
|
663
|
+
return default
|
|
664
|
+
current = current.get(key)
|
|
665
|
+
return current if current is not None else default
|
|
666
|
+
|
|
667
|
+
def with_installed_apps(self, installed_apps: tuple[str, ...]) -> "SettingsSnapshot":
|
|
668
|
+
declared_apps: list[str] = []
|
|
669
|
+
for index, item in enumerate(installed_apps):
|
|
670
|
+
normalized = _read_required_string(
|
|
671
|
+
item,
|
|
672
|
+
code="PZ_APP_PATH_INVALID",
|
|
673
|
+
message=f"installed_apps[{index}] must declare a non-empty string app module path.",
|
|
674
|
+
owner="installed_apps",
|
|
675
|
+
action="with_installed_apps",
|
|
676
|
+
)
|
|
677
|
+
declared_apps.append(normalized)
|
|
678
|
+
validate_installed_app_identities(declared_apps)
|
|
679
|
+
normalized_apps = _resolve_installed_apps(tuple(declared_apps), project_dir=self.project_dir)
|
|
680
|
+
validate_installed_app_identities(normalized_apps)
|
|
681
|
+
import_roots = _resolve_import_roots(tuple(declared_apps), project_dir=self.project_dir)
|
|
682
|
+
with active_project_import_root(self.project_dir, extra_import_roots=import_roots):
|
|
683
|
+
app_settings = _merge_app_settings(
|
|
684
|
+
dict(self.apps),
|
|
685
|
+
{},
|
|
686
|
+
tuple(normalized_apps),
|
|
687
|
+
)
|
|
688
|
+
data = dict(self.data)
|
|
689
|
+
data["apps"] = app_settings
|
|
690
|
+
return SettingsSnapshot(
|
|
691
|
+
project_dir=self.project_dir,
|
|
692
|
+
data=data,
|
|
693
|
+
installed_apps=tuple(normalized_apps),
|
|
694
|
+
import_roots=tuple(import_roots),
|
|
695
|
+
app_replacements=dict(self.app_replacements),
|
|
696
|
+
apps=app_settings,
|
|
697
|
+
declared_installed_apps=tuple(declared_apps),
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
def app_settings(self, app_id: str) -> dict[str, Any]:
|
|
701
|
+
return dict(self.apps.get(app_id, {}))
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def compile_settings_snapshot_from_payload(
|
|
705
|
+
project_dir: Path | str,
|
|
706
|
+
payload: dict[str, Any],
|
|
707
|
+
*,
|
|
708
|
+
env: dict[str, str] | None = None,
|
|
709
|
+
selected_pz_env: str | None = None,
|
|
710
|
+
) -> SettingsSnapshot:
|
|
711
|
+
resolved_project_dir = Path(project_dir).expanduser().resolve()
|
|
712
|
+
merged = _deep_merge({}, payload)
|
|
713
|
+
_reject_legacy_pz_env_selectors(
|
|
714
|
+
merged,
|
|
715
|
+
action="compile_settings_snapshot",
|
|
716
|
+
selected_pz_env=selected_pz_env,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
env_snapshot = {
|
|
720
|
+
str(key): str(_normalize_env_value(value))
|
|
721
|
+
for key, value in dict(env or {}).items()
|
|
722
|
+
if value is not None
|
|
723
|
+
}
|
|
724
|
+
validate_process_environment(env_snapshot)
|
|
725
|
+
_apply_top_level_env_overrides(merged, env_snapshot)
|
|
726
|
+
apply_host_project_env_overrides(merged, env_snapshot)
|
|
727
|
+
|
|
728
|
+
declared_installed_apps = _resolve_declared_installed_apps(merged)
|
|
729
|
+
configured_apps = _configured_app_settings_map(merged)
|
|
730
|
+
installed_apps = _resolve_installed_apps(declared_installed_apps, project_dir=resolved_project_dir)
|
|
731
|
+
validate_installed_app_identities(installed_apps)
|
|
732
|
+
import_roots = _resolve_import_roots(declared_installed_apps, project_dir=resolved_project_dir)
|
|
733
|
+
app_replacements = _resolve_app_replacements(merged)
|
|
734
|
+
if selected_pz_env:
|
|
735
|
+
project_namespace = merged.get("project")
|
|
736
|
+
if not isinstance(project_namespace, dict):
|
|
737
|
+
project_namespace = {}
|
|
738
|
+
merged["project"] = project_namespace
|
|
739
|
+
project_namespace["pz_env"] = selected_pz_env
|
|
740
|
+
project_namespace = merged.get("project")
|
|
741
|
+
if not isinstance(project_namespace, dict):
|
|
742
|
+
project_namespace = {}
|
|
743
|
+
merged["project"] = project_namespace
|
|
744
|
+
_apply_project_http_runtime_settings(project_namespace)
|
|
745
|
+
with active_project_import_root(resolved_project_dir, extra_import_roots=import_roots):
|
|
746
|
+
app_settings = _merge_app_settings(
|
|
747
|
+
configured_apps,
|
|
748
|
+
env_snapshot,
|
|
749
|
+
installed_apps,
|
|
750
|
+
)
|
|
751
|
+
merged["apps"] = app_settings
|
|
752
|
+
return SettingsSnapshot(
|
|
753
|
+
project_dir=resolved_project_dir,
|
|
754
|
+
data=dict(merged),
|
|
755
|
+
installed_apps=installed_apps,
|
|
756
|
+
import_roots=import_roots,
|
|
757
|
+
app_replacements=dict(app_replacements),
|
|
758
|
+
apps=app_settings,
|
|
759
|
+
declared_installed_apps=declared_installed_apps,
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def compile_settings_snapshot(project: "ProjectContext") -> SettingsSnapshot:
|
|
764
|
+
explicit_env_snapshot = _explicit_pz_env_snapshot()
|
|
765
|
+
pz_env = _resolve_pz_env_name(explicit_env_snapshot)
|
|
766
|
+
|
|
767
|
+
env_snapshot = load_project_env_file(project, pz_env=pz_env)
|
|
768
|
+
for key, value in explicit_env_snapshot.items():
|
|
769
|
+
env_snapshot[key] = _normalize_env_value(value)
|
|
770
|
+
|
|
771
|
+
merged: dict[str, Any] = _read_project_root_config(project)
|
|
772
|
+
_reject_legacy_pz_env_selectors(merged, action="read_project_config")
|
|
773
|
+
|
|
774
|
+
merged = _deep_merge(merged, _read_pz_env_override_config(project, pz_env))
|
|
775
|
+
project_namespace = merged.get("project")
|
|
776
|
+
if not isinstance(project_namespace, dict):
|
|
777
|
+
project_namespace = {}
|
|
778
|
+
merged["project"] = project_namespace
|
|
779
|
+
project_namespace["pz_env"] = pz_env
|
|
780
|
+
|
|
781
|
+
return compile_settings_snapshot_from_payload(
|
|
782
|
+
project.project_dir,
|
|
783
|
+
merged,
|
|
784
|
+
env={key: str(value) for key, value in env_snapshot.items()},
|
|
785
|
+
selected_pz_env=pz_env,
|
|
786
|
+
)
|