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,814 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import stat
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import tomllib
|
|
13
|
+
import uuid
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from html.parser import HTMLParser
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Callable, Iterable, Mapping, MutableMapping, cast
|
|
18
|
+
from urllib.parse import unquote, urljoin, urlsplit, urlunsplit
|
|
19
|
+
from urllib.request import Request, urlopen
|
|
20
|
+
|
|
21
|
+
import tomlkit # type: ignore[import-not-found]
|
|
22
|
+
from packaging.requirements import Requirement
|
|
23
|
+
from packaging.specifiers import SpecifierSet
|
|
24
|
+
from packaging.utils import canonicalize_name, parse_sdist_filename, parse_wheel_filename
|
|
25
|
+
from packaging.version import InvalidVersion, Version
|
|
26
|
+
from tomlkit.exceptions import TOMLKitError # type: ignore[import-not-found]
|
|
27
|
+
|
|
28
|
+
from pz_core.runtime.errors import PzError
|
|
29
|
+
|
|
30
|
+
from .project_state import (
|
|
31
|
+
add_declared_app,
|
|
32
|
+
ensure_project_state,
|
|
33
|
+
load_installed_apps,
|
|
34
|
+
remove_declared_app,
|
|
35
|
+
)
|
|
36
|
+
from .release import install_env, load_install_target
|
|
37
|
+
from .venv_service import ensure_project_virtualenv, resolve_virtualenv_python
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_OWNER = "pz_core.authoring.app_versions"
|
|
41
|
+
_OpenUrl = Callable[..., object]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class AvailableVersion:
|
|
46
|
+
version: str
|
|
47
|
+
prerelease: bool
|
|
48
|
+
yanked: bool
|
|
49
|
+
compatible: bool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class AppVersionState:
|
|
54
|
+
app_id: str
|
|
55
|
+
distribution: str
|
|
56
|
+
available: tuple[AvailableVersion, ...]
|
|
57
|
+
latest: str | None
|
|
58
|
+
declared: str | None
|
|
59
|
+
locked: str | None
|
|
60
|
+
installed: str | None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class AppDeclarationResult:
|
|
65
|
+
app_id: str
|
|
66
|
+
distribution: str
|
|
67
|
+
version: str | None
|
|
68
|
+
installed_apps: tuple[str, ...]
|
|
69
|
+
changed: bool
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, slots=True)
|
|
73
|
+
class AppInstallResult:
|
|
74
|
+
app_id: str
|
|
75
|
+
distribution: str
|
|
76
|
+
version: str
|
|
77
|
+
python: Path
|
|
78
|
+
lock_path: Path
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class _ReleaseFile:
|
|
83
|
+
version: Version
|
|
84
|
+
yanked: bool
|
|
85
|
+
requires_python: str | None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _SimpleLinksParser(HTMLParser):
|
|
89
|
+
def __init__(self) -> None:
|
|
90
|
+
super().__init__(convert_charrefs=True)
|
|
91
|
+
self.files: list[tuple[str, bool, str | None]] = []
|
|
92
|
+
|
|
93
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
94
|
+
if tag.lower() != "a":
|
|
95
|
+
return
|
|
96
|
+
values = {key.lower(): value for key, value in attrs}
|
|
97
|
+
href = values.get("href")
|
|
98
|
+
if not href:
|
|
99
|
+
return
|
|
100
|
+
filename = unquote(urlsplit(href).path.rsplit("/", 1)[-1])
|
|
101
|
+
self.files.append(
|
|
102
|
+
(
|
|
103
|
+
filename,
|
|
104
|
+
"data-yanked" in values,
|
|
105
|
+
values.get("data-requires-python"),
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _error(code: str, message: str, *, action: str, detail: str | None = None) -> PzError:
|
|
111
|
+
return PzError(
|
|
112
|
+
code=code,
|
|
113
|
+
message=message,
|
|
114
|
+
stage="cli",
|
|
115
|
+
owner=_OWNER,
|
|
116
|
+
action=action,
|
|
117
|
+
detail=detail,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def distribution_name(app_id: str) -> str:
|
|
122
|
+
return canonicalize_name(app_id.replace("_", "-"))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _redacted_index(url: str) -> str:
|
|
126
|
+
parsed = urlsplit(url)
|
|
127
|
+
host = parsed.hostname or ""
|
|
128
|
+
if parsed.port is not None:
|
|
129
|
+
host = f"{host}:{parsed.port}"
|
|
130
|
+
return urlunsplit((parsed.scheme, host, parsed.path, "", ""))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _redact_credentials(text: str) -> str:
|
|
134
|
+
return re.sub(
|
|
135
|
+
r"([A-Za-z][A-Za-z0-9+.-]*://)[^/@\s]+@",
|
|
136
|
+
r"\1<redacted>@",
|
|
137
|
+
text,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _index_request(url: str) -> Request:
|
|
142
|
+
parsed = urlsplit(url)
|
|
143
|
+
host = parsed.hostname or ""
|
|
144
|
+
if parsed.port is not None:
|
|
145
|
+
host = f"{host}:{parsed.port}"
|
|
146
|
+
clean = urlunsplit((parsed.scheme, host, parsed.path, parsed.query, ""))
|
|
147
|
+
request = Request(
|
|
148
|
+
clean,
|
|
149
|
+
headers={
|
|
150
|
+
"Accept": "application/vnd.pypi.simple.v1+json, text/html;q=0.9",
|
|
151
|
+
"User-Agent": "pz/5 app-versions",
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
if parsed.username is not None:
|
|
155
|
+
credentials = f"{unquote(parsed.username)}:{unquote(parsed.password or '')}"
|
|
156
|
+
encoded = base64.b64encode(credentials.encode("utf-8")).decode("ascii")
|
|
157
|
+
request.add_header("Authorization", f"Basic {encoded}")
|
|
158
|
+
return request
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _version_from_filename(filename: str, distribution: str) -> Version | None:
|
|
162
|
+
try:
|
|
163
|
+
if filename.endswith(".whl"):
|
|
164
|
+
name, version, _build, _tags = parse_wheel_filename(filename)
|
|
165
|
+
else:
|
|
166
|
+
name, version = parse_sdist_filename(filename)
|
|
167
|
+
except (InvalidVersion, ValueError):
|
|
168
|
+
return None
|
|
169
|
+
if canonicalize_name(name) != distribution:
|
|
170
|
+
return None
|
|
171
|
+
return version
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _release_files_from_json(payload: object, distribution: str) -> tuple[_ReleaseFile, ...]:
|
|
175
|
+
if not isinstance(payload, Mapping) or not isinstance(payload.get("files"), list):
|
|
176
|
+
raise ValueError("Simple API JSON payload does not contain files")
|
|
177
|
+
files: list[_ReleaseFile] = []
|
|
178
|
+
for raw in cast(list[object], payload["files"]):
|
|
179
|
+
if not isinstance(raw, Mapping):
|
|
180
|
+
continue
|
|
181
|
+
filename = raw.get("filename")
|
|
182
|
+
if not isinstance(filename, str):
|
|
183
|
+
continue
|
|
184
|
+
version = _version_from_filename(filename, distribution)
|
|
185
|
+
if version is None:
|
|
186
|
+
continue
|
|
187
|
+
requires_python = raw.get("requires-python")
|
|
188
|
+
files.append(
|
|
189
|
+
_ReleaseFile(
|
|
190
|
+
version=version,
|
|
191
|
+
yanked=bool(raw.get("yanked", False)),
|
|
192
|
+
requires_python=requires_python if isinstance(requires_python, str) else None,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
return tuple(files)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _release_files_from_html(text: str, distribution: str) -> tuple[_ReleaseFile, ...]:
|
|
199
|
+
parser = _SimpleLinksParser()
|
|
200
|
+
parser.feed(text)
|
|
201
|
+
files: list[_ReleaseFile] = []
|
|
202
|
+
for filename, yanked, requires_python in parser.files:
|
|
203
|
+
version = _version_from_filename(filename, distribution)
|
|
204
|
+
if version is not None:
|
|
205
|
+
files.append(_ReleaseFile(version, yanked, requires_python))
|
|
206
|
+
return tuple(files)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _python_compatible(specifier: str | None) -> bool:
|
|
210
|
+
if not specifier:
|
|
211
|
+
return True
|
|
212
|
+
current = Version(".".join(str(value) for value in sys.version_info[:3]))
|
|
213
|
+
try:
|
|
214
|
+
return current in SpecifierSet(specifier)
|
|
215
|
+
except Exception:
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _available_versions(files: Iterable[_ReleaseFile]) -> tuple[AvailableVersion, ...]:
|
|
220
|
+
grouped: dict[Version, list[_ReleaseFile]] = {}
|
|
221
|
+
for item in files:
|
|
222
|
+
grouped.setdefault(item.version, []).append(item)
|
|
223
|
+
result: list[AvailableVersion] = []
|
|
224
|
+
for version in sorted(grouped, reverse=True):
|
|
225
|
+
release_files = grouped[version]
|
|
226
|
+
compatible_files = [item for item in release_files if _python_compatible(item.requires_python)]
|
|
227
|
+
result.append(
|
|
228
|
+
AvailableVersion(
|
|
229
|
+
version=str(version),
|
|
230
|
+
prerelease=version.is_prerelease,
|
|
231
|
+
yanked=all(item.yanked for item in release_files),
|
|
232
|
+
compatible=any(not item.yanked for item in compatible_files),
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
return tuple(result)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _read_declared_version(project_dir: Path, distribution: str) -> str | None:
|
|
239
|
+
path = project_dir / "pyproject.toml"
|
|
240
|
+
if not path.is_file():
|
|
241
|
+
return None
|
|
242
|
+
try:
|
|
243
|
+
payload = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
244
|
+
except (OSError, UnicodeError, tomllib.TOMLDecodeError):
|
|
245
|
+
return None
|
|
246
|
+
project = payload.get("project")
|
|
247
|
+
dependencies = project.get("dependencies") if isinstance(project, Mapping) else None
|
|
248
|
+
if not isinstance(dependencies, list):
|
|
249
|
+
return None
|
|
250
|
+
for raw in dependencies:
|
|
251
|
+
if not isinstance(raw, str):
|
|
252
|
+
continue
|
|
253
|
+
try:
|
|
254
|
+
requirement = Requirement(raw)
|
|
255
|
+
except Exception:
|
|
256
|
+
continue
|
|
257
|
+
if canonicalize_name(requirement.name) != distribution:
|
|
258
|
+
continue
|
|
259
|
+
specs = list(requirement.specifier)
|
|
260
|
+
if len(specs) == 1 and specs[0].operator == "==" and not specs[0].version.endswith(".*"):
|
|
261
|
+
return str(Version(specs[0].version))
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _read_locked_version(project_dir: Path, distribution: str) -> str | None:
|
|
266
|
+
path = project_dir / "uv.lock"
|
|
267
|
+
if not path.is_file():
|
|
268
|
+
return None
|
|
269
|
+
try:
|
|
270
|
+
payload = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
271
|
+
except (OSError, UnicodeError, tomllib.TOMLDecodeError):
|
|
272
|
+
return None
|
|
273
|
+
packages = payload.get("package")
|
|
274
|
+
if not isinstance(packages, list):
|
|
275
|
+
return None
|
|
276
|
+
for package in packages:
|
|
277
|
+
if not isinstance(package, Mapping):
|
|
278
|
+
continue
|
|
279
|
+
if canonicalize_name(str(package.get("name") or "")) != distribution:
|
|
280
|
+
continue
|
|
281
|
+
version = package.get("version")
|
|
282
|
+
return str(version) if isinstance(version, str) and version else None
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _read_installed_version(project_dir: Path, distribution: str, *, action: str) -> str | None:
|
|
287
|
+
venv = project_dir / ".venv"
|
|
288
|
+
if not venv.is_dir():
|
|
289
|
+
return None
|
|
290
|
+
try:
|
|
291
|
+
python = resolve_virtualenv_python(venv, action=action)
|
|
292
|
+
except PzError:
|
|
293
|
+
return None
|
|
294
|
+
code = (
|
|
295
|
+
"import importlib.metadata, sys; "
|
|
296
|
+
"\ntry: print(importlib.metadata.version(sys.argv[1]))"
|
|
297
|
+
"\nexcept importlib.metadata.PackageNotFoundError: raise SystemExit(3)"
|
|
298
|
+
)
|
|
299
|
+
completed = subprocess.run(
|
|
300
|
+
(str(python), "-c", code, distribution),
|
|
301
|
+
check=False,
|
|
302
|
+
capture_output=True,
|
|
303
|
+
text=True,
|
|
304
|
+
)
|
|
305
|
+
if completed.returncode == 3:
|
|
306
|
+
return None
|
|
307
|
+
if completed.returncode != 0:
|
|
308
|
+
raise _error(
|
|
309
|
+
"PZ_APP_VERSION_STATE_MISMATCH",
|
|
310
|
+
f"failed to inspect installed version for {distribution}.",
|
|
311
|
+
action=action,
|
|
312
|
+
detail=(completed.stderr or completed.stdout or "metadata inspection failed").strip(),
|
|
313
|
+
)
|
|
314
|
+
value = completed.stdout.strip()
|
|
315
|
+
return value or None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def query_app_versions(
|
|
319
|
+
app_id: str,
|
|
320
|
+
*,
|
|
321
|
+
project_dir: Path,
|
|
322
|
+
action: str,
|
|
323
|
+
opener: _OpenUrl = urlopen,
|
|
324
|
+
) -> AppVersionState:
|
|
325
|
+
distribution = distribution_name(app_id)
|
|
326
|
+
target = load_install_target(app_id, project_dir=project_dir, action=action)
|
|
327
|
+
env = install_env(target=target, project_dir=project_dir, action=action)
|
|
328
|
+
base = env.get("UV_DEFAULT_INDEX") or target.index_url or "https://pypi.org/simple/"
|
|
329
|
+
url = urljoin(base.rstrip("/") + "/", distribution + "/")
|
|
330
|
+
try:
|
|
331
|
+
response = opener(_index_request(url), timeout=20)
|
|
332
|
+
content_type = str(getattr(response, "headers", {}).get("Content-Type", ""))
|
|
333
|
+
body = cast(bytes, response.read())
|
|
334
|
+
if "json" in content_type:
|
|
335
|
+
files = _release_files_from_json(json.loads(body.decode("utf-8")), distribution)
|
|
336
|
+
else:
|
|
337
|
+
files = _release_files_from_html(body.decode("utf-8"), distribution)
|
|
338
|
+
except Exception as exc:
|
|
339
|
+
raise _error(
|
|
340
|
+
"PZ_APP_INDEX_QUERY_FAILED",
|
|
341
|
+
f"failed to query versions for {distribution}.",
|
|
342
|
+
action=action,
|
|
343
|
+
detail=f"index={_redacted_index(url)}; error={exc}",
|
|
344
|
+
) from exc
|
|
345
|
+
available = _available_versions(files)
|
|
346
|
+
latest = next(
|
|
347
|
+
(item.version for item in available if item.compatible and not item.prerelease and not item.yanked),
|
|
348
|
+
None,
|
|
349
|
+
)
|
|
350
|
+
return AppVersionState(
|
|
351
|
+
app_id=app_id,
|
|
352
|
+
distribution=distribution,
|
|
353
|
+
available=available,
|
|
354
|
+
latest=latest,
|
|
355
|
+
declared=_read_declared_version(project_dir, distribution),
|
|
356
|
+
locked=_read_locked_version(project_dir, distribution),
|
|
357
|
+
installed=_read_installed_version(project_dir, distribution, action=action),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _candidate_resolves(
|
|
362
|
+
state: AppVersionState,
|
|
363
|
+
version: str,
|
|
364
|
+
*,
|
|
365
|
+
project_dir: Path,
|
|
366
|
+
action: str,
|
|
367
|
+
) -> bool:
|
|
368
|
+
uv_binary = shutil.which("uv")
|
|
369
|
+
if uv_binary is None:
|
|
370
|
+
raise _error(
|
|
371
|
+
"PZ_ENV_UV_MISSING",
|
|
372
|
+
"required dependency backend was not found on PATH.",
|
|
373
|
+
action=action,
|
|
374
|
+
)
|
|
375
|
+
source_pyproject = project_dir / "pyproject.toml"
|
|
376
|
+
if not source_pyproject.is_file():
|
|
377
|
+
raise _error(
|
|
378
|
+
"PZ_PROJECT_METADATA_INVALID",
|
|
379
|
+
"project pyproject.toml is missing.",
|
|
380
|
+
action=action,
|
|
381
|
+
)
|
|
382
|
+
with tempfile.TemporaryDirectory(prefix="pz-app-resolve-") as raw_temp:
|
|
383
|
+
temp = Path(raw_temp)
|
|
384
|
+
shutil.copy2(source_pyproject, temp / "pyproject.toml")
|
|
385
|
+
lock = project_dir / "uv.lock"
|
|
386
|
+
if lock.is_file():
|
|
387
|
+
shutil.copy2(lock, temp / "uv.lock")
|
|
388
|
+
_update_dependency(
|
|
389
|
+
temp,
|
|
390
|
+
state.distribution,
|
|
391
|
+
version,
|
|
392
|
+
action=action,
|
|
393
|
+
)
|
|
394
|
+
_absolutize_temp_source_paths(temp, source_project=project_dir, action=action)
|
|
395
|
+
target = load_install_target(
|
|
396
|
+
state.app_id, project_dir=project_dir, action=action
|
|
397
|
+
)
|
|
398
|
+
env = install_env(target=target, project_dir=project_dir, action=action)
|
|
399
|
+
completed = subprocess.run(
|
|
400
|
+
(
|
|
401
|
+
uv_binary,
|
|
402
|
+
"lock",
|
|
403
|
+
"--dry-run",
|
|
404
|
+
"--project",
|
|
405
|
+
str(temp),
|
|
406
|
+
"--upgrade-package",
|
|
407
|
+
state.distribution,
|
|
408
|
+
"--no-sources-package",
|
|
409
|
+
state.distribution,
|
|
410
|
+
),
|
|
411
|
+
check=False,
|
|
412
|
+
capture_output=True,
|
|
413
|
+
text=True,
|
|
414
|
+
env=env,
|
|
415
|
+
)
|
|
416
|
+
return completed.returncode == 0
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _absolutize_temp_source_paths(
|
|
420
|
+
temp_project: Path,
|
|
421
|
+
*,
|
|
422
|
+
source_project: Path,
|
|
423
|
+
action: str,
|
|
424
|
+
) -> None:
|
|
425
|
+
path = temp_project / "pyproject.toml"
|
|
426
|
+
try:
|
|
427
|
+
document = tomlkit.parse(path.read_text(encoding="utf-8"))
|
|
428
|
+
except (OSError, UnicodeError, TOMLKitError) as exc:
|
|
429
|
+
raise _error(
|
|
430
|
+
"PZ_PROJECT_METADATA_INVALID",
|
|
431
|
+
"temporary project metadata is invalid.",
|
|
432
|
+
action=action,
|
|
433
|
+
detail=str(exc),
|
|
434
|
+
) from exc
|
|
435
|
+
tool = document.get("tool")
|
|
436
|
+
uv = tool.get("uv") if isinstance(tool, MutableMapping) else None
|
|
437
|
+
sources = uv.get("sources") if isinstance(uv, MutableMapping) else None
|
|
438
|
+
if not isinstance(sources, MutableMapping):
|
|
439
|
+
return
|
|
440
|
+
changed = False
|
|
441
|
+
for raw_source in sources.values():
|
|
442
|
+
entries = raw_source if isinstance(raw_source, list) else (raw_source,)
|
|
443
|
+
for entry in entries:
|
|
444
|
+
if not isinstance(entry, MutableMapping):
|
|
445
|
+
continue
|
|
446
|
+
raw_path = entry.get("path")
|
|
447
|
+
if not isinstance(raw_path, str) or Path(raw_path).is_absolute():
|
|
448
|
+
continue
|
|
449
|
+
entry["path"] = str((source_project / raw_path).resolve(strict=False))
|
|
450
|
+
changed = True
|
|
451
|
+
if changed:
|
|
452
|
+
path.write_text(tomlkit.dumps(document), encoding="utf-8")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def resolve_app_version(
|
|
456
|
+
state: AppVersionState,
|
|
457
|
+
requested: str | None,
|
|
458
|
+
*,
|
|
459
|
+
action: str,
|
|
460
|
+
project_dir: Path | None = None,
|
|
461
|
+
) -> str:
|
|
462
|
+
if requested is None:
|
|
463
|
+
candidates = tuple(
|
|
464
|
+
item.version
|
|
465
|
+
for item in state.available
|
|
466
|
+
if item.compatible and not item.prerelease and not item.yanked
|
|
467
|
+
)
|
|
468
|
+
if not candidates:
|
|
469
|
+
raise _error(
|
|
470
|
+
"PZ_APP_VERSION_NOT_FOUND",
|
|
471
|
+
f"no compatible stable version is available for {state.distribution}.",
|
|
472
|
+
action=action,
|
|
473
|
+
)
|
|
474
|
+
if project_dir is None:
|
|
475
|
+
return candidates[0]
|
|
476
|
+
for candidate in candidates:
|
|
477
|
+
if _candidate_resolves(
|
|
478
|
+
state,
|
|
479
|
+
candidate,
|
|
480
|
+
project_dir=project_dir,
|
|
481
|
+
action=action,
|
|
482
|
+
):
|
|
483
|
+
return candidate
|
|
484
|
+
raise _error(
|
|
485
|
+
"PZ_APP_VERSION_INCOMPATIBLE",
|
|
486
|
+
f"no stable version of {state.distribution} satisfies the project dependency graph.",
|
|
487
|
+
action=action,
|
|
488
|
+
)
|
|
489
|
+
try:
|
|
490
|
+
version = Version(requested)
|
|
491
|
+
except InvalidVersion as exc:
|
|
492
|
+
raise _error(
|
|
493
|
+
"PZ_APP_VERSION_NOT_FOUND",
|
|
494
|
+
f"requested version is not an exact PEP 440 version: {requested}",
|
|
495
|
+
action=action,
|
|
496
|
+
) from exc
|
|
497
|
+
match = next((item for item in state.available if Version(item.version) == version), None)
|
|
498
|
+
if match is None:
|
|
499
|
+
raise _error(
|
|
500
|
+
"PZ_APP_VERSION_NOT_FOUND",
|
|
501
|
+
f"version {version} is not available for {state.distribution}.",
|
|
502
|
+
action=action,
|
|
503
|
+
)
|
|
504
|
+
if match.yanked or not match.compatible:
|
|
505
|
+
raise _error(
|
|
506
|
+
"PZ_APP_VERSION_INCOMPATIBLE",
|
|
507
|
+
f"version {version} is not installable for {state.distribution}.",
|
|
508
|
+
action=action,
|
|
509
|
+
)
|
|
510
|
+
normalized = str(version)
|
|
511
|
+
if project_dir is not None and not _candidate_resolves(
|
|
512
|
+
state,
|
|
513
|
+
normalized,
|
|
514
|
+
project_dir=project_dir,
|
|
515
|
+
action=action,
|
|
516
|
+
):
|
|
517
|
+
raise _error(
|
|
518
|
+
"PZ_APP_VERSION_INCOMPATIBLE",
|
|
519
|
+
f"version {normalized} conflicts with the project dependency graph.",
|
|
520
|
+
action=action,
|
|
521
|
+
)
|
|
522
|
+
return normalized
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _project_table(document: object, *, action: str) -> MutableMapping[str, object]:
|
|
526
|
+
if not isinstance(document, MutableMapping):
|
|
527
|
+
raise _error("PZ_PROJECT_METADATA_INVALID", "pyproject root must be a table.", action=action)
|
|
528
|
+
project = document.get("project")
|
|
529
|
+
if not isinstance(project, MutableMapping):
|
|
530
|
+
raise _error("PZ_PROJECT_METADATA_INVALID", "pyproject project must be a table.", action=action)
|
|
531
|
+
return cast(MutableMapping[str, object], project)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _write_atomic(path: Path, before: bytes, after: bytes, *, action: str) -> None:
|
|
535
|
+
if before == after:
|
|
536
|
+
return
|
|
537
|
+
mode = stat.S_IMODE(path.stat().st_mode)
|
|
538
|
+
staged = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
|
|
539
|
+
try:
|
|
540
|
+
descriptor = os.open(staged, os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode)
|
|
541
|
+
with os.fdopen(descriptor, "wb") as handle:
|
|
542
|
+
handle.write(after)
|
|
543
|
+
handle.flush()
|
|
544
|
+
os.fsync(handle.fileno())
|
|
545
|
+
if path.read_bytes() != before:
|
|
546
|
+
raise _error("PZ_PROJECT_METADATA_CHANGED", "pyproject.toml changed concurrently.", action=action)
|
|
547
|
+
os.replace(staged, path)
|
|
548
|
+
finally:
|
|
549
|
+
staged.unlink(missing_ok=True)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _restore_snapshot(
|
|
553
|
+
path: Path,
|
|
554
|
+
*,
|
|
555
|
+
before: bytes,
|
|
556
|
+
expected_current: bytes,
|
|
557
|
+
action: str,
|
|
558
|
+
) -> None:
|
|
559
|
+
try:
|
|
560
|
+
current = path.read_bytes()
|
|
561
|
+
except OSError as exc:
|
|
562
|
+
raise _error(
|
|
563
|
+
"PZ_PROJECT_METADATA_CHANGED",
|
|
564
|
+
f"cannot restore missing transaction file: {path.name}",
|
|
565
|
+
action=action,
|
|
566
|
+
detail=str(exc),
|
|
567
|
+
) from exc
|
|
568
|
+
if current == before:
|
|
569
|
+
return
|
|
570
|
+
if current != expected_current:
|
|
571
|
+
raise _error(
|
|
572
|
+
"PZ_PROJECT_METADATA_CHANGED",
|
|
573
|
+
f"cannot restore concurrently changed transaction file: {path.name}",
|
|
574
|
+
action=action,
|
|
575
|
+
)
|
|
576
|
+
_write_atomic(path, current, before, action=action)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _update_dependency(
|
|
580
|
+
project_dir: Path,
|
|
581
|
+
distribution: str,
|
|
582
|
+
version: str | None,
|
|
583
|
+
*,
|
|
584
|
+
action: str,
|
|
585
|
+
) -> bool:
|
|
586
|
+
path = project_dir / "pyproject.toml"
|
|
587
|
+
try:
|
|
588
|
+
before = path.read_bytes()
|
|
589
|
+
document = tomlkit.parse(before.decode("utf-8"))
|
|
590
|
+
except (OSError, UnicodeError, TOMLKitError) as exc:
|
|
591
|
+
raise _error("PZ_PROJECT_METADATA_INVALID", "project pyproject.toml is invalid.", action=action, detail=str(exc)) from exc
|
|
592
|
+
project = _project_table(document, action=action)
|
|
593
|
+
dependencies = project.get("dependencies")
|
|
594
|
+
if dependencies is None:
|
|
595
|
+
dependencies = tomlkit.array().multiline(True)
|
|
596
|
+
project["dependencies"] = dependencies
|
|
597
|
+
if not isinstance(dependencies, list):
|
|
598
|
+
raise _error("PZ_PROJECT_METADATA_INVALID", "project.dependencies must be an array.", action=action)
|
|
599
|
+
retained: list[object] = []
|
|
600
|
+
for raw in dependencies:
|
|
601
|
+
if isinstance(raw, str):
|
|
602
|
+
try:
|
|
603
|
+
if canonicalize_name(Requirement(raw).name) == distribution:
|
|
604
|
+
continue
|
|
605
|
+
except Exception:
|
|
606
|
+
pass
|
|
607
|
+
retained.append(raw)
|
|
608
|
+
if version is not None:
|
|
609
|
+
retained.append(f"{distribution}=={version}")
|
|
610
|
+
dependencies.clear()
|
|
611
|
+
dependencies.extend(retained)
|
|
612
|
+
if version is not None:
|
|
613
|
+
tool = document.get("tool")
|
|
614
|
+
uv = tool.get("uv") if isinstance(tool, MutableMapping) else None
|
|
615
|
+
sources = uv.get("sources") if isinstance(uv, MutableMapping) else None
|
|
616
|
+
if isinstance(sources, MutableMapping):
|
|
617
|
+
for key in tuple(sources):
|
|
618
|
+
if canonicalize_name(str(key)) == distribution:
|
|
619
|
+
sources.pop(key, None)
|
|
620
|
+
after = tomlkit.dumps(document).encode("utf-8")
|
|
621
|
+
_write_atomic(path, before, after, action=action)
|
|
622
|
+
return after != before
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def declare_app_version(
|
|
626
|
+
app_id: str,
|
|
627
|
+
version: str,
|
|
628
|
+
*,
|
|
629
|
+
project_dir: Path,
|
|
630
|
+
action: str,
|
|
631
|
+
) -> AppDeclarationResult:
|
|
632
|
+
ensure_project_state(project_dir, action=action)
|
|
633
|
+
normalized = str(Version(version))
|
|
634
|
+
distribution = distribution_name(app_id)
|
|
635
|
+
business_source = project_dir / "apps" / app_id
|
|
636
|
+
if business_source.exists() and not business_source.is_symlink():
|
|
637
|
+
raise _error(
|
|
638
|
+
"PZ_APP_SOURCE_OWNERSHIP_CONFLICT",
|
|
639
|
+
f"{app_id} is already owned as business source under apps/.",
|
|
640
|
+
action=action,
|
|
641
|
+
detail=str(business_source),
|
|
642
|
+
)
|
|
643
|
+
pyproject = project_dir / "pyproject.toml"
|
|
644
|
+
config = project_dir / "pz.jsonc"
|
|
645
|
+
pyproject_before = pyproject.read_bytes()
|
|
646
|
+
config_before = config.read_bytes()
|
|
647
|
+
changed = _update_dependency(project_dir, distribution, normalized, action=action)
|
|
648
|
+
pyproject_after = pyproject.read_bytes()
|
|
649
|
+
try:
|
|
650
|
+
installed_apps = tuple(add_declared_app(project_dir, app_id, action=action))
|
|
651
|
+
except Exception:
|
|
652
|
+
config_after = config.read_bytes()
|
|
653
|
+
_restore_snapshot(
|
|
654
|
+
pyproject,
|
|
655
|
+
before=pyproject_before,
|
|
656
|
+
expected_current=pyproject_after,
|
|
657
|
+
action=action,
|
|
658
|
+
)
|
|
659
|
+
_restore_snapshot(
|
|
660
|
+
config,
|
|
661
|
+
before=config_before,
|
|
662
|
+
expected_current=config_after,
|
|
663
|
+
action=action,
|
|
664
|
+
)
|
|
665
|
+
raise
|
|
666
|
+
return AppDeclarationResult(app_id, distribution, normalized, installed_apps, changed)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def undeclare_app_version(
|
|
670
|
+
app_id: str,
|
|
671
|
+
*,
|
|
672
|
+
project_dir: Path,
|
|
673
|
+
action: str,
|
|
674
|
+
) -> AppDeclarationResult:
|
|
675
|
+
ensure_project_state(project_dir, action=action)
|
|
676
|
+
if app_id not in load_installed_apps(project_dir, action=action):
|
|
677
|
+
raise _error("PZ_SCAFFOLD_APP_NOT_DECLARED", f"{app_id} is not declared.", action=action)
|
|
678
|
+
distribution = distribution_name(app_id)
|
|
679
|
+
pyproject = project_dir / "pyproject.toml"
|
|
680
|
+
config = project_dir / "pz.jsonc"
|
|
681
|
+
pyproject_before = pyproject.read_bytes()
|
|
682
|
+
config_before = config.read_bytes()
|
|
683
|
+
changed = _update_dependency(project_dir, distribution, None, action=action)
|
|
684
|
+
pyproject_after = pyproject.read_bytes()
|
|
685
|
+
try:
|
|
686
|
+
installed_apps = tuple(remove_declared_app(project_dir, app_id, action=action))
|
|
687
|
+
except Exception:
|
|
688
|
+
config_after = config.read_bytes()
|
|
689
|
+
_restore_snapshot(
|
|
690
|
+
pyproject,
|
|
691
|
+
before=pyproject_before,
|
|
692
|
+
expected_current=pyproject_after,
|
|
693
|
+
action=action,
|
|
694
|
+
)
|
|
695
|
+
_restore_snapshot(
|
|
696
|
+
config,
|
|
697
|
+
before=config_before,
|
|
698
|
+
expected_current=config_after,
|
|
699
|
+
action=action,
|
|
700
|
+
)
|
|
701
|
+
raise
|
|
702
|
+
return AppDeclarationResult(app_id, distribution, None, installed_apps, changed)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def install_declared_app(
|
|
706
|
+
app_id: str,
|
|
707
|
+
version: str,
|
|
708
|
+
*,
|
|
709
|
+
project_dir: Path,
|
|
710
|
+
action: str,
|
|
711
|
+
) -> AppInstallResult:
|
|
712
|
+
distribution = distribution_name(app_id)
|
|
713
|
+
normalized = str(Version(version))
|
|
714
|
+
declared = _read_declared_version(project_dir, distribution)
|
|
715
|
+
if declared != normalized:
|
|
716
|
+
raise _error(
|
|
717
|
+
"PZ_APP_VERSION_STATE_MISMATCH",
|
|
718
|
+
f"declared version for {distribution} is {declared!r}, expected {normalized}.",
|
|
719
|
+
action=action,
|
|
720
|
+
)
|
|
721
|
+
locked = _read_locked_version(project_dir, distribution)
|
|
722
|
+
actual = _read_installed_version(project_dir, distribution, action=action)
|
|
723
|
+
if locked == normalized and actual == normalized:
|
|
724
|
+
python = resolve_virtualenv_python(project_dir / ".venv", action=action)
|
|
725
|
+
return AppInstallResult(
|
|
726
|
+
app_id,
|
|
727
|
+
distribution,
|
|
728
|
+
normalized,
|
|
729
|
+
python,
|
|
730
|
+
project_dir / "uv.lock",
|
|
731
|
+
)
|
|
732
|
+
venv = ensure_project_virtualenv(project_dir, action=action)
|
|
733
|
+
python = resolve_virtualenv_python(venv.path, action=action)
|
|
734
|
+
uv_binary = shutil.which("uv")
|
|
735
|
+
if uv_binary is None:
|
|
736
|
+
raise _error("PZ_ENV_UV_MISSING", "required dependency backend was not found on PATH.", action=action)
|
|
737
|
+
target = load_install_target(app_id, project_dir=project_dir, action=action)
|
|
738
|
+
env = install_env(target=target, project_dir=project_dir, action=action)
|
|
739
|
+
env["VIRTUAL_ENV"] = str(venv.path)
|
|
740
|
+
completed = subprocess.run(
|
|
741
|
+
(uv_binary, "sync", "--active"),
|
|
742
|
+
cwd=str(project_dir),
|
|
743
|
+
check=False,
|
|
744
|
+
capture_output=True,
|
|
745
|
+
text=True,
|
|
746
|
+
env=env,
|
|
747
|
+
)
|
|
748
|
+
if completed.returncode != 0:
|
|
749
|
+
raise _error(
|
|
750
|
+
"PZ_ENV_SYNC_FAILED",
|
|
751
|
+
f"failed to install {distribution}=={normalized}.",
|
|
752
|
+
action=action,
|
|
753
|
+
detail=_redact_credentials(
|
|
754
|
+
(completed.stderr or completed.stdout or "dependency sync failed").strip()
|
|
755
|
+
),
|
|
756
|
+
)
|
|
757
|
+
actual = _read_installed_version(project_dir, distribution, action=action)
|
|
758
|
+
locked = _read_locked_version(project_dir, distribution)
|
|
759
|
+
if actual != normalized or locked != normalized:
|
|
760
|
+
raise _error(
|
|
761
|
+
"PZ_APP_VERSION_STATE_MISMATCH",
|
|
762
|
+
f"installed and locked versions for {distribution} did not converge to {normalized}.",
|
|
763
|
+
action=action,
|
|
764
|
+
detail=f"locked={locked!r}; installed={actual!r}",
|
|
765
|
+
)
|
|
766
|
+
return AppInstallResult(app_id, distribution, normalized, python, project_dir / "uv.lock")
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def reconcile_project_environment(
|
|
770
|
+
app_id: str,
|
|
771
|
+
*,
|
|
772
|
+
project_dir: Path,
|
|
773
|
+
action: str,
|
|
774
|
+
) -> Path:
|
|
775
|
+
venv = ensure_project_virtualenv(project_dir, action=action)
|
|
776
|
+
uv_binary = shutil.which("uv")
|
|
777
|
+
if uv_binary is None:
|
|
778
|
+
raise _error("PZ_ENV_UV_MISSING", "required dependency backend was not found on PATH.", action=action)
|
|
779
|
+
target = load_install_target(app_id, project_dir=project_dir, action=action)
|
|
780
|
+
env = install_env(target=target, project_dir=project_dir, action=action)
|
|
781
|
+
env["VIRTUAL_ENV"] = str(venv.path)
|
|
782
|
+
completed = subprocess.run(
|
|
783
|
+
(uv_binary, "sync", "--active"),
|
|
784
|
+
cwd=str(project_dir),
|
|
785
|
+
check=False,
|
|
786
|
+
capture_output=True,
|
|
787
|
+
text=True,
|
|
788
|
+
env=env,
|
|
789
|
+
)
|
|
790
|
+
if completed.returncode != 0:
|
|
791
|
+
raise _error(
|
|
792
|
+
"PZ_ENV_SYNC_FAILED",
|
|
793
|
+
"failed to reconcile the project environment.",
|
|
794
|
+
action=action,
|
|
795
|
+
detail=_redact_credentials(
|
|
796
|
+
(completed.stderr or completed.stdout or "dependency sync failed").strip()
|
|
797
|
+
),
|
|
798
|
+
)
|
|
799
|
+
return venv.path
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
__all__ = [
|
|
803
|
+
"AppDeclarationResult",
|
|
804
|
+
"AppInstallResult",
|
|
805
|
+
"AppVersionState",
|
|
806
|
+
"AvailableVersion",
|
|
807
|
+
"declare_app_version",
|
|
808
|
+
"distribution_name",
|
|
809
|
+
"install_declared_app",
|
|
810
|
+
"query_app_versions",
|
|
811
|
+
"reconcile_project_environment",
|
|
812
|
+
"resolve_app_version",
|
|
813
|
+
"undeclare_app_version",
|
|
814
|
+
]
|