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,1240 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
import importlib.util
|
|
7
|
+
from importlib.util import find_spec
|
|
8
|
+
from pathlib import Path, PurePosixPath
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import tomllib
|
|
12
|
+
import types
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Iterable
|
|
14
|
+
|
|
15
|
+
from pz_core.core.errors import PzError
|
|
16
|
+
from pz_core.project_sources import load_official_source_catalog
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pz_core.authoring.dev_manifest import DevBinding, DevManifest
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_SKIP_DISCOVERY_PARTS = {
|
|
23
|
+
".git",
|
|
24
|
+
".venv",
|
|
25
|
+
"__pycache__",
|
|
26
|
+
"build",
|
|
27
|
+
"dist",
|
|
28
|
+
"docs",
|
|
29
|
+
"test",
|
|
30
|
+
"tests",
|
|
31
|
+
}
|
|
32
|
+
_DEPENDENCY_NAME_RE = re.compile(r"^\s*([A-Za-z0-9_.-]+)")
|
|
33
|
+
_FORMAL_APP_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class DiscoveredAppReference:
|
|
38
|
+
module_path: str
|
|
39
|
+
import_root: Path | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class VisibleAppProjection:
|
|
44
|
+
app_id: str
|
|
45
|
+
module_path: str
|
|
46
|
+
visible_root: Path
|
|
47
|
+
source_root: Path
|
|
48
|
+
import_root: Path
|
|
49
|
+
binding_key: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class StaticAppDeclaration:
|
|
54
|
+
module_path: str
|
|
55
|
+
app_id: str
|
|
56
|
+
name: str
|
|
57
|
+
version: str
|
|
58
|
+
required_pz: str
|
|
59
|
+
priority: object | None = None
|
|
60
|
+
requires_apps: object | None = None
|
|
61
|
+
required_protocols: object | None = None
|
|
62
|
+
method_names: frozenset[str] = frozenset()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_MISSING_MODULE = object()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _prefer_minimal_package_root(candidate: Path) -> Path:
|
|
69
|
+
"""Prefer the inner package root for formal minimal-package containers."""
|
|
70
|
+
nested_package_root = candidate / candidate.name
|
|
71
|
+
if (nested_package_root / "__init__.py").is_file():
|
|
72
|
+
return nested_package_root.resolve()
|
|
73
|
+
return candidate.resolve()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_package_root(module_path: str) -> Path | None:
|
|
77
|
+
package_name = _validate_declared_app_token(
|
|
78
|
+
str(module_path or "").strip(),
|
|
79
|
+
action="resolve_package_root",
|
|
80
|
+
)
|
|
81
|
+
if not package_name:
|
|
82
|
+
return None
|
|
83
|
+
located = _resolve_package_root_cached(package_name, _sys_path_fingerprint())
|
|
84
|
+
if located is not None:
|
|
85
|
+
return Path(located)
|
|
86
|
+
try:
|
|
87
|
+
spec = find_spec(package_name)
|
|
88
|
+
except Exception:
|
|
89
|
+
return None
|
|
90
|
+
if spec is None:
|
|
91
|
+
return None
|
|
92
|
+
if spec.submodule_search_locations:
|
|
93
|
+
return Path(next(iter(spec.submodule_search_locations))).resolve()
|
|
94
|
+
if spec.origin:
|
|
95
|
+
return Path(spec.origin).resolve().parent
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@lru_cache(maxsize=512)
|
|
100
|
+
def _resolve_package_root_cached(
|
|
101
|
+
package_name: str,
|
|
102
|
+
sys_path_fingerprint: tuple[str, ...],
|
|
103
|
+
) -> str | None:
|
|
104
|
+
normalized = str(package_name or "").strip()
|
|
105
|
+
if not normalized:
|
|
106
|
+
return None
|
|
107
|
+
relative_path = Path(*normalized.split("."))
|
|
108
|
+
for entry in sys_path_fingerprint:
|
|
109
|
+
base = str(entry or "").strip()
|
|
110
|
+
if not base:
|
|
111
|
+
continue
|
|
112
|
+
candidate = Path(base).expanduser().resolve() / relative_path
|
|
113
|
+
if candidate.is_dir():
|
|
114
|
+
return str(_prefer_minimal_package_root(candidate))
|
|
115
|
+
package_file = candidate.with_suffix(".py")
|
|
116
|
+
if package_file.is_file():
|
|
117
|
+
return str(package_file.resolve().parent)
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def resolve_declared_app_id(module_path: str) -> str:
|
|
122
|
+
package_name = _validate_declared_app_token(
|
|
123
|
+
str(module_path or "").strip(),
|
|
124
|
+
action="resolve_app_id",
|
|
125
|
+
)
|
|
126
|
+
if not package_name:
|
|
127
|
+
return ""
|
|
128
|
+
try:
|
|
129
|
+
return read_static_app_declaration(package_name, action="resolve_app_id").app_id
|
|
130
|
+
except PzError as exc:
|
|
131
|
+
if exc.code == "PZ_APP_DECLARATION_NOT_FOUND":
|
|
132
|
+
raise PzError(
|
|
133
|
+
code="PZ_APP_IMPORT_FAILED",
|
|
134
|
+
message=f"Failed to locate app module '{package_name}.apps'.",
|
|
135
|
+
stage="apps",
|
|
136
|
+
owner=package_name,
|
|
137
|
+
action="resolve_app_id",
|
|
138
|
+
detail=exc.detail,
|
|
139
|
+
) from exc
|
|
140
|
+
raise
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def read_static_app_declaration(
|
|
144
|
+
module_path: str,
|
|
145
|
+
*,
|
|
146
|
+
action: str = "read_static_app_declaration",
|
|
147
|
+
) -> StaticAppDeclaration:
|
|
148
|
+
package_name = _validate_declared_app_token(
|
|
149
|
+
str(module_path or "").strip(),
|
|
150
|
+
action=action,
|
|
151
|
+
)
|
|
152
|
+
if not package_name:
|
|
153
|
+
raise PzError(
|
|
154
|
+
code="PZ_APP_INVALID_PATH",
|
|
155
|
+
message="App path must declare a directory/package name.",
|
|
156
|
+
stage="apps",
|
|
157
|
+
owner="apps",
|
|
158
|
+
action=action,
|
|
159
|
+
)
|
|
160
|
+
return _read_static_app_declaration_cached(
|
|
161
|
+
package_name,
|
|
162
|
+
_sys_path_fingerprint(),
|
|
163
|
+
action,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@lru_cache(maxsize=1024)
|
|
168
|
+
def _read_static_app_declaration_cached(
|
|
169
|
+
package_name: str,
|
|
170
|
+
sys_path_fingerprint: tuple[str, ...],
|
|
171
|
+
action: str,
|
|
172
|
+
) -> StaticAppDeclaration:
|
|
173
|
+
app_module_name = f"{package_name}.apps"
|
|
174
|
+
origin_path = _locate_apps_module_path(app_module_name, sys_path_fingerprint)
|
|
175
|
+
if origin_path is None:
|
|
176
|
+
raise PzError(
|
|
177
|
+
code="PZ_APP_DECLARATION_NOT_FOUND",
|
|
178
|
+
message=f"App module '{app_module_name}' was not found.",
|
|
179
|
+
stage="apps",
|
|
180
|
+
owner=package_name,
|
|
181
|
+
action=action,
|
|
182
|
+
)
|
|
183
|
+
if not origin_path.is_file():
|
|
184
|
+
raise PzError(
|
|
185
|
+
code="PZ_APP_ENTRY_MISSING",
|
|
186
|
+
message=f"App module '{app_module_name}' does not expose 'app'.",
|
|
187
|
+
stage="apps",
|
|
188
|
+
owner=package_name,
|
|
189
|
+
action=action,
|
|
190
|
+
)
|
|
191
|
+
try:
|
|
192
|
+
source = origin_path.read_text(encoding="utf-8")
|
|
193
|
+
tree = ast.parse(source, filename=str(origin_path))
|
|
194
|
+
except Exception as exc:
|
|
195
|
+
raise PzError(
|
|
196
|
+
code="PZ_APP_DECLARATION_INVALID",
|
|
197
|
+
message=f"Failed to parse app module '{app_module_name}': {exc}",
|
|
198
|
+
stage="apps",
|
|
199
|
+
owner=package_name,
|
|
200
|
+
action=action,
|
|
201
|
+
) from exc
|
|
202
|
+
|
|
203
|
+
classes = {node.name: node for node in tree.body if isinstance(node, ast.ClassDef)}
|
|
204
|
+
app_class_name = _read_module_app_class_name(tree)
|
|
205
|
+
if not app_class_name:
|
|
206
|
+
raise PzError(
|
|
207
|
+
code="PZ_APP_ENTRY_MISSING",
|
|
208
|
+
message=f"App module '{app_module_name}' does not expose 'app = <AppClass>()'.",
|
|
209
|
+
stage="apps",
|
|
210
|
+
owner=package_name,
|
|
211
|
+
action=action,
|
|
212
|
+
)
|
|
213
|
+
class_node = classes.get(app_class_name)
|
|
214
|
+
if class_node is None:
|
|
215
|
+
raise PzError(
|
|
216
|
+
code="PZ_APP_ENTRY_MISSING",
|
|
217
|
+
message=f"App module '{app_module_name}' declares app from unknown class '{app_class_name}'.",
|
|
218
|
+
stage="apps",
|
|
219
|
+
owner=package_name,
|
|
220
|
+
action=action,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
fields = _read_static_class_fields(class_node)
|
|
224
|
+
app_id = _read_static_required_string_field(
|
|
225
|
+
fields,
|
|
226
|
+
"id",
|
|
227
|
+
package_name=package_name,
|
|
228
|
+
code="PZ_APP_ID_MISSING",
|
|
229
|
+
invalid_code="PZ_APP_ID_INVALID",
|
|
230
|
+
action=action,
|
|
231
|
+
)
|
|
232
|
+
if not app_id.isidentifier():
|
|
233
|
+
raise PzError(
|
|
234
|
+
code="PZ_APP_ID_INVALID",
|
|
235
|
+
message=f"App '{package_name}' must declare a valid app.id identifier.",
|
|
236
|
+
stage="apps",
|
|
237
|
+
owner=package_name,
|
|
238
|
+
action=action,
|
|
239
|
+
)
|
|
240
|
+
return StaticAppDeclaration(
|
|
241
|
+
module_path=package_name,
|
|
242
|
+
app_id=app_id,
|
|
243
|
+
name=_read_static_required_string_field(
|
|
244
|
+
fields,
|
|
245
|
+
"name",
|
|
246
|
+
package_name=app_id,
|
|
247
|
+
code="PZ_APP_NAME_MISSING",
|
|
248
|
+
invalid_code="PZ_APP_NAME_MISSING",
|
|
249
|
+
action=action,
|
|
250
|
+
),
|
|
251
|
+
version=_read_static_required_string_field(
|
|
252
|
+
fields,
|
|
253
|
+
"version",
|
|
254
|
+
package_name=app_id,
|
|
255
|
+
code="PZ_APP_VERSION_MISSING",
|
|
256
|
+
invalid_code="PZ_APP_VERSION_MISSING",
|
|
257
|
+
action=action,
|
|
258
|
+
),
|
|
259
|
+
required_pz=_read_static_required_string_field(
|
|
260
|
+
fields,
|
|
261
|
+
"required_pz",
|
|
262
|
+
package_name=app_id,
|
|
263
|
+
code="PZ_APP_REQUIRED_PZ_MISSING",
|
|
264
|
+
invalid_code="PZ_APP_REQUIRED_PZ_MISSING",
|
|
265
|
+
action=action,
|
|
266
|
+
),
|
|
267
|
+
priority=_read_static_optional_field(fields, "priority"),
|
|
268
|
+
requires_apps=_read_static_optional_field(fields, "requires_apps"),
|
|
269
|
+
required_protocols=_read_static_optional_field(fields, "required_protocols"),
|
|
270
|
+
method_names=frozenset(
|
|
271
|
+
statement.name
|
|
272
|
+
for statement in class_node.body
|
|
273
|
+
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _sys_path_fingerprint() -> tuple[str, ...]:
|
|
279
|
+
return tuple(str(entry or "") for entry in sys.path)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def import_app_settings_module(module_path: str, *, action: str) -> Any | None:
|
|
283
|
+
module_name = f"{module_path}.settings"
|
|
284
|
+
module_file = _locate_declared_app_sibling_module_file(module_path, "settings.py")
|
|
285
|
+
if module_file is None:
|
|
286
|
+
return None
|
|
287
|
+
return _import_entrypoint_module_from_file(module_name, module_file, action=action)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def import_app_entrypoint_module_from_decl(module_path: str, *, action: str) -> Any:
|
|
291
|
+
module_name = f"{module_path}.apps"
|
|
292
|
+
module_file = _locate_declared_app_sibling_module_file(module_path, "apps.py")
|
|
293
|
+
if module_file is None:
|
|
294
|
+
raise PzError(
|
|
295
|
+
code="PZ_APP_IMPORT_FAILED",
|
|
296
|
+
message=f"Failed to locate app module '{module_name}'.",
|
|
297
|
+
stage="apps",
|
|
298
|
+
owner=module_path,
|
|
299
|
+
action=action,
|
|
300
|
+
)
|
|
301
|
+
return _import_entrypoint_module_from_file(module_name, module_file, action=action)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _import_entrypoint_module_from_file(
|
|
305
|
+
module_name: str, module_file: Path, *, action: str
|
|
306
|
+
) -> Any:
|
|
307
|
+
package_name, _, leaf_name = module_name.rpartition(".")
|
|
308
|
+
package_root = module_file.parent
|
|
309
|
+
restore_names = [module_name]
|
|
310
|
+
package_parts = package_name.split(".") if package_name else []
|
|
311
|
+
package_dirs = (
|
|
312
|
+
[
|
|
313
|
+
*(
|
|
314
|
+
package_root.parents[len(package_parts) - index - 2]
|
|
315
|
+
for index in range(len(package_parts) - 1)
|
|
316
|
+
),
|
|
317
|
+
package_root,
|
|
318
|
+
]
|
|
319
|
+
if package_parts
|
|
320
|
+
else []
|
|
321
|
+
)
|
|
322
|
+
for index in range(1, len(package_parts) + 1):
|
|
323
|
+
restore_names.append(".".join(package_parts[:index]))
|
|
324
|
+
saved_modules = {
|
|
325
|
+
name: sys.modules.get(name, _MISSING_MODULE) for name in restore_names
|
|
326
|
+
}
|
|
327
|
+
try:
|
|
328
|
+
for index in range(1, len(package_parts) + 1):
|
|
329
|
+
current_name = ".".join(package_parts[:index])
|
|
330
|
+
current_dir = package_dirs[index - 1]
|
|
331
|
+
package_module = types.ModuleType(current_name)
|
|
332
|
+
package_module.__file__ = str(current_dir / "__init__.py")
|
|
333
|
+
package_module.__path__ = [str(current_dir)]
|
|
334
|
+
package_module.__package__ = current_name
|
|
335
|
+
package_module.__spec__ = importlib.util.spec_from_loader(
|
|
336
|
+
current_name, loader=None, is_package=True
|
|
337
|
+
)
|
|
338
|
+
sys.modules[current_name] = package_module
|
|
339
|
+
spec = importlib.util.spec_from_file_location(
|
|
340
|
+
module_name,
|
|
341
|
+
module_file,
|
|
342
|
+
submodule_search_locations=[str(package_root)]
|
|
343
|
+
if leaf_name == "__init__"
|
|
344
|
+
else None,
|
|
345
|
+
)
|
|
346
|
+
if spec is None or spec.loader is None:
|
|
347
|
+
raise ImportError(f"cannot load module from {module_file}")
|
|
348
|
+
module = importlib.util.module_from_spec(spec)
|
|
349
|
+
sys.modules[module_name] = module
|
|
350
|
+
spec.loader.exec_module(module)
|
|
351
|
+
return module
|
|
352
|
+
except PzError:
|
|
353
|
+
raise
|
|
354
|
+
except Exception as exc:
|
|
355
|
+
raise PzError(
|
|
356
|
+
code="PZ_APP_IMPORT_FAILED",
|
|
357
|
+
message=f"Failed to import app module '{module_name}': {exc}",
|
|
358
|
+
stage="apps",
|
|
359
|
+
owner=module_name,
|
|
360
|
+
action=action,
|
|
361
|
+
) from exc
|
|
362
|
+
finally:
|
|
363
|
+
for name, saved in saved_modules.items():
|
|
364
|
+
if saved is _MISSING_MODULE:
|
|
365
|
+
sys.modules.pop(name, None)
|
|
366
|
+
else:
|
|
367
|
+
sys.modules[name] = saved
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _locate_declared_app_sibling_module_file(
|
|
371
|
+
module_path: str,
|
|
372
|
+
file_name: str,
|
|
373
|
+
) -> Path | None:
|
|
374
|
+
apps_module_path = _locate_apps_module_path(
|
|
375
|
+
f"{module_path}.apps", _sys_path_fingerprint()
|
|
376
|
+
)
|
|
377
|
+
if apps_module_path is None:
|
|
378
|
+
return None
|
|
379
|
+
candidate = apps_module_path.with_name(file_name)
|
|
380
|
+
return candidate if candidate.is_file() else None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _locate_apps_module_path(
|
|
384
|
+
app_module_name: str,
|
|
385
|
+
sys_path_fingerprint: tuple[str, ...],
|
|
386
|
+
) -> Path | None:
|
|
387
|
+
origin = _locate_apps_module_file_cached(app_module_name, sys_path_fingerprint)
|
|
388
|
+
if origin is not None:
|
|
389
|
+
return Path(origin)
|
|
390
|
+
original_sys_path = sys.path
|
|
391
|
+
sys.path = list(sys_path_fingerprint)
|
|
392
|
+
try:
|
|
393
|
+
spec = find_spec(app_module_name)
|
|
394
|
+
except Exception:
|
|
395
|
+
return None
|
|
396
|
+
finally:
|
|
397
|
+
sys.path = original_sys_path
|
|
398
|
+
if spec is None or not spec.origin:
|
|
399
|
+
return None
|
|
400
|
+
return Path(spec.origin)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _locate_apps_module_file(app_module_name: str) -> Path | None:
|
|
404
|
+
located = _locate_apps_module_file_cached(app_module_name, _sys_path_fingerprint())
|
|
405
|
+
return Path(located) if located is not None else None
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@lru_cache(maxsize=512)
|
|
409
|
+
def _locate_apps_module_file_cached(
|
|
410
|
+
app_module_name: str,
|
|
411
|
+
sys_path_fingerprint: tuple[str, ...],
|
|
412
|
+
) -> str | None:
|
|
413
|
+
module_name = str(app_module_name or "").strip()
|
|
414
|
+
if not module_name:
|
|
415
|
+
return None
|
|
416
|
+
parts = module_name.split(".")
|
|
417
|
+
relative_path = Path(*parts).with_suffix(".py")
|
|
418
|
+
for entry in sys_path_fingerprint:
|
|
419
|
+
base = str(entry or "").strip()
|
|
420
|
+
if not base:
|
|
421
|
+
continue
|
|
422
|
+
candidate = Path(base).expanduser().resolve() / relative_path
|
|
423
|
+
if candidate.is_file():
|
|
424
|
+
return str(candidate)
|
|
425
|
+
if len(parts) == 2 and parts[-1] == "apps":
|
|
426
|
+
nested_apps = (
|
|
427
|
+
Path(base).expanduser().resolve() / parts[0] / parts[0] / "apps.py"
|
|
428
|
+
)
|
|
429
|
+
if nested_apps.is_file():
|
|
430
|
+
return str(nested_apps.resolve())
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _read_static_class_app_id(node: ast.ClassDef) -> str:
|
|
435
|
+
fields = _read_static_class_fields(node)
|
|
436
|
+
value = fields.get("id")
|
|
437
|
+
if isinstance(value, str) and value.strip().isidentifier():
|
|
438
|
+
return value.strip()
|
|
439
|
+
return ""
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _read_module_app_class_name(tree: ast.Module) -> str:
|
|
443
|
+
for node in tree.body:
|
|
444
|
+
value: ast.AST | None
|
|
445
|
+
if isinstance(node, ast.Assign):
|
|
446
|
+
value = node.value
|
|
447
|
+
targets = node.targets
|
|
448
|
+
elif isinstance(node, ast.AnnAssign):
|
|
449
|
+
value = node.value
|
|
450
|
+
targets = (node.target,)
|
|
451
|
+
else:
|
|
452
|
+
continue
|
|
453
|
+
if value is None:
|
|
454
|
+
continue
|
|
455
|
+
for target in targets:
|
|
456
|
+
if isinstance(target, ast.Name) and target.id == "app":
|
|
457
|
+
return _read_static_app_instance_class_name(value)
|
|
458
|
+
return ""
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _read_static_app_instance_class_name(node: ast.AST) -> str:
|
|
462
|
+
if isinstance(node, ast.Call):
|
|
463
|
+
return _read_static_app_instance_class_name(node.func)
|
|
464
|
+
if isinstance(node, ast.Name):
|
|
465
|
+
return node.id
|
|
466
|
+
return ""
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _read_static_class_fields(node: ast.ClassDef) -> dict[str, object]:
|
|
470
|
+
fields: dict[str, object] = {}
|
|
471
|
+
for statement in node.body:
|
|
472
|
+
value: ast.AST | None
|
|
473
|
+
targets: Iterable[ast.AST]
|
|
474
|
+
if isinstance(statement, ast.Assign):
|
|
475
|
+
value = statement.value
|
|
476
|
+
targets = statement.targets
|
|
477
|
+
elif isinstance(statement, ast.AnnAssign):
|
|
478
|
+
value = statement.value
|
|
479
|
+
targets = (statement.target,)
|
|
480
|
+
else:
|
|
481
|
+
continue
|
|
482
|
+
if value is None:
|
|
483
|
+
continue
|
|
484
|
+
for target in targets:
|
|
485
|
+
if isinstance(target, ast.Name):
|
|
486
|
+
fields[target.id] = _read_static_literal(value)
|
|
487
|
+
return fields
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _read_static_literal(node: ast.AST) -> object:
|
|
491
|
+
try:
|
|
492
|
+
return ast.literal_eval(node)
|
|
493
|
+
except Exception:
|
|
494
|
+
return _MISSING_MODULE
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _read_static_required_string_field(
|
|
498
|
+
fields: dict[str, object],
|
|
499
|
+
field_name: str,
|
|
500
|
+
*,
|
|
501
|
+
package_name: str,
|
|
502
|
+
code: str,
|
|
503
|
+
invalid_code: str,
|
|
504
|
+
action: str,
|
|
505
|
+
) -> str:
|
|
506
|
+
value = fields.get(field_name, _MISSING_MODULE)
|
|
507
|
+
if value is _MISSING_MODULE:
|
|
508
|
+
raise PzError(
|
|
509
|
+
code=code,
|
|
510
|
+
message=f"App '{package_name}' must declare static string field '{field_name}'.",
|
|
511
|
+
stage="apps",
|
|
512
|
+
owner=package_name,
|
|
513
|
+
action=action,
|
|
514
|
+
)
|
|
515
|
+
if not isinstance(value, str) or not value.strip():
|
|
516
|
+
raise PzError(
|
|
517
|
+
code=invalid_code,
|
|
518
|
+
message=f"App '{package_name}' must declare non-empty static string field '{field_name}'.",
|
|
519
|
+
stage="apps",
|
|
520
|
+
owner=package_name,
|
|
521
|
+
action=action,
|
|
522
|
+
)
|
|
523
|
+
return value.strip()
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _read_static_optional_field(
|
|
527
|
+
fields: dict[str, object], field_name: str
|
|
528
|
+
) -> object | None:
|
|
529
|
+
value = fields.get(field_name, _MISSING_MODULE)
|
|
530
|
+
if value is _MISSING_MODULE:
|
|
531
|
+
return None
|
|
532
|
+
return value
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _normalize_package_name(module_path: str) -> str:
|
|
536
|
+
return str(module_path or "").strip()
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _validate_declared_app_token(module_path: str, *, action: str) -> str:
|
|
540
|
+
normalized = str(module_path or "").strip()
|
|
541
|
+
if not normalized:
|
|
542
|
+
return ""
|
|
543
|
+
if ":" in normalized or normalized.endswith(".apps"):
|
|
544
|
+
raise PzError(
|
|
545
|
+
code="PZ_APP_INVALID_PATH",
|
|
546
|
+
message=(
|
|
547
|
+
f"App path '{normalized}' must declare a directory/package name only; "
|
|
548
|
+
"installed_apps does not accept '<pkg>.apps' or '<pkg>.apps:app'."
|
|
549
|
+
),
|
|
550
|
+
stage="apps",
|
|
551
|
+
owner=normalized,
|
|
552
|
+
action=action,
|
|
553
|
+
)
|
|
554
|
+
return normalized
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _read_project_dir(project_dir: str | Path | None) -> Path | None:
|
|
558
|
+
if project_dir is None:
|
|
559
|
+
return None
|
|
560
|
+
return Path(project_dir).expanduser().resolve()
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _normalize_distribution_name(value: str) -> str:
|
|
564
|
+
return re.sub(r"[-_.]+", "-", str(value or "").strip().lower()).strip("-")
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _is_skipped_discovery_dir(path: Path) -> bool:
|
|
568
|
+
name = path.name
|
|
569
|
+
if name.startswith("."):
|
|
570
|
+
return True
|
|
571
|
+
return name in _SKIP_DISCOVERY_PARTS
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _read_standard_apps_py_app_id(apps_py: Path) -> str:
|
|
575
|
+
if not apps_py.is_file():
|
|
576
|
+
return ""
|
|
577
|
+
try:
|
|
578
|
+
source = apps_py.read_text(encoding="utf-8")
|
|
579
|
+
tree = ast.parse(source, filename=str(apps_py))
|
|
580
|
+
except Exception:
|
|
581
|
+
return ""
|
|
582
|
+
|
|
583
|
+
class_ids: dict[str, str] = {}
|
|
584
|
+
for node in tree.body:
|
|
585
|
+
if not isinstance(node, ast.ClassDef):
|
|
586
|
+
continue
|
|
587
|
+
class_id = _read_static_class_app_id(node)
|
|
588
|
+
if class_id:
|
|
589
|
+
class_ids[node.name] = class_id
|
|
590
|
+
|
|
591
|
+
for node in tree.body:
|
|
592
|
+
if not isinstance(node, ast.Assign):
|
|
593
|
+
continue
|
|
594
|
+
for target in node.targets:
|
|
595
|
+
if isinstance(target, ast.Name) and target.id == "app":
|
|
596
|
+
class_name = _read_static_app_instance_class_name(node.value)
|
|
597
|
+
if class_name and class_name in class_ids:
|
|
598
|
+
return class_ids[class_name]
|
|
599
|
+
return ""
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _has_standard_apps_py(apps_py: Path) -> bool:
|
|
603
|
+
return bool(_read_standard_apps_py_app_id(apps_py))
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _visible_app_error(
|
|
607
|
+
code: str,
|
|
608
|
+
message: str,
|
|
609
|
+
*,
|
|
610
|
+
owner: str,
|
|
611
|
+
) -> PzError:
|
|
612
|
+
return PzError(
|
|
613
|
+
code=code,
|
|
614
|
+
message=message,
|
|
615
|
+
stage="apps",
|
|
616
|
+
owner=owner,
|
|
617
|
+
action="iter_visible_app_projections",
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _read_visible_leaf_app_id(leaf_root: Path) -> str:
|
|
622
|
+
pyproject_path = leaf_root / "pyproject.toml"
|
|
623
|
+
if not pyproject_path.is_file():
|
|
624
|
+
return _read_bundled_visible_leaf_app_id(leaf_root)
|
|
625
|
+
try:
|
|
626
|
+
payload = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
|
|
627
|
+
except (OSError, UnicodeError, tomllib.TOMLDecodeError):
|
|
628
|
+
return ""
|
|
629
|
+
project = payload.get("project")
|
|
630
|
+
if not isinstance(project, dict):
|
|
631
|
+
return ""
|
|
632
|
+
distribution = project.get("name")
|
|
633
|
+
if not isinstance(distribution, str) or distribution != distribution.strip():
|
|
634
|
+
return ""
|
|
635
|
+
app_id = distribution.replace("-", "_")
|
|
636
|
+
if not _FORMAL_APP_ID_RE.fullmatch(app_id) or leaf_root.name != app_id:
|
|
637
|
+
return ""
|
|
638
|
+
module_root = leaf_root / app_id
|
|
639
|
+
if not (module_root / "__init__.py").is_file():
|
|
640
|
+
return ""
|
|
641
|
+
if _read_standard_apps_py_app_id(module_root / "apps.py") != app_id:
|
|
642
|
+
return ""
|
|
643
|
+
return app_id
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _read_project_distribution(project_root: Path) -> str:
|
|
647
|
+
pyproject_path = project_root / "pyproject.toml"
|
|
648
|
+
if not pyproject_path.is_file():
|
|
649
|
+
return ""
|
|
650
|
+
try:
|
|
651
|
+
payload = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
|
|
652
|
+
except (OSError, UnicodeError, tomllib.TOMLDecodeError):
|
|
653
|
+
return ""
|
|
654
|
+
project = payload.get("project")
|
|
655
|
+
if not isinstance(project, dict):
|
|
656
|
+
return ""
|
|
657
|
+
distribution = project.get("name")
|
|
658
|
+
return distribution if isinstance(distribution, str) else ""
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _read_bundled_visible_leaf_app_id(leaf_root: Path) -> str:
|
|
662
|
+
try:
|
|
663
|
+
resolved_leaf = leaf_root.resolve(strict=True)
|
|
664
|
+
except (OSError, RuntimeError):
|
|
665
|
+
return ""
|
|
666
|
+
for source in load_official_source_catalog().values():
|
|
667
|
+
if not source.bootstrap:
|
|
668
|
+
continue
|
|
669
|
+
project_distributions = {
|
|
670
|
+
distribution
|
|
671
|
+
for distribution, child in source.children.items()
|
|
672
|
+
if child.kind == "project" and child.subpath == PurePosixPath(".")
|
|
673
|
+
}
|
|
674
|
+
for child in source.children.values():
|
|
675
|
+
app_id = child.expected_app_id
|
|
676
|
+
if child.kind != "app" or app_id is None or app_id != leaf_root.name:
|
|
677
|
+
continue
|
|
678
|
+
project_root = resolved_leaf
|
|
679
|
+
for _part in child.subpath.parts:
|
|
680
|
+
project_root = project_root.parent
|
|
681
|
+
if project_root.joinpath(*child.subpath.parts) != resolved_leaf:
|
|
682
|
+
continue
|
|
683
|
+
if _read_project_distribution(project_root) not in project_distributions:
|
|
684
|
+
continue
|
|
685
|
+
module_root = resolved_leaf / app_id
|
|
686
|
+
if not (module_root / "__init__.py").is_file():
|
|
687
|
+
continue
|
|
688
|
+
if _read_standard_apps_py_app_id(module_root / "apps.py") == app_id:
|
|
689
|
+
return app_id
|
|
690
|
+
return ""
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _build_visible_projection(
|
|
694
|
+
leaf_root: Path,
|
|
695
|
+
*,
|
|
696
|
+
binding_key: str | None,
|
|
697
|
+
) -> VisibleAppProjection | None:
|
|
698
|
+
app_id = _read_visible_leaf_app_id(leaf_root)
|
|
699
|
+
if not app_id:
|
|
700
|
+
return None
|
|
701
|
+
try:
|
|
702
|
+
source_root = leaf_root.resolve(strict=True)
|
|
703
|
+
except (OSError, RuntimeError):
|
|
704
|
+
return None
|
|
705
|
+
return VisibleAppProjection(
|
|
706
|
+
app_id=app_id,
|
|
707
|
+
module_path=app_id,
|
|
708
|
+
visible_root=leaf_root,
|
|
709
|
+
source_root=source_root,
|
|
710
|
+
import_root=source_root,
|
|
711
|
+
binding_key=binding_key,
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _binding_source_root(
|
|
716
|
+
project_dir: Path,
|
|
717
|
+
manifest: DevManifest,
|
|
718
|
+
binding: DevBinding,
|
|
719
|
+
) -> Path:
|
|
720
|
+
if binding.source_kind == "link":
|
|
721
|
+
root = Path(binding.source)
|
|
722
|
+
else:
|
|
723
|
+
assert binding.repo_key is not None
|
|
724
|
+
root = project_dir / Path(*manifest.repos[binding.repo_key].checkout.parts)
|
|
725
|
+
if binding.subpath == PurePosixPath("."):
|
|
726
|
+
return root
|
|
727
|
+
return root / Path(*binding.subpath.parts)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _resolve_managed_link_target(
|
|
731
|
+
project_dir: Path,
|
|
732
|
+
manifest: DevManifest,
|
|
733
|
+
*,
|
|
734
|
+
binding_key: str,
|
|
735
|
+
binding: DevBinding,
|
|
736
|
+
) -> tuple[Path, Path]:
|
|
737
|
+
if len(binding.link.parts) != 2 or binding.link.parts[0] != "apps":
|
|
738
|
+
raise _visible_app_error(
|
|
739
|
+
"PZ_VISIBLE_APP_BINDING_INVALID",
|
|
740
|
+
f"managed binding {binding_key!r} must project directly below apps/.",
|
|
741
|
+
owner=binding_key,
|
|
742
|
+
)
|
|
743
|
+
link_name = binding.link.parts[-1]
|
|
744
|
+
if link_name.startswith(".") or link_name in _SKIP_DISCOVERY_PARTS:
|
|
745
|
+
raise _visible_app_error(
|
|
746
|
+
"PZ_VISIBLE_APP_BINDING_INVALID",
|
|
747
|
+
f"managed binding {binding_key!r} uses a hidden projection path.",
|
|
748
|
+
owner=binding_key,
|
|
749
|
+
)
|
|
750
|
+
link = project_dir / Path(*binding.link.parts)
|
|
751
|
+
if not link.is_symlink():
|
|
752
|
+
raise _visible_app_error(
|
|
753
|
+
"PZ_VISIBLE_APP_BINDING_INVALID",
|
|
754
|
+
f"managed binding {binding_key!r} is missing its symbolic link.",
|
|
755
|
+
owner=binding_key,
|
|
756
|
+
)
|
|
757
|
+
expected = _binding_source_root(project_dir, manifest, binding)
|
|
758
|
+
try:
|
|
759
|
+
actual_root = link.resolve(strict=True)
|
|
760
|
+
expected_root = expected.resolve(strict=True)
|
|
761
|
+
except (OSError, RuntimeError) as exc:
|
|
762
|
+
raise _visible_app_error(
|
|
763
|
+
"PZ_VISIBLE_APP_BINDING_INVALID",
|
|
764
|
+
f"managed binding {binding_key!r} has a broken or looping target.",
|
|
765
|
+
owner=binding_key,
|
|
766
|
+
) from exc
|
|
767
|
+
if actual_root != expected_root:
|
|
768
|
+
raise _visible_app_error(
|
|
769
|
+
"PZ_VISIBLE_APP_BINDING_INVALID",
|
|
770
|
+
f"managed binding {binding_key!r} does not match its manifest target.",
|
|
771
|
+
owner=binding_key,
|
|
772
|
+
)
|
|
773
|
+
return link, actual_root
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _managed_binding_projections(
|
|
777
|
+
project_dir: Path,
|
|
778
|
+
manifest: DevManifest,
|
|
779
|
+
) -> tuple[VisibleAppProjection, ...]:
|
|
780
|
+
projections: list[VisibleAppProjection] = []
|
|
781
|
+
for binding_key, binding in sorted(
|
|
782
|
+
manifest.bindings.items(),
|
|
783
|
+
key=lambda item: item[1].link.as_posix(),
|
|
784
|
+
):
|
|
785
|
+
link, _actual_root = _resolve_managed_link_target(
|
|
786
|
+
project_dir,
|
|
787
|
+
manifest,
|
|
788
|
+
binding_key=binding_key,
|
|
789
|
+
binding=binding,
|
|
790
|
+
)
|
|
791
|
+
if link.name != binding_key:
|
|
792
|
+
raise _visible_app_error(
|
|
793
|
+
"PZ_VISIBLE_APP_BINDING_INVALID",
|
|
794
|
+
f"managed binding {binding_key!r} must use the same link name.",
|
|
795
|
+
owner=binding_key,
|
|
796
|
+
)
|
|
797
|
+
if binding.kind == "app":
|
|
798
|
+
projection = _build_visible_projection(link, binding_key=binding_key)
|
|
799
|
+
if projection is None or projection.app_id != binding_key:
|
|
800
|
+
raise _visible_app_error(
|
|
801
|
+
"PZ_VISIBLE_APP_BINDING_INVALID",
|
|
802
|
+
f"managed app binding {binding_key!r} does not expose its declared leaf.",
|
|
803
|
+
owner=binding_key,
|
|
804
|
+
)
|
|
805
|
+
projections.append(projection)
|
|
806
|
+
continue
|
|
807
|
+
directory_projections: list[VisibleAppProjection] = []
|
|
808
|
+
try:
|
|
809
|
+
children = sorted(link.iterdir(), key=lambda item: item.name)
|
|
810
|
+
except OSError as exc:
|
|
811
|
+
raise _visible_app_error(
|
|
812
|
+
"PZ_VISIBLE_APP_BINDING_INVALID",
|
|
813
|
+
f"managed directory binding {binding_key!r} cannot be read.",
|
|
814
|
+
owner=binding_key,
|
|
815
|
+
) from exc
|
|
816
|
+
for child in children:
|
|
817
|
+
if _is_skipped_discovery_dir(child) or child.is_symlink():
|
|
818
|
+
continue
|
|
819
|
+
if not child.is_dir():
|
|
820
|
+
continue
|
|
821
|
+
projection = _build_visible_projection(child, binding_key=binding_key)
|
|
822
|
+
if projection is not None:
|
|
823
|
+
directory_projections.append(projection)
|
|
824
|
+
if not directory_projections:
|
|
825
|
+
raise _visible_app_error(
|
|
826
|
+
"PZ_VISIBLE_APP_BINDING_INVALID",
|
|
827
|
+
f"managed directory binding {binding_key!r} exposes no formal top-level leaf.",
|
|
828
|
+
owner=binding_key,
|
|
829
|
+
)
|
|
830
|
+
projections.extend(directory_projections)
|
|
831
|
+
return tuple(projections)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _is_safe_unmanaged_link(project_dir: Path, source_root: Path) -> bool:
|
|
835
|
+
try:
|
|
836
|
+
relative = source_root.relative_to(project_dir)
|
|
837
|
+
except ValueError:
|
|
838
|
+
return False
|
|
839
|
+
return not any(part.startswith(".") for part in relative.parts)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _unmanaged_local_projections(
|
|
843
|
+
project_dir: Path,
|
|
844
|
+
managed_links: frozenset[Path],
|
|
845
|
+
) -> tuple[VisibleAppProjection, ...]:
|
|
846
|
+
apps_root = project_dir / "apps"
|
|
847
|
+
if not apps_root.is_dir():
|
|
848
|
+
return ()
|
|
849
|
+
projections: list[VisibleAppProjection] = []
|
|
850
|
+
try:
|
|
851
|
+
entries = sorted(apps_root.iterdir(), key=lambda item: item.name)
|
|
852
|
+
except OSError:
|
|
853
|
+
return ()
|
|
854
|
+
for entry in entries:
|
|
855
|
+
if _is_skipped_discovery_dir(entry) or entry in managed_links:
|
|
856
|
+
continue
|
|
857
|
+
if entry.is_symlink():
|
|
858
|
+
try:
|
|
859
|
+
source_root = entry.resolve(strict=True)
|
|
860
|
+
except (OSError, RuntimeError):
|
|
861
|
+
continue
|
|
862
|
+
if not _is_safe_unmanaged_link(project_dir, source_root):
|
|
863
|
+
continue
|
|
864
|
+
elif entry.is_dir():
|
|
865
|
+
source_root = entry.resolve()
|
|
866
|
+
else:
|
|
867
|
+
continue
|
|
868
|
+
projection = _build_visible_projection(entry, binding_key=None)
|
|
869
|
+
if projection is None or projection.app_id != entry.name:
|
|
870
|
+
continue
|
|
871
|
+
if entry.is_symlink() and projection.source_root != source_root:
|
|
872
|
+
continue
|
|
873
|
+
projections.append(projection)
|
|
874
|
+
return tuple(projections)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def iter_visible_app_projections(
|
|
878
|
+
project_dir: str | Path,
|
|
879
|
+
) -> tuple[VisibleAppProjection, ...]:
|
|
880
|
+
from pz_core.authoring.dev_manifest import load_dev_manifest
|
|
881
|
+
|
|
882
|
+
resolved_project_dir = Path(project_dir).expanduser().resolve()
|
|
883
|
+
manifest = load_dev_manifest(resolved_project_dir)
|
|
884
|
+
managed = (
|
|
885
|
+
()
|
|
886
|
+
if manifest is None
|
|
887
|
+
else _managed_binding_projections(
|
|
888
|
+
resolved_project_dir,
|
|
889
|
+
manifest,
|
|
890
|
+
)
|
|
891
|
+
)
|
|
892
|
+
managed_links = (
|
|
893
|
+
frozenset()
|
|
894
|
+
if manifest is None
|
|
895
|
+
else frozenset(
|
|
896
|
+
resolved_project_dir / Path(*binding.link.parts)
|
|
897
|
+
for binding in manifest.bindings.values()
|
|
898
|
+
)
|
|
899
|
+
)
|
|
900
|
+
unmanaged = _unmanaged_local_projections(resolved_project_dir, managed_links)
|
|
901
|
+
ordered = sorted(
|
|
902
|
+
(*managed, *unmanaged),
|
|
903
|
+
key=lambda item: (item.visible_root.as_posix(), item.app_id),
|
|
904
|
+
)
|
|
905
|
+
by_id: dict[str, VisibleAppProjection] = {}
|
|
906
|
+
for projection in ordered:
|
|
907
|
+
existing = by_id.get(projection.app_id)
|
|
908
|
+
if existing is not None:
|
|
909
|
+
raise _visible_app_error(
|
|
910
|
+
"PZ_VISIBLE_APP_DUPLICATE",
|
|
911
|
+
(
|
|
912
|
+
f"visible app {projection.app_id!r} is exposed by both "
|
|
913
|
+
f"{existing.visible_root} and {projection.visible_root}."
|
|
914
|
+
),
|
|
915
|
+
owner=projection.app_id,
|
|
916
|
+
)
|
|
917
|
+
by_id[projection.app_id] = projection
|
|
918
|
+
return tuple(ordered)
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def _discover_project_local_app_references(
|
|
922
|
+
project_dir: Path,
|
|
923
|
+
module_path: str,
|
|
924
|
+
projections: tuple[VisibleAppProjection, ...],
|
|
925
|
+
) -> tuple[DiscoveredAppReference, ...]:
|
|
926
|
+
if module_path == "apps":
|
|
927
|
+
return tuple(
|
|
928
|
+
DiscoveredAppReference(
|
|
929
|
+
module_path=item.module_path,
|
|
930
|
+
import_root=item.import_root,
|
|
931
|
+
)
|
|
932
|
+
for item in projections
|
|
933
|
+
)
|
|
934
|
+
return tuple(
|
|
935
|
+
DiscoveredAppReference(
|
|
936
|
+
module_path=item.module_path,
|
|
937
|
+
import_root=item.import_root,
|
|
938
|
+
)
|
|
939
|
+
for item in projections
|
|
940
|
+
if module_path in {item.app_id, item.module_path}
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _has_project_local_app_candidate(
|
|
945
|
+
project_dir: Path,
|
|
946
|
+
module_path: str,
|
|
947
|
+
projections: tuple[VisibleAppProjection, ...],
|
|
948
|
+
) -> bool:
|
|
949
|
+
if module_path == "apps":
|
|
950
|
+
return (project_dir / "apps").is_dir()
|
|
951
|
+
return any(module_path in {item.app_id, item.module_path} for item in projections)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _discover_leaf_apps_from_import_root(
|
|
955
|
+
module_path: str,
|
|
956
|
+
package_root: Path,
|
|
957
|
+
*,
|
|
958
|
+
import_root: Path | None = None,
|
|
959
|
+
) -> tuple[DiscoveredAppReference, ...]:
|
|
960
|
+
if _has_standard_apps_py(package_root / "apps.py"):
|
|
961
|
+
return (
|
|
962
|
+
DiscoveredAppReference(module_path=module_path, import_root=import_root),
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
discovered: list[DiscoveredAppReference] = []
|
|
966
|
+
for child in sorted(package_root.iterdir(), key=lambda item: item.name):
|
|
967
|
+
if not child.is_dir():
|
|
968
|
+
continue
|
|
969
|
+
if child.name.startswith(".") or child.name in _SKIP_DISCOVERY_PARTS:
|
|
970
|
+
continue
|
|
971
|
+
if not (child / "__init__.py").is_file():
|
|
972
|
+
continue
|
|
973
|
+
child_module_path = f"{module_path}.{child.name}"
|
|
974
|
+
if _has_standard_apps_py(child / "apps.py"):
|
|
975
|
+
discovered.append(
|
|
976
|
+
DiscoveredAppReference(
|
|
977
|
+
module_path=child_module_path, import_root=import_root
|
|
978
|
+
)
|
|
979
|
+
)
|
|
980
|
+
continue
|
|
981
|
+
discovered.extend(
|
|
982
|
+
_discover_leaf_apps_from_import_root(
|
|
983
|
+
child_module_path, child, import_root=import_root
|
|
984
|
+
)
|
|
985
|
+
)
|
|
986
|
+
return tuple(discovered)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _derive_import_root(package_root: Path, module_path: str) -> Path | None:
|
|
990
|
+
parts = [
|
|
991
|
+
segment for segment in str(module_path or "").strip().split(".") if segment
|
|
992
|
+
]
|
|
993
|
+
if not parts:
|
|
994
|
+
return None
|
|
995
|
+
try:
|
|
996
|
+
return package_root.parents[len(parts) - 1].resolve()
|
|
997
|
+
except IndexError:
|
|
998
|
+
return None
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _is_physical_project_source_path(project_dir: Path, package_root: Path) -> bool:
|
|
1002
|
+
try:
|
|
1003
|
+
relative = package_root.resolve().relative_to(project_dir.resolve())
|
|
1004
|
+
except (OSError, ValueError):
|
|
1005
|
+
return False
|
|
1006
|
+
return ".sources" in relative.parts
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def discover_declared_app_references(
|
|
1010
|
+
declared_apps: Iterable[str],
|
|
1011
|
+
*,
|
|
1012
|
+
project_dir: str | Path | None = None,
|
|
1013
|
+
) -> tuple[DiscoveredAppReference, ...]:
|
|
1014
|
+
resolved_project_dir = _read_project_dir(project_dir)
|
|
1015
|
+
visible_projections = (
|
|
1016
|
+
()
|
|
1017
|
+
if resolved_project_dir is None
|
|
1018
|
+
else iter_visible_app_projections(resolved_project_dir)
|
|
1019
|
+
)
|
|
1020
|
+
discovered: list[DiscoveredAppReference] = []
|
|
1021
|
+
seen_paths: set[str] = set()
|
|
1022
|
+
|
|
1023
|
+
for raw_item in declared_apps:
|
|
1024
|
+
normalized = _validate_declared_app_token(
|
|
1025
|
+
str(raw_item or "").strip(),
|
|
1026
|
+
action="discover_declared_app_references",
|
|
1027
|
+
)
|
|
1028
|
+
if not normalized:
|
|
1029
|
+
continue
|
|
1030
|
+
|
|
1031
|
+
refs: tuple[DiscoveredAppReference, ...] = ()
|
|
1032
|
+
project_local_candidate_exists = False
|
|
1033
|
+
if resolved_project_dir is not None:
|
|
1034
|
+
project_local_candidate_exists = _has_project_local_app_candidate(
|
|
1035
|
+
resolved_project_dir,
|
|
1036
|
+
normalized,
|
|
1037
|
+
visible_projections,
|
|
1038
|
+
)
|
|
1039
|
+
refs = _discover_project_local_app_references(
|
|
1040
|
+
resolved_project_dir,
|
|
1041
|
+
normalized,
|
|
1042
|
+
visible_projections,
|
|
1043
|
+
)
|
|
1044
|
+
if not refs and not project_local_candidate_exists:
|
|
1045
|
+
package_root = resolve_package_root(normalized)
|
|
1046
|
+
if (
|
|
1047
|
+
package_root is not None
|
|
1048
|
+
and resolved_project_dir is not None
|
|
1049
|
+
and _is_physical_project_source_path(
|
|
1050
|
+
resolved_project_dir,
|
|
1051
|
+
package_root,
|
|
1052
|
+
)
|
|
1053
|
+
):
|
|
1054
|
+
project_local_candidate_exists = True
|
|
1055
|
+
elif package_root is not None:
|
|
1056
|
+
refs = _discover_leaf_apps_from_import_root(
|
|
1057
|
+
normalized,
|
|
1058
|
+
package_root,
|
|
1059
|
+
import_root=_derive_import_root(package_root, normalized),
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
if not refs and not project_local_candidate_exists:
|
|
1063
|
+
refs = (DiscoveredAppReference(module_path=normalized),)
|
|
1064
|
+
|
|
1065
|
+
for ref in refs:
|
|
1066
|
+
if ref.module_path in seen_paths:
|
|
1067
|
+
continue
|
|
1068
|
+
seen_paths.add(ref.module_path)
|
|
1069
|
+
discovered.append(ref)
|
|
1070
|
+
|
|
1071
|
+
return tuple(discovered)
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def resolve_import_roots_for_app_references(
|
|
1075
|
+
references: Iterable[DiscoveredAppReference],
|
|
1076
|
+
) -> tuple[Path, ...]:
|
|
1077
|
+
seen_roots: set[Path] = set()
|
|
1078
|
+
import_roots: list[Path] = []
|
|
1079
|
+
for ref in references:
|
|
1080
|
+
if ref.import_root is None:
|
|
1081
|
+
continue
|
|
1082
|
+
_append_import_root(
|
|
1083
|
+
ref.import_root, import_roots=import_roots, seen_roots=seen_roots
|
|
1084
|
+
)
|
|
1085
|
+
for dependency_root in _resolve_local_path_dependency_import_roots(
|
|
1086
|
+
ref.import_root
|
|
1087
|
+
):
|
|
1088
|
+
_append_import_root(
|
|
1089
|
+
dependency_root, import_roots=import_roots, seen_roots=seen_roots
|
|
1090
|
+
)
|
|
1091
|
+
return tuple(import_roots)
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def resolve_declared_app_import_roots(
|
|
1095
|
+
declared_apps: Iterable[str],
|
|
1096
|
+
*,
|
|
1097
|
+
project_dir: str | Path | None = None,
|
|
1098
|
+
) -> tuple[Path, ...]:
|
|
1099
|
+
return resolve_import_roots_for_app_references(
|
|
1100
|
+
discover_declared_app_references(declared_apps, project_dir=project_dir)
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def _append_import_root(
|
|
1105
|
+
root: Path, *, import_roots: list[Path], seen_roots: set[Path]
|
|
1106
|
+
) -> None:
|
|
1107
|
+
resolved = root.resolve()
|
|
1108
|
+
if resolved in seen_roots:
|
|
1109
|
+
return
|
|
1110
|
+
seen_roots.add(resolved)
|
|
1111
|
+
import_roots.append(resolved)
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
def _resolve_local_path_dependency_import_roots(import_root: Path) -> tuple[Path, ...]:
|
|
1115
|
+
roots: list[Path] = []
|
|
1116
|
+
seen_roots: set[Path] = {import_root.resolve()}
|
|
1117
|
+
|
|
1118
|
+
def visit(package_root: Path) -> None:
|
|
1119
|
+
source_paths = _local_uv_source_paths(package_root)
|
|
1120
|
+
if not source_paths:
|
|
1121
|
+
return
|
|
1122
|
+
for dependency_name in _declared_dependency_names(package_root):
|
|
1123
|
+
dependency_root = source_paths.get(dependency_name)
|
|
1124
|
+
if dependency_root is None:
|
|
1125
|
+
continue
|
|
1126
|
+
resolved = dependency_root.resolve()
|
|
1127
|
+
if resolved in seen_roots:
|
|
1128
|
+
continue
|
|
1129
|
+
seen_roots.add(resolved)
|
|
1130
|
+
roots.append(resolved)
|
|
1131
|
+
visit(resolved)
|
|
1132
|
+
|
|
1133
|
+
visit(import_root.resolve())
|
|
1134
|
+
return tuple(roots)
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def _declared_dependency_names(package_root: Path) -> tuple[str, ...]:
|
|
1138
|
+
payload = _read_pyproject_payload(package_root / "pyproject.toml")
|
|
1139
|
+
project = payload.get("project")
|
|
1140
|
+
if not isinstance(project, dict):
|
|
1141
|
+
return ()
|
|
1142
|
+
dependencies = project.get("dependencies")
|
|
1143
|
+
if not isinstance(dependencies, list):
|
|
1144
|
+
return ()
|
|
1145
|
+
names: list[str] = []
|
|
1146
|
+
for raw_dependency in dependencies:
|
|
1147
|
+
if not isinstance(raw_dependency, str):
|
|
1148
|
+
continue
|
|
1149
|
+
name = _dependency_distribution_name(raw_dependency)
|
|
1150
|
+
if name and name not in names:
|
|
1151
|
+
names.append(name)
|
|
1152
|
+
return tuple(names)
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def _dependency_distribution_name(value: str) -> str:
|
|
1156
|
+
match = _DEPENDENCY_NAME_RE.match(value)
|
|
1157
|
+
if match is None:
|
|
1158
|
+
return ""
|
|
1159
|
+
return _normalize_distribution_name(match.group(1))
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def _local_uv_source_paths(package_root: Path) -> dict[str, Path]:
|
|
1163
|
+
mapped: dict[str, Path] = {}
|
|
1164
|
+
for candidate in (package_root, *package_root.parents):
|
|
1165
|
+
pyproject_path = candidate / "pyproject.toml"
|
|
1166
|
+
payload = _read_pyproject_payload(pyproject_path)
|
|
1167
|
+
if not payload:
|
|
1168
|
+
continue
|
|
1169
|
+
tool = payload.get("tool")
|
|
1170
|
+
if not isinstance(tool, dict):
|
|
1171
|
+
continue
|
|
1172
|
+
uv = tool.get("uv")
|
|
1173
|
+
if not isinstance(uv, dict):
|
|
1174
|
+
continue
|
|
1175
|
+
sources = uv.get("sources")
|
|
1176
|
+
if not isinstance(sources, dict):
|
|
1177
|
+
continue
|
|
1178
|
+
for raw_name, raw_meta in sources.items():
|
|
1179
|
+
name = _normalize_distribution_name(str(raw_name))
|
|
1180
|
+
if not name or name in mapped:
|
|
1181
|
+
continue
|
|
1182
|
+
raw_path = ""
|
|
1183
|
+
if isinstance(raw_meta, dict):
|
|
1184
|
+
raw_path = str(raw_meta.get("path") or "").strip()
|
|
1185
|
+
elif isinstance(raw_meta, str):
|
|
1186
|
+
raw_path = raw_meta.strip()
|
|
1187
|
+
if not raw_path:
|
|
1188
|
+
continue
|
|
1189
|
+
source_path = Path(raw_path).expanduser()
|
|
1190
|
+
if not source_path.is_absolute():
|
|
1191
|
+
source_path = pyproject_path.parent / source_path
|
|
1192
|
+
if source_path.is_dir():
|
|
1193
|
+
mapped[name] = source_path.resolve()
|
|
1194
|
+
return mapped
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _read_pyproject_payload(path: Path) -> dict[str, Any]:
|
|
1198
|
+
if not path.is_file():
|
|
1199
|
+
return {}
|
|
1200
|
+
try:
|
|
1201
|
+
payload = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
1202
|
+
except Exception:
|
|
1203
|
+
return {}
|
|
1204
|
+
return payload if isinstance(payload, dict) else {}
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def discover_leaf_app_packages(
|
|
1208
|
+
module_path: str,
|
|
1209
|
+
*,
|
|
1210
|
+
project_dir: str | Path | None = None,
|
|
1211
|
+
) -> tuple[str, ...]:
|
|
1212
|
+
normalized = _validate_declared_app_token(
|
|
1213
|
+
str(module_path or "").strip(),
|
|
1214
|
+
action="discover_leaf_app_packages",
|
|
1215
|
+
)
|
|
1216
|
+
if not normalized:
|
|
1217
|
+
return ()
|
|
1218
|
+
return tuple(
|
|
1219
|
+
ref.module_path
|
|
1220
|
+
for ref in discover_declared_app_references(
|
|
1221
|
+
(normalized,), project_dir=project_dir
|
|
1222
|
+
)
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
__all__ = [
|
|
1227
|
+
"DiscoveredAppReference",
|
|
1228
|
+
"StaticAppDeclaration",
|
|
1229
|
+
"VisibleAppProjection",
|
|
1230
|
+
"discover_declared_app_references",
|
|
1231
|
+
"discover_leaf_app_packages",
|
|
1232
|
+
"import_app_entrypoint_module_from_decl",
|
|
1233
|
+
"import_app_settings_module",
|
|
1234
|
+
"iter_visible_app_projections",
|
|
1235
|
+
"read_static_app_declaration",
|
|
1236
|
+
"resolve_declared_app_import_roots",
|
|
1237
|
+
"resolve_declared_app_id",
|
|
1238
|
+
"resolve_import_roots_for_app_references",
|
|
1239
|
+
"resolve_package_root",
|
|
1240
|
+
]
|