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,1700 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import secrets
|
|
6
|
+
import shutil
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from contextlib import AbstractContextManager
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
from uuid import uuid4
|
|
15
|
+
|
|
16
|
+
from pz_core.assembly.apps import compile_app_registry, compile_conventional_app_order, merge_explicit_app_order
|
|
17
|
+
from pz_core.assembly.middlewares import compile_conventional_middleware_manifest, merge_explicit_middleware_order
|
|
18
|
+
from pz_core.assembly.settings import compile_settings_snapshot_from_payload
|
|
19
|
+
from pz_core.assembly.project import active_project_import_root
|
|
20
|
+
from pz_core.runtime.discovery import (
|
|
21
|
+
discover_declared_app_references,
|
|
22
|
+
discover_leaf_app_packages,
|
|
23
|
+
import_app_settings_module,
|
|
24
|
+
resolve_declared_app_id,
|
|
25
|
+
resolve_import_roots_for_app_references,
|
|
26
|
+
resolve_package_root,
|
|
27
|
+
)
|
|
28
|
+
from pz_core.runtime.errors import PzError
|
|
29
|
+
from pz_core.runtime.settings import active_settings_project
|
|
30
|
+
from pz_core.utils.project_jsonc import PROJECT_JSONC_NAME, dump_jsonc_document, load_jsonc_file, load_jsonc_text
|
|
31
|
+
from .templates.project.project_env import project_env_template
|
|
32
|
+
from .templates.project.pz_env_env import pz_env_env_template
|
|
33
|
+
from .templates.project.pz_env_jsonc import (
|
|
34
|
+
_annotate_project_base_url_comment,
|
|
35
|
+
pz_env_jsonc_template,
|
|
36
|
+
)
|
|
37
|
+
from .project_defaults import load_project_default_jsonc_payload
|
|
38
|
+
from .scaffold_service import render_project_agents_file_text, write_project_agents_file
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
try: # pragma: no cover - fcntl is unavailable on some platforms.
|
|
42
|
+
import fcntl
|
|
43
|
+
except Exception: # pragma: no cover
|
|
44
|
+
fcntl = None # type: ignore[assignment]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_OWNER = "pz_core.authoring.project_settings"
|
|
48
|
+
_MANAGED_PATHS = ("installed_apps", "middlewares")
|
|
49
|
+
_UPDATE_BACKUP_DIRNAME = "pz-update-settings"
|
|
50
|
+
_UPDATE_BACKUP_DEFAULT_KEEP_COUNT = 15
|
|
51
|
+
_UPDATE_LOCK_NAME = "settings-update.lock"
|
|
52
|
+
_ROOT_ENV_REDIRECTS: dict[tuple[str, ...], str] = {}
|
|
53
|
+
_SECRET_TEMPLATE_KEY_TOKENS = ("password", "secret", "token", "key", "webhook")
|
|
54
|
+
_SECRET_GENERATOR_PREFIX = "@pz:secret"
|
|
55
|
+
_ROUTING_DEFAULT_KEYS = frozenset({"default_language", "all_supported_languages", "language_aliases"})
|
|
56
|
+
_ROOT_DEFAULT_REFRESH_KEYS = frozenset({"commands"})
|
|
57
|
+
_PZ_ENV_SETTINGS_TEMPLATES = {
|
|
58
|
+
"pz.develop.jsonc": lambda: pz_env_jsonc_template("develop"),
|
|
59
|
+
"pz.test.jsonc": lambda: pz_env_jsonc_template("test"),
|
|
60
|
+
"pz.product.jsonc": lambda: pz_env_jsonc_template("product"),
|
|
61
|
+
}
|
|
62
|
+
_PZ_ENV_ENV_TEMPLATES = {
|
|
63
|
+
".env": project_env_template,
|
|
64
|
+
".env.develop": lambda: pz_env_env_template("develop"),
|
|
65
|
+
".env.test": lambda: pz_env_env_template("test"),
|
|
66
|
+
".env.product": lambda: pz_env_env_template("product"),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
SettingsPayloadNormalizer = Callable[[dict[str, Any]], dict[str, Any]]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class AppSettingsSource:
|
|
75
|
+
module_path: str
|
|
76
|
+
app_id: str
|
|
77
|
+
defaults: dict[str, Any]
|
|
78
|
+
normalize: SettingsPayloadNormalizer | None = None
|
|
79
|
+
secret_template: str = ""
|
|
80
|
+
root_defaults: dict[str, Any] | None = None
|
|
81
|
+
comments: dict[tuple[str, ...], tuple[str, ...]] | None = None
|
|
82
|
+
root_comments: dict[tuple[str, ...], tuple[str, ...]] | None = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, slots=True)
|
|
86
|
+
class SettingsWriteResult:
|
|
87
|
+
path: Path
|
|
88
|
+
module_paths: tuple[str, ...]
|
|
89
|
+
created: bool
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True, slots=True)
|
|
93
|
+
class SettingsUpdateDriftResult:
|
|
94
|
+
project_dir: Path
|
|
95
|
+
required: bool
|
|
96
|
+
changed_paths: tuple[Path, ...]
|
|
97
|
+
module_paths: tuple[str, ...]
|
|
98
|
+
command: str
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True, slots=True)
|
|
102
|
+
class _SettingsUpdateDryRun:
|
|
103
|
+
project_dir: Path
|
|
104
|
+
config_path: Path
|
|
105
|
+
current_payload: dict[str, Any]
|
|
106
|
+
next_payload: dict[str, Any]
|
|
107
|
+
discovered_sources: tuple[AppSettingsSource, ...]
|
|
108
|
+
selected_sources: tuple[AppSettingsSource, ...]
|
|
109
|
+
root_env_assignments: dict[str, str]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def discover_settings_sources(
|
|
113
|
+
project_dir: str | Path | None = None,
|
|
114
|
+
*,
|
|
115
|
+
include_project_apps_container: bool = False,
|
|
116
|
+
) -> tuple[AppSettingsSource, ...]:
|
|
117
|
+
resolved_project_dir = _resolve_existing_project_dir(project_dir) if project_dir is not None else None
|
|
118
|
+
refs = discover_declared_app_references(
|
|
119
|
+
_declared_app_entries(
|
|
120
|
+
resolved_project_dir,
|
|
121
|
+
include_project_apps_container=include_project_apps_container,
|
|
122
|
+
),
|
|
123
|
+
project_dir=resolved_project_dir,
|
|
124
|
+
)
|
|
125
|
+
import_roots = resolve_import_roots_for_app_references(refs)
|
|
126
|
+
with active_project_import_root(resolved_project_dir, extra_import_roots=import_roots):
|
|
127
|
+
source_pairs = [(item, _build_settings_source(item.module_path)) for item in refs]
|
|
128
|
+
sources = _dedupe_settings_sources_by_app_id(
|
|
129
|
+
resolved_project_dir,
|
|
130
|
+
source_pairs,
|
|
131
|
+
prefer_project_apps_leaf=include_project_apps_container,
|
|
132
|
+
)
|
|
133
|
+
return tuple(sorted(sources, key=lambda item: item.module_path))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def initialize_settings_file(
|
|
137
|
+
project_dir: str | Path,
|
|
138
|
+
*,
|
|
139
|
+
force: bool = False,
|
|
140
|
+
sources: tuple[AppSettingsSource, ...] | None = None,
|
|
141
|
+
) -> SettingsWriteResult:
|
|
142
|
+
resolved_project_dir = _resolve_project_dir(project_dir)
|
|
143
|
+
with _SettingsUpdateLock(resolved_project_dir):
|
|
144
|
+
config_path = resolved_project_dir / PROJECT_JSONC_NAME
|
|
145
|
+
existed_before = config_path.exists()
|
|
146
|
+
if existed_before and not force:
|
|
147
|
+
raise PzError(
|
|
148
|
+
code="PZ_SETTINGS_FILE_EXISTS",
|
|
149
|
+
message=f"Refusing to overwrite existing settings file: {config_path}",
|
|
150
|
+
stage="cli",
|
|
151
|
+
owner=_OWNER,
|
|
152
|
+
action="init_settings",
|
|
153
|
+
)
|
|
154
|
+
current_payload = _load_existing_document(config_path) if existed_before else {}
|
|
155
|
+
if existed_before and force:
|
|
156
|
+
_create_update_backup(resolved_project_dir, current_payload)
|
|
157
|
+
selected_sources = tuple(
|
|
158
|
+
sources
|
|
159
|
+
or discover_settings_sources(
|
|
160
|
+
resolved_project_dir,
|
|
161
|
+
include_project_apps_container=force,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
seed_payload = {} if force else current_payload
|
|
165
|
+
next_payload = _build_initialized_payload(resolved_project_dir, seed_payload, selected_sources)
|
|
166
|
+
root_env_assignments = _pop_root_env_redirects(next_payload)
|
|
167
|
+
_atomic_write_text(
|
|
168
|
+
config_path,
|
|
169
|
+
dump_jsonc_document(
|
|
170
|
+
next_payload,
|
|
171
|
+
managed_paths=_MANAGED_PATHS,
|
|
172
|
+
comments=_collect_project_jsonc_comments(selected_sources),
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
_ensure_pz_env_settings_placeholders(resolved_project_dir)
|
|
176
|
+
_ensure_pz_env_env_templates(
|
|
177
|
+
resolved_project_dir,
|
|
178
|
+
root_env_assignments=root_env_assignments,
|
|
179
|
+
app_secret_templates=_collect_app_secret_templates(selected_sources),
|
|
180
|
+
)
|
|
181
|
+
write_project_agents_file(resolved_project_dir)
|
|
182
|
+
return SettingsWriteResult(
|
|
183
|
+
path=config_path,
|
|
184
|
+
module_paths=tuple(item.module_path for item in selected_sources),
|
|
185
|
+
created=not existed_before,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def update_settings_file(
|
|
190
|
+
project_dir: str | Path,
|
|
191
|
+
*,
|
|
192
|
+
target: str | None = None,
|
|
193
|
+
sources: tuple[AppSettingsSource, ...] | None = None,
|
|
194
|
+
reset_middlewares_order: bool = False,
|
|
195
|
+
) -> SettingsWriteResult:
|
|
196
|
+
resolved_project_dir = _resolve_project_dir(project_dir)
|
|
197
|
+
with _SettingsUpdateLock(resolved_project_dir):
|
|
198
|
+
config_path = resolved_project_dir / PROJECT_JSONC_NAME
|
|
199
|
+
if not config_path.is_file():
|
|
200
|
+
raise PzError(
|
|
201
|
+
code="PZ_SETTINGS_FILE_MISSING",
|
|
202
|
+
message=f"Settings file does not exist: {config_path}",
|
|
203
|
+
stage="cli",
|
|
204
|
+
owner=_OWNER,
|
|
205
|
+
action="update_settings",
|
|
206
|
+
)
|
|
207
|
+
dry_run = _build_settings_update_dry_run(
|
|
208
|
+
resolved_project_dir,
|
|
209
|
+
target=target,
|
|
210
|
+
sources=sources,
|
|
211
|
+
reset_middlewares_order=reset_middlewares_order,
|
|
212
|
+
)
|
|
213
|
+
_create_update_backup(resolved_project_dir, dry_run.current_payload)
|
|
214
|
+
_atomic_write_text(
|
|
215
|
+
dry_run.config_path,
|
|
216
|
+
dump_jsonc_document(
|
|
217
|
+
dry_run.next_payload,
|
|
218
|
+
managed_paths=_MANAGED_PATHS,
|
|
219
|
+
comments=_collect_project_jsonc_comments(dry_run.discovered_sources),
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
_ensure_pz_env_settings_placeholders(resolved_project_dir)
|
|
223
|
+
_ensure_pz_env_env_templates(
|
|
224
|
+
resolved_project_dir,
|
|
225
|
+
root_env_assignments=dry_run.root_env_assignments,
|
|
226
|
+
app_secret_templates=_collect_app_secret_templates(dry_run.discovered_sources),
|
|
227
|
+
)
|
|
228
|
+
write_project_agents_file(resolved_project_dir)
|
|
229
|
+
return SettingsWriteResult(
|
|
230
|
+
path=dry_run.config_path,
|
|
231
|
+
module_paths=tuple(item.module_path for item in dry_run.selected_sources),
|
|
232
|
+
created=False,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def inspect_settings_update_drift(
|
|
237
|
+
project_dir: str | Path,
|
|
238
|
+
*,
|
|
239
|
+
target: str | None = None,
|
|
240
|
+
sources: tuple[AppSettingsSource, ...] | None = None,
|
|
241
|
+
reset_middlewares_order: bool = False,
|
|
242
|
+
) -> SettingsUpdateDriftResult:
|
|
243
|
+
resolved_project_dir = Path(project_dir).expanduser().resolve()
|
|
244
|
+
command = f"pz settings update --project-dir {resolved_project_dir}"
|
|
245
|
+
config_path = resolved_project_dir / PROJECT_JSONC_NAME
|
|
246
|
+
if not resolved_project_dir.is_dir() or not config_path.is_file():
|
|
247
|
+
return SettingsUpdateDriftResult(
|
|
248
|
+
project_dir=resolved_project_dir,
|
|
249
|
+
required=False,
|
|
250
|
+
changed_paths=(),
|
|
251
|
+
module_paths=(),
|
|
252
|
+
command=command,
|
|
253
|
+
)
|
|
254
|
+
dry_run = _build_settings_update_dry_run(
|
|
255
|
+
resolved_project_dir,
|
|
256
|
+
target=target,
|
|
257
|
+
sources=sources,
|
|
258
|
+
reset_middlewares_order=reset_middlewares_order,
|
|
259
|
+
)
|
|
260
|
+
changed_paths = _settings_update_changed_paths(dry_run)
|
|
261
|
+
return SettingsUpdateDriftResult(
|
|
262
|
+
project_dir=resolved_project_dir,
|
|
263
|
+
required=bool(changed_paths),
|
|
264
|
+
changed_paths=changed_paths,
|
|
265
|
+
module_paths=tuple(item.module_path for item in dry_run.selected_sources),
|
|
266
|
+
command=command,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _build_settings_update_dry_run(
|
|
271
|
+
project_dir: Path,
|
|
272
|
+
*,
|
|
273
|
+
target: str | None,
|
|
274
|
+
sources: tuple[AppSettingsSource, ...] | None,
|
|
275
|
+
reset_middlewares_order: bool,
|
|
276
|
+
) -> _SettingsUpdateDryRun:
|
|
277
|
+
config_path = project_dir / PROJECT_JSONC_NAME
|
|
278
|
+
discovered_sources = tuple(sources or discover_settings_sources(project_dir))
|
|
279
|
+
selected_sources = _resolve_target_sources(target, discovered_sources)
|
|
280
|
+
current_payload = _load_existing_document(config_path)
|
|
281
|
+
next_payload = _build_updated_payload(
|
|
282
|
+
project_dir,
|
|
283
|
+
current_payload,
|
|
284
|
+
selected_sources,
|
|
285
|
+
all_sources=discovered_sources,
|
|
286
|
+
reset_middlewares_order=reset_middlewares_order,
|
|
287
|
+
)
|
|
288
|
+
root_env_assignments = _pop_root_env_redirects(next_payload)
|
|
289
|
+
return _SettingsUpdateDryRun(
|
|
290
|
+
project_dir=project_dir,
|
|
291
|
+
config_path=config_path,
|
|
292
|
+
current_payload=current_payload,
|
|
293
|
+
next_payload=next_payload,
|
|
294
|
+
discovered_sources=discovered_sources,
|
|
295
|
+
selected_sources=selected_sources,
|
|
296
|
+
root_env_assignments=root_env_assignments,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _settings_update_changed_paths(dry_run: _SettingsUpdateDryRun) -> tuple[Path, ...]:
|
|
301
|
+
changed_paths: list[Path] = []
|
|
302
|
+
if not _json_payload_equivalent(dry_run.next_payload, dry_run.current_payload):
|
|
303
|
+
changed_paths.append(dry_run.config_path)
|
|
304
|
+
changed_paths.extend(_project_agents_changed_paths(dry_run.project_dir))
|
|
305
|
+
changed_paths.extend(_pz_env_settings_placeholder_changed_paths(dry_run.project_dir))
|
|
306
|
+
changed_paths.extend(
|
|
307
|
+
_pz_env_template_changed_paths(
|
|
308
|
+
dry_run.project_dir,
|
|
309
|
+
root_env_assignments=dry_run.root_env_assignments,
|
|
310
|
+
app_secret_templates=_collect_app_secret_templates(dry_run.discovered_sources),
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
return tuple(dict.fromkeys(changed_paths))
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _json_payload_equivalent(left: Any, right: Any) -> bool:
|
|
317
|
+
try:
|
|
318
|
+
return json.loads(json.dumps(left, ensure_ascii=False, sort_keys=True)) == json.loads(
|
|
319
|
+
json.dumps(right, ensure_ascii=False, sort_keys=True)
|
|
320
|
+
)
|
|
321
|
+
except TypeError:
|
|
322
|
+
return left == right
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _pz_env_settings_placeholder_changed_paths(project_dir: Path) -> tuple[Path, ...]:
|
|
326
|
+
changed_paths: list[Path] = []
|
|
327
|
+
for file_name, template in _PZ_ENV_SETTINGS_TEMPLATES.items():
|
|
328
|
+
path = project_dir / file_name
|
|
329
|
+
if not path.exists():
|
|
330
|
+
changed_paths.append(path)
|
|
331
|
+
continue
|
|
332
|
+
current_payload = _load_existing_document(path)
|
|
333
|
+
template_payload = _load_template_jsonc_payload(template())
|
|
334
|
+
merged_payload = _migrate_root_routing_language_fields(
|
|
335
|
+
_deep_merge_payload(template_payload, current_payload)
|
|
336
|
+
)
|
|
337
|
+
if merged_payload != current_payload:
|
|
338
|
+
changed_paths.append(path)
|
|
339
|
+
return tuple(changed_paths)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _pz_env_template_changed_paths(
|
|
343
|
+
project_dir: Path,
|
|
344
|
+
*,
|
|
345
|
+
root_env_assignments: dict[str, str],
|
|
346
|
+
app_secret_templates: tuple[tuple[str, tuple[tuple[str, str], ...]], ...],
|
|
347
|
+
) -> tuple[Path, ...]:
|
|
348
|
+
changed_paths: list[Path] = []
|
|
349
|
+
for file_name, template in _PZ_ENV_ENV_TEMPLATES.items():
|
|
350
|
+
path = project_dir / file_name
|
|
351
|
+
template_text = template() + _render_app_secret_template_block(
|
|
352
|
+
app_secret_templates,
|
|
353
|
+
commented=file_name != ".env",
|
|
354
|
+
)
|
|
355
|
+
preferred_assignments = root_env_assignments if file_name == ".env" else None
|
|
356
|
+
if not path.exists():
|
|
357
|
+
changed_paths.append(path)
|
|
358
|
+
continue
|
|
359
|
+
existing_text = path.read_text(encoding="utf-8")
|
|
360
|
+
merged_text = _merge_env_template_text(
|
|
361
|
+
template_text,
|
|
362
|
+
existing_text,
|
|
363
|
+
preferred_assignments=preferred_assignments,
|
|
364
|
+
)
|
|
365
|
+
if merged_text != existing_text:
|
|
366
|
+
changed_paths.append(path)
|
|
367
|
+
return tuple(changed_paths)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _resolve_project_dir(project_dir: str | Path) -> Path:
|
|
371
|
+
path = Path(project_dir).expanduser().resolve()
|
|
372
|
+
if not path.exists() or not path.is_dir():
|
|
373
|
+
raise PzError(
|
|
374
|
+
code="PZ_SETTINGS_PROJECT_DIR_INVALID",
|
|
375
|
+
message=f"project directory must exist: {path}",
|
|
376
|
+
stage="cli",
|
|
377
|
+
owner=_OWNER,
|
|
378
|
+
action="resolve_project_dir",
|
|
379
|
+
)
|
|
380
|
+
return path
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _resolve_existing_project_dir(project_dir: str | Path | None) -> Path | None:
|
|
384
|
+
if project_dir is None:
|
|
385
|
+
return None
|
|
386
|
+
return _resolve_project_dir(project_dir)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _settings_update_lock_path(project_dir: Path) -> Path:
|
|
390
|
+
return project_dir / ".runtime" / "run" / "locks" / _UPDATE_LOCK_NAME
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class _SettingsUpdateLock(AbstractContextManager):
|
|
394
|
+
def __init__(self, project_dir: Path) -> None:
|
|
395
|
+
self._lock_path = _settings_update_lock_path(project_dir)
|
|
396
|
+
self._handle = None
|
|
397
|
+
self._fallback_lock_dir: Path | None = None
|
|
398
|
+
|
|
399
|
+
def __enter__(self):
|
|
400
|
+
self.acquire()
|
|
401
|
+
return self
|
|
402
|
+
|
|
403
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
404
|
+
self.release()
|
|
405
|
+
|
|
406
|
+
def acquire(self) -> None:
|
|
407
|
+
self._lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
408
|
+
self._handle = self._lock_path.open("a+", encoding="utf-8")
|
|
409
|
+
if fcntl is not None:
|
|
410
|
+
try:
|
|
411
|
+
fcntl.flock(self._handle.fileno(), fcntl.LOCK_EX)
|
|
412
|
+
return
|
|
413
|
+
except Exception:
|
|
414
|
+
self._handle.close()
|
|
415
|
+
self._handle = None
|
|
416
|
+
raise
|
|
417
|
+
fallback_lock_dir = self._lock_path.with_suffix(self._lock_path.suffix + ".d")
|
|
418
|
+
try:
|
|
419
|
+
while True: # pragma: no cover
|
|
420
|
+
try:
|
|
421
|
+
fallback_lock_dir.mkdir(parents=False, exist_ok=False)
|
|
422
|
+
self._fallback_lock_dir = fallback_lock_dir
|
|
423
|
+
return
|
|
424
|
+
except FileExistsError:
|
|
425
|
+
time.sleep(0.05)
|
|
426
|
+
except Exception:
|
|
427
|
+
if self._handle is not None:
|
|
428
|
+
self._handle.close()
|
|
429
|
+
self._handle = None
|
|
430
|
+
raise
|
|
431
|
+
|
|
432
|
+
def release(self) -> None:
|
|
433
|
+
try:
|
|
434
|
+
if self._handle is not None and fcntl is not None:
|
|
435
|
+
fcntl.flock(self._handle.fileno(), fcntl.LOCK_UN)
|
|
436
|
+
finally:
|
|
437
|
+
if self._handle is not None:
|
|
438
|
+
self._handle.close()
|
|
439
|
+
self._handle = None
|
|
440
|
+
if self._fallback_lock_dir is not None:
|
|
441
|
+
try: # pragma: no cover
|
|
442
|
+
self._fallback_lock_dir.rmdir()
|
|
443
|
+
finally:
|
|
444
|
+
self._fallback_lock_dir = None
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _atomic_write_text(path: Path, content: str) -> None:
|
|
448
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
449
|
+
tmp_path = path.with_name(f".{path.name}.{uuid4().hex}.tmp")
|
|
450
|
+
try:
|
|
451
|
+
with tmp_path.open("w", encoding="utf-8") as handle:
|
|
452
|
+
handle.write(content)
|
|
453
|
+
handle.flush()
|
|
454
|
+
os.fsync(handle.fileno())
|
|
455
|
+
if path.exists():
|
|
456
|
+
try:
|
|
457
|
+
tmp_path.chmod(path.stat().st_mode)
|
|
458
|
+
except OSError:
|
|
459
|
+
pass
|
|
460
|
+
tmp_path.replace(path)
|
|
461
|
+
_fsync_parent_directory(path)
|
|
462
|
+
except Exception:
|
|
463
|
+
try:
|
|
464
|
+
tmp_path.unlink()
|
|
465
|
+
except FileNotFoundError:
|
|
466
|
+
pass
|
|
467
|
+
raise
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _fsync_parent_directory(path: Path) -> None:
|
|
471
|
+
try:
|
|
472
|
+
fd = os.open(str(path.parent), os.O_RDONLY)
|
|
473
|
+
except OSError:
|
|
474
|
+
return
|
|
475
|
+
try:
|
|
476
|
+
os.fsync(fd)
|
|
477
|
+
except OSError:
|
|
478
|
+
pass
|
|
479
|
+
finally:
|
|
480
|
+
os.close(fd)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _ensure_pz_env_settings_placeholders(project_dir: Path) -> None:
|
|
484
|
+
for file_name, template in _PZ_ENV_SETTINGS_TEMPLATES.items():
|
|
485
|
+
path = project_dir / file_name
|
|
486
|
+
template_payload = _load_template_jsonc_payload(template())
|
|
487
|
+
if not path.exists():
|
|
488
|
+
_atomic_write_text(path, template())
|
|
489
|
+
continue
|
|
490
|
+
current_payload = _load_existing_document(path)
|
|
491
|
+
merged_payload = _migrate_root_routing_language_fields(
|
|
492
|
+
_deep_merge_payload(template_payload, current_payload)
|
|
493
|
+
)
|
|
494
|
+
if merged_payload != current_payload:
|
|
495
|
+
_atomic_write_text(path, _annotate_project_base_url_comment(dump_jsonc_document(merged_payload)))
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _ensure_pz_env_env_templates(
|
|
499
|
+
project_dir: Path,
|
|
500
|
+
*,
|
|
501
|
+
root_env_assignments: dict[str, str] | None = None,
|
|
502
|
+
app_secret_templates: tuple[tuple[str, tuple[tuple[str, str], ...]], ...] = (),
|
|
503
|
+
) -> None:
|
|
504
|
+
for file_name, template in _PZ_ENV_ENV_TEMPLATES.items():
|
|
505
|
+
path = project_dir / file_name
|
|
506
|
+
template_text = template() + _render_app_secret_template_block(
|
|
507
|
+
app_secret_templates,
|
|
508
|
+
commented=file_name != ".env",
|
|
509
|
+
)
|
|
510
|
+
preferred_assignments = root_env_assignments if file_name == ".env" else None
|
|
511
|
+
if not path.exists():
|
|
512
|
+
_atomic_write_text(
|
|
513
|
+
path,
|
|
514
|
+
_merge_env_template_text(template_text, "", preferred_assignments=preferred_assignments),
|
|
515
|
+
)
|
|
516
|
+
continue
|
|
517
|
+
existing_text = path.read_text(encoding="utf-8")
|
|
518
|
+
merged_text = _merge_env_template_text(
|
|
519
|
+
template_text,
|
|
520
|
+
existing_text,
|
|
521
|
+
preferred_assignments=preferred_assignments,
|
|
522
|
+
)
|
|
523
|
+
if merged_text != existing_text:
|
|
524
|
+
_atomic_write_text(path, merged_text)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _load_template_jsonc_payload(template_text: str) -> dict[str, Any]:
|
|
528
|
+
payload = load_jsonc_text(
|
|
529
|
+
template_text,
|
|
530
|
+
code="PZ_SETTINGS_TEMPLATE_INVALID",
|
|
531
|
+
owner=_OWNER,
|
|
532
|
+
action="load_template_jsonc_payload",
|
|
533
|
+
source_name="pz_env template",
|
|
534
|
+
)
|
|
535
|
+
if not isinstance(payload, dict):
|
|
536
|
+
raise PzError(
|
|
537
|
+
code="PZ_SETTINGS_TEMPLATE_INVALID",
|
|
538
|
+
message="pz_env JSONC template must define an object payload.",
|
|
539
|
+
stage="cli",
|
|
540
|
+
owner=_OWNER,
|
|
541
|
+
action="load_template_jsonc_payload",
|
|
542
|
+
)
|
|
543
|
+
return payload
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _merge_env_template_text(
|
|
547
|
+
template_text: str,
|
|
548
|
+
existing_text: str,
|
|
549
|
+
*,
|
|
550
|
+
preferred_assignments: dict[str, str] | None = None,
|
|
551
|
+
) -> str:
|
|
552
|
+
existing_values = _parse_env_assignments(existing_text)
|
|
553
|
+
for key, value in (preferred_assignments or {}).items():
|
|
554
|
+
if key not in existing_values and str(value or "").strip():
|
|
555
|
+
existing_values[key] = str(value).strip()
|
|
556
|
+
emitted_keys: set[str] = set()
|
|
557
|
+
rendered_lines: list[str] = []
|
|
558
|
+
for line in template_text.splitlines():
|
|
559
|
+
assignment = _parse_env_assignment_line(line)
|
|
560
|
+
key = assignment[0] if assignment is not None else None
|
|
561
|
+
if key is None:
|
|
562
|
+
rendered_lines.append(line)
|
|
563
|
+
continue
|
|
564
|
+
template_value = assignment[1]
|
|
565
|
+
if key in existing_values:
|
|
566
|
+
value = existing_values[key]
|
|
567
|
+
if _is_secret_generator_directive(template_value) and not str(value or "").strip():
|
|
568
|
+
value = _generate_env_secret_value(template_value)
|
|
569
|
+
rendered_lines.append(f"{key}={value}")
|
|
570
|
+
elif _is_secret_generator_directive(template_value) and not assignment[2]:
|
|
571
|
+
rendered_lines.append(f"{key}={_generate_env_secret_value(template_value)}")
|
|
572
|
+
else:
|
|
573
|
+
rendered_lines.append(line)
|
|
574
|
+
emitted_keys.add(key)
|
|
575
|
+
extra_keys = [key for key in existing_values if key not in emitted_keys]
|
|
576
|
+
if extra_keys:
|
|
577
|
+
if rendered_lines and rendered_lines[-1] != "":
|
|
578
|
+
rendered_lines.append("")
|
|
579
|
+
rendered_lines.append("# Existing custom overrides")
|
|
580
|
+
for key in extra_keys:
|
|
581
|
+
rendered_lines.append(f"{key}={existing_values[key]}")
|
|
582
|
+
return "\n".join(rendered_lines).rstrip() + "\n"
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _parse_env_assignments(text: str) -> dict[str, str]:
|
|
586
|
+
assignments: dict[str, str] = {}
|
|
587
|
+
for raw_line in text.splitlines():
|
|
588
|
+
line = raw_line.strip()
|
|
589
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
590
|
+
continue
|
|
591
|
+
key, value = line.split("=", 1)
|
|
592
|
+
key = key.strip()
|
|
593
|
+
if key:
|
|
594
|
+
assignments[key] = value
|
|
595
|
+
return assignments
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _parse_env_assignment_line(line: str) -> tuple[str, str, bool] | None:
|
|
599
|
+
stripped = line.strip()
|
|
600
|
+
if not stripped:
|
|
601
|
+
return None
|
|
602
|
+
commented = stripped.startswith("#")
|
|
603
|
+
candidate = stripped[1:].strip() if commented else stripped
|
|
604
|
+
if "=" not in candidate:
|
|
605
|
+
return None
|
|
606
|
+
key, value = candidate.split("=", 1)
|
|
607
|
+
key = key.strip()
|
|
608
|
+
if not key or " " in key:
|
|
609
|
+
return None
|
|
610
|
+
return key, value, commented
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _parse_env_template_assignments(text: str) -> tuple[tuple[str, str], ...]:
|
|
614
|
+
assignments: list[tuple[str, str]] = []
|
|
615
|
+
seen: set[str] = set()
|
|
616
|
+
for raw_line in text.splitlines():
|
|
617
|
+
key = _extract_env_assignment_key(raw_line)
|
|
618
|
+
if key is None or key in seen:
|
|
619
|
+
continue
|
|
620
|
+
stripped = raw_line.strip()
|
|
621
|
+
candidate = stripped[1:].strip() if stripped.startswith("#") else stripped
|
|
622
|
+
_key, value = candidate.split("=", 1)
|
|
623
|
+
assignments.append((key, value))
|
|
624
|
+
seen.add(key)
|
|
625
|
+
return tuple(assignments)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _extract_env_assignment_key(line: str) -> str | None:
|
|
629
|
+
assignment = _parse_env_assignment_line(line)
|
|
630
|
+
return assignment[0] if assignment is not None else None
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _is_secret_generator_directive(value: str) -> bool:
|
|
634
|
+
return str(value or "").strip().lower().startswith(_SECRET_GENERATOR_PREFIX)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _generate_env_secret_value(value: str) -> str:
|
|
638
|
+
directive = str(value or "").strip().lower()
|
|
639
|
+
if directive in {"@pz:secret", "@pz:secret:urlsafe", "@pz:secret:urlsafe64"}:
|
|
640
|
+
return secrets.token_urlsafe(48)
|
|
641
|
+
if directive in {"@pz:secret:hex", "@pz:secret:hex64"}:
|
|
642
|
+
return secrets.token_hex(32)
|
|
643
|
+
raise PzError(
|
|
644
|
+
code="PZ_APP_DEFAULT_ENV_INVALID",
|
|
645
|
+
message=f"Unsupported env secret generator directive: {value}",
|
|
646
|
+
stage="cli",
|
|
647
|
+
owner=_OWNER,
|
|
648
|
+
action="merge_env_template",
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _collect_app_secret_templates(
|
|
653
|
+
sources: tuple[AppSettingsSource, ...],
|
|
654
|
+
) -> tuple[tuple[str, tuple[tuple[str, str], ...]], ...]:
|
|
655
|
+
collected: list[tuple[str, tuple[tuple[str, str], ...]]] = []
|
|
656
|
+
seen_apps: set[str] = set()
|
|
657
|
+
for source in sorted(sources, key=lambda item: item.app_id):
|
|
658
|
+
if source.app_id in seen_apps:
|
|
659
|
+
continue
|
|
660
|
+
assignments = _parse_env_template_assignments(source.secret_template)
|
|
661
|
+
if assignments:
|
|
662
|
+
collected.append((source.app_id, assignments))
|
|
663
|
+
seen_apps.add(source.app_id)
|
|
664
|
+
return tuple(collected)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _render_app_secret_template_block(
|
|
668
|
+
templates: tuple[tuple[str, tuple[tuple[str, str], ...]], ...],
|
|
669
|
+
*,
|
|
670
|
+
commented: bool,
|
|
671
|
+
) -> str:
|
|
672
|
+
if not templates:
|
|
673
|
+
return ""
|
|
674
|
+
lines = [
|
|
675
|
+
"",
|
|
676
|
+
"# Installed app secret placeholders",
|
|
677
|
+
"# Generated from installed apps' configs/default.env files.",
|
|
678
|
+
]
|
|
679
|
+
prefix = "# " if commented else ""
|
|
680
|
+
for app_id, assignments in templates:
|
|
681
|
+
lines.append("")
|
|
682
|
+
lines.append(f"# {app_id}")
|
|
683
|
+
for key, value in assignments:
|
|
684
|
+
rendered_value = "" if commented and _is_secret_generator_directive(value) else value
|
|
685
|
+
lines.append(f"{prefix}{key}={rendered_value}")
|
|
686
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _pop_root_env_redirects(payload: dict[str, Any]) -> dict[str, str]:
|
|
690
|
+
assignments: dict[str, str] = {}
|
|
691
|
+
for path, env_key in _ROOT_ENV_REDIRECTS.items():
|
|
692
|
+
current: Any = payload
|
|
693
|
+
for segment in path[:-1]:
|
|
694
|
+
if not isinstance(current, dict):
|
|
695
|
+
current = None
|
|
696
|
+
break
|
|
697
|
+
current = current.get(segment)
|
|
698
|
+
if not isinstance(current, dict):
|
|
699
|
+
continue
|
|
700
|
+
leaf_key = path[-1]
|
|
701
|
+
if leaf_key not in current:
|
|
702
|
+
continue
|
|
703
|
+
value = current.pop(leaf_key)
|
|
704
|
+
if value in (None, ""):
|
|
705
|
+
continue
|
|
706
|
+
assignments[env_key] = str(value)
|
|
707
|
+
return assignments
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _settings_update_backup_root(project_dir: Path) -> Path:
|
|
711
|
+
return project_dir / ".runtime" / "backups" / _UPDATE_BACKUP_DIRNAME
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _settings_backup_keep_count(payload: dict[str, Any]) -> int:
|
|
715
|
+
project = payload.get("project")
|
|
716
|
+
if isinstance(project, dict):
|
|
717
|
+
raw = project.get("settings_backup_keep_count", _UPDATE_BACKUP_DEFAULT_KEEP_COUNT)
|
|
718
|
+
else:
|
|
719
|
+
raw = _UPDATE_BACKUP_DEFAULT_KEEP_COUNT
|
|
720
|
+
try:
|
|
721
|
+
keep_count = int(raw)
|
|
722
|
+
except Exception:
|
|
723
|
+
keep_count = _UPDATE_BACKUP_DEFAULT_KEEP_COUNT
|
|
724
|
+
return keep_count if keep_count > 0 else _UPDATE_BACKUP_DEFAULT_KEEP_COUNT
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _update_backup_candidates(project_dir: Path) -> tuple[Path, ...]:
|
|
728
|
+
candidates = (
|
|
729
|
+
"AGENTS.md",
|
|
730
|
+
PROJECT_JSONC_NAME,
|
|
731
|
+
"pz.develop.jsonc",
|
|
732
|
+
"pz.test.jsonc",
|
|
733
|
+
"pz.product.jsonc",
|
|
734
|
+
".env",
|
|
735
|
+
".env.develop",
|
|
736
|
+
".env.test",
|
|
737
|
+
".env.product",
|
|
738
|
+
)
|
|
739
|
+
return tuple(project_dir / item for item in candidates)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _create_update_backup(project_dir: Path, current_payload: dict[str, Any]) -> Path:
|
|
743
|
+
backup_root = _settings_update_backup_root(project_dir)
|
|
744
|
+
backup_root.mkdir(parents=True, exist_ok=True)
|
|
745
|
+
backup_dir = backup_root / datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
|
|
746
|
+
backup_dir.mkdir(parents=True, exist_ok=False)
|
|
747
|
+
for source in _update_backup_candidates(project_dir):
|
|
748
|
+
if not source.exists():
|
|
749
|
+
continue
|
|
750
|
+
shutil.copy2(source, backup_dir / source.name)
|
|
751
|
+
_prune_update_backups(backup_root, keep_count=_settings_backup_keep_count(current_payload))
|
|
752
|
+
return backup_dir
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _project_agents_changed_paths(project_dir: Path) -> tuple[Path, ...]:
|
|
756
|
+
path = project_dir / "AGENTS.md"
|
|
757
|
+
current = path.read_text(encoding="utf-8") if path.exists() else None
|
|
758
|
+
expected = render_project_agents_file_text(project_dir)
|
|
759
|
+
if current == expected:
|
|
760
|
+
return ()
|
|
761
|
+
return (path,)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _prune_update_backups(backup_root: Path, *, keep_count: int) -> None:
|
|
765
|
+
existing_dirs = sorted(
|
|
766
|
+
[path for path in backup_root.iterdir() if path.is_dir()],
|
|
767
|
+
key=lambda item: item.name,
|
|
768
|
+
reverse=True,
|
|
769
|
+
)
|
|
770
|
+
for stale in existing_dirs[keep_count:]:
|
|
771
|
+
shutil.rmtree(stale, ignore_errors=True)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _declared_app_entries(
|
|
775
|
+
project_dir: Path | None,
|
|
776
|
+
*,
|
|
777
|
+
include_project_apps_container: bool = False,
|
|
778
|
+
) -> tuple[str, ...]:
|
|
779
|
+
if project_dir is None:
|
|
780
|
+
return ("apps",)
|
|
781
|
+
|
|
782
|
+
entries: list[str] = []
|
|
783
|
+
config_path = project_dir / PROJECT_JSONC_NAME
|
|
784
|
+
if config_path.is_file():
|
|
785
|
+
data = _load_existing_document(config_path)
|
|
786
|
+
raw = data.get("installed_apps")
|
|
787
|
+
if isinstance(raw, list):
|
|
788
|
+
entries.extend(
|
|
789
|
+
str(item).strip()
|
|
790
|
+
for item in raw
|
|
791
|
+
if isinstance(item, str)
|
|
792
|
+
and str(item).strip()
|
|
793
|
+
and _is_formal_app_or_container_entry(project_dir, str(item).strip())
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
if include_project_apps_container and (project_dir / "apps").is_dir() and "apps" not in entries:
|
|
797
|
+
entries.append("apps")
|
|
798
|
+
|
|
799
|
+
if entries:
|
|
800
|
+
return tuple(dict.fromkeys(entries))
|
|
801
|
+
|
|
802
|
+
if (project_dir / "apps").is_dir():
|
|
803
|
+
return ("apps",)
|
|
804
|
+
return ()
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def _build_settings_source(module_path: str) -> AppSettingsSource:
|
|
808
|
+
app_id = _load_app_id(module_path)
|
|
809
|
+
raw_defaults, raw_comments = _load_app_settings_defaults_with_comments(module_path)
|
|
810
|
+
defaults, root_defaults, comments, root_comments = _split_app_settings_defaults_with_comments(
|
|
811
|
+
raw_defaults,
|
|
812
|
+
raw_comments,
|
|
813
|
+
module_path=module_path,
|
|
814
|
+
)
|
|
815
|
+
normalize = _load_app_settings_normalizer(module_path)
|
|
816
|
+
secret_template = _load_app_settings_secret_template(module_path)
|
|
817
|
+
_validate_app_settings_secret_template(
|
|
818
|
+
app_id=app_id,
|
|
819
|
+
module_path=module_path,
|
|
820
|
+
defaults=defaults,
|
|
821
|
+
secret_template=secret_template,
|
|
822
|
+
)
|
|
823
|
+
return AppSettingsSource(
|
|
824
|
+
module_path=module_path,
|
|
825
|
+
app_id=app_id,
|
|
826
|
+
defaults=defaults,
|
|
827
|
+
normalize=normalize,
|
|
828
|
+
secret_template=secret_template,
|
|
829
|
+
root_defaults=root_defaults,
|
|
830
|
+
comments=comments,
|
|
831
|
+
root_comments=root_comments,
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _dedupe_settings_sources_by_app_id(
|
|
836
|
+
project_dir: Path | None,
|
|
837
|
+
source_pairs: list[tuple[Any, AppSettingsSource]],
|
|
838
|
+
*,
|
|
839
|
+
prefer_project_apps_leaf: bool,
|
|
840
|
+
) -> list[AppSettingsSource]:
|
|
841
|
+
selected: dict[str, tuple[Any, AppSettingsSource]] = {}
|
|
842
|
+
for ref, source in source_pairs:
|
|
843
|
+
existing = selected.get(source.app_id)
|
|
844
|
+
if existing is None:
|
|
845
|
+
selected[source.app_id] = (ref, source)
|
|
846
|
+
continue
|
|
847
|
+
if prefer_project_apps_leaf and _prefer_settings_source_over_existing(project_dir, ref, source, existing):
|
|
848
|
+
selected[source.app_id] = (ref, source)
|
|
849
|
+
continue
|
|
850
|
+
return [item[1] for item in selected.values()]
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _prefer_settings_source_over_existing(
|
|
854
|
+
project_dir: Path | None,
|
|
855
|
+
ref: Any,
|
|
856
|
+
source: AppSettingsSource,
|
|
857
|
+
existing: tuple[Any, AppSettingsSource],
|
|
858
|
+
) -> bool:
|
|
859
|
+
if not _is_project_apps_leaf_ref(project_dir, ref):
|
|
860
|
+
return False
|
|
861
|
+
existing_ref, existing_source = existing
|
|
862
|
+
if not _is_project_apps_leaf_ref(project_dir, existing_ref):
|
|
863
|
+
return True
|
|
864
|
+
return existing_source.module_path.startswith("apps.") and not source.module_path.startswith("apps.")
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _is_project_apps_leaf_ref(project_dir: Path | None, ref: Any) -> bool:
|
|
868
|
+
if project_dir is None:
|
|
869
|
+
return False
|
|
870
|
+
import_root = getattr(ref, "import_root", None)
|
|
871
|
+
if import_root is None:
|
|
872
|
+
return False
|
|
873
|
+
try:
|
|
874
|
+
root = Path(import_root).resolve()
|
|
875
|
+
apps_root = (project_dir / "apps").resolve()
|
|
876
|
+
return root == apps_root or root.is_relative_to(apps_root)
|
|
877
|
+
except Exception:
|
|
878
|
+
return False
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _load_app_id(module_path: str) -> str:
|
|
882
|
+
try:
|
|
883
|
+
return resolve_declared_app_id(module_path)
|
|
884
|
+
except PzError as exc:
|
|
885
|
+
raise PzError(
|
|
886
|
+
code="PZ_SETTINGS_APP_ID_INVALID",
|
|
887
|
+
message=f"App '{module_path}' must expose a valid app.id identifier.",
|
|
888
|
+
stage="cli",
|
|
889
|
+
owner=_OWNER,
|
|
890
|
+
action="discover_settings_sources",
|
|
891
|
+
detail=exc.detail,
|
|
892
|
+
) from exc
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _load_app_settings_defaults(module_path: str) -> dict[str, Any]:
|
|
896
|
+
defaults, _comments = _load_app_settings_defaults_with_comments(module_path)
|
|
897
|
+
return defaults
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def _load_app_settings_defaults_with_comments(
|
|
901
|
+
module_path: str,
|
|
902
|
+
) -> tuple[dict[str, Any], dict[tuple[str, ...], tuple[str, ...]]]:
|
|
903
|
+
package_root = resolve_package_root(module_path)
|
|
904
|
+
if package_root is None:
|
|
905
|
+
return {}, {}
|
|
906
|
+
default_jsonc_path = package_root / "configs" / "default.jsonc"
|
|
907
|
+
if not default_jsonc_path.is_file():
|
|
908
|
+
return {}, {}
|
|
909
|
+
raw_text = default_jsonc_path.read_text(encoding="utf-8")
|
|
910
|
+
payload = load_jsonc_file(
|
|
911
|
+
default_jsonc_path,
|
|
912
|
+
code="PZ_APP_DEFAULTS_INVALID",
|
|
913
|
+
owner=_OWNER,
|
|
914
|
+
action="load_app_settings_defaults",
|
|
915
|
+
)
|
|
916
|
+
if not isinstance(payload, dict):
|
|
917
|
+
raise PzError(
|
|
918
|
+
code="PZ_APP_DEFAULTS_INVALID",
|
|
919
|
+
message=f"App default settings file '{default_jsonc_path}' must define an object payload.",
|
|
920
|
+
stage="cli",
|
|
921
|
+
owner=_OWNER,
|
|
922
|
+
action="load_app_settings_defaults",
|
|
923
|
+
)
|
|
924
|
+
return dict(payload), _extract_default_jsonc_comments(raw_text)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def _split_app_settings_defaults(
|
|
928
|
+
payload: dict[str, Any],
|
|
929
|
+
*,
|
|
930
|
+
module_path: str,
|
|
931
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
932
|
+
defaults, root_defaults, _comments, _root_comments = _split_app_settings_defaults_with_comments(
|
|
933
|
+
payload,
|
|
934
|
+
{},
|
|
935
|
+
module_path=module_path,
|
|
936
|
+
)
|
|
937
|
+
return defaults, root_defaults
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _split_app_settings_defaults_with_comments(
|
|
941
|
+
payload: dict[str, Any],
|
|
942
|
+
comments: dict[tuple[str, ...], tuple[str, ...]],
|
|
943
|
+
*,
|
|
944
|
+
module_path: str,
|
|
945
|
+
) -> tuple[
|
|
946
|
+
dict[str, Any],
|
|
947
|
+
dict[str, Any],
|
|
948
|
+
dict[tuple[str, ...], tuple[str, ...]],
|
|
949
|
+
dict[tuple[str, ...], tuple[str, ...]],
|
|
950
|
+
]:
|
|
951
|
+
defaults = dict(payload)
|
|
952
|
+
app_comments = dict(comments)
|
|
953
|
+
root_defaults: dict[str, Any] = {}
|
|
954
|
+
root_comments: dict[tuple[str, ...], tuple[str, ...]] = {}
|
|
955
|
+
if "routing" in defaults:
|
|
956
|
+
root_defaults["routing"] = _normalize_routing_defaults(defaults.pop("routing"), module_path=module_path)
|
|
957
|
+
routing_prefix = ("routing",)
|
|
958
|
+
for path, lines in tuple(app_comments.items()):
|
|
959
|
+
if path == routing_prefix or path[:1] == routing_prefix:
|
|
960
|
+
root_comments[path] = lines
|
|
961
|
+
app_comments.pop(path, None)
|
|
962
|
+
if "commands" in defaults:
|
|
963
|
+
root_defaults["commands"] = _normalize_commands_defaults(defaults.pop("commands"), module_path=module_path)
|
|
964
|
+
commands_prefix = ("commands",)
|
|
965
|
+
for path, lines in tuple(app_comments.items()):
|
|
966
|
+
if path == commands_prefix or path[:1] == commands_prefix:
|
|
967
|
+
root_comments[path] = lines
|
|
968
|
+
app_comments.pop(path, None)
|
|
969
|
+
if "apps" in defaults:
|
|
970
|
+
root_defaults["apps"] = _normalize_apps_defaults(defaults.pop("apps"), module_path=module_path)
|
|
971
|
+
apps_prefix = ("apps",)
|
|
972
|
+
for path, lines in tuple(app_comments.items()):
|
|
973
|
+
if path == apps_prefix or path[:1] == apps_prefix:
|
|
974
|
+
root_comments[path] = lines
|
|
975
|
+
app_comments.pop(path, None)
|
|
976
|
+
return defaults, root_defaults, app_comments, root_comments
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _normalize_routing_defaults(payload: Any, *, module_path: str) -> dict[str, Any]:
|
|
980
|
+
if not isinstance(payload, dict):
|
|
981
|
+
raise PzError(
|
|
982
|
+
code="PZ_APP_DEFAULTS_INVALID",
|
|
983
|
+
message=(
|
|
984
|
+
f"App default settings file '{module_path}/configs/default.jsonc' field "
|
|
985
|
+
"'routing' must define an object payload."
|
|
986
|
+
),
|
|
987
|
+
stage="cli",
|
|
988
|
+
owner=_OWNER,
|
|
989
|
+
action="load_app_settings_defaults",
|
|
990
|
+
)
|
|
991
|
+
out: dict[str, Any] = {}
|
|
992
|
+
for raw_key, raw_value in payload.items():
|
|
993
|
+
key = str(raw_key or "").strip()
|
|
994
|
+
if key not in _ROUTING_DEFAULT_KEYS:
|
|
995
|
+
raise PzError(
|
|
996
|
+
code="PZ_APP_DEFAULTS_INVALID",
|
|
997
|
+
message=(
|
|
998
|
+
f"App default settings file '{module_path}/configs/default.jsonc' field "
|
|
999
|
+
f"'routing.{key}' is not supported."
|
|
1000
|
+
),
|
|
1001
|
+
stage="cli",
|
|
1002
|
+
owner=_OWNER,
|
|
1003
|
+
action="load_app_settings_defaults",
|
|
1004
|
+
)
|
|
1005
|
+
out[key] = raw_value
|
|
1006
|
+
return out
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _normalize_commands_defaults(payload: Any, *, module_path: str) -> dict[str, Any]:
|
|
1010
|
+
if not isinstance(payload, dict):
|
|
1011
|
+
raise PzError(
|
|
1012
|
+
code="PZ_APP_DEFAULTS_INVALID",
|
|
1013
|
+
message=(
|
|
1014
|
+
f"App default settings file '{module_path}/configs/default.jsonc' field "
|
|
1015
|
+
"'commands' must define an object payload."
|
|
1016
|
+
),
|
|
1017
|
+
stage="cli",
|
|
1018
|
+
owner=_OWNER,
|
|
1019
|
+
action="load_app_settings_defaults",
|
|
1020
|
+
)
|
|
1021
|
+
return dict(payload)
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def _normalize_apps_defaults(payload: Any, *, module_path: str) -> dict[str, Any]:
|
|
1025
|
+
if not isinstance(payload, dict):
|
|
1026
|
+
raise PzError(
|
|
1027
|
+
code="PZ_APP_DEFAULTS_INVALID",
|
|
1028
|
+
message=(
|
|
1029
|
+
f"App default settings file '{module_path}/configs/default.jsonc' field "
|
|
1030
|
+
"'apps' must define an object payload."
|
|
1031
|
+
),
|
|
1032
|
+
stage="cli",
|
|
1033
|
+
owner=_OWNER,
|
|
1034
|
+
action="load_app_settings_defaults",
|
|
1035
|
+
)
|
|
1036
|
+
return dict(payload)
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _should_refresh_root_default(path: tuple[str, ...]) -> bool:
|
|
1040
|
+
if path and path[0] in _ROOT_DEFAULT_REFRESH_KEYS:
|
|
1041
|
+
return True
|
|
1042
|
+
if len(path) >= 4 and path[0] == "apps" and path[2] == "sources" and path[3] == "source_mode":
|
|
1043
|
+
return True
|
|
1044
|
+
return False
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def _extract_default_jsonc_comments(text: str) -> dict[tuple[str, ...], tuple[str, ...]]:
|
|
1048
|
+
comments: dict[tuple[str, ...], tuple[str, ...]] = {}
|
|
1049
|
+
stack: list[tuple[str, int]] = []
|
|
1050
|
+
pending: list[str] = []
|
|
1051
|
+
for raw_line in text.splitlines():
|
|
1052
|
+
line = raw_line.strip()
|
|
1053
|
+
if not line:
|
|
1054
|
+
pending = []
|
|
1055
|
+
continue
|
|
1056
|
+
if line.startswith("//") and not line.startswith("// @pz:"):
|
|
1057
|
+
pending.append(line[2:].strip())
|
|
1058
|
+
continue
|
|
1059
|
+
key = _jsonc_object_key_from_line(line)
|
|
1060
|
+
if key is not None:
|
|
1061
|
+
depth = _leading_space_count(raw_line) // 2
|
|
1062
|
+
while stack and stack[-1][1] >= depth:
|
|
1063
|
+
stack.pop()
|
|
1064
|
+
path = tuple(item[0] for item in stack) + (key,)
|
|
1065
|
+
if pending:
|
|
1066
|
+
comments[path] = tuple(item for item in pending if item)
|
|
1067
|
+
pending = []
|
|
1068
|
+
if _line_opens_object(line):
|
|
1069
|
+
stack.append((key, depth))
|
|
1070
|
+
if _line_closes_object(line):
|
|
1071
|
+
while stack and stack[-1][1] >= depth:
|
|
1072
|
+
stack.pop()
|
|
1073
|
+
continue
|
|
1074
|
+
pending = []
|
|
1075
|
+
if _line_closes_object(line):
|
|
1076
|
+
depth = _leading_space_count(raw_line) // 2
|
|
1077
|
+
while stack and stack[-1][1] >= depth:
|
|
1078
|
+
stack.pop()
|
|
1079
|
+
return comments
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _leading_space_count(value: str) -> int:
|
|
1083
|
+
return len(value) - len(value.lstrip(" "))
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _jsonc_object_key_from_line(line: str) -> str | None:
|
|
1087
|
+
if not line.startswith('"'):
|
|
1088
|
+
return None
|
|
1089
|
+
escaped = False
|
|
1090
|
+
for index in range(1, len(line)):
|
|
1091
|
+
char = line[index]
|
|
1092
|
+
if escaped:
|
|
1093
|
+
escaped = False
|
|
1094
|
+
continue
|
|
1095
|
+
if char == "\\":
|
|
1096
|
+
escaped = True
|
|
1097
|
+
continue
|
|
1098
|
+
if char != '"':
|
|
1099
|
+
continue
|
|
1100
|
+
rest = line[index + 1 :].lstrip()
|
|
1101
|
+
if not rest.startswith(":"):
|
|
1102
|
+
return None
|
|
1103
|
+
try:
|
|
1104
|
+
key = json.loads(line[: index + 1])
|
|
1105
|
+
except Exception:
|
|
1106
|
+
return None
|
|
1107
|
+
return str(key)
|
|
1108
|
+
return None
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def _line_opens_object(line: str) -> bool:
|
|
1112
|
+
return line.rstrip().endswith("{") or line.rstrip().endswith("{,")
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def _line_closes_object(line: str) -> bool:
|
|
1116
|
+
stripped = line.lstrip()
|
|
1117
|
+
return stripped.startswith("}") or stripped.startswith("]")
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def _load_app_settings_secret_template(module_path: str) -> str:
|
|
1121
|
+
package_root = resolve_package_root(module_path)
|
|
1122
|
+
if package_root is None:
|
|
1123
|
+
return ""
|
|
1124
|
+
default_env_path = package_root / "configs" / "default.env"
|
|
1125
|
+
if not default_env_path.is_file():
|
|
1126
|
+
return ""
|
|
1127
|
+
return default_env_path.read_text(encoding="utf-8")
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def _flatten_payload_paths(payload: dict[str, Any], prefix: tuple[str, ...] = ()) -> set[tuple[str, ...]]:
|
|
1131
|
+
paths: set[tuple[str, ...]] = set()
|
|
1132
|
+
for key, value in payload.items():
|
|
1133
|
+
path = (*prefix, str(key).strip().lower())
|
|
1134
|
+
paths.add(path)
|
|
1135
|
+
if isinstance(value, dict):
|
|
1136
|
+
paths.update(_flatten_payload_paths(value, path))
|
|
1137
|
+
return paths
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
def _is_secret_template_path(path: tuple[str, ...]) -> bool:
|
|
1141
|
+
normalized_path = "__".join(path).lower()
|
|
1142
|
+
return any(token in normalized_path for token in _SECRET_TEMPLATE_KEY_TOKENS)
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _validate_app_settings_secret_template(
|
|
1146
|
+
*,
|
|
1147
|
+
app_id: str,
|
|
1148
|
+
module_path: str,
|
|
1149
|
+
defaults: dict[str, Any],
|
|
1150
|
+
secret_template: str,
|
|
1151
|
+
) -> None:
|
|
1152
|
+
assignments = _parse_env_template_assignments(secret_template)
|
|
1153
|
+
if not assignments:
|
|
1154
|
+
return
|
|
1155
|
+
allowed_paths = _flatten_payload_paths(defaults)
|
|
1156
|
+
prefix = app_id.replace("-", "_") + "__"
|
|
1157
|
+
for key, _value in assignments:
|
|
1158
|
+
if not key.startswith(prefix):
|
|
1159
|
+
raise PzError(
|
|
1160
|
+
code="PZ_APP_DEFAULT_ENV_INVALID",
|
|
1161
|
+
message=(
|
|
1162
|
+
f"App secret template key '{key}' must start with '{prefix}' "
|
|
1163
|
+
f"for app '{app_id}'."
|
|
1164
|
+
),
|
|
1165
|
+
stage="cli",
|
|
1166
|
+
owner=_OWNER,
|
|
1167
|
+
action="load_app_settings_secret_template",
|
|
1168
|
+
)
|
|
1169
|
+
nested_path = tuple(part.strip().lower() for part in key[len(prefix) :].split("__") if part.strip())
|
|
1170
|
+
if not nested_path or nested_path not in allowed_paths:
|
|
1171
|
+
raise PzError(
|
|
1172
|
+
code="PZ_APP_DEFAULT_ENV_INVALID",
|
|
1173
|
+
message=(
|
|
1174
|
+
f"App secret template key '{key}' is not backed by "
|
|
1175
|
+
f"{module_path}/configs/default.jsonc."
|
|
1176
|
+
),
|
|
1177
|
+
stage="cli",
|
|
1178
|
+
owner=_OWNER,
|
|
1179
|
+
action="load_app_settings_secret_template",
|
|
1180
|
+
)
|
|
1181
|
+
if not _is_secret_template_path(nested_path):
|
|
1182
|
+
raise PzError(
|
|
1183
|
+
code="PZ_APP_DEFAULT_ENV_INVALID",
|
|
1184
|
+
message=(
|
|
1185
|
+
f"App secret template key '{key}' must target a password, secret, token, "
|
|
1186
|
+
"key, or webhook setting declared by the app defaults."
|
|
1187
|
+
),
|
|
1188
|
+
stage="cli",
|
|
1189
|
+
owner=_OWNER,
|
|
1190
|
+
action="load_app_settings_secret_template",
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def _load_app_settings_normalizer(module_path: str) -> SettingsPayloadNormalizer | None:
|
|
1195
|
+
settings_module = import_app_settings_module(
|
|
1196
|
+
module_path,
|
|
1197
|
+
action="load_app_settings_normalizer",
|
|
1198
|
+
)
|
|
1199
|
+
if settings_module is None:
|
|
1200
|
+
return None
|
|
1201
|
+
normalizer = getattr(settings_module, "normalize_settings_payload", None)
|
|
1202
|
+
if normalizer is None:
|
|
1203
|
+
normalizer = getattr(settings_module, "_normalize_settings_payload", None)
|
|
1204
|
+
if normalizer is None:
|
|
1205
|
+
return None
|
|
1206
|
+
if not callable(normalizer):
|
|
1207
|
+
raise PzError(
|
|
1208
|
+
code="PZ_APP_SETTINGS_NORMALIZER_INVALID",
|
|
1209
|
+
message=(
|
|
1210
|
+
f"App settings entrypoint '{module_path}.settings' must expose "
|
|
1211
|
+
"'normalize_settings_payload()' or '_normalize_settings_payload()' as a callable."
|
|
1212
|
+
),
|
|
1213
|
+
stage="cli",
|
|
1214
|
+
owner=_OWNER,
|
|
1215
|
+
action="load_app_settings_normalizer",
|
|
1216
|
+
)
|
|
1217
|
+
return normalizer
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def _resolve_target_sources(target: str | None, sources: tuple[AppSettingsSource, ...]) -> tuple[AppSettingsSource, ...]:
|
|
1221
|
+
if target is None:
|
|
1222
|
+
return sources
|
|
1223
|
+
token = str(target).strip()
|
|
1224
|
+
if not token:
|
|
1225
|
+
raise PzError(
|
|
1226
|
+
code="PZ_SETTINGS_TARGET_INVALID",
|
|
1227
|
+
message="target must be a non-empty app selector when provided.",
|
|
1228
|
+
stage="cli",
|
|
1229
|
+
owner=_OWNER,
|
|
1230
|
+
action="update_settings",
|
|
1231
|
+
)
|
|
1232
|
+
if token.startswith("apps."):
|
|
1233
|
+
prefixed_matches = tuple(item for item in sources if item.module_path == token or item.module_path.startswith(token + "."))
|
|
1234
|
+
if prefixed_matches:
|
|
1235
|
+
return prefixed_matches
|
|
1236
|
+
exact_matches = tuple(
|
|
1237
|
+
item
|
|
1238
|
+
for item in sources
|
|
1239
|
+
if item.app_id == token or item.module_path == token or item.module_path.rsplit(".", 1)[-1] == token
|
|
1240
|
+
)
|
|
1241
|
+
if exact_matches:
|
|
1242
|
+
return exact_matches
|
|
1243
|
+
raise PzError(
|
|
1244
|
+
code="PZ_SETTINGS_TARGET_NOT_FOUND",
|
|
1245
|
+
message=f"No app settings source matched target '{token}'.",
|
|
1246
|
+
stage="cli",
|
|
1247
|
+
owner=_OWNER,
|
|
1248
|
+
action="update_settings",
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def _load_existing_document(path: Path) -> dict[str, Any]:
|
|
1253
|
+
return load_jsonc_file(
|
|
1254
|
+
path,
|
|
1255
|
+
code="PZ_PROJECT_CONFIG_INVALID",
|
|
1256
|
+
owner=_OWNER,
|
|
1257
|
+
action="read_project_jsonc",
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
def _build_initialized_payload(
|
|
1262
|
+
project_dir: Path,
|
|
1263
|
+
current_payload: dict[str, Any],
|
|
1264
|
+
sources: tuple[AppSettingsSource, ...],
|
|
1265
|
+
) -> dict[str, Any]:
|
|
1266
|
+
next_payload = _build_project_payload_with_defaults(current_payload)
|
|
1267
|
+
installed_apps = _managed_installed_app_entries(
|
|
1268
|
+
project_dir,
|
|
1269
|
+
current_payload,
|
|
1270
|
+
sources,
|
|
1271
|
+
preserve_existing=False,
|
|
1272
|
+
)
|
|
1273
|
+
next_payload["installed_apps"] = installed_apps
|
|
1274
|
+
next_payload["apps"] = _managed_app_settings_payload(
|
|
1275
|
+
project_dir,
|
|
1276
|
+
current_payload,
|
|
1277
|
+
sources,
|
|
1278
|
+
installed_apps=installed_apps,
|
|
1279
|
+
)
|
|
1280
|
+
_apply_app_root_defaults(next_payload, sources, preserve_existing=False)
|
|
1281
|
+
next_payload["middlewares"] = _managed_middlewares_payload(
|
|
1282
|
+
project_dir,
|
|
1283
|
+
current_payload,
|
|
1284
|
+
next_payload,
|
|
1285
|
+
preserve_existing=False,
|
|
1286
|
+
)
|
|
1287
|
+
return _ordered_root_payload(next_payload)
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def _build_updated_payload(
|
|
1291
|
+
project_dir: Path,
|
|
1292
|
+
current_payload: dict[str, Any],
|
|
1293
|
+
sources: tuple[AppSettingsSource, ...],
|
|
1294
|
+
all_sources: tuple[AppSettingsSource, ...],
|
|
1295
|
+
*,
|
|
1296
|
+
reset_middlewares_order: bool,
|
|
1297
|
+
) -> dict[str, Any]:
|
|
1298
|
+
next_payload = _build_project_payload_with_defaults(current_payload)
|
|
1299
|
+
installed_apps = _managed_installed_app_entries(
|
|
1300
|
+
project_dir,
|
|
1301
|
+
current_payload,
|
|
1302
|
+
all_sources,
|
|
1303
|
+
preserve_existing=True,
|
|
1304
|
+
)
|
|
1305
|
+
next_payload["installed_apps"] = installed_apps
|
|
1306
|
+
|
|
1307
|
+
next_payload["apps"] = _managed_app_settings_payload(
|
|
1308
|
+
project_dir,
|
|
1309
|
+
current_payload,
|
|
1310
|
+
sources,
|
|
1311
|
+
installed_apps=installed_apps,
|
|
1312
|
+
)
|
|
1313
|
+
_apply_app_root_defaults(next_payload, sources, preserve_existing=True)
|
|
1314
|
+
next_payload["middlewares"] = _managed_middlewares_payload(
|
|
1315
|
+
project_dir,
|
|
1316
|
+
current_payload,
|
|
1317
|
+
next_payload,
|
|
1318
|
+
preserve_existing=not reset_middlewares_order,
|
|
1319
|
+
)
|
|
1320
|
+
return _ordered_root_payload(next_payload)
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
def _apply_app_root_defaults(
|
|
1324
|
+
payload: dict[str, Any],
|
|
1325
|
+
sources: tuple[AppSettingsSource, ...],
|
|
1326
|
+
*,
|
|
1327
|
+
preserve_existing: bool,
|
|
1328
|
+
) -> None:
|
|
1329
|
+
for source in sorted(sources, key=lambda item: (item.module_path, item.app_id)):
|
|
1330
|
+
defaults = source.root_defaults if isinstance(source.root_defaults, dict) else {}
|
|
1331
|
+
if not defaults:
|
|
1332
|
+
continue
|
|
1333
|
+
_merge_root_defaults(
|
|
1334
|
+
payload,
|
|
1335
|
+
defaults,
|
|
1336
|
+
source=source.module_path,
|
|
1337
|
+
preserve_existing=preserve_existing,
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def _merge_root_defaults(
|
|
1342
|
+
payload: dict[str, Any],
|
|
1343
|
+
defaults: dict[str, Any],
|
|
1344
|
+
*,
|
|
1345
|
+
source: str,
|
|
1346
|
+
preserve_existing: bool,
|
|
1347
|
+
path: tuple[str, ...] = (),
|
|
1348
|
+
) -> None:
|
|
1349
|
+
for raw_key, raw_value in defaults.items():
|
|
1350
|
+
key = str(raw_key)
|
|
1351
|
+
current_path = path + (key,)
|
|
1352
|
+
if isinstance(raw_value, dict):
|
|
1353
|
+
existing = payload.get(key)
|
|
1354
|
+
if existing is not None and not isinstance(existing, dict):
|
|
1355
|
+
if preserve_existing:
|
|
1356
|
+
continue
|
|
1357
|
+
raise PzError(
|
|
1358
|
+
code="PZ_APP_ROOT_DEFAULT_CONFLICT",
|
|
1359
|
+
message=(
|
|
1360
|
+
f"App root default from '{source}' cannot merge "
|
|
1361
|
+
f"'{'.'.join(current_path)}' into a non-object value."
|
|
1362
|
+
),
|
|
1363
|
+
stage="cli",
|
|
1364
|
+
owner=_OWNER,
|
|
1365
|
+
action="merge_app_root_defaults",
|
|
1366
|
+
)
|
|
1367
|
+
target = existing if isinstance(existing, dict) else {}
|
|
1368
|
+
payload[key] = target
|
|
1369
|
+
_merge_root_defaults(
|
|
1370
|
+
target,
|
|
1371
|
+
raw_value,
|
|
1372
|
+
source=source,
|
|
1373
|
+
preserve_existing=preserve_existing,
|
|
1374
|
+
path=current_path,
|
|
1375
|
+
)
|
|
1376
|
+
continue
|
|
1377
|
+
refresh_root_default = _should_refresh_root_default(current_path)
|
|
1378
|
+
if key in payload and preserve_existing and not refresh_root_default:
|
|
1379
|
+
continue
|
|
1380
|
+
payload[key] = raw_value
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
def _build_project_payload_with_defaults(current_payload: dict[str, Any]) -> dict[str, Any]:
|
|
1384
|
+
sanitized_payload = _migrate_root_routing_language_fields(dict(current_payload))
|
|
1385
|
+
sanitized_payload.pop("catalog", None)
|
|
1386
|
+
return _migrate_root_routing_language_fields(
|
|
1387
|
+
_deep_merge_payload(load_project_default_jsonc_payload(), sanitized_payload)
|
|
1388
|
+
)
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
def _collect_project_jsonc_comments(sources: tuple[AppSettingsSource, ...]) -> dict[tuple[str, ...], tuple[str, ...]]:
|
|
1392
|
+
comments: dict[tuple[str, ...], tuple[str, ...]] = {}
|
|
1393
|
+
for source in sources:
|
|
1394
|
+
for path, lines in (source.comments or {}).items():
|
|
1395
|
+
comments[("apps", source.app_id, *path)] = lines
|
|
1396
|
+
for path, lines in (source.root_comments or {}).items():
|
|
1397
|
+
comments[path] = lines
|
|
1398
|
+
return comments
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
def _deep_merge_payload(base: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]:
|
|
1402
|
+
merged: dict[str, Any] = dict(base)
|
|
1403
|
+
for key, value in incoming.items():
|
|
1404
|
+
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
1405
|
+
merged[key] = _deep_merge_payload(merged[key], value)
|
|
1406
|
+
continue
|
|
1407
|
+
merged[key] = value
|
|
1408
|
+
return merged
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
def _migrate_root_routing_language_fields(payload: dict[str, Any]) -> dict[str, Any]:
|
|
1412
|
+
routing = payload.get("routing")
|
|
1413
|
+
if not isinstance(routing, dict) or "supported_languages" not in routing:
|
|
1414
|
+
return payload
|
|
1415
|
+
migrated = dict(routing)
|
|
1416
|
+
legacy_value = migrated.pop("supported_languages", None)
|
|
1417
|
+
if "all_supported_languages" not in migrated:
|
|
1418
|
+
migrated["all_supported_languages"] = legacy_value
|
|
1419
|
+
payload["routing"] = migrated
|
|
1420
|
+
return payload
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
def _read_installed_apps(payload: dict[str, Any]) -> list[str]:
|
|
1424
|
+
raw = payload.get("installed_apps")
|
|
1425
|
+
if isinstance(raw, list):
|
|
1426
|
+
return [str(item).strip() for item in raw if isinstance(item, str) and str(item).strip()]
|
|
1427
|
+
return []
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
def _managed_installed_app_entries(
|
|
1431
|
+
project_dir: Path,
|
|
1432
|
+
current_payload: dict[str, Any],
|
|
1433
|
+
sources: tuple[AppSettingsSource, ...],
|
|
1434
|
+
*,
|
|
1435
|
+
preserve_existing: bool,
|
|
1436
|
+
) -> list[str]:
|
|
1437
|
+
seeded_entries = _seed_installed_app_entries(project_dir, current_payload, sources)
|
|
1438
|
+
if not preserve_existing:
|
|
1439
|
+
return list(seeded_entries)
|
|
1440
|
+
existing = [
|
|
1441
|
+
item
|
|
1442
|
+
for item in _read_installed_apps(current_payload)
|
|
1443
|
+
if _is_formal_app_or_container_entry(project_dir, item)
|
|
1444
|
+
]
|
|
1445
|
+
if existing:
|
|
1446
|
+
if len(existing) > 1 and any(_is_container_entry(project_dir, item) for item in existing):
|
|
1447
|
+
return list(_conventional_installed_app_order(project_dir, current_payload, existing))
|
|
1448
|
+
return existing
|
|
1449
|
+
return list(seeded_entries)
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
def _seed_installed_app_entries(
|
|
1453
|
+
project_dir: Path,
|
|
1454
|
+
current_payload: dict[str, Any],
|
|
1455
|
+
sources: tuple[AppSettingsSource, ...],
|
|
1456
|
+
) -> list[str]:
|
|
1457
|
+
declared = [item for item in _read_installed_apps(current_payload) if _is_discoverable_app_entry(project_dir, item)]
|
|
1458
|
+
discovered = [item.module_path for item in sources if _is_discoverable_app_entry(project_dir, item.module_path)]
|
|
1459
|
+
conventional = _conventional_installed_app_order(project_dir, current_payload, discovered)
|
|
1460
|
+
if declared:
|
|
1461
|
+
merged_entries = list(merge_explicit_app_order(tuple(declared), conventional))
|
|
1462
|
+
return list(_conventional_installed_app_order(project_dir, current_payload, merged_entries))
|
|
1463
|
+
if discovered:
|
|
1464
|
+
return list(conventional)
|
|
1465
|
+
if (project_dir / "apps").is_dir():
|
|
1466
|
+
return list(_conventional_installed_app_order(project_dir, current_payload, ["apps"]))
|
|
1467
|
+
return list(conventional)
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def _conventional_installed_app_order(
|
|
1471
|
+
project_dir: Path,
|
|
1472
|
+
current_payload: dict[str, Any],
|
|
1473
|
+
discovered_entries: list[str],
|
|
1474
|
+
) -> tuple[str, ...]:
|
|
1475
|
+
if not discovered_entries:
|
|
1476
|
+
return ()
|
|
1477
|
+
payload = _build_project_payload_with_defaults(current_payload)
|
|
1478
|
+
payload["installed_apps"] = list(discovered_entries)
|
|
1479
|
+
with active_settings_project(project_dir):
|
|
1480
|
+
settings = compile_settings_snapshot_from_payload(project_dir, payload)
|
|
1481
|
+
return compile_conventional_app_order(settings, project_dir=project_dir)
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
def _is_discoverable_app_entry(project_dir: Path, module_path: str) -> bool:
|
|
1485
|
+
return _is_formal_app_or_container_entry(project_dir, module_path)
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
def _is_container_entry(project_dir: Path, module_path: str) -> bool:
|
|
1489
|
+
token = str(module_path or "").strip()
|
|
1490
|
+
if not token:
|
|
1491
|
+
return False
|
|
1492
|
+
if token == "apps":
|
|
1493
|
+
return True
|
|
1494
|
+
expanded = discover_leaf_app_packages(token, project_dir=project_dir)
|
|
1495
|
+
return bool(expanded and expanded != (token,))
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
def _is_formal_app_or_container_entry(project_dir: Path, module_path: str) -> bool:
|
|
1499
|
+
token = str(module_path or "").strip()
|
|
1500
|
+
if not token:
|
|
1501
|
+
return False
|
|
1502
|
+
if token == "apps":
|
|
1503
|
+
return (project_dir / "apps").is_dir()
|
|
1504
|
+
if "." not in token:
|
|
1505
|
+
if (project_dir / token).is_dir() or (project_dir / "apps" / token).is_dir():
|
|
1506
|
+
return True
|
|
1507
|
+
refs = discover_declared_app_references((token,), project_dir=project_dir)
|
|
1508
|
+
if any(item.import_root is not None for item in refs):
|
|
1509
|
+
return True
|
|
1510
|
+
expanded = tuple(item.module_path for item in refs)
|
|
1511
|
+
if expanded and expanded != (token,):
|
|
1512
|
+
return True
|
|
1513
|
+
package_root = resolve_package_root(token)
|
|
1514
|
+
if package_root is None:
|
|
1515
|
+
return False
|
|
1516
|
+
if (package_root / "apps.py").is_file():
|
|
1517
|
+
return True
|
|
1518
|
+
expanded = discover_leaf_app_packages(token, project_dir=project_dir)
|
|
1519
|
+
return bool(expanded and expanded != (token,))
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
def _managed_app_settings_payload(
|
|
1523
|
+
project_dir: Path,
|
|
1524
|
+
current_payload: dict[str, Any],
|
|
1525
|
+
sources: tuple[AppSettingsSource, ...],
|
|
1526
|
+
*,
|
|
1527
|
+
installed_apps: list[str],
|
|
1528
|
+
) -> dict[str, Any]:
|
|
1529
|
+
current_apps = dict(current_payload.get("apps") if isinstance(current_payload.get("apps"), dict) else {})
|
|
1530
|
+
merged_apps: dict[str, Any] = {}
|
|
1531
|
+
source_by_app_id = {item.app_id: item for item in sources}
|
|
1532
|
+
ordered_app_ids = _ordered_app_settings_app_ids(
|
|
1533
|
+
project_dir,
|
|
1534
|
+
installed_apps=installed_apps,
|
|
1535
|
+
current_apps=current_apps,
|
|
1536
|
+
source_by_app_id=source_by_app_id,
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
for app_id in ordered_app_ids:
|
|
1540
|
+
source = source_by_app_id.get(app_id)
|
|
1541
|
+
if source is None:
|
|
1542
|
+
payload = current_apps.get(app_id)
|
|
1543
|
+
if payload is not None:
|
|
1544
|
+
merged_apps[app_id] = payload
|
|
1545
|
+
continue
|
|
1546
|
+
existing = current_apps.get(source.app_id)
|
|
1547
|
+
if isinstance(existing, dict):
|
|
1548
|
+
merged_apps[source.app_id] = _merge_project_app_settings(source, existing)
|
|
1549
|
+
elif existing is not None:
|
|
1550
|
+
merged_apps[source.app_id] = existing
|
|
1551
|
+
else:
|
|
1552
|
+
merged_apps[source.app_id] = _merge_project_app_settings(source, None)
|
|
1553
|
+
|
|
1554
|
+
for item in sources:
|
|
1555
|
+
if item.app_id in merged_apps:
|
|
1556
|
+
continue
|
|
1557
|
+
existing = current_apps.get(item.app_id)
|
|
1558
|
+
if isinstance(existing, dict):
|
|
1559
|
+
merged_apps[item.app_id] = _merge_project_app_settings(item, existing)
|
|
1560
|
+
elif existing is not None:
|
|
1561
|
+
merged_apps[item.app_id] = existing
|
|
1562
|
+
else:
|
|
1563
|
+
merged_apps[item.app_id] = _merge_project_app_settings(item, None)
|
|
1564
|
+
|
|
1565
|
+
for app_id, payload in current_apps.items():
|
|
1566
|
+
if not isinstance(app_id, str) or app_id in merged_apps:
|
|
1567
|
+
continue
|
|
1568
|
+
merged_apps[app_id] = payload
|
|
1569
|
+
|
|
1570
|
+
return merged_apps
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
def _merge_project_app_settings(source: AppSettingsSource, existing: dict[str, Any] | None) -> dict[str, Any]:
|
|
1574
|
+
existing_payload = dict(existing or {})
|
|
1575
|
+
if source.normalize is None:
|
|
1576
|
+
return _deep_merge_payload(source.defaults, existing_payload)
|
|
1577
|
+
merged_payload = _deep_merge_payload(source.defaults, existing_payload)
|
|
1578
|
+
normalized = source.normalize(merged_payload)
|
|
1579
|
+
if not isinstance(normalized, dict):
|
|
1580
|
+
raise PzError(
|
|
1581
|
+
code="PZ_APP_SETTINGS_NORMALIZER_INVALID",
|
|
1582
|
+
message=f"App settings normalizer for '{source.module_path}' must return a mapping payload.",
|
|
1583
|
+
stage="cli",
|
|
1584
|
+
owner=_OWNER,
|
|
1585
|
+
action="merge_project_app_settings",
|
|
1586
|
+
)
|
|
1587
|
+
return dict(normalized)
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
def _ordered_app_settings_app_ids(
|
|
1591
|
+
project_dir: Path,
|
|
1592
|
+
*,
|
|
1593
|
+
installed_apps: list[str],
|
|
1594
|
+
current_apps: dict[str, Any],
|
|
1595
|
+
source_by_app_id: dict[str, AppSettingsSource],
|
|
1596
|
+
) -> tuple[str, ...]:
|
|
1597
|
+
source_by_module_path = {item.module_path: item for item in source_by_app_id.values()}
|
|
1598
|
+
ordered: list[str] = []
|
|
1599
|
+
seen: set[str] = set()
|
|
1600
|
+
|
|
1601
|
+
for ref in discover_declared_app_references(installed_apps, project_dir=project_dir):
|
|
1602
|
+
source = source_by_module_path.get(ref.module_path)
|
|
1603
|
+
if source is None or source.app_id in seen:
|
|
1604
|
+
continue
|
|
1605
|
+
seen.add(source.app_id)
|
|
1606
|
+
ordered.append(source.app_id)
|
|
1607
|
+
|
|
1608
|
+
for app_id in current_apps:
|
|
1609
|
+
if not isinstance(app_id, str) or app_id in seen:
|
|
1610
|
+
continue
|
|
1611
|
+
seen.add(app_id)
|
|
1612
|
+
ordered.append(app_id)
|
|
1613
|
+
|
|
1614
|
+
for app_id in source_by_app_id:
|
|
1615
|
+
if app_id in seen:
|
|
1616
|
+
continue
|
|
1617
|
+
seen.add(app_id)
|
|
1618
|
+
ordered.append(app_id)
|
|
1619
|
+
|
|
1620
|
+
return tuple(ordered)
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
def _managed_middlewares_payload(
|
|
1624
|
+
project_dir: Path,
|
|
1625
|
+
current_payload: dict[str, Any],
|
|
1626
|
+
payload: dict[str, Any],
|
|
1627
|
+
*,
|
|
1628
|
+
preserve_existing: bool,
|
|
1629
|
+
) -> list[dict[str, Any]]:
|
|
1630
|
+
generation_payload = _deep_merge_payload({}, payload)
|
|
1631
|
+
generation_payload.pop("middlewares", None)
|
|
1632
|
+
with active_settings_project(project_dir):
|
|
1633
|
+
settings = compile_settings_snapshot_from_payload(project_dir, generation_payload)
|
|
1634
|
+
with active_project_import_root(project_dir, extra_import_roots=settings.import_roots):
|
|
1635
|
+
app_registry = compile_app_registry(settings, project_dir=project_dir, run_init_hooks=False)
|
|
1636
|
+
manifest = compile_conventional_middleware_manifest(settings, app_registry)
|
|
1637
|
+
if preserve_existing:
|
|
1638
|
+
manifest = merge_explicit_middleware_order(_read_middleware_ids(current_payload), manifest)
|
|
1639
|
+
return [_middleware_decl_payload(item) for item in manifest.middlewares]
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
def _read_middleware_ids(payload: dict[str, Any]) -> tuple[str, ...]:
|
|
1643
|
+
raw = payload.get("middlewares")
|
|
1644
|
+
if not isinstance(raw, list):
|
|
1645
|
+
return ()
|
|
1646
|
+
out: list[str] = []
|
|
1647
|
+
for item in raw:
|
|
1648
|
+
if isinstance(item, dict):
|
|
1649
|
+
middleware_id = str(item.get("id") or "").strip()
|
|
1650
|
+
if middleware_id:
|
|
1651
|
+
out.append(middleware_id)
|
|
1652
|
+
return tuple(out)
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
def _middleware_decl_payload(item: Any) -> dict[str, Any]:
|
|
1656
|
+
priority = getattr(item, "priority", 100)
|
|
1657
|
+
payload: dict[str, Any] = {
|
|
1658
|
+
"id": str(item.id),
|
|
1659
|
+
"owner": str(item.owner),
|
|
1660
|
+
"factory": str(item.factory),
|
|
1661
|
+
"priority": int(100 if priority is None else priority),
|
|
1662
|
+
}
|
|
1663
|
+
if getattr(item, "replace", None):
|
|
1664
|
+
payload["replace"] = str(item.replace)
|
|
1665
|
+
if getattr(item, "params_model", None):
|
|
1666
|
+
payload["params_model"] = str(item.params_model)
|
|
1667
|
+
if getattr(item, "params", None):
|
|
1668
|
+
payload["params"] = dict(item.params)
|
|
1669
|
+
return payload
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
def _ordered_root_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
1673
|
+
ordered: dict[str, Any] = {}
|
|
1674
|
+
for key in (
|
|
1675
|
+
"installed_apps",
|
|
1676
|
+
"middlewares",
|
|
1677
|
+
"catalog",
|
|
1678
|
+
"project",
|
|
1679
|
+
"routing",
|
|
1680
|
+
"events",
|
|
1681
|
+
"app_replacements",
|
|
1682
|
+
"apps",
|
|
1683
|
+
):
|
|
1684
|
+
if key in payload:
|
|
1685
|
+
ordered[key] = payload[key]
|
|
1686
|
+
for key, value in payload.items():
|
|
1687
|
+
if key not in ordered:
|
|
1688
|
+
ordered[key] = value
|
|
1689
|
+
return ordered
|
|
1690
|
+
|
|
1691
|
+
|
|
1692
|
+
__all__ = [
|
|
1693
|
+
"AppSettingsSource",
|
|
1694
|
+
"SettingsUpdateDriftResult",
|
|
1695
|
+
"SettingsWriteResult",
|
|
1696
|
+
"discover_settings_sources",
|
|
1697
|
+
"initialize_settings_file",
|
|
1698
|
+
"inspect_settings_update_drift",
|
|
1699
|
+
"update_settings_file",
|
|
1700
|
+
]
|