diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 3d1f67c..950f45d 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -5,14 +5,14 @@
},
"metadata": {
"description": "Orchestrator skill for RHDH plugin development - onboard, update, and maintain plugins in the Extensions Catalog",
- "version": "0.5.0"
+ "version": "0.5.1"
},
"plugins": [
{
"name": "rhdh",
"source": "./",
"description": "Skills for RHDH plugin lifecycle management",
- "version": "0.5.0",
+ "version": "0.5.1",
"strict": true
}
]
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 758136d..33826cb 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "rhdh",
"description": "All-in-one toolkit for Red Hat Developer Hub (RHDH). Covers plugin development, overlay management, environment setup, version compatibility, CI/CD, and RHDH ecosystem navigation.",
- "version": "0.5.0",
+ "version": "0.5.1",
"author": {
"name": "RHDH Store Manager"
},
diff --git a/README.md b/README.md
index 940b349..e4b1fa7 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,29 @@ Build dynamic plugins from scratch — backend or frontend — and get them depl
- **[export](./skills/create-plugin/references/export.md)** — Export, package (OCI/tgz/npm), and push to a container registry.
- **[wiring](./skills/create-plugin/references/wiring.md)** — Analyze plugin source and generate `dynamic-plugins.yaml` wiring config.
+### Software Templates
+
+Author RHDH Scaffolder templates with guided workflows — templatize existing codebases, create from scratch, and fix common gotchas.
+
+- **[rhdh-templates](./skills/rhdh-templates/SKILL.md)** — Interactive authoring and validation for Software Templates. Includes curated reference catalog (official library + AI quickstarts), worked examples (`nodejs-backend`, `java-springboot`) and bundled JSON Schema validation. Sub-commands:
+ - **[init](./skills/rhdh-templates/references/init.md)** — Check tooling, scaffold template repo layout, optional RHDH connectivity.
+ - **[templatize](./skills/rhdh-templates/references/templatize.md)** — Convert existing codebase into a parameterized template.
+ - **[create](./skills/rhdh-templates/references/create.md)** — Guided from-scratch template authoring when no reference code exists.
+ - **[add-parameter](./skills/rhdh-templates/references/add-parameter.md)** — Add a parameter or parameter group to existing `template.yaml`.
+ - **[add-step](./skills/rhdh-templates/references/add-step.md)** — Add a scaffolder step to existing `template.yaml`.
+ - **[add-skeleton](./skills/rhdh-templates/references/add-skeleton.md)** — Add or parameterize skeleton files with Nunjucks.
+ - **[create-location](./skills/rhdh-templates/references/create-location.md)** — Generate or update root `location.yaml` for catalog registration.
+ - **[fix-gotchas](./skills/rhdh-templates/references/fix-gotchas.md)** — Auto-fix common RHDH template mistakes (raw/endraw blocks, catalog-info path, etc.).
+ - **[validate](./skills/rhdh-templates/references/validate.md)** — Local YAML schema, gotcha validation, and optional Nunjucks lint via `--lint-skeleton` (no RHDH required).
+ - **[list-actions](./skills/rhdh-templates/references/list-actions.md)** — List available Scaffolder actions from a running RHDH instance.
+ - **[dry-run](./skills/rhdh-templates/references/dry-run.md)** — Test template execution via Scaffolder v2 dry-run API.
+ - **[explain-action](./skills/rhdh-templates/references/explain-action.md)** — Show action input schema or template parameter schema.
+ - **[example-catalog](./skills/rhdh-templates/references/example-catalog.md)** — Browse curated reference templates (official library, AI quickstarts, bundled).
+
+```bash
+npx skills add redhat-developer/rhdh-skill --skill rhdh-templates
+```
+
### Extensions Catalog
Manage plugins in the [rhdh-plugin-export-overlays](https://github.com/redhat-developer/rhdh-plugin-export-overlays) repository.
diff --git a/pyproject.toml b/pyproject.toml
index bad9a86..726c0d1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "rhdh-skill"
-version = "0.5.0"
+version = "0.5.1"
description = "Claude Code skill for RHDH plugin development"
readme = "README.md"
license = "Apache-2.0"
@@ -15,7 +15,8 @@ include = ["rhdh*"]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
- "pyyaml>=6.0", # Only for SKILL.md structure tests
+ "pyyaml>=6.0", # SKILL.md structure tests and template YAML parsing
+ "jsonschema>=4.0", # Optional full JSON Schema validation in rhdh-templates
"ruff>=0.4.0",
]
diff --git a/skills/rhdh-templates/SKILL.md b/skills/rhdh-templates/SKILL.md
new file mode 100644
index 0000000..d77cbe6
--- /dev/null
+++ b/skills/rhdh-templates/SKILL.md
@@ -0,0 +1,139 @@
+---
+name: rhdh-templates
+description: >-
+ Author and validate RHDH Software Templates (Scaffolder) with AI-guided workflows. Use when
+ asked to "create software template", "templatize a codebase", "convert repo to
+ template", "write template.yaml", "location.yaml for templates", "scaffolder
+ template", "golden path template", "parameterize skeleton files", "fix template
+ gotchas", "validate template", "dry-run template", "list scaffolder actions",
+ "explain scaffolder action", "Nunjucks in template", "template best practices",
+ "reference templates", "example templates", "what templates do customers use",
+ "Template Editor", or mentions RHDH template
+ authoring, Software Catalog templates, or /rhdh-templates commands. Covers setup,
+ templatize (highest value), from-scratch create, reference example discovery,
+ incremental parameter/step/skeleton authoring, location.yaml generation, common
+ convention fixes, local validation, and live Scaffolder API dry-run/action discovery.
+---
+
+
+
+## Domain
+
+Software Templates are `kind: Template` entities processed by the Scaffolder. Each template has:
+
+- `template.yaml` — metadata, `spec.parameters` (form), `spec.steps` (actions), `spec.output`
+- `skeleton/` — files copied/templated into the target repo (Nunjucks `{% raw %}` blocks where needed)
+- `location.yaml` (repo root) — `kind: Location` registering all `template.yaml` files for catalog import
+
+Read `references/conventions.md` before editing any template artifact — it encodes RHDH-specific rules that differ from generic Backstage docs.
+
+Read `references/best-practices.md` when authoring or reviewing templates — it encodes Red Hat's 10 tips for repository layout, Template Editor workflow, custom fields, Nunjucks, secrets, type/tags, TechDocs, and maintenance.
+
+## Authoring stance
+
+- **Interactive, not fully automatic.** Templatize proposes parameterization; the user confirms each literal-to-parameter mapping.
+- **Conservative parameterization.** Under-parameterize rather than expose every string — users can add parameters incrementally.
+- **First-try correctness.** Generated artifacts should pass local `validate` with zero critical findings before merge.
+
+## Script paths
+
+All `scripts/` and `references/` paths are relative to this SKILL.md directory. Resolve them before invoking.
+
+
+
+
+
+## Setup gates (non-optional before file edits)
+
+| Gate | Required check | If fail |
+|------|----------------|---------|
+| Command | Sub-command reference loaded | Load the matching `references/.md` (or `example-catalog.md` for `examples`) |
+| Layout | Template project initialized or path confirmed | Run `init` or ask user for template repo root |
+| Conventions | `references/conventions.md` read for authoring commands | Read it first |
+
+
+
+
+
+## What would you like to do?
+
+| # | Command |
+|---|---------|
+| 1 | `init` |
+| 2 | `templatize` |
+| 3 | `create` |
+| 4 | `add-parameter` |
+| 5 | `add-step` |
+| 6 | `add-skeleton` |
+| 7 | `create-location` |
+| 8 | `fix-gotchas` |
+| 9 | `validate` |
+| 10 | `list-actions` |
+| 11 | `dry-run` |
+| 12 | `explain-action` |
+| 13 | `examples` |
+
+Command descriptions and argument hints: `scripts/command-metadata.json`
+
+**Wait for response before proceeding.**
+
+
+
+
+
+| Response | Reference |
+|----------|-----------|
+| 1, "init", "setup", "scaffold", "prerequisites" | [references/init.md](references/init.md) |
+| 2, "templatize", "convert", "parameterize repo", "existing codebase" | [references/templatize.md](references/templatize.md) |
+| 3, "create", "from scratch", "new template" | [references/create.md](references/create.md) |
+| 4, "add-parameter", "add parameter", "form field" | [references/add-parameter.md](references/add-parameter.md) |
+| 5, "add-step", "add step", "scaffolder action", "pipeline step" | [references/add-step.md](references/add-step.md) |
+| 6, "add-skeleton", "skeleton file", "nunjucks" | [references/add-skeleton.md](references/add-skeleton.md) |
+| 7, "create-location", "location.yaml", "register templates" | [references/create-location.md](references/create-location.md) |
+| 8, "fix-gotchas", "fix template", "gotchas", "raw endraw" | [references/fix-gotchas.md](references/fix-gotchas.md) |
+| 9, "validate", "lint template", "check template", "lint-nunjucks", "lint nunjucks", "djlint", "nunjucks lint" | [references/validate.md](references/validate.md) |
+| 10, "list-actions", "list actions", "scaffolder actions" | [references/list-actions.md](references/list-actions.md) |
+| 11, "dry-run", "dry run", "test template remotely" | [references/dry-run.md](references/dry-run.md) |
+| 12, "explain-action", "action schema", "parameter schema" | [references/explain-action.md](references/explain-action.md) |
+| 13, "examples", "reference templates", "show me templates", "what templates exist" | [references/example-catalog.md](references/example-catalog.md) |
+| First word doesn't match | Infer from context. "Turn my Spring Boot app into a template" → `templatize`. "Add owner picker to my template" → `add-parameter`. "Does my template validate?" → `validate`. "What templates do customers use?" → `examples`. |
+
+
+
+
+
+## Bundled scripts
+
+```bash
+# Resolve skill directory (adjust if SKILL.md path differs)
+SKILL_DIR="/skills/rhdh-templates"
+
+python "$SKILL_DIR/scripts/init.py" --help
+python "$SKILL_DIR/scripts/analyze.py" --help
+python "$SKILL_DIR/scripts/create_location.py" --help
+python "$SKILL_DIR/scripts/fix_gotchas.py" --help
+python "$SKILL_DIR/scripts/validate.py" --help
+python "$SKILL_DIR/scripts/list_actions.py" --help
+python "$SKILL_DIR/scripts/dry_run.py" --help
+python "$SKILL_DIR/scripts/explain_action.py" --help
+python "$SKILL_DIR/scripts/list_examples.py" --help
+```
+
+Run `init.py` for deterministic tooling checks and project scaffolding. Use `analyze.py` during `templatize` Phase 1. Use `list_examples.py` during `create`, `templatize`, or `examples` to rank upstream reference templates. Use `create_location.py` and `fix_gotchas.py` where the reference files direct you — they produce structured JSON when piped.
+
+Validation scripts: `validate.py` for local checks (include `--lint-skeleton` for Nunjucks/djLint); `list_actions.py`, `dry_run.py`, and `explain_action.py` require a reachable RHDH `--rhdh-url` and optional bearer token (`RHDH_TOKEN` env or `--token`).
+
+
+
+
+
+| File | Load when... |
+|------|-------------|
+| `references/conventions.md` | Any authoring command — RHDH template rules |
+| `references/best-practices.md` | Authoring/review — Red Hat 10 tips and pre-merge checklist |
+| `references/template-structure.md` | Writing or reviewing `template.yaml` anatomy |
+| `references/parameter-widgets.md` | Choosing form fields and UI widgets for parameters |
+| `references/example-catalog.md` | Command `examples` or picking upstream study references (`assets/example-catalog.json` is the data source; bundled templates under `assets/examples/`) |
+| `references/schemas/template-v1beta3.schema.json` | Bundled JSON Schema for deep `validate` checks |
+
+
diff --git a/skills/rhdh-templates/assets/example-catalog.json b/skills/rhdh-templates/assets/example-catalog.json
new file mode 100644
index 0000000..e9b4615
--- /dev/null
+++ b/skills/rhdh-templates/assets/example-catalog.json
@@ -0,0 +1,386 @@
+{
+ "version": 1,
+ "disclaimer": "Upstream templates are learning aids, not production-ready. Validate, test, and customize before use in your organization.",
+ "sources": [
+ {
+ "id": "rhdh-software-templates",
+ "repo": "redhat-developer/red-hat-developer-hub-software-templates",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates",
+ "description": "Official RHDH Software Templates library (registered via templates.yaml)",
+ "official": true
+ },
+ {
+ "id": "ai-quickstart-templates",
+ "repo": "redhat-developer/aiquickstarttemplates",
+ "url": "https://github.com/redhat-developer/aiquickstarttemplates",
+ "description": "Red Hat AI Quickstart Backstage templates for OpenShift AI",
+ "official": true
+ },
+ {
+ "id": "ai-lab-template",
+ "repo": "redhat-ai-dev/ai-lab-template",
+ "url": "https://github.com/redhat-ai-dev/ai-lab-template",
+ "description": "Podman Desktop AI Lab sample templates (RAG, chatbot, codegen, etc.)",
+ "official": false
+ },
+ {
+ "id": "bundled",
+ "repo": "redhat-developer/rhdh-skill",
+ "url": "https://github.com/redhat-developer/rhdh-skill/tree/main/skills/rhdh-templates/assets/examples",
+ "description": "Minimal worked examples shipped with the rhdh-templates skill for local validation",
+ "official": true
+ }
+ ],
+ "examples": [
+ {
+ "id": "go-backend",
+ "title": "Create a Go Backend application with a CI pipeline",
+ "source": "rhdh-software-templates",
+ "path": "templates/github/go-backend",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/github/go-backend",
+ "category": "backend",
+ "tags": ["recommended", "go", "backend", "ci", "github"],
+ "stack": ["go"],
+ "use_cases": ["golden path backend service", "new microservice with pipeline"],
+ "recommended": true,
+ "in_templates_yaml": true,
+ "local_bundled": null
+ },
+ {
+ "id": "nodejs-backend",
+ "title": "Create a Node.js Backend application with a CI pipeline",
+ "source": "rhdh-software-templates",
+ "path": "templates/github/nodejs-backend",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/github/nodejs-backend",
+ "category": "backend",
+ "tags": ["recommended", "nodejs", "javascript", "typescript", "express", "backend", "ci", "github"],
+ "stack": ["nodejs"],
+ "use_cases": ["nodejs api service", "express backend with publish and register"],
+ "recommended": true,
+ "in_templates_yaml": true,
+ "local_bundled": "nodejs-backend"
+ },
+ {
+ "id": "spring-boot-backend",
+ "title": "Create a Spring Boot Backend application with a CI pipeline",
+ "source": "rhdh-software-templates",
+ "path": "templates/github/spring-boot-backend",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/github/spring-boot-backend",
+ "category": "backend",
+ "tags": ["recommended", "spring-boot", "java", "maven", "backend", "ci", "github"],
+ "stack": ["java", "spring-boot"],
+ "use_cases": ["java spring boot service", "maven backend golden path"],
+ "recommended": true,
+ "in_templates_yaml": true,
+ "local_bundled": "java-springboot"
+ },
+ {
+ "id": "quarkus-backend",
+ "title": "Create a Quarkus Backend application with a CI pipeline",
+ "source": "rhdh-software-templates",
+ "path": "templates/github/quarkus-backend",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/github/quarkus-backend",
+ "category": "backend",
+ "tags": ["quarkus", "java", "maven", "backend", "ci", "github"],
+ "stack": ["java", "quarkus"],
+ "use_cases": ["quarkus microservice", "cloud-native java backend"],
+ "recommended": false,
+ "in_templates_yaml": true,
+ "local_bundled": null
+ },
+ {
+ "id": "python-backend-gitlab",
+ "title": "Create a Python backend application in GitLab with a CI pipeline",
+ "source": "rhdh-software-templates",
+ "path": "templates/gitlab/python-backend",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/gitlab/python-backend",
+ "category": "backend",
+ "tags": ["python", "flask", "backend", "ci", "gitlab"],
+ "stack": ["python"],
+ "use_cases": ["python api on gitlab", "flask backend service"],
+ "recommended": false,
+ "in_templates_yaml": true,
+ "local_bundled": null
+ },
+ {
+ "id": "dotnet-frontend",
+ "title": "Create a .NET Frontend application with a CI pipeline",
+ "source": "rhdh-software-templates",
+ "path": "templates/azure/dotnet-frontend",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/azure/dotnet-frontend",
+ "category": "frontend",
+ "tags": ["dotnet", "csharp", "frontend", "ci", "azure"],
+ "stack": ["dotnet"],
+ "use_cases": ["dotnet web frontend", "azure devops pipeline"],
+ "recommended": false,
+ "in_templates_yaml": true,
+ "local_bundled": null
+ },
+ {
+ "id": "register-component",
+ "title": "Register existing component to Software Catalog",
+ "source": "rhdh-software-templates",
+ "path": "templates/github/register-component",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/github/register-component",
+ "category": "catalog",
+ "tags": ["recommended", "import", "catalog", "register", "github"],
+ "stack": [],
+ "use_cases": ["import existing repo", "onboard legacy service to catalog"],
+ "recommended": true,
+ "in_templates_yaml": true,
+ "local_bundled": null
+ },
+ {
+ "id": "tekton",
+ "title": "Create a Tekton CI Pipeline",
+ "source": "rhdh-software-templates",
+ "path": "templates/github/tekton",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/github/tekton",
+ "category": "cicd",
+ "tags": ["tekton", "ci", "openshift", "pipeline"],
+ "stack": ["tekton"],
+ "use_cases": ["add tekton pipeline to project", "openshift ci"],
+ "recommended": false,
+ "in_templates_yaml": true,
+ "local_bundled": null
+ },
+ {
+ "id": "argocd",
+ "title": "Add ArgoCD to an existing project",
+ "source": "rhdh-software-templates",
+ "path": "templates/github/argocd",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/github/argocd",
+ "category": "gitops",
+ "tags": ["recommended", "argocd", "gitops", "kubernetes"],
+ "stack": ["argocd"],
+ "use_cases": ["gitops bootstrap", "add argocd app to repo"],
+ "recommended": true,
+ "in_templates_yaml": true,
+ "local_bundled": null
+ },
+ {
+ "id": "define-ansible-job",
+ "title": "Define an Ansible Job Template within Ansible Automation Platform",
+ "source": "rhdh-software-templates",
+ "path": "templates/github/define-ansible-job",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/github/define-ansible-job",
+ "category": "automation",
+ "tags": ["recommended", "ansible", "aap", "automation"],
+ "stack": ["ansible"],
+ "use_cases": ["ansible job template", "aap integration"],
+ "recommended": true,
+ "in_templates_yaml": true,
+ "local_bundled": null
+ },
+ {
+ "id": "launch-ansible-job",
+ "title": "Launch an Ansible Job within Ansible Automation Platform",
+ "source": "rhdh-software-templates",
+ "path": "templates/github/launch-ansible-job",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/github/launch-ansible-job",
+ "category": "automation",
+ "tags": ["ansible", "aap", "automation"],
+ "stack": ["ansible"],
+ "use_cases": ["run ansible job from template", "aap workflow trigger"],
+ "recommended": false,
+ "in_templates_yaml": true,
+ "local_bundled": null
+ },
+ {
+ "id": "techdocs",
+ "title": "Create a TechDocs sample",
+ "source": "rhdh-software-templates",
+ "path": "templates/github/techdocs",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/github/techdocs",
+ "category": "docs",
+ "tags": ["techdocs", "documentation", "mkdocs"],
+ "stack": ["techdocs"],
+ "use_cases": ["documentation starter", "techdocs onboarding"],
+ "recommended": false,
+ "in_templates_yaml": true,
+ "local_bundled": null
+ },
+ {
+ "id": "create-backend-plugin",
+ "title": "Create Backend Plugin Template",
+ "source": "rhdh-software-templates",
+ "path": "templates/create-backend-plugin",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/create-backend-plugin",
+ "category": "plugin",
+ "tags": ["backend-plugin", "backstage-plugin", "dynamic-plugin"],
+ "stack": ["typescript"],
+ "use_cases": ["scaffold rhdh backend plugin", "plugin golden path"],
+ "recommended": false,
+ "in_templates_yaml": true,
+ "local_bundled": null
+ },
+ {
+ "id": "create-frontend-plugin",
+ "title": "Create Frontend Plugin Template",
+ "source": "rhdh-software-templates",
+ "path": "templates/create-frontend-plugin",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/create-frontend-plugin",
+ "category": "plugin",
+ "tags": ["frontend-plugin", "backstage-plugin", "dynamic-plugin"],
+ "stack": ["typescript", "react"],
+ "use_cases": ["scaffold rhdh frontend plugin", "plugin golden path"],
+ "recommended": false,
+ "in_templates_yaml": true,
+ "local_bundled": null
+ },
+ {
+ "id": "obc",
+ "title": "Add OBC to an existing project",
+ "source": "rhdh-software-templates",
+ "path": "templates/github/obc",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/github/obc",
+ "category": "infrastructure",
+ "tags": ["obc", "openshift", "storage"],
+ "stack": ["openshift"],
+ "use_cases": ["object bucket claim", "openshift storage"],
+ "recommended": false,
+ "in_templates_yaml": false,
+ "local_bundled": null
+ },
+ {
+ "id": "sdlc-app",
+ "title": "SDLC environments Application",
+ "source": "rhdh-software-templates",
+ "path": "templates/github/sdlc-app",
+ "url": "https://github.com/redhat-developer/red-hat-developer-hub-software-templates/tree/main/templates/github/sdlc-app",
+ "category": "application",
+ "tags": ["application", "sdlc", "environments"],
+ "stack": [],
+ "use_cases": ["multi-component application", "sdlc environment setup"],
+ "recommended": false,
+ "in_templates_yaml": false,
+ "local_bundled": null
+ },
+ {
+ "id": "enterprise-rag-chatbot",
+ "title": "Enterprise RAG Chatbot",
+ "source": "ai-quickstart-templates",
+ "path": "templates/enterprise-rag-chatbot",
+ "url": "https://github.com/redhat-developer/aiquickstarttemplates/tree/main/templates/enterprise-rag-chatbot",
+ "category": "ai",
+ "tags": ["ai", "rag", "llm", "chatbot", "vector-db", "openshift-ai"],
+ "stack": ["python", "ai", "rag"],
+ "use_cases": ["internal knowledge base", "document q&a chatbot", "enterprise rag"],
+ "recommended": true,
+ "in_templates_yaml": false,
+ "local_bundled": null
+ },
+ {
+ "id": "it-self-service-agent",
+ "title": "IT Self-Service Agent",
+ "source": "ai-quickstart-templates",
+ "path": "templates/it-self-service-agent",
+ "url": "https://github.com/redhat-developer/aiquickstarttemplates/tree/main/templates/it-self-service-agent",
+ "category": "ai",
+ "tags": ["ai", "agent", "llm", "automation", "openshift-ai"],
+ "stack": ["python", "ai", "agent"],
+ "use_cases": ["it automation agent", "self-service helpdesk bot"],
+ "recommended": true,
+ "in_templates_yaml": false,
+ "local_bundled": null
+ },
+ {
+ "id": "llm-cpu-serving",
+ "title": "LLM CPU Serving",
+ "source": "ai-quickstart-templates",
+ "path": "templates/llm-cpu-serving",
+ "url": "https://github.com/redhat-developer/aiquickstarttemplates/tree/main/templates/llm-cpu-serving",
+ "category": "ai",
+ "tags": ["ai", "llm", "cpu", "inference", "openshift-ai"],
+ "stack": ["python", "ai", "llm"],
+ "use_cases": ["lightweight llm endpoint", "dev/test model serving"],
+ "recommended": false,
+ "in_templates_yaml": false,
+ "local_bundled": null
+ },
+ {
+ "id": "generic-ai-quickstart",
+ "title": "Generic AI Quickstart",
+ "source": "ai-quickstart-templates",
+ "path": "templates/generic-ai-quickstart",
+ "url": "https://github.com/redhat-developer/aiquickstarttemplates/tree/main/templates/generic-ai-quickstart",
+ "category": "ai",
+ "tags": ["ai", "quickstart", "openshift-ai", "flexible"],
+ "stack": ["python", "ai"],
+ "use_cases": ["custom ai project bootstrap", "adaptable ai template"],
+ "recommended": false,
+ "in_templates_yaml": false,
+ "local_bundled": null
+ },
+ {
+ "id": "ai-virtual-agent",
+ "title": "AI Virtual Agent",
+ "source": "ai-quickstart-templates",
+ "path": "templates/ai-virtual-agent",
+ "url": "https://github.com/redhat-developer/aiquickstarttemplates/tree/main/templates/ai-virtual-agent",
+ "category": "ai",
+ "tags": ["ai", "agent", "virtual-agent", "openshift-ai"],
+ "stack": ["python", "ai", "agent"],
+ "use_cases": ["virtual assistant deployment", "agentic ai application"],
+ "recommended": false,
+ "in_templates_yaml": false,
+ "local_bundled": null
+ },
+ {
+ "id": "ai-lab-rag",
+ "title": "RAG Chatbot Application",
+ "source": "ai-lab-template",
+ "path": "templates/rag",
+ "url": "https://github.com/redhat-ai-dev/ai-lab-template/tree/main/templates/rag",
+ "category": "ai",
+ "tags": ["ai", "rag", "chatbot", "llm"],
+ "stack": ["python", "ai", "rag"],
+ "use_cases": ["rag chatbot sample", "ai lab starter"],
+ "recommended": false,
+ "in_templates_yaml": false,
+ "local_bundled": null
+ },
+ {
+ "id": "ai-lab-chatbot",
+ "title": "Chatbot Application",
+ "source": "ai-lab-template",
+ "path": "templates/chatbot",
+ "url": "https://github.com/redhat-ai-dev/ai-lab-template/tree/main/templates/chatbot",
+ "category": "ai",
+ "tags": ["ai", "chatbot", "llm"],
+ "stack": ["python", "ai"],
+ "use_cases": ["simple chatbot sample", "ai lab starter"],
+ "recommended": false,
+ "in_templates_yaml": false,
+ "local_bundled": null
+ },
+ {
+ "id": "ai-lab-codegen",
+ "title": "Codegen Application",
+ "source": "ai-lab-template",
+ "path": "templates/codegen",
+ "url": "https://github.com/redhat-ai-dev/ai-lab-template/tree/main/templates/codegen",
+ "category": "ai",
+ "tags": ["ai", "codegen", "llm"],
+ "stack": ["python", "ai"],
+ "use_cases": ["code generation sample", "developer assistant"],
+ "recommended": false,
+ "in_templates_yaml": false,
+ "local_bundled": null
+ },
+ {
+ "id": "minimal-template",
+ "title": "Minimal starter template (bundled)",
+ "source": "bundled",
+ "path": "assets/examples/minimal-template",
+ "url": "https://github.com/redhat-developer/rhdh-skill/tree/main/skills/rhdh-templates/assets/examples/minimal-template",
+ "category": "starter",
+ "tags": ["starter", "minimal", "validation"],
+ "stack": [],
+ "use_cases": ["learn template structure", "validate locally without network"],
+ "recommended": false,
+ "in_templates_yaml": false,
+ "local_bundled": "minimal-template"
+ }
+ ]
+}
diff --git a/skills/rhdh-templates/assets/examples/README.md b/skills/rhdh-templates/assets/examples/README.md
new file mode 100644
index 0000000..5d16bd7
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/README.md
@@ -0,0 +1,29 @@
+# RHDH Templates Examples
+
+Bundled worked examples for **local learning and validation**. Each passes `validate.py` with zero critical findings.
+
+For the full curated catalog of upstream reference templates (official library, AI quickstarts), run:
+
+```bash
+python skills/rhdh-templates/scripts/list_examples.py --recommended --json
+```
+
+See [../references/example-catalog.md](../references/example-catalog.md) for category guide and customer-demand context.
+
+| Example | Stack | Highlights |
+|---------|-------|------------|
+| [minimal-template](./minimal-template/) | Generic | Starter scaffold from `init` — single parameter form |
+| [nodejs-backend](./nodejs-backend/) | Node.js | `EntityPicker`, `RepoUrlPicker`, publish + register, GitHub Actions `{% raw %}` |
+| [java-springboot](./java-springboot/) | Java / Spring Boot | Maven `pom.xml`, `Application.java`, multi-section forms |
+
+These bundled examples correspond to upstream references in [red-hat-developer-hub-software-templates](https://github.com/redhat-developer/red-hat-developer-hub-software-templates): `nodejs-backend` → `templates/github/nodejs-backend`, `java-springboot` → `templates/github/spring-boot-backend`.
+
+Validate any example locally:
+
+```bash
+python skills/rhdh-templates/scripts/validate.py \
+ --path skills/rhdh-templates/assets/examples/nodejs-backend \
+ --repo --lint-skeleton --json
+```
+
+Replace `nodejs-backend` with `java-springboot` or `minimal-template` as needed.
diff --git a/skills/rhdh-templates/assets/examples/java-springboot/README.md b/skills/rhdh-templates/assets/examples/java-springboot/README.md
new file mode 100644
index 0000000..cb54c5a
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/java-springboot/README.md
@@ -0,0 +1,24 @@
+# Java Spring Boot Service
+
+Scaffolds a minimal Spring Boot 3 service with:
+
+- Maven `pom.xml` parameterized by component ID and Java version
+- `Application.java` and `application.properties`
+- `catalog-info.yaml` for Software Catalog registration
+
+## Parameters
+
+| Parameter | Purpose |
+|-----------|---------|
+| `componentId` | Catalog entity name and Maven artifact ID |
+| `description` | Shown in catalog and repository |
+| `owner` | Catalog owner entity ref |
+| `javaVersion` | Java LTS version (`17` or `21`) |
+| `packageName` | Java base package for generated sources |
+| `repoUrl` | Target GitHub repository |
+
+## Post-scaffold steps
+
+1. Run `./mvnw spring-boot:run` locally
+2. Confirm CI passes after first push
+3. Verify catalog registration in RHDH
diff --git a/skills/rhdh-templates/assets/examples/java-springboot/skeleton/README.md b/skills/rhdh-templates/assets/examples/java-springboot/skeleton/README.md
new file mode 100644
index 0000000..ad34bdd
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/java-springboot/skeleton/README.md
@@ -0,0 +1,13 @@
+# {{ values.componentId }}
+
+{{ values.description }}
+
+Owner: {{ values.owner }}
+
+## Development
+
+```bash
+./mvnw spring-boot:run
+```
+
+Uses Java {{ values.javaVersion }} and package `{{ values.packageName }}`.
diff --git a/skills/rhdh-templates/assets/examples/java-springboot/skeleton/catalog-info.yaml b/skills/rhdh-templates/assets/examples/java-springboot/skeleton/catalog-info.yaml
new file mode 100644
index 0000000..acea76f
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/java-springboot/skeleton/catalog-info.yaml
@@ -0,0 +1,9 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: {{ values.componentId }}
+ description: {{ values.description }}
+spec:
+ type: service
+ lifecycle: experimental
+ owner: {{ values.owner }}
diff --git a/skills/rhdh-templates/assets/examples/java-springboot/skeleton/pom.xml b/skills/rhdh-templates/assets/examples/java-springboot/skeleton/pom.xml
new file mode 100644
index 0000000..7946fc2
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/java-springboot/skeleton/pom.xml
@@ -0,0 +1,44 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.0
+
+
+
+ {{ values.packageName }}
+ {{ values.componentId }}
+ 0.0.1-SNAPSHOT
+ {{ values.componentId }}
+ {{ values.description }}
+
+
+ {{ values.javaVersion }}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/skills/rhdh-templates/assets/examples/java-springboot/skeleton/src/main/java/com/example/demo/Application.java b/skills/rhdh-templates/assets/examples/java-springboot/skeleton/src/main/java/com/example/demo/Application.java
new file mode 100644
index 0000000..4973964
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/java-springboot/skeleton/src/main/java/com/example/demo/Application.java
@@ -0,0 +1,22 @@
+package {{ values.packageName }};
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+ @RestController
+ static class HelloController {
+ @GetMapping("/")
+ public String hello() {
+ return "{{ values.componentId }} is running";
+ }
+ }
+}
diff --git a/skills/rhdh-templates/assets/examples/java-springboot/skeleton/src/main/resources/application.properties b/skills/rhdh-templates/assets/examples/java-springboot/skeleton/src/main/resources/application.properties
new file mode 100644
index 0000000..3a3779e
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/java-springboot/skeleton/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+spring.application.name={{ values.componentId }}
+server.port=8080
diff --git a/skills/rhdh-templates/assets/examples/java-springboot/template.yaml b/skills/rhdh-templates/assets/examples/java-springboot/template.yaml
new file mode 100644
index 0000000..1c98baa
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/java-springboot/template.yaml
@@ -0,0 +1,101 @@
+apiVersion: scaffolder.backstage.io/v1beta3
+kind: Template
+metadata:
+ name: java-springboot
+ title: Java Spring Boot Service
+ description: Scaffold a Spring Boot microservice with Maven, catalog registration, and GitHub publish
+ tags:
+ - recommended
+ - java
+ - spring-boot
+ - microservice
+spec:
+ owner: group:default/platform-team
+ type: service
+
+ parameters:
+ - title: Component details
+ required:
+ - componentId
+ - owner
+ - description
+ - javaVersion
+ properties:
+ componentId:
+ title: Component ID
+ type: string
+ description: Unique ID used for artifact name and catalog entity
+ pattern: '^[a-z0-9-]*[a-z0-9]$'
+ ui:autofocus: true
+ description:
+ title: Description
+ type: string
+ description: What this service does
+ owner:
+ title: Owner
+ type: string
+ ui:field: EntityPicker
+ ui:options:
+ catalogFilter:
+ kind:
+ - Group
+ - User
+ javaVersion:
+ title: Java version
+ type: string
+ enum:
+ - "17"
+ - "21"
+ default: "21"
+ packageName:
+ title: Java package
+ type: string
+ description: Base package (e.g. com.example.demo)
+ pattern: '^[a-z][a-z0-9.]*$'
+ - title: Repository location
+ required:
+ - repoUrl
+ properties:
+ repoUrl:
+ title: Repository location
+ type: string
+ ui:field: RepoUrlPicker
+ ui:options:
+ allowedHosts:
+ - github.com
+
+ steps:
+ - id: fetch-base
+ name: Fetch skeleton
+ action: fetch:template
+ input:
+ url: ./skeleton
+ values:
+ componentId: ${{ parameters.componentId }}
+ description: ${{ parameters.description }}
+ owner: ${{ parameters.owner }}
+ javaVersion: ${{ parameters.javaVersion }}
+ packageName: ${{ parameters.packageName }}
+
+ - id: publish
+ name: Publish to GitHub
+ action: publish:github
+ input:
+ repoUrl: ${{ parameters.repoUrl }}
+ description: ${{ parameters.description }}
+ defaultBranch: main
+
+ - id: register
+ name: Register in catalog
+ action: catalog:register
+ input:
+ repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
+ catalogInfoPath: /catalog-info.yaml
+
+ output:
+ links:
+ - title: Open repository
+ url: ${{ steps.publish.output.remoteUrl }}
+ - title: View in catalog
+ icon: catalog
+ entityRef: ${{ steps.register.output.entityRef }}
diff --git a/skills/rhdh-templates/assets/examples/minimal-template/skeleton/README.md b/skills/rhdh-templates/assets/examples/minimal-template/skeleton/README.md
new file mode 100644
index 0000000..6553204
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/minimal-template/skeleton/README.md
@@ -0,0 +1,5 @@
+# {{ values.componentId }}
+
+{{ values.description }}
+
+Owner: {{ values.owner }}
diff --git a/skills/rhdh-templates/assets/examples/minimal-template/skeleton/catalog-info.yaml b/skills/rhdh-templates/assets/examples/minimal-template/skeleton/catalog-info.yaml
new file mode 100644
index 0000000..acea76f
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/minimal-template/skeleton/catalog-info.yaml
@@ -0,0 +1,9 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: {{ values.componentId }}
+ description: {{ values.description }}
+spec:
+ type: service
+ lifecycle: experimental
+ owner: {{ values.owner }}
diff --git a/skills/rhdh-templates/assets/examples/minimal-template/template.yaml b/skills/rhdh-templates/assets/examples/minimal-template/template.yaml
new file mode 100644
index 0000000..29f1eac
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/minimal-template/template.yaml
@@ -0,0 +1,54 @@
+apiVersion: scaffolder.backstage.io/v1beta3
+kind: Template
+metadata:
+ name: minimal-example
+ title: Minimal Example Template
+ description: Starter template scaffolded by rhdh-templates init
+ tags:
+ - example
+spec:
+ owner: group:default/platform-team
+ type: service
+
+ parameters:
+ - title: Component details
+ required:
+ - componentId
+ - owner
+ properties:
+ componentId:
+ title: Component ID
+ type: string
+ pattern: '^[a-z0-9-]*[a-z0-9]$'
+ description: Unique ID for the new component
+ ui:help: Lowercase letters, digits, and dashes only
+ ui:autofocus: true
+ description:
+ title: Description
+ type: string
+ description: What this component does
+ owner:
+ title: Owner
+ type: string
+ ui:field: EntityPicker
+ ui:options:
+ catalogFilter:
+ kind:
+ - Group
+ - User
+
+ steps:
+ - id: fetch-base
+ name: Fetch skeleton
+ action: fetch:template
+ input:
+ url: ./skeleton
+ values:
+ componentId: ${{ parameters.componentId }}
+ description: ${{ parameters.description }}
+ owner: ${{ parameters.owner }}
+
+ output:
+ links:
+ - title: Template output
+ url: https://backstage.io/docs/features/software-templates/
diff --git a/skills/rhdh-templates/assets/examples/nodejs-backend/README.md b/skills/rhdh-templates/assets/examples/nodejs-backend/README.md
new file mode 100644
index 0000000..afca09d
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/nodejs-backend/README.md
@@ -0,0 +1,23 @@
+# Node.js Backend Service
+
+Scaffolds a minimal Express-style Node.js service with:
+
+- `package.json` parameterized by component ID and Node version
+- `catalog-info.yaml` for Software Catalog registration
+- GitHub Actions CI workflow (with `{% raw %}` for Actions syntax)
+
+## Parameters
+
+| Parameter | Purpose |
+|-----------|---------|
+| `componentId` | Catalog entity name and npm package name |
+| `description` | Shown in catalog and repository |
+| `owner` | Catalog owner entity ref |
+| `nodeVersion` | Node.js LTS version (`20` or `22`) |
+| `repoUrl` | Target GitHub repository |
+
+## Post-scaffold steps
+
+1. Run `npm install` in the new repository
+2. Push triggers CI via GitHub Actions
+3. Confirm the component appears in the Software Catalog
diff --git a/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/.github/workflows/ci.yaml b/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/.github/workflows/ci.yaml
new file mode 100644
index 0000000..88fcd3b
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/.github/workflows/ci.yaml
@@ -0,0 +1,20 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+{% raw %}
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ - run: npm install
+ - run: npm test
+{% endraw %}
diff --git a/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/README.md b/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/README.md
new file mode 100644
index 0000000..976d4de
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/README.md
@@ -0,0 +1,14 @@
+# {{ values.componentId }}
+
+{{ values.description }}
+
+Owner: {{ values.owner }}
+
+## Development
+
+```bash
+npm install
+npm start
+```
+
+Runs on Node.js {{ values.nodeVersion }}.
diff --git a/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/catalog-info.yaml b/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/catalog-info.yaml
new file mode 100644
index 0000000..acea76f
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/catalog-info.yaml
@@ -0,0 +1,9 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: {{ values.componentId }}
+ description: {{ values.description }}
+spec:
+ type: service
+ lifecycle: experimental
+ owner: {{ values.owner }}
diff --git a/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/package.json b/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/package.json
new file mode 100644
index 0000000..794ac07
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "{{ values.componentId }}",
+ "version": "0.1.0",
+ "private": true,
+ "description": "{{ values.description }}",
+ "main": "src/index.js",
+ "engines": {
+ "node": ">={{ values.nodeVersion }}.0.0"
+ },
+ "scripts": {
+ "start": "node src/index.js",
+ "test": "node --test"
+ }
+}
diff --git a/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/src/index.js b/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/src/index.js
new file mode 100644
index 0000000..946c96a
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/nodejs-backend/skeleton/src/index.js
@@ -0,0 +1,12 @@
+const http = require('http');
+
+const port = process.env.PORT || 3000;
+
+const server = http.createServer((_req, res) => {
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ service: '{{ values.componentId }}', status: 'ok' }));
+});
+
+server.listen(port, () => {
+ console.log(`{{ values.componentId }} listening on port ${port}`);
+});
diff --git a/skills/rhdh-templates/assets/examples/nodejs-backend/template.yaml b/skills/rhdh-templates/assets/examples/nodejs-backend/template.yaml
new file mode 100644
index 0000000..2a38035
--- /dev/null
+++ b/skills/rhdh-templates/assets/examples/nodejs-backend/template.yaml
@@ -0,0 +1,93 @@
+apiVersion: scaffolder.backstage.io/v1beta3
+kind: Template
+metadata:
+ name: nodejs-backend
+ title: Node.js Backend Service
+ description: Scaffold a Node.js REST API with catalog registration and GitHub publish
+ tags:
+ - recommended
+ - nodejs
+ - backend
+spec:
+ owner: group:default/platform-team
+ type: service
+
+ parameters:
+ - title: Component details
+ required:
+ - componentId
+ - owner
+ - description
+ properties:
+ componentId:
+ title: Component ID
+ type: string
+ description: Unique ID for the new component
+ pattern: '^[a-z0-9-]*[a-z0-9]$'
+ ui:autofocus: true
+ description:
+ title: Description
+ type: string
+ description: What this service does
+ owner:
+ title: Owner
+ type: string
+ ui:field: EntityPicker
+ ui:options:
+ catalogFilter:
+ kind:
+ - Group
+ - User
+ nodeVersion:
+ title: Node.js version
+ type: string
+ enum:
+ - "20"
+ - "22"
+ default: "20"
+ - title: Repository location
+ required:
+ - repoUrl
+ properties:
+ repoUrl:
+ title: Repository location
+ type: string
+ ui:field: RepoUrlPicker
+ ui:options:
+ allowedHosts:
+ - github.com
+
+ steps:
+ - id: fetch-base
+ name: Fetch skeleton
+ action: fetch:template
+ input:
+ url: ./skeleton
+ values:
+ componentId: ${{ parameters.componentId }}
+ description: ${{ parameters.description }}
+ owner: ${{ parameters.owner }}
+ nodeVersion: ${{ parameters.nodeVersion }}
+
+ - id: publish
+ name: Publish to GitHub
+ action: publish:github
+ input:
+ repoUrl: ${{ parameters.repoUrl }}
+ description: ${{ parameters.description }}
+ defaultBranch: main
+
+ - id: register
+ name: Register in catalog
+ action: catalog:register
+ input:
+ repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
+ catalogInfoPath: /catalog-info.yaml
+
+ output:
+ links:
+ - title: Open repository
+ url: ${{ steps.publish.output.remoteUrl }}
+ - title: View in catalog
+ icon: catalog
+ entityRef: ${{ steps.register.output.entityRef }}
diff --git a/skills/rhdh-templates/references/add-parameter.md b/skills/rhdh-templates/references/add-parameter.md
new file mode 100644
index 0000000..5e9e37d
--- /dev/null
+++ b/skills/rhdh-templates/references/add-parameter.md
@@ -0,0 +1,74 @@
+# add-parameter — Incremental Parameter Authoring
+
+
+
+- `conventions.md`
+- `template-structure.md`
+- `parameter-widgets.md`
+
+
+
+
+
+Extend an existing `template.yaml` without re-running full `templatize` or `create`.
+
+## Step 1: Locate template
+
+Confirm path to `templates//template.yaml`.
+
+## Step 2: Define parameter
+
+Gather from user:
+
+| Field | Notes |
+|-------|-------|
+| Name | camelCase key (e.g., `repoName`) |
+| Title | Form label |
+| Type | `string`, `number`, `boolean`, `array` |
+| Widget | default, `EntityPicker`, `RepoUrlPicker`, `radio`, etc. |
+| Section | existing `parameters[].title` or new section |
+| Required | yes/no |
+
+## Step 3: Edit template.yaml
+
+Add to appropriate `parameters` section:
+
+```yaml
+repoName:
+ title: Repository Name
+ type: string
+ description: GitHub repository name for the new component
+ ui:autofocus: true
+```
+
+For conditional fields, add `dependencies` block per `template-structure.md`.
+
+## Step 4: Wire into steps
+
+Every new parameter used in skeleton or actions must appear in a `fetch:template` `values` map:
+
+```yaml
+values:
+ repoName: ${{ parameters.repoName }}
+```
+
+Search all `steps[].input` for missing wiring after the edit.
+
+## Step 5: Update skeleton (if needed)
+
+If the parameter replaces a literal in skeleton files, update Nunjucks to `{{ values. }}`.
+
+## Step 6: Verify
+
+Run `fix_gotchas.py` on the template path. Confirm parameter appears in form and values map.
+
+
+
+
+
+- Parameter added to correct form section with type and UI field
+- All `fetch:template` steps pass the parameter in `values`
+- Skeleton references updated when parameter replaces literals
+- No duplicate parameter keys
+
+
diff --git a/skills/rhdh-templates/references/add-skeleton.md b/skills/rhdh-templates/references/add-skeleton.md
new file mode 100644
index 0000000..a1af475
--- /dev/null
+++ b/skills/rhdh-templates/references/add-skeleton.md
@@ -0,0 +1,87 @@
+# add-skeleton — Incremental Skeleton Authoring
+
+
+
+- `conventions.md`
+
+
+
+
+
+Add or extend files under `templates//skeleton/` for an existing template.
+
+## Step 1: Confirm template context
+
+Locate `templates//template.yaml` and existing `skeleton/` tree.
+
+## Step 2: Determine file role
+
+| File type | Templating approach |
+|-----------|---------------------|
+| App source, README, YAML config | Nunjucks `{{ values.* }}` |
+| GitHub Actions, Helm with `{{` | `{% raw %}` … `{% endraw %}` OR `copyWithoutTemplating` |
+| Binary / images | Do not template — document manual copy |
+
+## Step 3: Add file
+
+Create file under `skeleton/` mirroring target repo layout.
+
+Example `skeleton/catalog-info.yaml`:
+
+```yaml
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: {{ values.componentId }}
+ description: {{ values.description }}
+spec:
+ type: service
+ lifecycle: experimental
+ owner: {{ values.owner }}
+```
+
+## Step 4: Sync template.yaml
+
+1. Ensure parameters exist for every `values.*` key used.
+2. Update `fetch:template` step `values` map.
+3. If adding CI workflows, set `copyWithoutTemplating` or raw blocks:
+
+```yaml
+input:
+ url: ./skeleton
+ copyWithoutTemplating:
+ - .github/workflows/
+```
+
+## Step 5: Optional additional fetch step
+
+When skeleton has CI overlay from shared path (RHDH software-templates pattern):
+
+```yaml
+- id: ci-template
+ name: Add CI skeleton
+ action: fetch:template
+ input:
+ url: ${{ parameters.ci }}
+ copyWithoutTemplating:
+ - .github/workflows/
+ values:
+ repoName: ${{ parameters.repoName }}
+```
+
+## Step 6: Verify
+
+- Grep skeleton for `parameters.` — should be **zero** matches (use `values.` only).
+- Grep for unwrapped `{{` in workflow files — should be inside `{% raw %}` or excluded via `copyWithoutTemplating`.
+- Run `validate --lint-skeleton` for Nunjucks syntax checks (see `validate.md`).
+
+
+
+
+
+- New skeleton files use `values.*` references only
+- Workflow/chart files protected from accidental Nunjucks processing
+- `fetch:template` `values` map includes all new placeholders
+- File paths match expected output repo structure
+
+
diff --git a/skills/rhdh-templates/references/add-step.md b/skills/rhdh-templates/references/add-step.md
new file mode 100644
index 0000000..7b1adf6
--- /dev/null
+++ b/skills/rhdh-templates/references/add-step.md
@@ -0,0 +1,80 @@
+# add-step — Incremental Step Authoring
+
+
+
+- `conventions.md`
+- `template-structure.md`
+
+
+
+
+
+Add a scaffolder step to an existing template without rebuilding from scratch.
+
+## Step 1: Identify action
+
+Ask user what the step should do. Map to a scaffolder action:
+
+| Intent | Typical action |
+|--------|----------------|
+| Copy/template files | `fetch:template` |
+| Fetch plain files | `fetch:plain` |
+| Publish to GitHub | `publish:github` |
+| Register catalog entity | `catalog:register` |
+| Run custom action | `custom:` |
+
+Action IDs are camelCase. When unsure of installed actions, use the `list-actions` command to query the live instance.
+
+## Step 2: Choose position
+
+Steps run **in series**. Ask where to insert:
+
+- Before publish (materialize content)
+- After publish (register, notify, trigger CI)
+
+Assign unique `id` (kebab-case) and human-readable `name`.
+
+## Step 3: Build input
+
+Reference parameters and prior step outputs:
+
+```yaml
+- id: notify-team
+ name: Notify platform team
+ action: notification:send
+ input:
+ recipients: entity:group:default/platform-team
+ title: New component ${{ parameters.componentId }}
+ info: ${{ steps.publish.output.remoteUrl }}
+```
+
+## Step 4: Update output (if needed)
+
+If the step produces user-facing results, add `spec.output.links` referencing `${{ steps..output.* }}`.
+
+## Step 5: Verify wiring
+
+Checklist:
+
+- [ ] `id` unique among all steps
+- [ ] `action` uses correct camelCase ID
+- [ ] All `${{ parameters.* }}` exist in form
+- [ ] All `${{ steps.*.output.* }}` reference prior step IDs
+- [ ] `fetch:template` steps include complete `values` map
+
+## Step 6: fix-gotchas
+
+```bash
+python /scripts/fix_gotchas.py --path [--apply] [--json]
+```
+
+
+
+
+
+- New step inserted at correct position with unique `id`
+- Action ID and inputs match conventions.md
+- Downstream steps and `output` updated if they depend on new step
+- fix-gotchas reports no critical action-casing or expression errors
+
+
diff --git a/skills/rhdh-templates/references/best-practices.md b/skills/rhdh-templates/references/best-practices.md
new file mode 100644
index 0000000..4f0db06
--- /dev/null
+++ b/skills/rhdh-templates/references/best-practices.md
@@ -0,0 +1,150 @@
+# Backstage Software Template Best Practices
+
+
+
+Load when authoring, reviewing, or improving templates — especially during `create`, `templatize`, `add-parameter`, `add-skeleton`, and pre-merge `validate`.
+
+Source: [10 tips for better Backstage Software Templates](https://developers.redhat.com/articles/2025/03/17/10-tips-better-backstage-software-templates) (Red Hat Developer, 2025).
+
+
+
+
+
+## 1. Structure your template repository
+
+Use a **central template repository** with one folder per template and a root `location.yaml` that registers all templates via glob — new templates appear in the catalog after commit without per-template import.
+
+**Why:** Reduces operational toil — platform engineers commit a folder; catalog sync picks it up. Split into multiple repos only when authorship or access boundaries require it.
+
+**RHDH convention:** See `conventions.md` for the `./templates/**/template.yaml` layout, `location.yaml` pattern, and catalog registration via the location URL (not individual template files).
+
+Reference implementations: [backstage/software-templates](https://github.com/backstage/software-templates/), [rhdh-demo-gh/templates](https://github.com/rhdh-demo-gh/templates/).
+
+## 2. Experiment with the Template Editor
+
+Avoid the slow loop of push → wait for catalog sync → test run. Use Backstage's **Template Editor** at `/create/edit` (or **Template Editor** link on the Software Templates page) to:
+
+- Edit templates from a local directory or paste YAML
+- Preview form rendering live on the right
+- Experiment with custom field extensions before committing
+
+**When to use:** After local `validate` passes, paste into Template Editor for form UX review. Copy final YAML back to the repo.
+
+**Skill workflow:** `validate` locally first → Template Editor for UX → `dry-run` against live RHDH for execution.
+
+## 3. Explore installed actions
+
+Action IDs and schemas vary by RHDH instance (installed plugins). Never guess action names from docs alone.
+
+| Method | Path / command |
+|--------|----------------|
+| RHDH UI | Software Catalog → **Installed Actions**, or `/create/actions` |
+| Skill CLI | `list-actions --rhdh-url …` |
+| Single action schema | `explain-action --action-id …` |
+
+Use the exact `id` string from the live instance in `steps[].action`. Plugin actions follow `namespace:actionName` (e.g., `quay:create-repository`).
+
+## 4. Improve DevEx with custom field extensions
+
+Forms use [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form). Built-in **Custom Field Extensions** reduce errors vs free-text entry:
+
+| Field | Use when |
+|-------|----------|
+| `EntityPicker` | Owner, system, component, domain — catalog-backed selection |
+| `RepoUrlPicker` | SCM URL with host/org validation |
+| `OwnerPicker` | Shortcut for owner groups |
+| `Secret` | Passwords, tokens, API keys (see tip 7) |
+
+Set `allowArbitraryValues: false` when the value must resolve to a catalog entity. See `parameter-widgets.md` for wiring patterns and examples.
+
+## 5. Process structured data with template filters
+
+[Template filters](https://backstage.io/docs/features/software-templates/template-extensions) transform values in step expressions. Entity pickers return string refs like `component:default/my-service`.
+
+```yaml
+title: "Deploy ${{ parameters.component | parseEntityRef | pick('name') }}"
+targetPath: ./${{ parameters.component | parseEntityRef | pick('name') }}
+```
+
+Common filters: `parseEntityRef`, `pick`, `json`, `replace`. Use filters in step `input` and `output` — not in skeleton Nunjucks (skeleton uses `values.*` from `fetch:template`; see `conventions.md`).
+
+## 6. Use the Nunjucks API in skeletons
+
+`fetch:template` processes skeleton files with [Nunjucks](https://mozilla.github.io/nunjucks/templating.html). Pass data via the step `values` map; reference as `{{ values.name }}` in skeleton files.
+
+**Tags beyond substitution:**
+
+| Tag | Use when |
+|-----|----------|
+| `{% if %}…{% endif %}` | Conditional files or sections based on parameters |
+| `{% for %}…{% endfor %}` | Iterate arrays passed in `values` |
+| `{% raw %}…{% endraw %}` | Preserve literal `{{` / `{%` (GitHub Actions, Helm) |
+
+GitHub Actions and Helm files need `{% raw %}` or `copyWithoutTemplating` — see `conventions.md`, `template-structure.md`, and `add-skeleton.md`. `fix-gotchas` and `validate --lint-skeleton` detect common mistakes.
+
+## 7. Protect secrets
+
+Never collect credentials with a plain `type: string` text field. Use Backstage's **Secret** field — see `parameter-widgets.md` for the form definition.
+
+Secrets are masked in the form, review screen, and logs. In steps, reference integration secrets (e.g. `${{ secrets.user.github.token }}`) — never hardcode tokens. Exact secret paths depend on configured integrations; confirm against your RHDH instance. `fix-gotchas` flags obvious hardcoded tokens in step inputs.
+
+## 8. Specify template type and tags
+
+`spec.type` is required but often left as generic `service`. Set a meaningful type (`website`, `microservice`, `library`, `infrastructure`) so the Create UI groups templates. Add `metadata.tags` for subcategory filtering.
+
+See `template-structure.md` for `metadata` and `spec.type` fields. Use the `recommended` tag to highlight golden paths.
+
+**Why:** The Software Templates page becomes unusable at scale without type/tag filters.
+
+## 9. Document your templates
+
+Self-service templates need docs beyond `description`. Two levels:
+
+**Human README** — `templates//README.md` with purpose, parameters, post-scaffold steps.
+
+**TechDocs** (when RHDH has TechDocs configured): add `backstage.io/techdocs-ref: dir:.` annotation and `mkdocs.yml` beside `template.yaml`. See `template-structure.md` for the annotation pattern.
+
+Documented templates show a **View TechDocs** link in the Create UI. Example: [rhdh-demo-gh/templates/deploy-component](https://github.com/rhdh-demo-gh/templates/tree/main/deploy-component).
+
+## 10. Plan for maintenance
+
+Templates codify best practices — outdated templates undermine trust. Treat template repos like application code:
+
+| Practice | How the skill supports it |
+|----------|---------------------------|
+| Keep skeleton dependencies current | Re-run `templatize` when source repos change |
+| Automated regression | `dry-run` via Scaffolder HTTP API after changes |
+| Pre-merge checks | `validate` + `fix-gotchas` with zero critical findings |
+| Periodic review | Schedule dependency bumps in skeleton `package.json`, Dockerfiles, CI versions |
+
+**Failure modes to avoid:** Scaffolded apps that fail on first build, frameworks with known CVEs, broken publish/register wiring after SCM API changes.
+
+## Bonus: Accelerate the development loop
+
+Shrink feedback time with local RHDH:
+
+| Approach | When |
+|----------|------|
+| **rhdh-local** skill | Fast local RHDH for plugin/template testing |
+| Template Editor | Form UX without catalog sync |
+| `validate --lint-skeleton` | Nunjucks/skeleton checks without RHDH |
+| `dry-run` | End-to-end step execution against live instance |
+
+Recommended loop: `init` → author → `validate` → Template Editor → `dry-run` → commit.
+
+
+
+
+
+Before merge, confirm:
+
+- [ ] Repo follows central `location.yaml` + `templates//` layout
+- [ ] `spec.type` and `metadata.tags` set for discoverability
+- [ ] EntityPicker / RepoUrlPicker / Secret used instead of free-text where appropriate
+- [ ] Skeleton uses `values.*`; workflows use `{% raw %}` or `copyWithoutTemplating`
+- [ ] Sensitive inputs use `ui:field: Secret`; step tokens use `${{ secrets.* }}`
+- [ ] README or TechDocs present for non-trivial templates
+- [ ] `validate.py` reports zero critical findings
+- [ ] Template tested in Template Editor or via `dry-run`
+
+
diff --git a/skills/rhdh-templates/references/conventions.md b/skills/rhdh-templates/references/conventions.md
new file mode 100644
index 0000000..0c654c5
--- /dev/null
+++ b/skills/rhdh-templates/references/conventions.md
@@ -0,0 +1,115 @@
+# RHDH Software Template Conventions
+
+
+
+Load this file before editing `template.yaml`, skeleton files, or `location.yaml`.
+
+
+
+
+
+## API versions
+
+| Artifact | apiVersion | kind |
+|----------|------------|------|
+| `template.yaml` | `scaffolder.backstage.io/v1beta3` | `Template` |
+| `location.yaml` | `backstage.io/v1alpha1` | `Location` |
+| `catalog-info.yaml` (in skeleton) | `backstage.io/v1alpha1` | `Component` (typical) |
+
+Use v1beta3 for new templates — it uses `${{ }}` step expressions. Do not mix v1beta2 `{{ }}` syntax in the same template.
+
+## Action IDs
+
+Scaffolder actions use **camelCase** IDs:
+
+| Correct | Wrong |
+|---------|-------|
+| `fetch:template` | `fetch:template` with wrong casing in docs only — verify against live instance |
+| `publish:github` | `publish:GitHub` |
+| `catalog:register` | `catalog:Register` |
+
+When unsure, list actions from a running RHDH instance with the `list-actions` command.
+
+## Parameter form conventions
+
+- Group related fields under `parameters[].title` sections (e.g., "Provide information about the new component").
+- Use `ui:field: EntityPicker` with `catalogFilter.kind` for owner/system pickers.
+- Use `ui:field: RepoUrlPicker` with `allowedHosts` for repo URL parameters.
+- Use `pattern` + `ui:help` for constrained IDs (see `assets/examples/minimal-template/template.yaml`).
+
+## Skeleton templating
+
+Skeleton files use Nunjucks with **values from `fetch:template` steps**, not `parameters` directly:
+
+```yaml
+# template.yaml step
+action: fetch:template
+input:
+ url: ./skeleton
+ values:
+ repoName: ${{ parameters.repoName }}
+```
+
+```text
+# skeleton/README.md
+# Project: {{ values.repoName }}
+```
+
+### When to use `{% raw %}` … `{% endraw %}`
+
+Wrap content that must pass through unchanged and contains `{{` or `{%` — common in:
+
+- GitHub Actions workflows (`.github/workflows/*.yaml`)
+- Helm charts with Go templates
+- Any file where braces are literal syntax, not Nunjucks
+
+## Secrets in templates
+
+Use Backstage/RHDH secrets syntax in step inputs — never hardcode tokens in skeleton files:
+
+```yaml
+token: ${{ secrets.user.github.token }}
+```
+
+Exact secret paths depend on configured integrations; confirm against your RHDH instance.
+
+## Repository layout
+
+```
+template-repo/
+├── location.yaml # kind: Location — registers all templates
+└── templates/
+ └── my-template/
+ ├── template.yaml
+ ├── skeleton/ # optional README, catalog-info.yaml, app source
+ └── README.md # optional human docs
+```
+
+## location.yaml pattern
+
+```yaml
+apiVersion: backstage.io/v1alpha1
+kind: Location
+metadata:
+ name: my-org-templates
+ description: Software Templates for My Org
+spec:
+ targets:
+ - ./templates/**/template.yaml
+```
+
+Register the **location.yaml URL** in RHDH (Catalog Import or `catalog.locations` in app-config), not individual template files.
+
+For repository layout rationale and multi-repo splitting guidance, see `best-practices.md` tip 1.
+
+## Common publish + register sequence
+
+Most service templates end with:
+
+1. `fetch:template` — materialize skeleton
+2. `publish:github` (or `publish:gitlab`, etc.) — push to remote
+3. `catalog:register` — register `catalog-info.yaml` from the new repo
+
+Wire `repoContentsUrl` from publish output into register input.
+
+
diff --git a/skills/rhdh-templates/references/create-location.md b/skills/rhdh-templates/references/create-location.md
new file mode 100644
index 0000000..b76726a
--- /dev/null
+++ b/skills/rhdh-templates/references/create-location.md
@@ -0,0 +1,70 @@
+# create-location — Generate location.yaml
+
+
+
+- `conventions.md`
+
+
+
+
+
+Standalone utility for generating or refreshing root `location.yaml`. Templatize and create flows may also produce this file — use this command when templates were added manually or the glob target is stale.
+
+## Step 1: Confirm repo root
+
+The template repository root contains `templates/` with one or more `template.yaml` files.
+
+## Step 2: Run script
+
+```bash
+python /scripts/create_location.py \
+ --path \
+ --name \
+ [--description "Human description"] \
+ [--json]
+```
+
+| Flag | Default |
+|------|---------|
+| `--path` | current directory |
+| `--name` | derived from directory name + `-templates` suffix |
+| `--description` | auto-generated |
+
+The script:
+
+1. Discovers `templates/**/template.yaml`
+2. Writes or updates `location.yaml` at repo root with glob target `./templates/**/template.yaml`
+3. Lists discovered templates in JSON output
+
+## Step 3: Review output
+
+Show user the generated `location.yaml` and template count.
+
+If zero templates found, stop — run `init` or `create` first.
+
+## Step 4: Registration reminder
+
+Tell user to register **the location.yaml URL** in RHDH:
+
+- Catalog Import UI: `/catalog-import`
+- Or `catalog.locations` in app-config:
+
+```yaml
+catalog:
+ locations:
+ - type: url
+ target: https://github.com/acme-corp/templates/blob/main/location.yaml
+ rules:
+ - allow: [Location, Template]
+```
+
+
+
+
+
+- `location.yaml` exists at repo root with `kind: Location`
+- `spec.targets` includes `./templates/**/template.yaml`
+- Script JSON reports all discovered template paths
+- User knows how to register the Location in RHDH
+
+
diff --git a/skills/rhdh-templates/references/create.md b/skills/rhdh-templates/references/create.md
new file mode 100644
index 0000000..1576042
--- /dev/null
+++ b/skills/rhdh-templates/references/create.md
@@ -0,0 +1,122 @@
+# create — From-Scratch Template
+
+
+
+- `conventions.md`
+- `template-structure.md`
+- `parameter-widgets.md`
+- `assets/examples/minimal-template/template.yaml`
+- `assets/examples/nodejs-backend/template.yaml` — full publish/register pipeline
+- `assets/examples/java-springboot/template.yaml` — Spring Boot + Maven
+
+
+
+
+
+Use when **no reference codebase** exists. For converting existing code, use `templatize` instead.
+
+## Step 1: Gather intent
+
+Ask (one round, not necessarily one question each):
+
+1. **Template purpose** — what golden path does this enable?
+2. **Target type** — `service`, `website`, `library`, `plugin`, etc.
+3. **Parameters** — minimum form fields (name, owner, repo URL, …)
+4. **Steps** — fetch skeleton only, or publish + register too?
+5. **SCM** — GitHub, GitLab, Bitbucket?
+
+## Step 1b: Match reference examples (recommended)
+
+Before writing files, query the curated catalog and suggest 1–3 study references:
+
+```bash
+python /scripts/list_examples.py \
+ --match "" \
+ --limit 3 --json
+```
+
+Present upstream URLs from the matches. When a match includes `local_bundled`, also point at `assets/examples//` for offline patterns. Ask whether to mirror a reference's step sequence or start from `assets/examples/minimal-template/`.
+
+Load `example-catalog.md` when the user asks what customers typically build.
+
+## Step 2: Ensure layout
+
+If no template repo exists, run `init` first.
+
+## Step 3: Create template directory
+
+```
+templates//
+├── template.yaml
+├── skeleton/
+│ ├── README.md
+│ └── catalog-info.yaml # when registering a Component
+└── README.md # optional
+```
+
+Use `assets/examples/minimal-template/` as the starting skeleton for simple templates.
+
+## Step 4: Build template.yaml
+
+1. `metadata` — descriptive `name`, `title`, `description`, meaningful `tags` (see `best-practices.md` tip 8)
+2. `spec.type` — set to a discoverable category, not generic default when possible
+3. `spec.parameters` — at least one section with required fields; use EntityPicker/Secret widgets per `best-practices.md` tips 4 and 7
+4. `spec.steps` — minimal path:
+
+```yaml
+steps:
+ - id: fetch-base
+ name: Fetch skeleton
+ action: fetch:template
+ input:
+ url: ./skeleton
+ values:
+ componentId: ${{ parameters.componentId }}
+ owner: ${{ parameters.owner }}
+ description: ${{ parameters.description }}
+```
+
+Add publish/register steps when user wants full end-to-end flow.
+
+## Step 5: Write skeleton files
+
+Keep skeleton minimal but valid:
+
+- `README.md` with `{{ values.componentId }}` placeholder
+- `catalog-info.yaml` when using `catalog:register`
+
+## Step 6: Register templates
+
+```bash
+python /scripts/create_location.py --path [--json]
+```
+
+## Step 7: fix-gotchas
+
+```bash
+python /scripts/fix_gotchas.py --path templates//template.yaml [--apply] [--json]
+```
+
+### Critique and fix loop
+
+Before finishing, verify:
+
+1. Parameters use appropriate widgets from `parameter-widgets.md`
+2. Steps match the user's SCM choice (publish + register when requested)
+3. `fix_gotchas.py` reports zero critical findings after `--apply`
+4. `create_location.py` lists the new template
+
+5. Add `README.md` (or TechDocs per `best-practices.md` tip 9) for non-trivial templates
+
+Exit bar: minimal but complete template for the described use case, passes `validate`, ready for Template Editor (`best-practices.md` tip 2).
+
+
+
+
+
+- Valid v1beta3 `template.yaml` with parameters, steps, and output appropriate to use case
+- `skeleton/` contains at least one templated file
+- `location.yaml` registers the new template
+- fix-gotchas passes with no critical findings
+
+
diff --git a/skills/rhdh-templates/references/dry-run.md b/skills/rhdh-templates/references/dry-run.md
new file mode 100644
index 0000000..770183e
--- /dev/null
+++ b/skills/rhdh-templates/references/dry-run.md
@@ -0,0 +1,77 @@
+# dry-run — Remote Template Execution Test
+
+
+
+- `conventions.md`
+
+
+
+
+
+Execute a template against the Scaffolder v2 dry-run API without creating real resources. Mutation steps (publish, register) are simulated — warn the user that full E2E testing still requires running the template in RHDH.
+
+**Prerequisite:** run local `validate` first (route to the `validate` command) until `critical_count` is 0.
+
+## Capability gate
+
+Requires:
+
+- Reachable RHDH with Scaffolder v2 API
+- PyYAML available (`uv sync --extra dev` in this repo; or system PyYAML)
+- Valid `template.yaml` and optional `skeleton/` directory
+
+If RHDH is unreachable, skip and suggest local `validate` only.
+
+## Step 1: Prepare values
+
+Create a JSON file with parameter values matching `spec.parameters`:
+
+```json
+{
+ "componentId": "demo-service",
+ "owner": "group:default/team-a",
+ "description": "Demo from dry-run"
+}
+```
+
+Use fake data — never real tokens or production identifiers unless the user explicitly provides them.
+
+## Step 2: Run dry-run script
+
+```bash
+python /scripts/dry_run.py \
+ --rhdh-url https://rhdh.example.com \
+ --path templates/my-template/ \
+ --values /tmp/values.json \
+ [--secrets /tmp/secrets.json] \
+ [--token TOKEN] \
+ [--json]
+```
+
+The script:
+
+1. Parses `template.yaml`
+2. Serializes `skeleton/` as base64 directory contents
+3. POSTs to `/api/scaffolder/v2/dry-run`
+4. Returns log lines, output metadata, and generated file count
+
+## Step 3: Interpret failures
+
+| Symptom | Likely cause |
+|---------|--------------|
+| 401/403 | Missing or expired token |
+| 400 with `errors` | Invalid parameter values or malformed template |
+| Action not found | Wrong action ID — run `list-actions` |
+| Empty skeleton output | Missing `fetch:template` step or wrong `values` wiring |
+
+Re-run local `validate` and `fix-gotchas` before retrying dry-run.
+
+
+
+
+
+- Dry-run completes with exit code 0
+- Log shows expected steps executed (especially `fetch:template`)
+- User understands dry-run skips real mutations and may need manual E2E verification
+
+
diff --git a/skills/rhdh-templates/references/example-catalog.md b/skills/rhdh-templates/references/example-catalog.md
new file mode 100644
index 0000000..5742bcb
--- /dev/null
+++ b/skills/rhdh-templates/references/example-catalog.md
@@ -0,0 +1,116 @@
+# example-catalog — Reference Template Catalog
+
+
+
+- `assets/example-catalog.json` (data source — do not edit by hand during normal authoring)
+
+
+
+
+
+Surface curated reference templates customers reach for most often. Use when the user asks "what templates exist?", "show me an example", "what do customers usually build?", or before `create` / `templatize` to pick a study reference.
+
+## Step 1: Parse intent
+
+Extract from the user's message:
+
+- **Stack** — go, nodejs, java, spring-boot, quarkus, python, ai, rag, ansible, …
+- **Workflow** — new service, import existing, add CI/GitOps, AI agent, plugin scaffold
+- **Constraints** — offline/local only, official sources only, recommended starters
+
+## Step 2: Query the catalog
+
+```bash
+python /scripts/list_examples.py --match "" --limit 5 --json
+```
+
+Use filters when intent is narrow:
+
+```bash
+python /scripts/list_examples.py --category backend --recommended --json
+python /scripts/list_examples.py --stack python --json
+python /scripts/list_examples.py --local-only --json
+```
+
+Consume full JSON output. Never pipe through `head`, `tail`, or `grep`.
+
+## Step 3: Present results
+
+For each match, show:
+
+1. **Title** and **category**
+2. **URL** to upstream `template.yaml` (or bundled `assets/examples/` path)
+3. **Why it matches** — stack, use case, or recommended tag
+4. **Next step** — "Study this before authoring" or "Open bundled `assets/examples/` for offline validation"
+
+Repeat the catalog **disclaimer** once (see below).
+
+## Step 4: Offer handoff
+
+Ask whether to:
+
+- **`create`** — build a similar template from scratch
+- **`templatize`** — convert their existing codebase using the reference as a pattern guide
+- **`validate`** — check their current template against conventions
+
+If the user only wanted a list, stop after Step 3.
+
+---
+
+## Disclaimer
+
+Upstream templates in GitHub are **learning aids**, not production-ready golden paths. Always validate, test in a safe RHDH environment, and customize for your organization's standards. See the upstream repo caution in [red-hat-developer-hub-software-templates](https://github.com/redhat-developer/red-hat-developer-hub-software-templates).
+
+## Primary sources
+
+| Source | Repo | When to study it |
+|--------|------|------------------|
+| Official library | [red-hat-developer-hub-software-templates](https://github.com/redhat-developer/red-hat-developer-hub-software-templates) | Backend + CI, catalog import, Tekton/ArgoCD, Ansible, plugin scaffolding |
+| AI quickstarts | [aiquickstarttemplates](https://github.com/redhat-developer/aiquickstarttemplates) | RAG chatbots, IT agents, LLM serving on OpenShift AI |
+| AI Lab samples | [ai-lab-template](https://github.com/redhat-ai-dev/ai-lab-template) | Smaller AI samples (RAG, chatbot, codegen) |
+| Bundled skill examples | `assets/examples/` in this skill | Local validation, minimal patterns without cloning repos |
+
+## Categories (customer demand order)
+
+1. **backend** — Go, Node.js, Spring Boot, Quarkus, Python services with CI (most common golden path)
+2. **catalog** — Register existing repos into the Software Catalog
+3. **cicd / gitops** — Tekton pipelines, ArgoCD bootstrap
+4. **automation** — Ansible Automation Platform job templates
+5. **ai** — RAG, agents, LLM serving (fastest-growing ask)
+6. **plugin** — Dynamic frontend/backend plugin scaffolding
+7. **docs** — TechDocs starter
+8. **starter** — Bundled minimal examples for local learning
+
+Templates marked **recommended** upstream (`tags: recommended` in their `template.yaml`) are the default "start here" references.
+
+## How to use references during authoring
+
+| Situation | Action |
+|-----------|--------|
+| User describes a new template | Run `--match` on their description; suggest top 1–3 upstream URLs to study |
+| Match has `local_bundled` | Also open `assets/examples//` for offline validation patterns |
+| Templatizing existing code | Compare detected stack from `analyze.py` to `--stack` filter |
+| AI / RAG / agent request | Prefer `ai-quickstart-templates` over generic backend examples |
+| Import-only workflow | Point at `register-component` |
+
+Do not copy upstream skeletons wholesale into customer repos without review — study their **parameter forms**, **step sequences**, and **conventions**, then adapt.
+
+## Catalog maintenance
+
+Data lives in `assets/example-catalog.json`. Refresh when:
+
+- `templates.yaml` changes in the official library
+- New AI quickstart templates ship
+- Field teams report a recurring customer pattern not yet listed
+
+Run `list_examples.py --json` after edits to verify parsing.
+
+
+
+
+
+- `list_examples.py --json` returns at least one relevant match for the stated intent, or clearly reports zero matches with suggested broader filters
+- User receives clickable upstream URLs plus local bundled paths when available
+- Disclaimer stated once per session when showing upstream templates
+
+
diff --git a/skills/rhdh-templates/references/explain-action.md b/skills/rhdh-templates/references/explain-action.md
new file mode 100644
index 0000000..5ac80fb
--- /dev/null
+++ b/skills/rhdh-templates/references/explain-action.md
@@ -0,0 +1,65 @@
+# explain-action — Action or Template Schema Reference
+
+
+
+- `conventions.md`
+
+
+
+
+
+Show input/output JSON Schema for a Scaffolder action, or the parameter form schema for a catalog Template entity.
+
+## Mode selection
+
+| User asks about | Flag |
+|-----------------|------|
+| Scaffolder action input fields (`publish:github`, `fetch:template`, …) | `--action ` |
+| Template form parameters for a registered template | `--template-ref template:namespace/name` |
+
+Provide exactly one mode per invocation.
+
+## Step 1: Run explain script
+
+**Action schema** (from list-actions response):
+
+```bash
+python /scripts/explain_action.py \
+ --rhdh-url https://rhdh.example.com \
+ --action publish:github \
+ [--token TOKEN] \
+ [--json]
+```
+
+**Template parameter schema** (from parameter-schema endpoint):
+
+```bash
+python /scripts/explain_action.py \
+ --rhdh-url https://rhdh.example.com \
+ --template-ref template:default/my-template \
+ [--token TOKEN] \
+ [--json]
+```
+
+## Step 2: Apply schema to authoring
+
+For `add-step`:
+
+- Wire `input:` fields to match `schema.input` required properties
+- Reference prior step outputs using `${{ steps..output. }}`
+
+For `add-parameter`:
+
+- Compare proposed fields against an existing template's parameter schema when templating from a registered example
+
+If action is not found, run `list-actions --filter ` to discover the correct id.
+
+
+
+
+
+- Schema JSON returned for the requested action or template
+- User can author a step or parameter block without guessing field names
+- Action id matches live instance casing
+
+
diff --git a/skills/rhdh-templates/references/fix-gotchas.md b/skills/rhdh-templates/references/fix-gotchas.md
new file mode 100644
index 0000000..db39c25
--- /dev/null
+++ b/skills/rhdh-templates/references/fix-gotchas.md
@@ -0,0 +1,65 @@
+# fix-gotchas — Apply Common Template Corrections
+
+
+
+- `conventions.md`
+
+
+
+
+
+Apply packaged rules to catch mistakes that pass YAML parsing but fail in Template Editor or at execution time.
+
+## Step 1: Target path
+
+Accept:
+
+- Path to `template.yaml`
+- Path to `templates//` directory (script finds `template.yaml`)
+
+## Step 2: Run checker
+
+```bash
+python /scripts/fix_gotchas.py \
+ --path \
+ [--apply] \
+ [--json]
+```
+
+Without `--apply`: report findings only.
+With `--apply`: write safe automatic fixes in place.
+
+## Step 3: Review findings
+
+| Severity | Action |
+|----------|--------|
+| critical | Must fix before merge — apply or manual edit |
+| warning | Recommend fix — explain to user |
+| info | Convention suggestion |
+
+Rule definitions (id, severity, check, auto-fix availability) live in `references/gotchas-rules.json`. The same rules run inside `validate.py`.
+
+## Step 4: Manual fixes
+
+Some rules are **detect-only** (require human judgment):
+
+- Correct `owner` entity refs
+- Choosing `copyWithoutTemplating` vs `{% raw %}`
+- Secret path selection for the user's integrations
+
+Document manual items in the response.
+
+## Step 5: Re-run until clean
+
+Loop until no critical findings remain, then run `validate` for full pre-merge checks.
+
+
+
+
+
+- `fix_gotchas.py` executed with JSON output reviewed
+- All critical findings resolved (auto or manual)
+- User informed of detect-only items
+- Template ready for Template Editor validation
+
+
diff --git a/skills/rhdh-templates/references/gotchas-rules.json b/skills/rhdh-templates/references/gotchas-rules.json
new file mode 100644
index 0000000..5ae335e
--- /dev/null
+++ b/skills/rhdh-templates/references/gotchas-rules.json
@@ -0,0 +1,88 @@
+{
+ "rules": [
+ {
+ "id": "api-version",
+ "severity": "critical",
+ "description": "template.yaml must use scaffolder.backstage.io/v1beta3",
+ "check": "api_version",
+ "fix": "set_api_version_v1beta3"
+ },
+ {
+ "id": "action-pascal-case",
+ "severity": "critical",
+ "description": "Scaffolder action IDs use camelCase after the colon (e.g., publish:github not publish:GitHub)",
+ "check": "action_pascal_case",
+ "fix": "lowercase_action_segment"
+ },
+ {
+ "id": "v1beta2-expressions",
+ "severity": "critical",
+ "description": "v1beta3 templates must use ${{ }} not bare {{ parameters. }}",
+ "check": "v1beta2_expression_syntax",
+ "fix": "convert_to_v1beta3_expressions"
+ },
+ {
+ "id": "hardcoded-token",
+ "severity": "critical",
+ "description": "Step inputs should not contain obvious hardcoded tokens or API keys",
+ "check": "hardcoded_secret",
+ "fix": null
+ },
+ {
+ "id": "missing-parameters-section",
+ "severity": "warning",
+ "description": "Production templates should define spec.parameters",
+ "check": "missing_parameters",
+ "fix": null
+ },
+ {
+ "id": "missing-steps-section",
+ "severity": "warning",
+ "description": "Templates should define at least one spec.steps entry",
+ "check": "missing_steps",
+ "fix": null
+ },
+ {
+ "id": "fetch-template-values",
+ "severity": "warning",
+ "description": "fetch:template steps should pass a values map when skeleton uses placeholders",
+ "check": "fetch_template_values",
+ "fix": null
+ },
+ {
+ "id": "workflow-raw-blocks",
+ "severity": "warning",
+ "description": "GitHub workflow files in skeleton with {{ should use {% raw %} or copyWithoutTemplating",
+ "check": "workflow_raw_blocks",
+ "fix": null
+ },
+ {
+ "id": "skeleton-parameters-ref",
+ "severity": "info",
+ "description": "Skeleton files should reference values.* not parameters.*",
+ "check": "skeleton_parameters_ref",
+ "fix": null
+ },
+ {
+ "id": "metadata-tags",
+ "severity": "warning",
+ "description": "metadata.tags improves template discoverability in the Create UI (best-practices tip 8)",
+ "check": "metadata_tags",
+ "fix": null
+ },
+ {
+ "id": "sensitive-param-secret-field",
+ "severity": "warning",
+ "description": "Sensitive parameters (password, token, secret) should use ui:field: Secret (best-practices tip 7)",
+ "check": "sensitive_param_secret_field",
+ "fix": null
+ },
+ {
+ "id": "template-docs",
+ "severity": "info",
+ "description": "Non-trivial templates should include README.md or TechDocs annotation (best-practices tip 9)",
+ "check": "template_docs",
+ "fix": null
+ }
+ ]
+}
diff --git a/skills/rhdh-templates/references/init.md b/skills/rhdh-templates/references/init.md
new file mode 100644
index 0000000..7d1548a
--- /dev/null
+++ b/skills/rhdh-templates/references/init.md
@@ -0,0 +1,60 @@
+# init — Setup Template Authoring Environment
+
+
+
+- `conventions.md` — target repository layout
+
+
+
+
+
+## Step 1: Run the init script
+
+From the user's target directory (or current directory):
+
+```bash
+python /scripts/init.py --path . [--rhdh-url https://rhdh.example.com] [--json]
+```
+
+The script:
+
+1. Checks required tools (`python3`, `git`)
+2. Reports recommended tools (`djlint` for Nunjucks — used by `validate --lint-skeleton`)
+3. Scaffolds `templates/example-template/` with starter `template.yaml` and `skeleton/`
+4. Creates root `location.yaml` if missing
+5. Optionally probes `GET /api/scaffolder/v2/actions` when `--rhdh-url` is set
+
+Consume full JSON output when `--json` is passed. Do not pipe through `head`, `tail`, or `grep`.
+
+## Step 2: Interpret results
+
+| Exit code | Meaning |
+|-----------|---------|
+| 0 | Ready — required tools present, layout scaffolded or already valid |
+| 1 | Partial — layout created but optional tools or RHDH unreachable |
+| 2 | Usage error |
+
+If RHDH is unreachable, tell the user local authoring and `validate` still work; use `list-actions` / `dry-run` when connectivity is available.
+
+## Step 3: Confirm with user
+
+Show:
+
+- Which tools are missing (if any) and install hints
+- Scaffolded paths
+- RHDH connectivity status (if checked)
+
+Ask whether to rename `example-template` or start `templatize` / `create` on the new layout.
+
+Mention the recommended dev loop from `best-practices.md`: local `validate` → Template Editor (`/create/edit`) → `dry-run` when RHDH is reachable. Route to `rhdh-local` skill if the user wants a local RHDH instance.
+
+
+
+
+
+- `init.py` exit code 0 or 1 with clear user messaging
+- `templates/` directory exists with at least one template folder
+- Root `location.yaml` exists with glob target `./templates/**/template.yaml`
+- User knows next command (`templatize`, `create`, or incremental add-*)
+
+
diff --git a/skills/rhdh-templates/references/list-actions.md b/skills/rhdh-templates/references/list-actions.md
new file mode 100644
index 0000000..2ab5db4
--- /dev/null
+++ b/skills/rhdh-templates/references/list-actions.md
@@ -0,0 +1,58 @@
+# list-actions — Discover Scaffolder Actions
+
+
+
+- `conventions.md` — action ID casing rules
+
+
+
+
+
+Query a running RHDH instance for installed Scaffolder actions. Requires network access to the RHDH backend.
+
+Equivalent UI: **Installed Actions** in Software Catalog or `/create/actions` (`best-practices.md` tip 3).
+
+## Capability gate
+
+Run only when:
+
+- User provides `--rhdh-url` or confirms their RHDH base URL
+- RHDH instance is reachable from the agent environment
+
+If unreachable, state in one line that the step is skipped and offer local `validate` instead. Do not ask the user to install tooling.
+
+## Step 1: Run list-actions script
+
+```bash
+python /scripts/list_actions.py --rhdh-url https://rhdh.example.com [--filter publish] [--token TOKEN] [--json]
+```
+
+Token resolution order:
+
+1. `--token` flag
+2. `RHDH_TOKEN` environment variable
+3. `BACKSTAGE_TOKEN` environment variable
+
+When RHDH uses backend permissions, the user may need a browser session token from the frontend.
+
+## Step 2: Use results
+
+Each action includes:
+
+- `id` — use this exact string in `template.yaml` `action:` fields
+- `description` — human-readable summary
+- `schema.input` / `schema.output` — JSON Schema for step wiring
+
+Filter with `--filter` when the user asks about a specific action family (e.g., `publish`, `fetch`, `catalog`).
+
+For detailed schema of one action, route to `explain-action`.
+
+
+
+
+
+- Script exits 0 with action list JSON
+- User can pick correct action IDs for `add-step` or template authoring
+- Action IDs match live instance (not guessed from docs)
+
+
diff --git a/skills/rhdh-templates/references/parameter-widgets.md b/skills/rhdh-templates/references/parameter-widgets.md
new file mode 100644
index 0000000..0086d96
--- /dev/null
+++ b/skills/rhdh-templates/references/parameter-widgets.md
@@ -0,0 +1,135 @@
+# Parameter Form Widgets
+
+
+
+Load when adding parameters via `add-parameter`, building `spec.parameters` in `templatize` or `create`, or choosing the right UI field for a form input.
+
+
+
+
+
+## Common property patterns
+
+| Use case | Type | Widget / options |
+|----------|------|------------------|
+| Component or repo ID | `string` | `pattern` + `ui:help` for kebab-case IDs |
+| Free text description | `string` | default text field |
+| Catalog owner | `string` | `ui:field: EntityPicker`, filter `Group`/`User` |
+| Catalog system | `string` | `ui:field: EntityPicker`, filter `System` |
+| Repository URL | `string` | `ui:field: RepoUrlPicker` + `allowedHosts` |
+| Environment choice | `string` | `enum` or `ui:widget: radio` |
+| Boolean toggle | `boolean` | default checkbox |
+| Numeric replicas / port | `number` | `minimum` / `maximum` when bounded |
+| Multi-select owners | `array` | `ui:widget: checkboxes` or EntityPicker multi |
+| Password / API token | `string` | `ui:field: Secret` (masks input in form, review, logs) |
+
+## EntityPicker (owner, system, domain)
+
+```yaml
+owner:
+ title: Owner
+ type: string
+ ui:field: EntityPicker
+ ui:options:
+ catalogFilter:
+ kind:
+ - Group
+ - User
+```
+
+Use `allowArbitraryValues: false` when the picker must resolve to catalog entities only.
+
+## RepoUrlPicker (GitHub / GitLab / Bitbucket)
+
+```yaml
+repoUrl:
+ title: Repository Location
+ type: string
+ ui:field: RepoUrlPicker
+ ui:options:
+ allowedHosts:
+ - github.com
+ - gitlab.com
+```
+
+Pair with `allowedOwners` when restricting orgs. The publish step reads `${{ parameters.repoUrl }}`.
+
+## Constrained IDs
+
+```yaml
+componentId:
+ title: Component ID
+ type: string
+ pattern: '^[a-z0-9-]*[a-z0-9]$'
+ ui:help: Lowercase letters, digits, and dashes only
+ ui:autofocus: true
+```
+
+## Conditional fields (dependencies)
+
+Show fields only when another field matches:
+
+```yaml
+parameters:
+ - title: Repository details
+ properties:
+ repoChoice:
+ title: Repository host
+ type: string
+ enum:
+ - github
+ - gitlab
+ default: github
+ githubOrg:
+ title: GitHub organization
+ type: string
+ dependencies:
+ repoChoice:
+ oneOf:
+ - properties:
+ repoChoice:
+ enum: [github]
+ githubOrg:
+ title: GitHub organization
+ type: string
+ required: [githubOrg]
+```
+
+## Parameter → skeleton wiring
+
+Form keys use `parameters.`. Skeleton files use `values.` populated in `fetch:template`:
+
+```yaml
+values:
+ componentId: ${{ parameters.componentId }}
+ owner: ${{ parameters.owner }}
+```
+
+Every parameter referenced in skeleton or downstream steps must appear in a `values` map or step `input`.
+
+## Secret field (sensitive inputs)
+
+Use for passwords, tokens, and API keys — never a plain text field:
+
+```yaml
+apiToken:
+ title: API Token
+ type: string
+ ui:field: Secret
+ ui:options:
+ visibilityToggle: true
+```
+
+See `best-practices.md` tip 7 for step-level `${{ secrets.* }}` wiring.
+
+## Template filters in step expressions
+
+EntityPicker values are string refs (`component:default/my-service`). Use filters in step `input`, not skeleton files:
+
+```yaml
+repoName: ${{ parameters.component | parseEntityRef | pick('name') }}
+```
+
+See `best-practices.md` tip 5 for common filters.
+
+
diff --git a/skills/rhdh-templates/references/schemas/template-v1beta3.schema.json b/skills/rhdh-templates/references/schemas/template-v1beta3.schema.json
new file mode 100644
index 0000000..f285e2b
--- /dev/null
+++ b/skills/rhdh-templates/references/schemas/template-v1beta3.schema.json
@@ -0,0 +1,156 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://rhdh-skill.local/schemas/template-v1beta3.schema.json",
+ "title": "Backstage Software Template v1beta3",
+ "description": "Standalone subset of backstage/backstage Template.v1beta3.schema.json for local validation (no external $ref).",
+ "type": "object",
+ "required": ["apiVersion", "kind", "metadata", "spec"],
+ "additionalProperties": true,
+ "properties": {
+ "apiVersion": {
+ "type": "string",
+ "enum": ["scaffolder.backstage.io/v1beta3"]
+ },
+ "kind": {
+ "type": "string",
+ "enum": ["Template"]
+ },
+ "metadata": {
+ "type": "object",
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "title": { "type": "string", "minLength": 1 },
+ "description": { "type": "string" },
+ "tags": {
+ "type": "array",
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "annotations": {
+ "type": "object",
+ "additionalProperties": { "type": "string" }
+ }
+ },
+ "additionalProperties": true
+ },
+ "spec": {
+ "type": "object",
+ "required": ["type", "steps"],
+ "properties": {
+ "owner": { "type": "string", "minLength": 1 },
+ "type": { "type": "string", "minLength": 1 },
+ "lifecycle": { "type": "string", "minLength": 1 },
+ "parameters": {
+ "oneOf": [
+ { "$ref": "#/definitions/parameterForm" },
+ {
+ "type": "array",
+ "items": { "$ref": "#/definitions/parameterForm" }
+ }
+ ]
+ },
+ "steps": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#/definitions/templateStep" }
+ },
+ "output": {
+ "type": "object",
+ "properties": {
+ "links": {
+ "type": "array",
+ "items": { "$ref": "#/definitions/outputLink" }
+ },
+ "text": {
+ "type": "array",
+ "items": { "type": "object" }
+ }
+ },
+ "additionalProperties": { "type": "string" }
+ },
+ "secrets": {
+ "type": "object",
+ "properties": {
+ "schema": { "type": "object" }
+ },
+ "additionalProperties": true
+ }
+ },
+ "additionalProperties": true
+ }
+ },
+ "definitions": {
+ "parameterForm": {
+ "type": "object",
+ "properties": {
+ "title": { "type": "string" },
+ "description": { "type": "string" },
+ "required": {
+ "type": "array",
+ "items": { "type": "string" }
+ },
+ "properties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#/definitions/jsonSchemaProperty" }
+ },
+ "dependencies": { "type": "object" },
+ "oneOf": { "type": "array" },
+ "allOf": { "type": "array" }
+ },
+ "additionalProperties": true
+ },
+ "jsonSchemaProperty": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["string", "number", "integer", "boolean", "array", "object", "null"]
+ },
+ "title": { "type": "string" },
+ "description": { "type": "string" },
+ "enum": { "type": "array" },
+ "items": { "type": "object" },
+ "properties": { "type": "object" },
+ "ui:field": { "type": "string" },
+ "ui:widget": { "type": "string" },
+ "ui:options": { "type": "object" }
+ },
+ "additionalProperties": true
+ },
+ "templateStep": {
+ "type": "object",
+ "required": ["action"],
+ "properties": {
+ "id": { "type": "string", "minLength": 1 },
+ "name": { "type": "string" },
+ "action": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^[a-z][a-z0-9-]*:[a-z][a-zA-Z0-9]*$"
+ },
+ "input": { "type": "object" },
+ "if": {
+ "oneOf": [{ "type": "string" }, { "type": "boolean" }]
+ }
+ },
+ "additionalProperties": true
+ },
+ "outputLink": {
+ "type": "object",
+ "properties": {
+ "title": { "type": "string" },
+ "url": { "type": "string" },
+ "entityRef": { "type": "string" },
+ "icon": { "type": "string" },
+ "if": {
+ "oneOf": [{ "type": "string" }, { "type": "boolean" }]
+ }
+ },
+ "additionalProperties": true
+ }
+ }
+}
diff --git a/skills/rhdh-templates/references/template-structure.md b/skills/rhdh-templates/references/template-structure.md
new file mode 100644
index 0000000..5ca2364
--- /dev/null
+++ b/skills/rhdh-templates/references/template-structure.md
@@ -0,0 +1,125 @@
+# template.yaml Structure
+
+
+
+Load when writing, reviewing, or explaining `template.yaml` sections.
+
+
+
+
+
+## Minimal anatomy
+
+```yaml
+apiVersion: scaffolder.backstage.io/v1beta3
+kind: Template
+metadata:
+ name: example-template
+ title: Example Template
+ description: Short description shown in the Create UI
+ tags:
+ - recommended
+spec:
+ owner: group:default/platform-team
+ type: service
+ parameters: [] # form sections
+ steps: [] # scaffolder actions in order
+ output: {} # links shown after completion
+```
+
+## metadata
+
+| Field | Purpose |
+|-------|---------|
+| `name` | Machine ID (lowercase, hyphens) — unique in catalog |
+| `title` | Human label in Create UI |
+| `description` | Shown in template picker |
+| `tags` | Filtering in UI (`recommended` highlights template) |
+| `annotations.backstage.io/techdocs-ref` | Optional — `dir:.` when template ships TechDocs |
+
+## spec.parameters
+
+Array of **form sections**. Each section has `title`, optional `required`, `properties`, and optional `dependencies` for conditional fields.
+
+```yaml
+parameters:
+ - title: Component details
+ required:
+ - repoName
+ - owner
+ properties:
+ repoName:
+ title: Repository Name
+ type: string
+ owner:
+ title: Owner
+ type: string
+ ui:field: EntityPicker
+ ui:options:
+ catalogFilter:
+ kind: [Group, User]
+```
+
+Use `dependencies` + `oneOf` / `allOf` for conditional fields (see RHDH software-templates examples).
+
+## spec.steps
+
+Ordered list of actions. Each step needs unique `id`, human `name`, `action`, and `input`.
+
+```yaml
+steps:
+ - id: fetch-base
+ name: Fetch skeleton
+ action: fetch:template
+ input:
+ url: ./skeleton
+ values:
+ repoName: ${{ parameters.repoName }}
+ owner: ${{ parameters.owner }}
+
+ - id: publish
+ name: Publish to GitHub
+ action: publish:github
+ input:
+ repoUrl: ${{ parameters.repoUrl }}
+ description: ${{ parameters.description }}
+ defaultBranch: main
+
+ - id: register
+ name: Register in catalog
+ action: catalog:register
+ input:
+ repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
+ catalogInfoPath: /catalog-info.yaml
+```
+
+Reference prior step outputs as `${{ steps..output. }}`.
+
+## spec.output
+
+Optional links/icons after success:
+
+```yaml
+output:
+ links:
+ - title: Open repository
+ url: ${{ steps.publish.output.remoteUrl }}
+ - title: View in catalog
+ icon: catalog
+ entityRef: ${{ steps.register.output.entityRef }}
+```
+
+## copyWithoutTemplating
+
+On `fetch:template`, exclude paths that must not be Nunjucks-processed:
+
+```yaml
+input:
+ url: ./skeleton
+ copyWithoutTemplating:
+ - .github/workflows/
+```
+
+Use when workflow files are parameterized separately or wrapped in `{% raw %}`.
+
+
diff --git a/skills/rhdh-templates/references/templatize.md b/skills/rhdh-templates/references/templatize.md
new file mode 100644
index 0000000..9ef4b52
--- /dev/null
+++ b/skills/rhdh-templates/references/templatize.md
@@ -0,0 +1,156 @@
+# templatize — Convert Existing Codebase to Template
+
+
+
+- `conventions.md`
+- `template-structure.md`
+- `example-catalog.md` — upstream references by stack and workflow
+
+
+
+
+
+Templatize is the **highest-value workflow**: most platform engineers start from working code, not a blank template.
+
+## Overview
+
+```
+analyze → review → scaffold → template.yaml → location.yaml → fix-gotchas handoff
+```
+
+Interactive at every decision point. Do not auto-parameterize without user confirmation.
+
+---
+
+### Phase 1: Analyze
+
+1. Confirm source path — local directory or cloned repo.
+2. Run the analyzer script (deterministic scan):
+
+```bash
+python /scripts/analyze.py --path [--json]
+```
+
+Consume full JSON output. It reports:
+
+- `project_types` — detected stack markers (nodejs, java-maven, python, …)
+- `candidate_literals` — heuristic list with category, sources, and `usually_parameterize` hint
+- `workflow_files` — `.github/workflows/*` files that may need `{% raw %}`
+
+3. Supplement script output with manual review for business-specific literals the heuristics miss.
+4. Match reference templates for the detected stack:
+
+```bash
+python /scripts/list_examples.py \
+ --match "" \
+ --limit 3 --json
+```
+
+Suggest upstream examples to study (parameter forms, publish/register steps, CI patterns). Prefer `local_bundled` paths when offline validation is needed.
+5. List **candidate literals** for parameterization:
+
+| Category | Examples | Usually parameterize? |
+|----------|----------|----------------------|
+| Names | repo name, app name, namespace | Yes |
+| Org/owner | GitHub org, catalog owner | Yes |
+| URLs/hosts | registry, cluster API | Often |
+| Ports, replicas | `8080`, `3` | Sometimes |
+| Boilerplate | framework defaults, LICENSE | Rarely |
+
+6. Flag files needing `{% raw %}` (`.github/workflows/`, Helm templates).
+
+Output a **candidate table** for user review before editing files.
+
+---
+
+### Phase 2: Review (user gate)
+
+Present the candidate table. For each row ask: parameterize / keep literal / skip.
+
+Principles:
+
+- **Conservative** — when uncertain, keep literal; user can `add-parameter` later.
+- **Group parameters** — repo + owner + system belong in one form section.
+- **Match RHDH examples** — compare against [red-hat-developer-hub-software-templates](https://github.com/redhat-developer/red-hat-developer-hub-software-templates) patterns for similar stacks.
+
+Do not proceed until user confirms the parameter list.
+
+---
+
+### Phase 3: Scaffold
+
+1. Create `templates//skeleton/` in the **template repo** (not inside source repo unless user directs).
+2. Copy source files into `skeleton/`, preserving structure.
+3. Replace confirmed literals with Nunjucks `{{ values. }}` placeholders.
+4. Add `{% raw %}` blocks to CI/workflow files as needed.
+5. Include `catalog-info.yaml` in skeleton when the template should register a Component.
+
+---
+
+### Phase 4: template.yaml
+
+1. Read `template-structure.md` and `assets/examples/minimal-template/template.yaml`.
+2. Set `metadata.name`, `title`, `description`, `tags` from user input.
+3. Build `spec.parameters` from confirmed parameter list with appropriate `ui:field` widgets.
+4. Build `spec.steps`:
+
+ | Typical order | Action |
+ |---------------|--------|
+ | 1 | `fetch:template` → `./skeleton` with full `values` map |
+ | 2 | `publish:github` or user's SCM action |
+ | 3 | `catalog:register` |
+
+5. Add `spec.output` links to repo and catalog entity.
+
+---
+
+### Phase 5: location.yaml
+
+If root `location.yaml` missing or stale:
+
+```bash
+python /scripts/create_location.py --path [--json]
+```
+
+Templatize may emit `location.yaml` inline; `create-location` remains the standalone utility for updates.
+
+---
+
+### Phase 6: fix-gotchas handoff
+
+```bash
+python /scripts/fix_gotchas.py --path templates//template.yaml [--apply] [--json]
+```
+
+Review script output. Apply fixes with `--apply`. Re-run until no critical findings remain.
+
+Run local validation:
+
+```bash
+python /scripts/validate.py --path templates// --repo [--lint-skeleton] [--json]
+```
+
+When RHDH is reachable, optionally run `dry-run` with sample parameter values.
+
+### Critique and fix loop
+
+Before finishing, self-check and patch until no material issues remain:
+
+1. Did the user confirm every parameterized literal?
+2. Does `validate.py --json` report zero critical findings?
+3. Are all skeleton placeholders wired through `fetch:template` `values`?
+4. Are workflow files either wrapped in `{% raw %}` or listed in `copyWithoutTemplating`?
+
+Exit bar: template passes `validate.py` with no critical issues and matches `conventions.md`.
+
+
+
+
+
+- User confirmed parameter map before skeleton edits
+- `templates//template.yaml`, `skeleton/`, and root `location.yaml` exist
+- Skeleton uses `values.*` not `parameters.*`
+- `validate.py` reports zero critical issues
+- Template structure matches conventions.md
+
+
diff --git a/skills/rhdh-templates/references/validate.md b/skills/rhdh-templates/references/validate.md
new file mode 100644
index 0000000..0da3b07
--- /dev/null
+++ b/skills/rhdh-templates/references/validate.md
@@ -0,0 +1,81 @@
+# validate — Local Template Validation
+
+
+
+- `conventions.md`
+
+
+
+
+
+Validate without a running RHDH instance. Combines YAML structure checks, JSON Schema validation (structural subset always; full bundled schema when `jsonschema` is installed), packaged gotcha rules, optional repo `location.yaml` verification, and optional djLint for skeleton files.
+
+## Step 1: Run validate script
+
+```bash
+python /scripts/validate.py --path [--repo] [--lint-skeleton] [--json]
+```
+
+| Flag | Purpose |
+|------|---------|
+| `--repo` | Also check root `location.yaml` for the template repository |
+| `--lint-skeleton` | Run djLint on `skeleton/` when installed |
+| `--no-json-schema` | Skip optional full JSON Schema validation (structural checks still run) |
+| `--json` | Structured output for agent consumption |
+
+Consume full JSON output when `--json` is passed. Do not pipe through `head`, `tail`, or `grep`.
+
+## Step 2: Interpret results
+
+| Exit code | Meaning |
+|-----------|---------|
+| 0 | No critical findings |
+| 1 | Critical findings remain |
+| 2 | Usage error |
+
+Severity levels:
+
+- **critical** — likely Template Editor failure (wrong apiVersion, action casing, invalid schema, v1beta2 expressions)
+- **warning** — should fix before merge (missing parameters, unknown parameter refs, workflow raw blocks)
+- **info** — optional tooling skipped (PyYAML/djlint/jsonschema not installed)
+
+## Step 3: Fix and re-run
+
+1. Run `fix-gotchas` with `--apply` for auto-fixable critical issues.
+2. Manually address remaining findings.
+3. Re-run `validate.py` until `critical_count` is 0.
+
+For remote execution validation against a live RHDH instance, use `dry-run` after local validation passes.
+
+Review warning/info findings against the `` in `best-practices.md` (tags, docs, Secret fields, maintenance).
+
+## Nunjucks skeleton lint (`--lint-skeleton`)
+
+Use when the user asks to lint Nunjucks, run djLint, or check skeleton syntax only. There is no separate script — pass `--lint-skeleton` to `validate.py`.
+
+Install djLint when missing:
+
+```bash
+pip install djlint
+# or: uv tool install djlint
+```
+
+If djLint is absent, `validate` reports an **info** finding and continues — YAML and gotcha checks still run.
+
+Common Nunjucks fixes:
+
+- Wrap GitHub Actions `${{ }}` in `{% raw %}…{% endraw %}` (see `conventions.md`)
+- Use `{{ values.field }}` not `{{ parameters.field }}` in skeleton files
+- Add missing `{% endif %}` / `{% endfor %}` closers
+
+For pre-merge checks, run full `validate` with and without `--lint-skeleton`, or combine both in one invocation.
+
+
+
+
+
+- `validate.py --json` reports `ok: true` and `critical_count: 0`
+- User understands any warnings and whether to fix them before merge
+- When `--lint-skeleton` is used, Nunjucks issues are surfaced or djlint skip is documented
+
+
diff --git a/skills/rhdh-templates/scripts/analyze.py b/skills/rhdh-templates/scripts/analyze.py
new file mode 100644
index 0000000..9db5782
--- /dev/null
+++ b/skills/rhdh-templates/scripts/analyze.py
@@ -0,0 +1,329 @@
+#!/usr/bin/env python3
+"""Analyze a source codebase for Software Template templatize workflow.
+
+Detects project type, candidate literals for parameterization, and files
+that likely need {% raw %} blocks or copyWithoutTemplating.
+
+Stdlib only per project ADR-0002.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import re
+import sys
+from pathlib import Path
+
+EXIT_SUCCESS = 0
+EXIT_USAGE = 2
+
+_no_color = os.environ.get("NO_COLOR") is not None
+_is_tty = sys.stderr.isatty() and not _no_color
+
+TEXT_EXTENSIONS = {
+ ".md",
+ ".yaml",
+ ".yml",
+ ".json",
+ ".xml",
+ ".properties",
+ ".env",
+ ".txt",
+ ".gradle",
+ ".kts",
+ ".toml",
+ ".ini",
+ ".cfg",
+ ".sh",
+ ".bash",
+ ".Dockerfile",
+}
+TEXT_FILENAMES = {
+ "Dockerfile",
+ "Makefile",
+ "pom.xml",
+ "build.gradle",
+ "settings.gradle",
+ "go.mod",
+ "catalog-info.yaml",
+ "package.json",
+ "pyproject.toml",
+ "Chart.yaml",
+}
+
+PROJECT_MARKERS = [
+ ("nodejs", ["package.json"]),
+ ("java-maven", ["pom.xml"]),
+ ("java-gradle", ["build.gradle", "build.gradle.kts"]),
+ ("python", ["pyproject.toml", "setup.py", "requirements.txt"]),
+ ("go", ["go.mod"]),
+ ("dotnet", ["*.csproj"]),
+ ("helm", ["Chart.yaml"]),
+ ("quarkus", ["pom.xml", ".quarkus"]),
+ ("spring-boot", ["pom.xml", "src/main/resources/application.properties"]),
+ ("kubernetes", ["k8s", "kubernetes", "deploy", "manifests"]),
+]
+
+GITHUB_URL = re.compile(r"github\.com[/:]([\w.-]+)/([\w.-]+)")
+GITLAB_URL = re.compile(r"gitlab\.(?:com|[^/]+)[/:]([\w.-]+)/([\w.-]+)")
+K8S_NAME = re.compile(r"^\s*name:\s*['\"]?([\w.-]+)['\"]?\s*$", re.MULTILINE)
+K8S_NAMESPACE = re.compile(r"^\s*namespace:\s*['\"]?([\w.-]+)['\"]?\s*$", re.MULTILINE)
+PORT = re.compile(r"\b(?:port|PORT)\s*[=:]\s*(\d{2,5})\b")
+
+
+def _read_text(path: Path, max_bytes: int = 256_000) -> str | None:
+ try:
+ data = path.read_bytes()
+ except OSError:
+ return None
+ if len(data) > max_bytes:
+ data = data[:max_bytes]
+ try:
+ return data.decode("utf-8")
+ except UnicodeDecodeError:
+ return None
+
+
+def _is_text_candidate(path: Path) -> bool:
+ if path.name in TEXT_FILENAMES:
+ return True
+ if path.suffix.lower() in TEXT_EXTENSIONS:
+ return True
+ return False
+
+
+def detect_project_types(root: Path) -> list[str]:
+ found: list[str] = []
+ names = {p.name for p in root.rglob("*") if p.is_file()}
+ rel_dirs = {p.relative_to(root).parts[0] for p in root.rglob("*") if p.is_dir() and p != root}
+
+ for project_type, markers in PROJECT_MARKERS:
+ for marker in markers:
+ if marker.startswith("*"):
+ if any(n.endswith(marker[1:]) for n in names):
+ found.append(project_type)
+ break
+ elif marker in names:
+ found.append(project_type)
+ break
+ elif marker in rel_dirs:
+ found.append(project_type)
+ break
+ return sorted(set(found))
+
+
+def find_workflow_files(root: Path) -> list[dict]:
+ results = []
+ for path in sorted(root.rglob("*")):
+ if not path.is_file():
+ continue
+ rel = path.relative_to(root).as_posix()
+ if ".github/workflows" not in rel:
+ continue
+ text = _read_text(path)
+ if text is None:
+ continue
+ needs_raw = "{{" in text and "{% raw %}" not in text
+ results.append(
+ {
+ "path": rel,
+ "needs_raw_block": needs_raw,
+ "reason": "Contains '{{' without {% raw %} wrapper" if needs_raw else None,
+ }
+ )
+ return results
+
+
+def _add_candidate(
+ candidates: dict[str, dict],
+ value: str,
+ *,
+ category: str,
+ source: str,
+ usually_parameterize: str = "maybe",
+) -> None:
+ value = value.strip().strip("'\"")
+ if not value or len(value) < 2:
+ return
+ if value in {".", "..", "main", "master", "true", "false", "null"}:
+ return
+ key = value.lower()
+ if key not in candidates:
+ candidates[key] = {
+ "value": value,
+ "category": category,
+ "sources": [source],
+ "usually_parameterize": usually_parameterize,
+ }
+ elif source not in candidates[key]["sources"]:
+ candidates[key]["sources"].append(source)
+
+
+def extract_from_package_json(path: Path, rel: str, candidates: dict[str, dict]) -> None:
+ text = _read_text(path)
+ if not text:
+ return
+ try:
+ data = json.loads(text)
+ except json.JSONDecodeError:
+ return
+ if isinstance(data.get("name"), str):
+ _add_candidate(
+ candidates, data["name"], category="name", source=rel, usually_parameterize="yes"
+ )
+ if isinstance(data.get("description"), str) and len(data["description"]) < 120:
+ _add_candidate(
+ candidates,
+ data["description"],
+ category="description",
+ source=rel,
+ usually_parameterize="sometimes",
+ )
+
+
+def extract_from_catalog_info(path: Path, rel: str, candidates: dict[str, dict]) -> None:
+ text = _read_text(path)
+ if not text:
+ return
+ for match in re.finditer(r"^\s*name:\s*['\"]?([\w.-]+)['\"]?\s*$", text, re.MULTILINE):
+ _add_candidate(
+ candidates, match.group(1), category="name", source=rel, usually_parameterize="yes"
+ )
+ for match in re.finditer(r"^\s*owner:\s*['\"]?([\w:./-]+)['\"]?\s*$", text, re.MULTILINE):
+ _add_candidate(
+ candidates, match.group(1), category="owner", source=rel, usually_parameterize="yes"
+ )
+
+
+def extract_from_pom(path: Path, rel: str, candidates: dict[str, dict]) -> None:
+ text = _read_text(path)
+ if not text:
+ return
+ for tag in ("artifactId", "groupId", "name"):
+ for match in re.finditer(rf"<{tag}>([^<]+){tag}>", text):
+ cat = "name" if tag != "groupId" else "org"
+ usually = "yes" if tag in {"artifactId", "name"} else "often"
+ _add_candidate(
+ candidates, match.group(1), category=cat, source=rel, usually_parameterize=usually
+ )
+
+
+def extract_urls(text: str, rel: str, candidates: dict[str, dict]) -> None:
+ for match in GITHUB_URL.finditer(text):
+ org, repo = match.group(1), match.group(2).removesuffix(".git")
+ _add_candidate(candidates, org, category="org", source=rel, usually_parameterize="yes")
+ _add_candidate(candidates, repo, category="name", source=rel, usually_parameterize="yes")
+ for match in GITLAB_URL.finditer(text):
+ org, repo = match.group(1), match.group(2).removesuffix(".git")
+ _add_candidate(candidates, org, category="org", source=rel, usually_parameterize="yes")
+ _add_candidate(candidates, repo, category="name", source=rel, usually_parameterize="yes")
+
+
+def scan_candidates(root: Path) -> list[dict]:
+ candidates: dict[str, dict] = {}
+ for path in sorted(root.rglob("*")):
+ if not path.is_file():
+ continue
+ rel = path.relative_to(root).as_posix()
+ if path.name == "package.json":
+ extract_from_package_json(path, rel, candidates)
+ elif path.name == "catalog-info.yaml":
+ extract_from_catalog_info(path, rel, candidates)
+ elif path.name == "pom.xml":
+ extract_from_pom(path, rel, candidates)
+ if not _is_text_candidate(path):
+ continue
+ text = _read_text(path)
+ if not text:
+ continue
+ extract_urls(text, rel, candidates)
+ for match in K8S_NAME.finditer(text):
+ _add_candidate(
+ candidates,
+ match.group(1),
+ category="name",
+ source=rel,
+ usually_parameterize="often",
+ )
+ for match in K8S_NAMESPACE.finditer(text):
+ _add_candidate(
+ candidates,
+ match.group(1),
+ category="namespace",
+ source=rel,
+ usually_parameterize="often",
+ )
+ for match in PORT.finditer(text):
+ _add_candidate(
+ candidates,
+ match.group(1),
+ category="port",
+ source=rel,
+ usually_parameterize="sometimes",
+ )
+ rows = list(candidates.values())
+ rows.sort(key=lambda r: (r["usually_parameterize"] != "yes", r["category"], r["value"]))
+ return rows
+
+
+def analyze(root: Path) -> dict:
+ project_types = detect_project_types(root)
+ workflows = find_workflow_files(root)
+ candidates = scan_candidates(root)
+ return {
+ "ok": True,
+ "path": str(root),
+ "project_types": project_types or ["unknown"],
+ "file_count": sum(1 for p in root.rglob("*") if p.is_file()),
+ "workflow_files": workflows,
+ "candidate_literals": candidates,
+ "candidate_count": len(candidates),
+ }
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Analyze a codebase for RHDH Software Template templatize workflow.",
+ )
+ parser.add_argument("source", type=Path, nargs="?", help="Source directory to analyze")
+ parser.add_argument("--path", type=Path, help="Alias for source directory")
+ parser.add_argument("--json", action="store_true", help="Emit JSON result")
+ args = parser.parse_args()
+
+ source = args.path or args.source
+ if source is None:
+ parser.print_help()
+ return EXIT_USAGE
+
+ root = source.resolve()
+ if not root.exists() or not root.is_dir():
+ print(f"Not a directory: {root}", file=sys.stderr)
+ return EXIT_USAGE
+
+ result = analyze(root)
+
+ if args.json:
+ print(json.dumps(result, indent=2 if _is_tty else None))
+ else:
+ print(f"Source: {root}")
+ print(f"Project types: {', '.join(result['project_types'])}")
+ print(f"Files: {result['file_count']}")
+ print(f"Candidate literals: {result['candidate_count']}")
+ for row in result["candidate_literals"][:20]:
+ print(
+ f" [{row['category']}] {row['value']} "
+ f"({row['usually_parameterize']}) — {', '.join(row['sources'][:2])}"
+ )
+ if result["workflow_files"]:
+ print("Workflow files:")
+ for wf in result["workflow_files"]:
+ flag = "needs raw" if wf["needs_raw_block"] else "ok"
+ print(f" {wf['path']}: {flag}")
+
+ return EXIT_SUCCESS
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/rhdh-templates/scripts/command-metadata.json b/skills/rhdh-templates/scripts/command-metadata.json
new file mode 100644
index 0000000..ec5ee9d
--- /dev/null
+++ b/skills/rhdh-templates/scripts/command-metadata.json
@@ -0,0 +1,54 @@
+{
+ "init": {
+ "description": "Check required tooling, scaffold a recommended template-authoring layout, and optionally verify RHDH connectivity.",
+ "argumentHint": "[--rhdh-url URL] [--path DIR]"
+ },
+ "templatize": {
+ "description": "Convert an existing codebase into a parameterized Software Template via interactive analyze → review → scaffold → template.yaml → location.yaml flow.",
+ "argumentHint": "[source directory or repo path]"
+ },
+ "create": {
+ "description": "Guide from-scratch Software Template creation when no reference codebase exists.",
+ "argumentHint": "[template name or description]"
+ },
+ "add-parameter": {
+ "description": "Add a parameter (or parameter group) to an existing template.yaml with RHDH form conventions.",
+ "argumentHint": "[parameter name or description]"
+ },
+ "add-step": {
+ "description": "Add a scaffolder step to an existing template.yaml with correct action IDs and input wiring.",
+ "argumentHint": "[step description or action name]"
+ },
+ "add-skeleton": {
+ "description": "Add or extend skeleton files for an existing template, including Nunjucks parameterization.",
+ "argumentHint": "[file path or skeleton description]"
+ },
+ "create-location": {
+ "description": "Generate or update location.yaml that registers all template.yaml files under templates/.",
+ "argumentHint": "[--path DIR] [--name NAME]"
+ },
+ "fix-gotchas": {
+ "description": "Apply common RHDH template corrections (action casing, secrets syntax, raw/endraw, apiVersion) using packaged rules.",
+ "argumentHint": "[template directory or template.yaml path]"
+ },
+ "validate": {
+ "description": "Local template validation — YAML structure, JSON Schema checks, gotcha rules, optional location.yaml and djLint skeleton lint (--lint-skeleton). No RHDH instance required.",
+ "argumentHint": "[--path DIR] [--repo] [--lint-skeleton] [--no-json-schema]"
+ },
+ "list-actions": {
+ "description": "List Scaffolder actions from a running RHDH instance via GET /api/scaffolder/v2/actions.",
+ "argumentHint": "[--rhdh-url URL] [--filter substring]"
+ },
+ "dry-run": {
+ "description": "Execute template dry-run against RHDH Scaffolder v2 API without creating real resources.",
+ "argumentHint": "[--rhdh-url URL] [--path template-dir] [--values values.json]"
+ },
+ "explain-action": {
+ "description": "Show input/output JSON Schema for a Scaffolder action or parameter schema for a catalog Template.",
+ "argumentHint": "[--action id | --template-ref template:ns/name] [--rhdh-url URL]"
+ },
+ "examples": {
+ "description": "Browse curated reference Software Templates from the official library, AI quickstarts, and bundled skill examples.",
+ "argumentHint": "[--match \"intent\" | --category NAME | --stack NAME | --recommended]"
+ }
+}
diff --git a/skills/rhdh-templates/scripts/create_location.py b/skills/rhdh-templates/scripts/create_location.py
new file mode 100755
index 0000000..3ae4b3e
--- /dev/null
+++ b/skills/rhdh-templates/scripts/create_location.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+"""Generate or update location.yaml for an RHDH template repository.
+
+Discovers templates/**/template.yaml and writes a Location entity at repo root.
+
+Stdlib only per project ADR-0002.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import sys
+from pathlib import Path
+
+EXIT_SUCCESS = 0
+EXIT_FAILURE = 1
+EXIT_USAGE = 2
+
+_no_color = os.environ.get("NO_COLOR") is not None
+_is_tty = sys.stderr.isatty() and not _no_color
+
+
+def discover_templates(root: Path) -> list[str]:
+ templates_dir = root / "templates"
+ if not templates_dir.is_dir():
+ return []
+ found: list[str] = []
+ for path in sorted(templates_dir.rglob("template.yaml")):
+ rel = path.relative_to(root).as_posix()
+ found.append(rel)
+ return found
+
+
+def build_location_yaml(name: str, description: str, targets: list[str]) -> str:
+ lines = [
+ "apiVersion: backstage.io/v1alpha1",
+ "kind: Location",
+ "metadata:",
+ f" name: {name}",
+ f" description: {description}",
+ "spec:",
+ " targets:",
+ ]
+ if targets:
+ for target in targets:
+ lines.append(f" - ./{target}")
+ else:
+ lines.append(" - ./templates/**/template.yaml")
+ lines.append("")
+ return "\n".join(lines)
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="Generate location.yaml for template repo.")
+ parser.add_argument("--path", type=Path, default=Path.cwd(), help="Repository root")
+ parser.add_argument("--name", help="metadata.name for Location (default:
-templates)")
+ parser.add_argument("--description", help="metadata.description")
+ parser.add_argument("--json", action="store_true", help="Emit JSON result")
+ parser.add_argument("--dry-run", action="store_true", help="Print YAML without writing")
+ args = parser.parse_args()
+
+ root = args.path.resolve()
+ if not root.exists():
+ root.mkdir(parents=True, exist_ok=True)
+ elif not root.is_dir():
+ print(f"Not a directory: {root}", file=sys.stderr)
+ return EXIT_USAGE
+
+ templates = discover_templates(root)
+ name = args.name or f"{root.name}-templates"
+ description = args.description or f"Software Templates in {root.name}"
+ content = build_location_yaml(name, description, templates)
+
+ location_path = root / "location.yaml"
+ written = False
+ if not args.dry_run:
+ location_path.write_text(content, encoding="utf-8")
+ written = True
+
+ result = {
+ "ok": True,
+ "path": str(root),
+ "location_file": str(location_path),
+ "written": written,
+ "template_count": len(templates),
+ "templates": templates,
+ "metadata_name": name,
+ }
+
+ if args.json:
+ print(json.dumps(result, indent=2 if _is_tty else None))
+ else:
+ print(f"Location: {location_path}")
+ print(f"Templates discovered: {len(templates)}")
+ for t in templates:
+ print(f" - {t}")
+ if written:
+ print("Wrote location.yaml")
+ elif args.dry_run:
+ print("--- dry-run ---")
+ print(content)
+
+ return EXIT_SUCCESS
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/rhdh-templates/scripts/dry_run.py b/skills/rhdh-templates/scripts/dry_run.py
new file mode 100755
index 0000000..54b8a22
--- /dev/null
+++ b/skills/rhdh-templates/scripts/dry_run.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python3
+"""Dry-run a Software Template against a running RHDH Scaffolder.
+
+Uses POST /api/scaffolder/v2/dry-run with template.yaml, skeleton files, and
+parameter values.
+
+Requires PyYAML to parse template.yaml (available in project dev dependencies).
+
+Stdlib + optional PyYAML per project ADR-0002.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import sys
+from pathlib import Path
+
+EXIT_SUCCESS = 0
+EXIT_FAILURE = 1
+EXIT_USAGE = 2
+
+SCRIPTS_DIR = Path(__file__).resolve().parent
+sys.path.insert(0, str(SCRIPTS_DIR))
+
+from scaffolder_api import dry_run, load_directory_contents # noqa: E402
+
+_no_color = os.environ.get("NO_COLOR") is not None
+_is_tty = sys.stderr.isatty() and not _no_color
+
+
+def load_yaml_file(path: Path) -> dict:
+ try:
+ import yaml # type: ignore[import-untyped]
+ except ImportError as exc:
+ raise RuntimeError(
+ "PyYAML is required for dry-run. Install dev deps: uv sync --extra dev"
+ ) from exc
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
+ if not isinstance(data, dict):
+ raise ValueError(f"{path} must contain a YAML mapping")
+ return data
+
+
+def resolve_template_dir(path: Path) -> tuple[Path, Path]:
+ path = path.resolve()
+ if path.is_file():
+ template_yaml = path
+ template_dir = path.parent
+ elif path.is_dir():
+ template_dir = path
+ template_yaml = path / "template.yaml"
+ else:
+ raise FileNotFoundError(f"Path not found: {path}")
+ if not template_yaml.is_file():
+ raise FileNotFoundError(f"No template.yaml at {template_yaml}")
+ return template_yaml, template_dir
+
+
+def summarize_log(log: list) -> list[str]:
+ lines: list[str] = []
+ for entry in log or []:
+ body = entry.get("body") if isinstance(entry, dict) else None
+ if isinstance(body, dict) and body.get("message"):
+ lines.append(str(body["message"]))
+ return lines
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="Dry-run an RHDH Software Template.")
+ parser.add_argument("--rhdh-url", required=True, help="RHDH base URL")
+ parser.add_argument(
+ "--path",
+ required=True,
+ type=Path,
+ help="Template directory or template.yaml path",
+ )
+ parser.add_argument(
+ "--values",
+ type=Path,
+ help="JSON file with parameter values (default: {})",
+ )
+ parser.add_argument(
+ "--secrets",
+ type=Path,
+ help="JSON file with secrets map (optional)",
+ )
+ parser.add_argument(
+ "--skeleton-dir",
+ type=Path,
+ help="Skeleton directory (default: /skeleton)",
+ )
+ parser.add_argument("--token", help="Bearer token (default: RHDH_TOKEN or BACKSTAGE_TOKEN env)")
+ parser.add_argument("--json", action="store_true", help="Emit JSON result")
+ args = parser.parse_args()
+
+ try:
+ template_yaml, template_dir = resolve_template_dir(args.path)
+ template = load_yaml_file(template_yaml)
+ values: dict = {}
+ if args.values:
+ values = json.loads(args.values.read_text(encoding="utf-8"))
+ if not isinstance(values, dict):
+ raise ValueError("--values file must contain a JSON object")
+ secrets: dict[str, str] | None = None
+ if args.secrets:
+ raw = json.loads(args.secrets.read_text(encoding="utf-8"))
+ if not isinstance(raw, dict):
+ raise ValueError("--secrets file must contain a JSON object")
+ secrets = {str(k): str(v) for k, v in raw.items()}
+
+ directory_contents: list[dict[str, str]] = []
+ content_root = args.skeleton_dir or template_dir
+ if content_root.is_dir():
+ if args.skeleton_dir:
+ prefix = content_root.relative_to(template_dir).as_posix()
+ for item in load_directory_contents(content_root):
+ directory_contents.append(
+ {
+ "path": f"{prefix}/{item['path']}",
+ "base64Content": item["base64Content"],
+ }
+ )
+ else:
+ directory_contents = load_directory_contents(content_root)
+
+ response = dry_run(
+ args.rhdh_url,
+ template=template,
+ values=values,
+ directory_contents=directory_contents,
+ secrets=secrets,
+ token=args.token,
+ )
+ except (RuntimeError, ValueError, FileNotFoundError, json.JSONDecodeError) as exc:
+ print(str(exc), file=sys.stderr)
+ return EXIT_FAILURE if isinstance(exc, RuntimeError) else EXIT_USAGE
+
+ log_lines = summarize_log(response.get("log", []))
+ output_files = response.get("directoryContents") or []
+ result = {
+ "ok": True,
+ "template": str(template_yaml),
+ "log_line_count": len(log_lines),
+ "log": log_lines,
+ "output_file_count": len(output_files),
+ "output": response.get("output"),
+ "steps": response.get("steps"),
+ }
+
+ if args.json:
+ print(json.dumps(result, indent=2 if _is_tty else None))
+ else:
+ print(f"Dry-run succeeded for {template_yaml}")
+ print(f"Log lines: {len(log_lines)}")
+ for line in log_lines[-10:]:
+ print(f" {line}")
+ print(f"Output files: {len(output_files)}")
+ if response.get("output"):
+ print("Output:")
+ print(json.dumps(response["output"], indent=2))
+
+ return EXIT_SUCCESS
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/rhdh-templates/scripts/explain_action.py b/skills/rhdh-templates/scripts/explain_action.py
new file mode 100755
index 0000000..1a490a5
--- /dev/null
+++ b/skills/rhdh-templates/scripts/explain_action.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+"""Explain a Scaffolder action or template parameter schema.
+
+For actions: fetches GET /api/scaffolder/v2/actions and returns the matching
+action's input/output JSON Schema.
+
+For templates: fetches GET /api/scaffolder/v2/templates/:ns/:kind/:name/parameter-schema.
+
+Stdlib only per project ADR-0002.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import sys
+from pathlib import Path
+
+EXIT_SUCCESS = 0
+EXIT_FAILURE = 1
+EXIT_USAGE = 2
+
+SCRIPTS_DIR = Path(__file__).resolve().parent
+sys.path.insert(0, str(SCRIPTS_DIR))
+
+from scaffolder_api import get_action_schema, get_template_parameter_schema # noqa: E402
+
+_no_color = os.environ.get("NO_COLOR") is not None
+_is_tty = sys.stderr.isatty() and not _no_color
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="Explain Scaffolder action or template schema.")
+ parser.add_argument("--rhdh-url", required=True, help="RHDH base URL")
+ parser.add_argument("--action", help="Action id (e.g. publish:github)")
+ parser.add_argument(
+ "--template-ref",
+ help="Catalog template ref (e.g. template:default/my-template)",
+ )
+ parser.add_argument("--token", help="Bearer token (default: RHDH_TOKEN or BACKSTAGE_TOKEN env)")
+ parser.add_argument("--json", action="store_true", help="Emit JSON result")
+ args = parser.parse_args()
+
+ if bool(args.action) == bool(args.template_ref):
+ print("Provide exactly one of --action or --template-ref", file=sys.stderr)
+ return EXIT_USAGE
+
+ try:
+ if args.action:
+ action = get_action_schema(args.rhdh_url, args.action, token=args.token)
+ if action is None:
+ print(f"Action not found: {args.action}", file=sys.stderr)
+ return EXIT_FAILURE
+ result = {
+ "ok": True,
+ "type": "action",
+ "id": action.get("id"),
+ "description": action.get("description"),
+ "schema": action.get("schema"),
+ "examples": action.get("examples"),
+ }
+ else:
+ schema = get_template_parameter_schema(
+ args.rhdh_url,
+ args.template_ref,
+ token=args.token,
+ )
+ result = {
+ "ok": True,
+ "type": "template-parameter-schema",
+ "template_ref": args.template_ref,
+ "schema": schema,
+ }
+ except (RuntimeError, ValueError) as exc:
+ print(str(exc), file=sys.stderr)
+ return EXIT_FAILURE
+
+ if args.json:
+ print(json.dumps(result, indent=2 if _is_tty else None))
+ else:
+ if result["type"] == "action":
+ print(f"Action: {result['id']}")
+ if result.get("description"):
+ print(result["description"])
+ schema = result.get("schema") or {}
+ if schema.get("input"):
+ print("\nInput schema:")
+ print(json.dumps(schema["input"], indent=2))
+ if schema.get("output"):
+ print("\nOutput schema:")
+ print(json.dumps(schema["output"], indent=2))
+ else:
+ print(f"Template: {result['template_ref']}")
+ print(json.dumps(result["schema"], indent=2))
+
+ return EXIT_SUCCESS
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/rhdh-templates/scripts/fix_gotchas.py b/skills/rhdh-templates/scripts/fix_gotchas.py
new file mode 100755
index 0000000..c953d37
--- /dev/null
+++ b/skills/rhdh-templates/scripts/fix_gotchas.py
@@ -0,0 +1,367 @@
+#!/usr/bin/env python3
+"""Check and fix common RHDH Software Template gotchas.
+
+Loads rules from references/gotchas-rules.json adjacent to the skill root.
+
+Stdlib only per project ADR-0002.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import re
+import sys
+from pathlib import Path
+
+EXIT_SUCCESS = 0
+EXIT_FINDINGS = 1
+EXIT_USAGE = 2
+
+_no_color = os.environ.get("NO_COLOR") is not None
+_is_tty = sys.stderr.isatty() and not _no_color
+
+TOKEN_PATTERNS = [
+ re.compile(r"(?i)(ghp_[a-zA-Z0-9]{20,})"),
+ re.compile(r"(?i)(github_pat_[a-zA-Z0-9_]{20,})"),
+ re.compile(r"(?i)(glpat-[a-zA-Z0-9\-]{20,})"),
+ re.compile(r"(?i)token:\s*['\"]?[a-zA-Z0-9_\-]{24,}"),
+]
+
+ACTION_PATTERN = re.compile(r"^\s*action:\s*([a-zA-Z]+:[a-zA-Z][a-zA-Z0-9]*)", re.MULTILINE)
+V1BETA2_EXPR = re.compile(r"(? list[dict]:
+ rules_path = skill_dir / "references" / "gotchas-rules.json"
+ data = json.loads(rules_path.read_text(encoding="utf-8"))
+ return data.get("rules", [])
+
+
+def resolve_template_path(path: Path) -> Path:
+ path = path.resolve()
+ if path.is_file():
+ return path
+ if path.is_dir():
+ candidate = path / "template.yaml"
+ if candidate.is_file():
+ return candidate
+ raise FileNotFoundError(f"No template.yaml at {path}")
+
+
+def check_api_version(content: str) -> list[dict]:
+ findings = []
+ match = API_VERSION_PATTERN.search(content)
+ if not match:
+ findings.append({"line": 0, "message": "Missing apiVersion field"})
+ elif "scaffolder.backstage.io/v1beta3" not in match.group(1):
+ findings.append({"line": 0, "message": f"Unexpected apiVersion: {match.group(1).strip()}"})
+ return findings
+
+
+def fix_api_version(content: str) -> str:
+ if API_VERSION_PATTERN.search(content):
+ return API_VERSION_PATTERN.sub(
+ "apiVersion: scaffolder.backstage.io/v1beta3", content, count=1
+ )
+ return "apiVersion: scaffolder.backstage.io/v1beta3\n" + content
+
+
+def check_action_casing(content: str) -> list[dict]:
+ findings = []
+ for match in ACTION_PATTERN.finditer(content):
+ action = match.group(1)
+ if ":" not in action:
+ continue
+ ns, name = action.split(":", 1)
+ if name != name.lower() and any(c.isupper() for c in name):
+ line = content[: match.start()].count("\n") + 1
+ findings.append(
+ {
+ "line": line,
+ "message": f"Action '{action}' may use wrong casing — expected camelCase segment",
+ "action": action,
+ }
+ )
+ return findings
+
+
+def fix_action_casing(content: str) -> str:
+ def repl(m: re.Match) -> str:
+ action = m.group(1)
+ if ":" not in action:
+ return m.group(0)
+ ns, name = action.split(":", 1)
+ if not name:
+ return m.group(0)
+ if name != name.lower():
+ # Backstage/RHDH built-in actions use lowercase segments (publish:github).
+ fixed = f"{ns}:{name.lower()}"
+ else:
+ fixed = action
+ return f"action: {fixed}"
+
+ return ACTION_PATTERN.sub(repl, content)
+
+
+def check_v1beta2_expressions(content: str) -> list[dict]:
+ findings = []
+ for match in V1BETA2_EXPR.finditer(content):
+ line = content[: match.start()].count("\n") + 1
+ findings.append(
+ {
+ "line": line,
+ "message": "v1beta2 expression '{{ parameters.' in v1beta3 template",
+ }
+ )
+ break
+ return findings
+
+
+def convert_expressions(content: str) -> str:
+ return re.sub(
+ r"\{\{\s*parameters\.([^}]+)\}\}",
+ r"${{ parameters.\1 }}",
+ content,
+ )
+
+
+def check_hardcoded_secrets(content: str) -> list[dict]:
+ findings = []
+ for i, line in enumerate(content.splitlines(), start=1):
+ for pattern in TOKEN_PATTERNS:
+ if pattern.search(line) and "secrets." not in line:
+ findings.append({"line": i, "message": "Possible hardcoded token in step input"})
+ break
+ return findings
+
+
+def check_missing_section(content: str, section: str) -> list[dict]:
+ if re.search(rf"^ {section}:", content, re.MULTILINE):
+ return []
+ return [{"line": 0, "message": f"Missing spec.{section} section"}]
+
+
+def check_fetch_template_values(content: str) -> list[dict]:
+ findings = []
+ blocks = re.split(r"\n\s*-\s+id:", content)
+ for block in blocks:
+ if "action: fetch:template" not in block:
+ continue
+ if "values:" not in block:
+ findings.append({"line": 0, "message": "fetch:template step missing values map"})
+ return findings
+
+
+def check_skeleton_parameters(template_path: Path) -> list[dict]:
+ findings = []
+ skeleton = template_path.parent / "skeleton"
+ if not skeleton.is_dir():
+ return findings
+ for file in skeleton.rglob("*"):
+ if not file.is_file():
+ continue
+ try:
+ text = file.read_text(encoding="utf-8")
+ except (UnicodeDecodeError, OSError):
+ continue
+ if "parameters." in text and "values." not in text:
+ findings.append(
+ {
+ "line": 0,
+ "message": f"Skeleton file {file.relative_to(template_path.parent)} may use parameters.* instead of values.*",
+ }
+ )
+ return findings
+
+
+def check_workflow_raw_blocks(template_path: Path) -> list[dict]:
+ findings = []
+ skeleton = template_path.parent / "skeleton"
+ if not skeleton.is_dir():
+ return findings
+ for wf in skeleton.rglob(".github/workflows/*"):
+ if not wf.is_file():
+ continue
+ try:
+ text = wf.read_text(encoding="utf-8")
+ except (UnicodeDecodeError, OSError):
+ continue
+ if "{{" in text and "{% raw %}" not in text:
+ findings.append(
+ {
+ "line": 0,
+ "message": f"Workflow {wf.relative_to(template_path.parent)} contains '{{' without raw block",
+ }
+ )
+ return findings
+
+
+SENSITIVE_PARAM_NAMES = re.compile(
+ r"(?i)(password|passwd|secret|api[_-]?key|auth[_-]?token|access[_-]?token|token)"
+)
+
+
+def check_metadata_tags(content: str) -> list[dict]:
+ if re.search(r"^\s*tags:\s*\n\s*-\s+", content, re.MULTILINE):
+ return []
+ if re.search(r"^\s*tags:\s*\[.+\]", content, re.MULTILINE):
+ return []
+ return [{"line": 0, "message": "metadata.tags is missing — add tags for Create UI filtering"}]
+
+
+def check_sensitive_param_secret_field(content: str) -> list[dict]:
+ """Flag password/token-like parameter keys without ui:field: Secret."""
+ findings = []
+ # Focus on spec.parameters section only
+ spec_match = re.search(r"^spec:\s*\n(.*)", content, re.MULTILINE | re.DOTALL)
+ if not spec_match:
+ return findings
+ params_section = spec_match.group(1)
+ if "parameters:" not in params_section:
+ return findings
+
+ skip_keys = {"properties", "title", "required", "dependencies", "oneOf", "allOf", "enum"}
+ lines = params_section.splitlines()
+ i = 0
+ while i < len(lines):
+ key_match = re.match(r"^\s{8,}([a-zA-Z][a-zA-Z0-9]*):\s*$", lines[i])
+ if key_match:
+ key = key_match.group(1)
+ if key not in skip_keys and SENSITIVE_PARAM_NAMES.search(key):
+ block_end = min(i + 12, len(lines))
+ block = "\n".join(lines[i:block_end])
+ if "ui:field: Secret" not in block:
+ findings.append(
+ {
+ "line": 0,
+ "message": f"Parameter '{key}' looks sensitive — use ui:field: Secret",
+ }
+ )
+ i += 1
+ return findings
+
+
+def check_template_docs(content: str, template_path: Path) -> list[dict]:
+ if "backstage.io/techdocs-ref" in content:
+ return []
+ readme = template_path.parent / "README.md"
+ if readme.is_file():
+ return []
+ return [
+ {
+ "line": 0,
+ "message": "No README.md or backstage.io/techdocs-ref annotation — add template documentation",
+ }
+ ]
+
+
+CHECKERS = {
+ "api_version": lambda c, p: check_api_version(c),
+ "action_pascal_case": lambda c, p: check_action_casing(c),
+ "v1beta2_expression_syntax": lambda c, p: check_v1beta2_expressions(c),
+ "hardcoded_secret": lambda c, p: check_hardcoded_secrets(c),
+ "missing_parameters": lambda c, p: check_missing_section(c, "parameters"),
+ "missing_steps": lambda c, p: check_missing_section(c, "steps"),
+ "fetch_template_values": lambda c, p: check_fetch_template_values(c),
+ "workflow_raw_blocks": lambda c, p: check_workflow_raw_blocks(p),
+ "skeleton_parameters_ref": lambda c, p: check_skeleton_parameters(p),
+ "metadata_tags": lambda c, p: check_metadata_tags(c),
+ "sensitive_param_secret_field": lambda c, p: check_sensitive_param_secret_field(c),
+ "template_docs": lambda c, p: check_template_docs(c, p),
+}
+
+FIXERS = {
+ "set_api_version_v1beta3": fix_api_version,
+ "lowercase_action_segment": fix_action_casing,
+ "convert_to_v1beta3_expressions": convert_expressions,
+}
+
+
+def run_checks(content: str, template_path: Path, rules: list[dict]) -> list[dict]:
+ results = []
+ for rule in rules:
+ checker = CHECKERS.get(rule.get("check", ""))
+ if not checker:
+ continue
+ for finding in checker(content, template_path):
+ results.append(
+ {
+ "rule_id": rule["id"],
+ "severity": rule["severity"],
+ "description": rule["description"],
+ **finding,
+ }
+ )
+ return results
+
+
+def apply_fixes(content: str, rules: list[dict]) -> str:
+ updated = content
+ for rule in rules:
+ fix_name = rule.get("fix")
+ if not fix_name:
+ continue
+ fixer = FIXERS.get(fix_name)
+ if fixer:
+ updated = fixer(updated)
+ return updated
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="Check and fix RHDH template gotchas.")
+ parser.add_argument(
+ "--path", required=True, type=Path, help="template.yaml or template directory"
+ )
+ parser.add_argument("--apply", action="store_true", help="Apply automatic fixes")
+ parser.add_argument("--json", action="store_true", help="Emit JSON result")
+ args = parser.parse_args()
+
+ skill_dir = Path(__file__).resolve().parent.parent
+ try:
+ template_path = resolve_template_path(args.path)
+ except FileNotFoundError as exc:
+ print(str(exc), file=sys.stderr)
+ return EXIT_USAGE
+
+ rules = load_rules(skill_dir)
+ original = template_path.read_text(encoding="utf-8")
+ findings = run_checks(original, template_path, rules)
+
+ updated = original
+ if args.apply:
+ updated = apply_fixes(original, rules)
+ if updated != original:
+ template_path.write_text(updated, encoding="utf-8")
+ findings = run_checks(updated, template_path, rules)
+
+ critical = [f for f in findings if f["severity"] == "critical"]
+ result = {
+ "ok": len(critical) == 0,
+ "template": str(template_path),
+ "finding_count": len(findings),
+ "critical_count": len(critical),
+ "findings": findings,
+ "applied": args.apply and updated != original,
+ }
+
+ if args.json:
+ print(json.dumps(result, indent=2 if _is_tty else None))
+ else:
+ print(f"Template: {template_path}")
+ print(f"Findings: {len(findings)} ({len(critical)} critical)")
+ for f in findings:
+ sev = f["severity"].upper()
+ line = f.get("line", 0)
+ loc = f"line {line}: " if line else ""
+ print(f" [{sev}] {loc}{f['message']}")
+ if args.apply and updated != original:
+ print("Applied automatic fixes.")
+
+ return EXIT_SUCCESS if len(critical) == 0 else EXIT_FINDINGS
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/rhdh-templates/scripts/init.py b/skills/rhdh-templates/scripts/init.py
new file mode 100755
index 0000000..b76f84c
--- /dev/null
+++ b/skills/rhdh-templates/scripts/init.py
@@ -0,0 +1,213 @@
+#!/usr/bin/env python3
+"""Initialize an RHDH Software Template authoring workspace.
+
+Checks required tooling, scaffolds recommended directory layout, and
+optionally probes RHDH Scaffolder API connectivity.
+
+Stdlib only per project ADR-0002.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import shutil
+import sys
+import urllib.error
+import urllib.request
+from pathlib import Path
+
+EXIT_SUCCESS = 0
+EXIT_PARTIAL = 1
+EXIT_USAGE = 2
+
+REQUIRED_TOOLS = ("python3", "git")
+RECOMMENDED_TOOLS = ("djlint",)
+
+_no_color = os.environ.get("NO_COLOR") is not None
+_is_tty = sys.stderr.isatty() and not _no_color
+
+
+def _c(code: str, text: str) -> str:
+ return f"{code}{text}\033[0m" if _is_tty else text
+
+
+def green(t: str) -> str:
+ return _c("\033[0;32m", t)
+
+
+def yellow(t: str) -> str:
+ return _c("\033[1;33m", t)
+
+
+def red(t: str) -> str:
+ return _c("\033[0;31m", t)
+
+
+def tool_available(name: str) -> bool:
+ return shutil.which(name) is not None
+
+
+def check_tools() -> dict:
+ results = {"required": {}, "recommended": {}}
+ for tool in REQUIRED_TOOLS:
+ results["required"][tool] = tool_available(tool)
+ for tool in RECOMMENDED_TOOLS:
+ results["recommended"][tool] = tool_available(tool)
+ return results
+
+
+def probe_rhdh(url: str, timeout: int = 10) -> dict:
+ base = url.rstrip("/")
+ endpoint = f"{base}/api/scaffolder/v2/actions"
+ try:
+ req = urllib.request.Request(endpoint, headers={"Accept": "application/json"})
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
+ body = resp.read().decode("utf-8")
+ data = json.loads(body) if body else []
+ count = len(data) if isinstance(data, list) else 0
+ return {"reachable": True, "endpoint": endpoint, "action_count": count}
+ except (
+ urllib.error.URLError,
+ urllib.error.HTTPError,
+ json.JSONDecodeError,
+ TimeoutError,
+ ) as exc:
+ return {"reachable": False, "endpoint": endpoint, "error": str(exc)}
+
+
+def scaffold_layout(root: Path, skill_dir: Path) -> dict:
+ created: list[str] = []
+ existing: list[str] = []
+
+ templates_dir = root / "templates"
+ example_dir = templates_dir / "example-template"
+ skeleton_dir = example_dir / "skeleton"
+
+ for path in (templates_dir, example_dir, skeleton_dir):
+ if path.exists():
+ existing.append(str(path.relative_to(root)))
+ else:
+ path.mkdir(parents=True, exist_ok=True)
+ created.append(str(path.relative_to(root)))
+
+ example_template = skill_dir / "assets" / "examples" / "minimal-template" / "template.yaml"
+ target_template = example_dir / "template.yaml"
+ if not target_template.exists() and example_template.exists():
+ shutil.copy2(example_template, target_template)
+ created.append(str(target_template.relative_to(root)))
+
+ example_skeleton = skill_dir / "assets" / "examples" / "minimal-template" / "skeleton"
+ if example_skeleton.is_dir():
+ for item in example_skeleton.iterdir():
+ dest = skeleton_dir / item.name
+ if not dest.exists():
+ if item.is_dir():
+ shutil.copytree(item, dest)
+ else:
+ shutil.copy2(item, dest)
+ created.append(str(dest.relative_to(root)))
+
+ example_readme = example_dir / "README.md"
+ if not example_readme.exists():
+ example_readme.write_text(
+ "# example-template\n\nRename this directory and customize `template.yaml`.\n",
+ encoding="utf-8",
+ )
+ created.append(str(example_readme.relative_to(root)))
+
+ location_path = root / "location.yaml"
+ if not location_path.exists():
+ content = (
+ "apiVersion: backstage.io/v1alpha1\n"
+ "kind: Location\n"
+ "metadata:\n"
+ f" name: {root.name}-templates\n"
+ " description: Software Templates\n"
+ "spec:\n"
+ " targets:\n"
+ " - ./templates/**/template.yaml\n"
+ )
+ location_path.write_text(content, encoding="utf-8")
+ created.append("location.yaml")
+
+ return {"created": created, "existing": existing}
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Initialize RHDH Software Template authoring workspace.",
+ )
+ parser.add_argument(
+ "--path",
+ type=Path,
+ default=Path.cwd(),
+ help="Template repository root (default: current directory)",
+ )
+ parser.add_argument(
+ "--rhdh-url",
+ help="Optional RHDH base URL to probe Scaffolder API",
+ )
+ parser.add_argument("--json", action="store_true", help="Emit JSON result")
+ args = parser.parse_args()
+
+ root = args.path.resolve()
+ skill_dir = Path(__file__).resolve().parent.parent
+
+ if not root.exists():
+ root.mkdir(parents=True, exist_ok=True)
+ elif not root.is_dir():
+ print(red(f"Not a directory: {root}"), file=sys.stderr)
+ return EXIT_USAGE
+
+ tools = check_tools()
+ missing_required = [k for k, v in tools["required"].items() if not v]
+ missing_recommended = [k for k, v in tools["recommended"].items() if not v]
+
+ layout = scaffold_layout(root, skill_dir)
+ rhdh = probe_rhdh(args.rhdh_url) if args.rhdh_url else None
+
+ ok = not missing_required
+ partial = bool(missing_recommended) or (rhdh and not rhdh.get("reachable"))
+
+ result = {
+ "ok": ok,
+ "path": str(root),
+ "tools": tools,
+ "missing_required": missing_required,
+ "missing_recommended": missing_recommended,
+ "layout": layout,
+ "rhdh": rhdh,
+ }
+
+ if args.json:
+ print(json.dumps(result, indent=2 if _is_tty else None))
+ else:
+ print(green("RHDH Templates init"))
+ print(f" Path: {root}")
+ for tool, present in tools["required"].items():
+ status = green("ok") if present else red("missing")
+ print(f" {tool}: {status}")
+ for tool, present in tools["recommended"].items():
+ status = green("ok") if present else yellow("missing (optional)")
+ print(f" {tool}: {status}")
+ if layout["created"]:
+ print(green("Created:"))
+ for p in layout["created"]:
+ print(f" {p}")
+ if rhdh:
+ if rhdh.get("reachable"):
+ print(green(f"RHDH reachable — {rhdh.get('action_count', 0)} actions"))
+ else:
+ print(yellow(f"RHDH unreachable: {rhdh.get('error')}"))
+ if missing_required:
+ print(red("Install missing required tools before authoring."))
+
+ if not ok:
+ return EXIT_PARTIAL
+ return EXIT_PARTIAL if partial else EXIT_SUCCESS
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/rhdh-templates/scripts/list_actions.py b/skills/rhdh-templates/scripts/list_actions.py
new file mode 100755
index 0000000..5d50661
--- /dev/null
+++ b/skills/rhdh-templates/scripts/list_actions.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+"""List Scaffolder actions from a running RHDH instance.
+
+Uses GET /api/scaffolder/v2/actions.
+
+Stdlib only per project ADR-0002.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import sys
+from pathlib import Path
+
+EXIT_SUCCESS = 0
+EXIT_FAILURE = 1
+EXIT_USAGE = 2
+
+SCRIPTS_DIR = Path(__file__).resolve().parent
+sys.path.insert(0, str(SCRIPTS_DIR))
+
+from scaffolder_api import list_actions # noqa: E402
+
+_no_color = os.environ.get("NO_COLOR") is not None
+_is_tty = sys.stderr.isatty() and not _no_color
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="List Scaffolder actions from RHDH.")
+ parser.add_argument(
+ "--rhdh-url", required=True, help="RHDH base URL (e.g. https://rhdh.example.com)"
+ )
+ parser.add_argument("--token", help="Bearer token (default: RHDH_TOKEN or BACKSTAGE_TOKEN env)")
+ parser.add_argument("--filter", help="Case-insensitive substring filter on action id")
+ parser.add_argument("--json", action="store_true", help="Emit JSON result")
+ args = parser.parse_args()
+
+ try:
+ actions = list_actions(args.rhdh_url, token=args.token)
+ except RuntimeError as exc:
+ print(str(exc), file=sys.stderr)
+ return EXIT_FAILURE
+
+ if args.filter:
+ needle = args.filter.lower()
+ actions = [a for a in actions if needle in a.get("id", "").lower()]
+
+ result = {
+ "ok": True,
+ "rhdh_url": args.rhdh_url.rstrip("/"),
+ "action_count": len(actions),
+ "actions": actions,
+ }
+
+ if args.json:
+ print(json.dumps(result, indent=2 if _is_tty else None))
+ else:
+ print(f"RHDH: {args.rhdh_url.rstrip('/')}")
+ print(f"Actions: {len(actions)}")
+ for action in actions:
+ desc = action.get("description") or ""
+ if desc:
+ desc = desc.split("\n", 1)[0][:80]
+ print(f" {action['id']} — {desc}")
+ else:
+ print(f" {action['id']}")
+
+ return EXIT_SUCCESS
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/rhdh-templates/scripts/list_examples.py b/skills/rhdh-templates/scripts/list_examples.py
new file mode 100644
index 0000000..645a88a
--- /dev/null
+++ b/skills/rhdh-templates/scripts/list_examples.py
@@ -0,0 +1,243 @@
+#!/usr/bin/env python3
+"""List curated RHDH Software Template reference examples.
+
+Reads the bundled example catalog and filters or ranks entries for authoring
+workflows. Stdlib only per project ADR-0002.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import sys
+from pathlib import Path
+
+EXIT_SUCCESS = 0
+EXIT_USAGE = 2
+
+SKILL_DIR = Path(__file__).resolve().parent.parent
+DEFAULT_CATALOG = SKILL_DIR / "assets" / "example-catalog.json"
+BUNDLED_EXAMPLES_DIR = "assets/examples"
+
+
+def load_catalog(path: Path) -> dict:
+ if not path.is_file():
+ raise FileNotFoundError(f"Catalog not found: {path}")
+ return json.loads(path.read_text(encoding="utf-8"))
+
+
+def source_map(catalog: dict) -> dict[str, dict]:
+ return {item["id"]: item for item in catalog.get("sources", [])}
+
+
+def tokenize(text: str) -> list[str]:
+ return [t for t in re.split(r"[^a-z0-9]+", text.lower()) if t]
+
+
+def example_blob(example: dict) -> str:
+ parts = [
+ example.get("id", ""),
+ example.get("title", ""),
+ example.get("category", ""),
+ " ".join(example.get("tags", [])),
+ " ".join(example.get("stack", [])),
+ " ".join(example.get("use_cases", [])),
+ ]
+ return " ".join(parts).lower()
+
+
+def score_example(example: dict, query_tokens: list[str]) -> int:
+ if not query_tokens:
+ return 0
+ blob = example_blob(example)
+ score = 0
+ for token in query_tokens:
+ if token in blob:
+ score += 2
+ for part in blob.split():
+ if token in part or part in token:
+ score += 1
+ if example.get("recommended"):
+ score += 1
+ return score
+
+
+def filter_examples(
+ catalog: dict,
+ *,
+ category: str | None = None,
+ tag: str | None = None,
+ stack: str | None = None,
+ recommended: bool = False,
+ local_only: bool = False,
+ official_only: bool = False,
+ query: str | None = None,
+ match: str | None = None,
+ limit: int | None = None,
+) -> list[dict]:
+ examples = list(catalog.get("examples", []))
+ sources = source_map(catalog)
+
+ if category:
+ examples = [e for e in examples if e.get("category") == category]
+ if tag:
+ examples = [e for e in examples if tag in e.get("tags", [])]
+ if stack:
+ examples = [
+ e for e in examples if stack in e.get("stack", []) or stack in e.get("tags", [])
+ ]
+ if recommended:
+ examples = [e for e in examples if e.get("recommended")]
+ if local_only:
+ examples = [e for e in examples if e.get("local_bundled")]
+ if official_only:
+ official_ids = {sid for sid, src in sources.items() if src.get("official")}
+ examples = [e for e in examples if e.get("source") in official_ids]
+
+ search_text = match or query
+ if search_text:
+ tokens = tokenize(search_text)
+ scored = [(score_example(e, tokens), e) for e in examples]
+ scored = [(score, e) for score, e in scored if score > 0]
+ scored.sort(key=lambda item: (-item[0], item[1].get("title", "")))
+ examples = [e for _, e in scored]
+ if match:
+ for score, example in scored:
+ example["_match_score"] = score
+ elif match is not None:
+ examples = []
+
+ if limit is not None and limit >= 0:
+ examples = examples[:limit]
+
+ return examples
+
+
+def enrich_example(example: dict, catalog: dict) -> dict:
+ sources = source_map(catalog)
+ source = sources.get(example.get("source", ""), {})
+ enriched = dict(example)
+ enriched["source_repo"] = source.get("repo")
+ enriched["source_url"] = source.get("url")
+ if example.get("local_bundled"):
+ enriched["local_path"] = f"{BUNDLED_EXAMPLES_DIR}/{example['local_bundled']}"
+ return enriched
+
+
+def format_text(examples: list[dict], catalog: dict) -> str:
+ if not examples:
+ return "No matching reference templates found."
+
+ lines = []
+ disclaimer = catalog.get("disclaimer")
+ if disclaimer:
+ lines.append(disclaimer)
+ lines.append("")
+
+ for example in examples:
+ score = example.get("_match_score")
+ prefix = f"[{score}] " if score is not None else ""
+ lines.append(f"{prefix}{example['title']} ({example['id']})")
+ lines.append(f" category: {example.get('category')}")
+ lines.append(f" url: {example.get('url')}")
+ if example.get("local_bundled"):
+ lines.append(f" local example: {BUNDLED_EXAMPLES_DIR}/{example['local_bundled']}")
+ if example.get("recommended"):
+ lines.append(" recommended: yes")
+ lines.append("")
+
+ return "\n".join(lines).rstrip()
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ description="List curated RHDH Software Template reference examples.",
+ )
+ parser.add_argument(
+ "--catalog",
+ type=Path,
+ default=DEFAULT_CATALOG,
+ help=f"Path to example catalog JSON (default: {DEFAULT_CATALOG})",
+ )
+ parser.add_argument("--category", help="Filter by category (backend, ai, catalog, ...)")
+ parser.add_argument("--tag", help="Filter by tag (recommended, go, rag, ...)")
+ parser.add_argument("--stack", help="Filter by stack marker (go, java, python, ai, ...)")
+ parser.add_argument(
+ "--recommended",
+ action="store_true",
+ help="Only templates tagged recommended upstream",
+ )
+ parser.add_argument(
+ "--local-only",
+ action="store_true",
+ help="Only examples with a bundled local counterpart in assets/examples/",
+ )
+ parser.add_argument(
+ "--official-only",
+ action="store_true",
+ help="Only examples from official Red Hat Developer sources",
+ )
+ parser.add_argument("--query", help="Substring token search across title, tags, and use cases")
+ parser.add_argument(
+ "--match",
+ help="Rank examples by relevance to a natural-language intent (e.g. 'spring boot backend with ci')",
+ )
+ parser.add_argument(
+ "--limit",
+ type=int,
+ default=None,
+ help="Maximum number of results (default: all)",
+ )
+ parser.add_argument(
+ "--json",
+ action="store_true",
+ help="Emit structured JSON (compact when piped)",
+ )
+ return parser
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = build_parser()
+ args = parser.parse_args(argv)
+
+ try:
+ catalog = load_catalog(args.catalog)
+ except FileNotFoundError as exc:
+ print(str(exc), file=sys.stderr)
+ return EXIT_USAGE
+
+ examples = filter_examples(
+ catalog,
+ category=args.category,
+ tag=args.tag,
+ stack=args.stack,
+ recommended=args.recommended,
+ local_only=args.local_only,
+ official_only=args.official_only,
+ query=args.query,
+ match=args.match,
+ limit=args.limit,
+ )
+
+ enriched = [enrich_example(example, catalog) for example in examples]
+ for item in enriched:
+ item.pop("_match_score", None)
+
+ if args.json:
+ payload = {
+ "ok": True,
+ "count": len(enriched),
+ "disclaimer": catalog.get("disclaimer"),
+ "examples": enriched,
+ }
+ indent = 2 if sys.stdout.isatty() else None
+ print(json.dumps(payload, indent=indent))
+ return EXIT_SUCCESS
+
+ print(format_text(examples, catalog))
+ return EXIT_SUCCESS
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/skills/rhdh-templates/scripts/scaffolder_api.py b/skills/rhdh-templates/scripts/scaffolder_api.py
new file mode 100644
index 0000000..578cd07
--- /dev/null
+++ b/skills/rhdh-templates/scripts/scaffolder_api.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+"""Shared HTTP helpers for RHDH Scaffolder v2 API calls.
+
+Stdlib only per project ADR-0002.
+"""
+
+from __future__ import annotations
+
+import base64
+import json
+import os
+import urllib.error
+import urllib.request
+from pathlib import Path
+from typing import Any
+
+DEFAULT_TIMEOUT = 30
+
+
+def auth_headers(token: str | None = None) -> dict[str, str]:
+ headers = {"Accept": "application/json", "Content-Type": "application/json"}
+ resolved = token or os.environ.get("RHDH_TOKEN") or os.environ.get("BACKSTAGE_TOKEN")
+ if resolved:
+ headers["Authorization"] = f"Bearer {resolved}"
+ return headers
+
+
+def api_url(base_url: str, path: str) -> str:
+ return f"{base_url.rstrip('/')}{path}"
+
+
+def request_json(
+ method: str,
+ url: str,
+ *,
+ token: str | None = None,
+ body: dict[str, Any] | None = None,
+ timeout: int = DEFAULT_TIMEOUT,
+) -> tuple[int, Any]:
+ data = None
+ headers = auth_headers(token)
+ if body is not None:
+ data = json.dumps(body).encode("utf-8")
+ req = urllib.request.Request(url, data=data, headers=headers, method=method.upper())
+ try:
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
+ raw = resp.read().decode("utf-8")
+ if not raw:
+ return resp.status, None
+ return resp.status, json.loads(raw)
+ except urllib.error.HTTPError as exc:
+ raw = exc.read().decode("utf-8", errors="replace")
+ try:
+ payload = json.loads(raw) if raw else {"error": exc.reason}
+ except json.JSONDecodeError:
+ payload = {"error": raw or exc.reason}
+ return exc.code, payload
+
+
+def list_actions(base_url: str, *, token: str | None = None) -> list[dict[str, Any]]:
+ status, payload = request_json(
+ "GET",
+ api_url(base_url, "/api/scaffolder/v2/actions"),
+ token=token,
+ )
+ if status != 200:
+ raise RuntimeError(f"list-actions failed ({status}): {payload}")
+ if not isinstance(payload, list):
+ raise RuntimeError(f"Unexpected list-actions response: {payload!r}")
+ return payload
+
+
+def get_action_schema(
+ base_url: str,
+ action_id: str,
+ *,
+ token: str | None = None,
+) -> dict[str, Any] | None:
+ actions = list_actions(base_url, token=token)
+ for action in actions:
+ if action.get("id") == action_id:
+ return action
+ return None
+
+
+def get_template_parameter_schema(
+ base_url: str,
+ template_ref: str,
+ *,
+ token: str | None = None,
+) -> dict[str, Any]:
+ """Fetch parameter schema for a catalog Template entity."""
+ kind, namespace, name = parse_template_ref(template_ref)
+ path = f"/api/scaffolder/v2/templates/{namespace}/{kind}/{name}/parameter-schema"
+ status, payload = request_json("GET", api_url(base_url, path), token=token)
+ if status != 200:
+ raise RuntimeError(f"parameter-schema failed ({status}): {payload}")
+ return payload
+
+
+def parse_template_ref(template_ref: str) -> tuple[str, str, str]:
+ """Parse template:namespace/name into (kind, namespace, name)."""
+ ref = template_ref.strip()
+ if ":" not in ref or "/" not in ref:
+ raise ValueError(
+ f"Invalid template ref {template_ref!r} — expected template:namespace/name"
+ )
+ kind_part, rest = ref.split(":", 1)
+ namespace, name = rest.split("/", 1)
+ return kind_part.lower(), namespace, name
+
+
+def load_directory_contents(root: Path) -> list[dict[str, str]]:
+ """Serialize a directory tree for Scaffolder dry-run API."""
+ contents: list[dict[str, str]] = []
+ for path in sorted(root.rglob("*")):
+ if not path.is_file():
+ continue
+ rel = path.relative_to(root).as_posix()
+ raw = path.read_bytes()
+ contents.append(
+ {
+ "path": rel,
+ "base64Content": base64.b64encode(raw).decode("ascii"),
+ }
+ )
+ return contents
+
+
+def dry_run(
+ base_url: str,
+ *,
+ template: dict[str, Any],
+ values: dict[str, Any],
+ directory_contents: list[dict[str, str]],
+ secrets: dict[str, str] | None = None,
+ token: str | None = None,
+) -> dict[str, Any]:
+ body: dict[str, Any] = {
+ "template": template,
+ "values": values,
+ "directoryContents": directory_contents,
+ }
+ if secrets:
+ body["secrets"] = secrets
+ status, payload = request_json(
+ "POST",
+ api_url(base_url, "/api/scaffolder/v2/dry-run"),
+ token=token,
+ body=body,
+ )
+ if status != 200:
+ raise RuntimeError(f"dry-run failed ({status}): {payload}")
+ if not isinstance(payload, dict):
+ raise RuntimeError(f"Unexpected dry-run response: {payload!r}")
+ return payload
diff --git a/skills/rhdh-templates/scripts/schema_validate.py b/skills/rhdh-templates/scripts/schema_validate.py
new file mode 100644
index 0000000..ed2998c
--- /dev/null
+++ b/skills/rhdh-templates/scripts/schema_validate.py
@@ -0,0 +1,658 @@
+#!/usr/bin/env python3
+"""JSON Schema and structural validation for Software Template entities.
+
+Provides stdlib structural checks (always available) and optional full JSON
+Schema validation when jsonschema is installed.
+
+Stdlib only per project ADR-0002 (optional jsonschema when installed).
+"""
+
+from __future__ import annotations
+
+import json
+import re
+from pathlib import Path
+from typing import Any
+
+PARAM_EXPR = re.compile(r"\$\{\{\s*parameters\.([a-zA-Z0-9_.]+)\s*\}\}")
+STEP_OUTPUT_EXPR = re.compile(
+ r"\$\{\{\s*steps(?:\[['\"]([^'\"]+)['\"]\]|\.([a-zA-Z0-9_-]+))\.output"
+)
+STEP_REF_BRACKET = re.compile(r"steps\[['\"]([^'\"]+)['\"]\]")
+STEP_REF_DOT = re.compile(r"steps\.([a-zA-Z0-9_-]+)\.output")
+
+VALID_PARAM_TYPES = frozenset({"string", "number", "integer", "boolean", "array", "object", "null"})
+RESERVED_PARAM_KEYS = frozenset(
+ {
+ "properties",
+ "required",
+ "dependencies",
+ "oneOf",
+ "allOf",
+ "anyOf",
+ "not",
+ "if",
+ "then",
+ "else",
+ "title",
+ "description",
+ "type",
+ "enum",
+ "const",
+ "default",
+ "items",
+ "additionalProperties",
+ "pattern",
+ "minLength",
+ "maxLength",
+ "minimum",
+ "maximum",
+ "format",
+ "ui:field",
+ "ui:widget",
+ "ui:options",
+ "ui:help",
+ "ui:autofocus",
+ "ui:placeholder",
+ "ui:disabled",
+ "ui:readonly",
+ "backstage:permissions",
+ }
+)
+
+
+def _finding(
+ check: str,
+ severity: str,
+ message: str,
+ *,
+ path: str = "",
+ line: int = 0,
+) -> dict:
+ return {
+ "check": check,
+ "severity": severity,
+ "message": message,
+ "path": path,
+ "line": line,
+ }
+
+
+def schema_path(skill_dir: Path) -> Path:
+ return skill_dir / "references" / "schemas" / "template-v1beta3.schema.json"
+
+
+def normalize_parameter_forms(parameters: Any) -> list[dict]:
+ if parameters is None:
+ return []
+ if isinstance(parameters, dict):
+ return [parameters]
+ if isinstance(parameters, list):
+ return [item for item in parameters if isinstance(item, dict)]
+ return []
+
+
+def collect_parameter_keys(parameters: Any) -> set[str]:
+ keys: set[str] = set()
+ for form in normalize_parameter_forms(parameters):
+ props = form.get("properties")
+ if isinstance(props, dict):
+ keys.update(props.keys())
+ required = form.get("required")
+ if isinstance(required, list):
+ keys.update(str(item) for item in required if isinstance(item, str))
+ return keys
+
+
+def validate_parameter_property(name: str, prop: Any, path: str) -> list[dict]:
+ findings: list[dict] = []
+ if not isinstance(prop, dict):
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ f"Parameter '{name}' must be an object",
+ path=path,
+ )
+ )
+ return findings
+
+ prop_type = prop.get("type")
+ if prop_type is not None:
+ if isinstance(prop_type, str):
+ if prop_type not in VALID_PARAM_TYPES:
+ findings.append(
+ _finding(
+ "json_schema",
+ "warning",
+ f"Parameter '{name}' has unknown type '{prop_type}'",
+ path=f"{path}.type",
+ )
+ )
+ elif isinstance(prop_type, list):
+ unknown = [t for t in prop_type if t not in VALID_PARAM_TYPES]
+ if unknown:
+ findings.append(
+ _finding(
+ "json_schema",
+ "warning",
+ f"Parameter '{name}' has unknown types: {', '.join(unknown)}",
+ path=f"{path}.type",
+ )
+ )
+ else:
+ findings.append(
+ _finding(
+ "json_schema",
+ "warning",
+ f"Parameter '{name}' type must be a string or list",
+ path=f"{path}.type",
+ )
+ )
+ elif "enum" not in prop and "const" not in prop and "oneOf" not in prop:
+ findings.append(
+ _finding(
+ "json_schema",
+ "warning",
+ f"Parameter '{name}' missing type (add type or enum)",
+ path=path,
+ )
+ )
+
+ if prop_type == "object" or (isinstance(prop_type, list) and "object" in prop_type):
+ nested = prop.get("properties")
+ if isinstance(nested, dict):
+ for nested_name, nested_prop in nested.items():
+ findings.extend(
+ validate_parameter_property(
+ f"{name}.{nested_name}",
+ nested_prop,
+ f"{path}.properties.{nested_name}",
+ )
+ )
+
+ if prop_type == "array" or (isinstance(prop_type, list) and "array" in prop_type):
+ items = prop.get("items")
+ if items is not None and not isinstance(items, dict):
+ findings.append(
+ _finding(
+ "json_schema",
+ "warning",
+ f"Parameter '{name}' array items must be an object schema",
+ path=f"{path}.items",
+ )
+ )
+
+ return findings
+
+
+def validate_parameter_forms(parameters: Any) -> list[dict]:
+ findings: list[dict] = []
+ forms = normalize_parameter_forms(parameters)
+ if parameters is not None and not forms:
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "spec.parameters must be an object or array of form sections",
+ path="spec.parameters",
+ )
+ )
+ return findings
+
+ for index, form in enumerate(forms):
+ base = f"spec.parameters[{index}]"
+ if not form.get("title"):
+ findings.append(
+ _finding(
+ "json_schema",
+ "warning",
+ "Parameter form section missing title",
+ path=f"{base}.title",
+ )
+ )
+ props = form.get("properties")
+ if props is None:
+ findings.append(
+ _finding(
+ "json_schema",
+ "warning",
+ "Parameter form section missing properties",
+ path=f"{base}.properties",
+ )
+ )
+ continue
+ if not isinstance(props, dict):
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "Parameter form properties must be an object",
+ path=f"{base}.properties",
+ )
+ )
+ continue
+
+ required = form.get("required", [])
+ if required is not None and not isinstance(required, list):
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "Parameter form required must be an array",
+ path=f"{base}.required",
+ )
+ )
+ elif isinstance(required, list):
+ for req_key in required:
+ if not isinstance(req_key, str):
+ continue
+ if req_key not in props:
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ f"Required parameter '{req_key}' is not defined in properties",
+ path=f"{base}.required",
+ )
+ )
+
+ for name, prop in props.items():
+ if name in RESERVED_PARAM_KEYS:
+ continue
+ findings.extend(validate_parameter_property(name, prop, f"{base}.properties.{name}"))
+
+ return findings
+
+
+def validate_steps(steps: Any) -> list[dict]:
+ findings: list[dict] = []
+ if not isinstance(steps, list):
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "spec.steps must be an array",
+ path="spec.steps",
+ )
+ )
+ return findings
+ if not steps:
+ findings.append(
+ _finding(
+ "json_schema",
+ "warning",
+ "spec.steps is empty",
+ path="spec.steps",
+ )
+ )
+
+ seen_ids: set[str] = set()
+ for index, step in enumerate(steps):
+ path = f"spec.steps[{index}]"
+ if not isinstance(step, dict):
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "Each step must be an object",
+ path=path,
+ )
+ )
+ continue
+
+ step_id = step.get("id")
+ if step_id is not None:
+ if not isinstance(step_id, str) or not step_id.strip():
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "Step id must be a non-empty string",
+ path=f"{path}.id",
+ )
+ )
+ elif step_id in seen_ids:
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ f"Duplicate step id '{step_id}'",
+ path=f"{path}.id",
+ )
+ )
+ else:
+ seen_ids.add(step_id)
+
+ action = step.get("action")
+ if not action:
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "Step missing required action",
+ path=f"{path}.action",
+ )
+ )
+ elif not isinstance(action, str) or ":" not in action:
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ f"Step action '{action}' must use namespace:actionName format",
+ path=f"{path}.action",
+ )
+ )
+
+ step_input = step.get("input")
+ if step_input is not None and not isinstance(step_input, dict):
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "Step input must be an object",
+ path=f"{path}.input",
+ )
+ )
+
+ return findings
+
+
+def validate_output(output: Any) -> list[dict]:
+ findings: list[dict] = []
+ if output is None:
+ return findings
+ if not isinstance(output, dict):
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "spec.output must be an object",
+ path="spec.output",
+ )
+ )
+ return findings
+
+ links = output.get("links")
+ if links is not None:
+ if not isinstance(links, list):
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "spec.output.links must be an array",
+ path="spec.output.links",
+ )
+ )
+ else:
+ for index, link in enumerate(links):
+ path = f"spec.output.links[{index}]"
+ if not isinstance(link, dict):
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "Output link must be an object",
+ path=path,
+ )
+ )
+ continue
+ if not any(link.get(key) for key in ("title", "url", "entityRef")):
+ findings.append(
+ _finding(
+ "json_schema",
+ "warning",
+ "Output link should include title, url, or entityRef",
+ path=path,
+ )
+ )
+
+ return findings
+
+
+def _extract_param_refs_from_value(value: Any) -> set[str]:
+ refs: set[str] = set()
+ if isinstance(value, str):
+ for match in PARAM_EXPR.finditer(value):
+ refs.add(match.group(1).split(".")[0])
+ elif isinstance(value, dict):
+ for nested in value.values():
+ refs.update(_extract_param_refs_from_value(nested))
+ elif isinstance(value, list):
+ for nested in value:
+ refs.update(_extract_param_refs_from_value(nested))
+ return refs
+
+
+def _extract_step_refs_from_value(value: Any) -> set[str]:
+ refs: set[str] = set()
+ if isinstance(value, str):
+ for match in STEP_OUTPUT_EXPR.finditer(value):
+ step_id = match.group(1) or match.group(2)
+ if step_id:
+ refs.add(step_id)
+ for match in STEP_REF_BRACKET.finditer(value):
+ refs.add(match.group(1))
+ for match in STEP_REF_DOT.finditer(value):
+ refs.add(match.group(1))
+ elif isinstance(value, dict):
+ for nested in value.values():
+ refs.update(_extract_step_refs_from_value(nested))
+ elif isinstance(value, list):
+ for nested in value:
+ refs.update(_extract_step_refs_from_value(nested))
+ return refs
+
+
+def validate_cross_references(data: dict) -> list[dict]:
+ findings: list[dict] = []
+ spec = data.get("spec")
+ if not isinstance(spec, dict):
+ return findings
+
+ param_keys = collect_parameter_keys(spec.get("parameters"))
+ steps = spec.get("steps")
+ if not isinstance(steps, list):
+ return findings
+
+ step_ids = {
+ step.get("id")
+ for step in steps
+ if isinstance(step, dict) and isinstance(step.get("id"), str)
+ }
+
+ for index, step in enumerate(steps):
+ if not isinstance(step, dict):
+ continue
+ path = f"spec.steps[{index}]"
+ for ref in _extract_param_refs_from_value(step.get("input")):
+ if param_keys and ref not in param_keys:
+ findings.append(
+ _finding(
+ "json_schema",
+ "warning",
+ f"Step references unknown parameter '{ref}'",
+ path=path,
+ )
+ )
+
+ for ref in _extract_step_refs_from_value(spec.get("output")):
+ if step_ids and ref not in step_ids:
+ findings.append(
+ _finding(
+ "json_schema",
+ "warning",
+ f"Output references unknown step id '{ref}'",
+ path="spec.output",
+ )
+ )
+
+ return findings
+
+
+def validate_metadata(data: dict) -> list[dict]:
+ findings: list[dict] = []
+ metadata = data.get("metadata")
+ if not isinstance(metadata, dict):
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "metadata must be an object",
+ path="metadata",
+ )
+ )
+ return findings
+
+ name = metadata.get("name")
+ if not name:
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "metadata.name is required",
+ path="metadata.name",
+ )
+ )
+ elif not isinstance(name, str) or not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name):
+ findings.append(
+ _finding(
+ "json_schema",
+ "warning",
+ "metadata.name should be lowercase alphanumeric with hyphens",
+ path="metadata.name",
+ )
+ )
+
+ tags = metadata.get("tags")
+ if tags is not None and not isinstance(tags, list):
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "metadata.tags must be an array",
+ path="metadata.tags",
+ )
+ )
+
+ return findings
+
+
+def validate_spec_root(data: dict) -> list[dict]:
+ findings: list[dict] = []
+ spec = data.get("spec")
+ if not isinstance(spec, dict):
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "spec must be an object",
+ path="spec",
+ )
+ )
+ return findings
+
+ template_type = spec.get("type")
+ if not template_type:
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "spec.type is required",
+ path="spec.type",
+ )
+ )
+ elif not isinstance(template_type, str) or not template_type.strip():
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "spec.type must be a non-empty string",
+ path="spec.type",
+ )
+ )
+
+ findings.extend(validate_parameter_forms(spec.get("parameters")))
+ findings.extend(validate_steps(spec.get("steps")))
+ findings.extend(validate_output(spec.get("output")))
+ findings.extend(validate_cross_references(data))
+ return findings
+
+
+def validate_structural(data: dict) -> list[dict]:
+ """Always-available structural and JSON Schema subset validation."""
+ findings: list[dict] = []
+ if data.get("apiVersion") != "scaffolder.backstage.io/v1beta3":
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "apiVersion must be scaffolder.backstage.io/v1beta3",
+ path="apiVersion",
+ )
+ )
+ if data.get("kind") != "Template":
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ "kind must be Template",
+ path="kind",
+ )
+ )
+ findings.extend(validate_metadata(data))
+ findings.extend(validate_spec_root(data))
+ return findings
+
+
+def validate_with_jsonschema(data: dict, skill_dir: Path) -> tuple[list[dict], str | None]:
+ """Optional full JSON Schema validation when jsonschema is installed."""
+ try:
+ import jsonschema # type: ignore[import-untyped]
+ except ImportError:
+ return [], "jsonschema not installed — structural checks only"
+
+ schema_file = schema_path(skill_dir)
+ if not schema_file.is_file():
+ return [
+ _finding(
+ "json_schema",
+ "info",
+ f"Bundled schema not found at {schema_file}",
+ )
+ ], None
+
+ schema = json.loads(schema_file.read_text(encoding="utf-8"))
+ validator = jsonschema.Draft7Validator(schema)
+ findings: list[dict] = []
+ for error in sorted(validator.iter_errors(data), key=lambda e: list(e.path)):
+ path = ".".join(str(part) for part in error.path) or "(root)"
+ findings.append(
+ _finding(
+ "json_schema",
+ "critical",
+ error.message,
+ path=path,
+ )
+ )
+ return findings, None
+
+
+def run_schema_validation(data: dict, skill_dir: Path, *, use_jsonschema: bool = True) -> dict:
+ """Run structural validation and optional jsonschema validation."""
+ structural = validate_structural(data)
+ jsonschema_findings: list[dict] = []
+ note: str | None = None
+
+ if use_jsonschema:
+ jsonschema_findings, note = validate_with_jsonschema(data, skill_dir)
+
+ # Structural checks are more specific for cross-refs; prefer them over duplicate
+ # jsonschema messages for the same paths when both fire.
+ combined = structural + jsonschema_findings
+ return {
+ "findings": combined,
+ "structural_count": len(structural),
+ "jsonschema_count": len(jsonschema_findings),
+ "note": note,
+ }
diff --git a/skills/rhdh-templates/scripts/validate.py b/skills/rhdh-templates/scripts/validate.py
new file mode 100755
index 0000000..504824f
--- /dev/null
+++ b/skills/rhdh-templates/scripts/validate.py
@@ -0,0 +1,338 @@
+#!/usr/bin/env python3
+"""Local validation for RHDH Software Templates.
+
+Combines YAML structure checks, gotcha rules from fix_gotchas.py, JSON Schema
+validation (structural subset always; bundled schema when jsonschema is installed), optional
+location.yaml verification, and optional djLint for skeleton Nunjucks files.
+
+Stdlib only per project ADR-0002 (optional PyYAML and djlint when installed).
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+EXIT_SUCCESS = 0
+EXIT_FINDINGS = 1
+EXIT_USAGE = 2
+
+_no_color = os.environ.get("NO_COLOR") is not None
+_is_tty = sys.stderr.isatty() and not _no_color
+
+SCRIPTS_DIR = Path(__file__).resolve().parent
+SKILL_DIR = SCRIPTS_DIR.parent
+sys.path.insert(0, str(SCRIPTS_DIR))
+
+from fix_gotchas import load_rules, resolve_template_path, run_checks # noqa: E402
+from schema_validate import run_schema_validation # noqa: E402
+
+
+def _c(code: str, text: str) -> str:
+ return f"{code}{text}\033[0m" if _is_tty else text
+
+
+def green(t: str) -> str:
+ return _c("\033[0;32m", t)
+
+
+def red(t: str) -> str:
+ return _c("\033[0;31m", t)
+
+
+def yellow(t: str) -> str:
+ return _c("\033[1;33m", t)
+
+
+def load_yaml(text: str) -> tuple[dict | None, str | None]:
+ try:
+ import yaml # type: ignore[import-untyped]
+ except ImportError:
+ return None, "PyYAML not installed — skipping YAML syntax parse (gotcha checks still run)"
+ try:
+ data = yaml.safe_load(text)
+ except yaml.YAMLError as exc:
+ return None, f"YAML syntax error: {exc}"
+ if not isinstance(data, dict):
+ return None, "template.yaml root must be a mapping"
+ return data, None
+
+
+def check_yaml_structure(data: dict) -> list[dict]:
+ findings: list[dict] = []
+ if data.get("kind") != "Template":
+ findings.append(
+ {
+ "check": "yaml_structure",
+ "severity": "critical",
+ "message": f"Expected kind: Template, got {data.get('kind')!r}",
+ }
+ )
+ api = data.get("apiVersion", "")
+ if api != "scaffolder.backstage.io/v1beta3":
+ findings.append(
+ {
+ "check": "yaml_structure",
+ "severity": "critical",
+ "message": f"Expected apiVersion scaffolder.backstage.io/v1beta3, got {api!r}",
+ }
+ )
+ spec = data.get("spec")
+ if not isinstance(spec, dict):
+ findings.append(
+ {
+ "check": "yaml_structure",
+ "severity": "critical",
+ "message": "Missing or invalid spec section",
+ }
+ )
+ return findings
+ if "parameters" not in spec:
+ findings.append(
+ {
+ "check": "yaml_structure",
+ "severity": "warning",
+ "message": "spec.parameters is missing",
+ }
+ )
+ if "steps" not in spec:
+ findings.append(
+ {
+ "check": "yaml_structure",
+ "severity": "warning",
+ "message": "spec.steps is missing",
+ }
+ )
+ elif isinstance(spec.get("steps"), list) and len(spec["steps"]) == 0:
+ findings.append(
+ {
+ "check": "yaml_structure",
+ "severity": "warning",
+ "message": "spec.steps is empty",
+ }
+ )
+ metadata = data.get("metadata")
+ if not isinstance(metadata, dict) or not metadata.get("name"):
+ findings.append(
+ {
+ "check": "yaml_structure",
+ "severity": "critical",
+ "message": "metadata.name is required",
+ }
+ )
+ return findings
+
+
+def check_location_yaml(repo_root: Path) -> list[dict]:
+ findings: list[dict] = []
+ location = repo_root / "location.yaml"
+ if not location.is_file():
+ findings.append(
+ {
+ "check": "location",
+ "severity": "warning",
+ "message": "Root location.yaml not found",
+ }
+ )
+ return findings
+ text = location.read_text(encoding="utf-8")
+ if "kind: Location" not in text:
+ findings.append(
+ {
+ "check": "location",
+ "severity": "critical",
+ "message": "location.yaml missing kind: Location",
+ }
+ )
+ if "templates/**/template.yaml" not in text and "targets:" not in text:
+ findings.append(
+ {
+ "check": "location",
+ "severity": "warning",
+ "message": "location.yaml may not register template.yaml files",
+ }
+ )
+ return findings
+
+
+def run_djlint(skeleton_dir: Path) -> list[dict]:
+ import shutil
+
+ if not shutil.which("djlint"):
+ return [
+ {
+ "check": "nunjucks_lint",
+ "severity": "info",
+ "message": "djlint not installed — skipping Nunjucks lint",
+ }
+ ]
+ cmd = [
+ "djlint",
+ str(skeleton_dir),
+ "--profile=jinja",
+ "--lint",
+ "--quiet",
+ ]
+ try:
+ proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
+ except OSError as exc:
+ return [
+ {
+ "check": "nunjucks_lint",
+ "severity": "info",
+ "message": f"djlint skipped: {exc}",
+ }
+ ]
+ if proc.returncode == 0:
+ return []
+ findings: list[dict] = []
+ for line in proc.stdout.splitlines():
+ line = line.strip()
+ if not line:
+ continue
+ findings.append(
+ {
+ "check": "nunjucks_lint",
+ "severity": "warning",
+ "message": line,
+ }
+ )
+ if not findings:
+ findings.append(
+ {
+ "check": "nunjucks_lint",
+ "severity": "warning",
+ "message": proc.stderr.strip() or "djlint reported issues",
+ }
+ )
+ return findings
+
+
+def validate_template(
+ path: Path, *, check_repo: bool, lint_skeleton: bool, use_jsonschema: bool = True
+) -> dict:
+ template_path = resolve_template_path(path)
+ content = template_path.read_text(encoding="utf-8")
+ template_dir = template_path.parent
+ repo_root = (
+ template_dir.parent.parent if template_dir.parent.name == "templates" else template_dir
+ )
+
+ findings: list[dict] = []
+
+ parsed, yaml_note = load_yaml(content)
+ if yaml_note and parsed is None and yaml_note.startswith("YAML syntax"):
+ findings.append({"check": "yaml_syntax", "severity": "critical", "message": yaml_note})
+ elif parsed is not None:
+ findings.extend(check_yaml_structure(parsed))
+ schema_result = run_schema_validation(parsed, SKILL_DIR, use_jsonschema=use_jsonschema)
+ for item in schema_result["findings"]:
+ findings.append(
+ {
+ "check": item["check"],
+ "severity": item["severity"],
+ "message": item["message"],
+ "path": item.get("path", ""),
+ }
+ )
+ if schema_result.get("note"):
+ findings.append(
+ {
+ "check": "json_schema",
+ "severity": "info",
+ "message": schema_result["note"],
+ }
+ )
+ elif yaml_note:
+ findings.append({"check": "yaml_syntax", "severity": "info", "message": yaml_note})
+
+ rules = load_rules(SKILL_DIR)
+ for item in run_checks(content, template_path, rules):
+ findings.append(
+ {
+ "check": item.get("rule_id", "gotcha"),
+ "severity": item["severity"],
+ "message": item.get("message") or item.get("description", ""),
+ "line": item.get("line", 0),
+ }
+ )
+
+ if check_repo:
+ findings.extend(check_location_yaml(repo_root))
+
+ skeleton = template_dir / "skeleton"
+ if lint_skeleton and skeleton.is_dir():
+ findings.extend(run_djlint(skeleton))
+
+ critical = [f for f in findings if f["severity"] == "critical"]
+ warnings = [f for f in findings if f["severity"] == "warning"]
+ return {
+ "ok": len(critical) == 0,
+ "template": str(template_path),
+ "finding_count": len(findings),
+ "critical_count": len(critical),
+ "warning_count": len(warnings),
+ "findings": findings,
+ }
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="Validate RHDH Software Template locally.")
+ parser.add_argument(
+ "--path", required=True, type=Path, help="template.yaml or template directory"
+ )
+ parser.add_argument(
+ "--repo",
+ action="store_true",
+ help="Also validate root location.yaml for the template repo",
+ )
+ parser.add_argument(
+ "--lint-skeleton",
+ action="store_true",
+ help="Run djlint on skeleton/ when djlint is installed",
+ )
+ parser.add_argument(
+ "--no-json-schema",
+ action="store_true",
+ help="Skip optional full JSON Schema validation (structural checks still run)",
+ )
+ parser.add_argument("--json", action="store_true", help="Emit JSON result")
+ args = parser.parse_args()
+
+ try:
+ result = validate_template(
+ args.path,
+ check_repo=args.repo,
+ lint_skeleton=args.lint_skeleton,
+ use_jsonschema=not args.no_json_schema,
+ )
+ except FileNotFoundError as exc:
+ print(str(exc), file=sys.stderr)
+ return EXIT_USAGE
+
+ if args.json:
+ print(json.dumps(result, indent=2 if _is_tty else None))
+ else:
+ status = green("PASS") if result["ok"] else red("FAIL")
+ print(f"Validation: {status}")
+ print(f"Template: {result['template']}")
+ print(
+ f"Findings: {result['finding_count']} "
+ f"({result['critical_count']} critical, {result['warning_count']} warnings)"
+ )
+ for finding in result["findings"]:
+ sev = finding["severity"].upper()
+ line = finding.get("line", 0)
+ loc = f"line {line}: " if line else ""
+ color = red if finding["severity"] == "critical" else yellow
+ print(f" {color(f'[{sev}]')} {loc}{finding['message']}")
+
+ return EXIT_SUCCESS if result["ok"] else EXIT_FINDINGS
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/rhdh/SKILL.md b/skills/rhdh/SKILL.md
index 381a956..cfa1cb6 100644
--- a/skills/rhdh/SKILL.md
+++ b/skills/rhdh/SKILL.md
@@ -128,6 +128,11 @@ What would you like to do?
*For RHDH release tracking, status, announcements*
10. **Release management** — Release dates, status, team breakdown, freeze announcements, blocker bugs, CVEs, release notes
+### Software Template Tasks
+
+*For authoring RHDH Scaffolder / Software Templates*
+
+10. **Template authoring** — Templatize codebases, create templates, fix gotchas
### General Tasks
@@ -193,6 +198,13 @@ What would you like to do?
| 10, "release", "release manager", "release dates", "release status", "feature freeze", "code freeze", "blocker bugs", "CVEs", "release notes", "team breakdown", "freeze announcement" | Route to `@rhdh-release` skill |
**To route:** Read `../rhdh-release/SKILL.md` and follow its intake process.
+### Software Template Routes
+
+| Response | Skill |
+|----------|-------|
+| 10, "template", "software template", "templatize", "scaffolder", "template.yaml", "location.yaml", "golden path", "rhdh-templates" | Route to `@rhdh-templates` skill |
+
+**To route:** Read `../rhdh-templates/SKILL.md` and follow its intake process.
### General Routes
@@ -340,6 +352,7 @@ Todos must be **self-contained**—a new session should understand the task with
| rhdh-pr-review | PR code review and live cluster testing | `../rhdh-pr-review/SKILL.md` |
| rhdh-test-plan-review | Reviews an RHDH test plan Jira ticket and suggests platform/integration version updates | `../rhdh-test-plan-review/SKILL.md` |
| rhdh-release | Release dates, status tracking, team coordination, freeze announcements, blocker bugs, CVEs, release notes | `../rhdh-release/SKILL.md` |
+| rhdh-templates | Author RHDH Software Templates (Scaffolder) | `../rhdh-templates/SKILL.md` |
### Shared References
diff --git a/tests/integration/test_rhdh_templates_live.py b/tests/integration/test_rhdh_templates_live.py
new file mode 100644
index 0000000..de6b430
--- /dev/null
+++ b/tests/integration/test_rhdh_templates_live.py
@@ -0,0 +1,122 @@
+"""Optional live RHDH integration tests for rhdh-templates scripts.
+
+Skipped when RHDH_URL is unset or the Scaffolder API is unreachable.
+Run manually against rhdh-local:
+
+ RHDH_URL=http://localhost:7007 uv run pytest tests/integration/test_rhdh_templates_live.py -v
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import subprocess
+import sys
+import urllib.error
+import urllib.request
+from pathlib import Path
+
+import pytest
+import yaml
+
+SKILL_DIR = Path(__file__).resolve().parents[2] / "skills" / "rhdh-templates"
+SCRIPTS = SKILL_DIR / "scripts"
+MINIMAL_TEMPLATE = SKILL_DIR / "assets" / "examples" / "minimal-template"
+
+
+def _rhdh_url() -> str | None:
+ return os.environ.get("RHDH_URL", "http://localhost:7007").strip() or None
+
+
+def _rhdh_reachable(url: str) -> bool:
+ headers = {"Accept": "application/json"}
+ token = os.environ.get("RHDH_TOKEN") or os.environ.get("BACKSTAGE_TOKEN")
+ if token:
+ headers["Authorization"] = f"Bearer {token}"
+ try:
+ req = urllib.request.Request(
+ f"{url.rstrip('/')}/api/scaffolder/v2/actions",
+ headers=headers,
+ method="GET",
+ )
+ with urllib.request.urlopen(req, timeout=5) as resp:
+ return resp.status == 200
+ except (urllib.error.URLError, TimeoutError, OSError):
+ return False
+
+
+@pytest.fixture(scope="module")
+def rhdh_url() -> str:
+ url = _rhdh_url()
+ if not url or not _rhdh_reachable(url):
+ pytest.skip("RHDH Scaffolder API not reachable — set RHDH_URL to run live tests")
+ return url
+
+
+def run_script(script: str, *args: str) -> subprocess.CompletedProcess:
+ return subprocess.run(
+ [sys.executable, str(SCRIPTS / script), *args],
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+
+
+class TestLiveScaffolderApi:
+ def test_list_actions(self, rhdh_url: str) -> None:
+ result = run_script("list_actions.py", "--rhdh-url", rhdh_url, "--json")
+ assert result.returncode == 0, result.stderr
+ data = json.loads(result.stdout)
+ assert data["ok"] is True
+ assert data["action_count"] >= 1
+ ids = {a["id"] for a in data["actions"]}
+ assert "fetch:template" in ids or "debug:log" in ids
+
+ def test_explain_action_debug_log(self, rhdh_url: str) -> None:
+ result = run_script(
+ "explain_action.py",
+ "--rhdh-url",
+ rhdh_url,
+ "--action",
+ "debug:log",
+ "--json",
+ )
+ if result.returncode != 0:
+ pytest.skip("debug:log action not available on this instance")
+ data = json.loads(result.stdout)
+ assert data["ok"] is True
+ assert data["id"] == "debug:log"
+ assert data.get("schema") is not None
+
+ def test_dry_run_minimal_template(self, rhdh_url: str, tmp_path: Path) -> None:
+ values = {
+ "componentId": "dogfood-demo",
+ "owner": "group:default/team-a",
+ "description": "Live integration test",
+ }
+ values_file = tmp_path / "values.json"
+ values_file.write_text(json.dumps(values), encoding="utf-8")
+
+ result = run_script(
+ "dry_run.py",
+ "--rhdh-url",
+ rhdh_url,
+ "--path",
+ str(MINIMAL_TEMPLATE),
+ "--values",
+ str(values_file),
+ "--json",
+ )
+ assert result.returncode == 0, result.stderr or result.stdout
+ data = json.loads(result.stdout)
+ assert data["ok"] is True
+ assert data.get("log_line_count", 0) >= 0
+
+ def test_minimal_template_passes_local_validate_before_live(self) -> None:
+ """Gate: bundled example must pass local validation (dogfood prerequisite)."""
+ result = run_script("validate.py", "--path", str(MINIMAL_TEMPLATE), "--repo", "--json")
+ data = json.loads(result.stdout)
+ assert result.returncode == 0, data
+ assert data["ok"] is True
+ template = yaml.safe_load((MINIMAL_TEMPLATE / "template.yaml").read_text(encoding="utf-8"))
+ assert template["kind"] == "Template"
diff --git a/tests/unit/test_rhdh_templates.py b/tests/unit/test_rhdh_templates.py
new file mode 100644
index 0000000..66cdb9a
--- /dev/null
+++ b/tests/unit/test_rhdh_templates.py
@@ -0,0 +1,521 @@
+"""Tests for rhdh-templates skill scripts."""
+
+from __future__ import annotations
+
+import json
+import re
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+import yaml
+
+SKILL_DIR = Path(__file__).resolve().parents[2] / "skills" / "rhdh-templates"
+SCRIPTS = SKILL_DIR / "scripts"
+REFERENCES = SKILL_DIR / "references"
+BUNDLED_EXAMPLES = SKILL_DIR / "assets" / "examples"
+
+
+def run_script(script: str, *args: str) -> subprocess.CompletedProcess:
+ return subprocess.run(
+ [sys.executable, str(SCRIPTS / script), *args],
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+
+
+class TestRhdhTemplatesSkillMd:
+ @pytest.fixture
+ def skill_md(self) -> str:
+ return (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8")
+
+ @pytest.fixture
+ def skill_frontmatter(self, skill_md: str) -> dict:
+ match = re.match(r"^---\n(.*?)\n---", skill_md, re.DOTALL)
+ assert match, "SKILL.md missing YAML frontmatter"
+ return yaml.safe_load(match.group(1))
+
+ def test_frontmatter_name(self, skill_frontmatter: dict) -> None:
+ assert skill_frontmatter["name"] == "rhdh-templates"
+ assert SKILL_DIR.name == skill_frontmatter["name"]
+
+ def test_frontmatter_description(self, skill_frontmatter: dict) -> None:
+ assert len(skill_frontmatter["description"]) > 50
+ assert len(skill_frontmatter["description"]) <= 1024
+
+ def test_has_intake_and_routing(self, skill_md: str) -> None:
+ assert "" in skill_md
+ assert "" in skill_md
+ assert "**Wait for response before proceeding.**" in skill_md
+
+ def test_has_essential_principles(self, skill_md: str) -> None:
+ assert "" in skill_md
+ assert "" not in skill_md
+
+ def test_command_metadata_exists(self) -> None:
+ meta = SCRIPTS / "command-metadata.json"
+ data = json.loads(meta.read_text(encoding="utf-8"))
+ expected = {
+ "init",
+ "templatize",
+ "create",
+ "add-parameter",
+ "add-step",
+ "add-skeleton",
+ "create-location",
+ "fix-gotchas",
+ "validate",
+ "list-actions",
+ "dry-run",
+ "explain-action",
+ "examples",
+ }
+ assert expected.issubset(set(data.keys()))
+
+
+class TestRhdhTemplatesReferences:
+ @pytest.fixture
+ def skill_md(self) -> str:
+ return (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8")
+
+ def test_all_referenced_files_exist(self, skill_md: str) -> None:
+ refs = re.findall(r"references/([\w./-]+\.(?:md|json))", skill_md)
+ for ref in sorted(set(refs)):
+ path = SKILL_DIR / "references" / ref
+ assert path.is_file(), f"Missing reference file: {ref}"
+
+ def test_schema_file_exists(self) -> None:
+ schema = SKILL_DIR / "references" / "schemas" / "template-v1beta3.schema.json"
+ assert schema.is_file()
+ data = json.loads(schema.read_text(encoding="utf-8"))
+ assert data["properties"]["apiVersion"]["enum"] == ["scaffolder.backstage.io/v1beta3"]
+
+ def test_best_practices_reference_structure(self) -> None:
+ content = (REFERENCES / "best-practices.md").read_text(encoding="utf-8")
+ assert "" in content
+ assert "" in content
+ assert "Template Editor" in content
+ assert "parameter-widgets.md" in content
+ assert "parseEntityRef" in content
+
+ def test_command_references_have_xml_structure(self) -> None:
+ command_refs = [
+ "init.md",
+ "templatize.md",
+ "create.md",
+ "add-parameter.md",
+ "add-step.md",
+ "add-skeleton.md",
+ "create-location.md",
+ "fix-gotchas.md",
+ "validate.md",
+ "list-actions.md",
+ "dry-run.md",
+ "explain-action.md",
+ "example-catalog.md",
+ ]
+ for name in command_refs:
+ content = (REFERENCES / name).read_text(encoding="utf-8")
+ assert "" in content, f"{name} missing "
+ assert "" in content, f"{name} missing "
+
+
+class TestInitScript:
+ def test_scaffolds_layout(self, tmp_path: Path) -> None:
+ result = run_script("init.py", "--path", str(tmp_path), "--json")
+ assert result.returncode in (0, 1)
+ data = json.loads(result.stdout)
+ assert (tmp_path / "templates" / "example-template" / "template.yaml").exists()
+ assert (tmp_path / "location.yaml").exists()
+ assert data["ok"] is True
+
+
+class TestAnalyzeScript:
+ def test_detects_nodejs_project(self, tmp_path: Path) -> None:
+ (tmp_path / "package.json").write_text(
+ json.dumps({"name": "demo-service", "description": "Demo app"}),
+ encoding="utf-8",
+ )
+ (tmp_path / "catalog-info.yaml").write_text(
+ "metadata:\n name: demo-service\n owner: group:default/team-a\n",
+ encoding="utf-8",
+ )
+ wf_dir = tmp_path / ".github" / "workflows"
+ wf_dir.mkdir(parents=True)
+ (wf_dir / "ci.yaml").write_text(
+ "jobs:\n build:\n steps:\n - run: echo ${{ github.ref }}\n"
+ )
+
+ result = run_script("analyze.py", "--path", str(tmp_path), "--json")
+ assert result.returncode == 0
+ data = json.loads(result.stdout)
+ assert "nodejs" in data["project_types"]
+ assert data["candidate_count"] >= 1
+ assert data["workflow_files"][0]["needs_raw_block"] is True
+
+
+class TestCreateLocationScript:
+ def test_discovers_templates(self, tmp_path: Path) -> None:
+ template_dir = tmp_path / "templates" / "demo"
+ template_dir.mkdir(parents=True)
+ (template_dir / "template.yaml").write_text("kind: Template\n", encoding="utf-8")
+
+ result = run_script("create_location.py", "--path", str(tmp_path), "--json")
+ assert result.returncode == 0
+ data = json.loads(result.stdout)
+ assert data["template_count"] == 1
+ assert (tmp_path / "location.yaml").exists()
+
+ def test_dry_run_does_not_write(self, tmp_path: Path) -> None:
+ template_dir = tmp_path / "templates" / "demo"
+ template_dir.mkdir(parents=True)
+ (template_dir / "template.yaml").write_text("kind: Template\n", encoding="utf-8")
+
+ result = run_script("create_location.py", "--path", str(tmp_path), "--dry-run", "--json")
+ assert result.returncode == 0
+ data = json.loads(result.stdout)
+ assert data["written"] is False
+ assert not (tmp_path / "location.yaml").exists()
+
+
+class TestFixGotchasScript:
+ def test_minimal_example_passes(self) -> None:
+ template = BUNDLED_EXAMPLES / "minimal-template" / "template.yaml"
+ result = run_script("fix_gotchas.py", "--path", str(template), "--json")
+ assert result.returncode == 0
+ data = json.loads(result.stdout)
+ assert data["critical_count"] == 0
+
+ def test_detects_wrong_api_version(self, tmp_path: Path) -> None:
+ bad = tmp_path / "template.yaml"
+ bad.write_text(
+ "apiVersion: backstage.io/v1beta2\nkind: Template\nspec:\n steps: []\n",
+ encoding="utf-8",
+ )
+ result = run_script("fix_gotchas.py", "--path", str(bad), "--json")
+ data = json.loads(result.stdout)
+ assert data["critical_count"] >= 1
+
+ apply = run_script("fix_gotchas.py", "--path", str(bad), "--apply", "--json")
+ assert "scaffolder.backstage.io/v1beta3" in bad.read_text(encoding="utf-8")
+ assert json.loads(apply.stdout)["applied"] is True
+
+ def test_fixes_pascal_case_action(self, tmp_path: Path) -> None:
+ bad = tmp_path / "template.yaml"
+ bad.write_text(
+ "apiVersion: scaffolder.backstage.io/v1beta3\n"
+ "kind: Template\n"
+ "spec:\n"
+ " parameters: []\n"
+ " steps:\n"
+ " - id: pub\n"
+ " action: publish:GitHub\n"
+ " input: {}\n",
+ encoding="utf-8",
+ )
+ run_script("fix_gotchas.py", "--path", str(bad), "--apply")
+ assert "action: publish:github" in bad.read_text(encoding="utf-8")
+
+ def test_detects_sensitive_param_without_secret_field(self, tmp_path: Path) -> None:
+ bad = tmp_path / "template.yaml"
+ bad.write_text(
+ "apiVersion: scaffolder.backstage.io/v1beta3\n"
+ "kind: Template\n"
+ "metadata:\n"
+ " name: bad\n"
+ " tags:\n"
+ " - test\n"
+ "spec:\n"
+ " parameters:\n"
+ " - title: Auth\n"
+ " properties:\n"
+ " apiToken:\n"
+ " title: API Token\n"
+ " type: string\n"
+ " steps: []\n",
+ encoding="utf-8",
+ )
+ result = run_script("fix_gotchas.py", "--path", str(bad), "--json")
+ data = json.loads(result.stdout)
+ rule_ids = {f["rule_id"] for f in data["findings"]}
+ assert "sensitive-param-secret-field" in rule_ids
+
+ def test_detects_missing_metadata_tags(self, tmp_path: Path) -> None:
+ bad = tmp_path / "template.yaml"
+ bad.write_text(
+ "apiVersion: scaffolder.backstage.io/v1beta3\n"
+ "kind: Template\n"
+ "metadata:\n"
+ " name: bad\n"
+ "spec:\n"
+ " parameters: []\n"
+ " steps: []\n",
+ encoding="utf-8",
+ )
+ result = run_script("fix_gotchas.py", "--path", str(bad), "--json")
+ data = json.loads(result.stdout)
+ rule_ids = {f["rule_id"] for f in data["findings"]}
+ assert "metadata-tags" in rule_ids
+
+
+class TestExampleTemplates:
+ @pytest.mark.parametrize(
+ "example_dir",
+ [
+ "minimal-template",
+ "nodejs-backend",
+ "java-springboot",
+ ],
+ )
+ def test_examples_pass_validate(self, example_dir: str) -> None:
+ path = BUNDLED_EXAMPLES / example_dir
+ result = run_script("validate.py", "--path", str(path), "--json")
+ data = json.loads(result.stdout)
+ assert result.returncode == 0, data
+ assert data["ok"] is True
+ assert data["critical_count"] == 0
+
+ def test_examples_readme_exists(self) -> None:
+ readme = BUNDLED_EXAMPLES / "README.md"
+ assert readme.is_file()
+ text = readme.read_text(encoding="utf-8")
+ assert "nodejs-backend" in text
+ assert "java-springboot" in text
+
+
+class TestSchemaValidateModule:
+ @pytest.fixture
+ def schema_module(self):
+ sys.path.insert(0, str(SCRIPTS))
+ import schema_validate
+
+ return schema_validate
+
+ def test_detects_missing_required_parameter(self, schema_module) -> None:
+ data = {
+ "apiVersion": "scaffolder.backstage.io/v1beta3",
+ "kind": "Template",
+ "metadata": {"name": "bad"},
+ "spec": {
+ "type": "service",
+ "parameters": [
+ {
+ "title": "Details",
+ "required": ["missing"],
+ "properties": {"name": {"type": "string", "title": "Name"}},
+ }
+ ],
+ "steps": [{"id": "x", "action": "debug:log", "input": {}}],
+ },
+ }
+ findings = schema_module.validate_structural(data)
+ messages = " ".join(f["message"] for f in findings)
+ assert "missing" in messages
+
+ def test_detects_duplicate_step_ids(self, schema_module) -> None:
+ data = {
+ "apiVersion": "scaffolder.backstage.io/v1beta3",
+ "kind": "Template",
+ "metadata": {"name": "dup"},
+ "spec": {
+ "type": "service",
+ "parameters": [],
+ "steps": [
+ {"id": "same", "action": "debug:log", "input": {}},
+ {"id": "same", "action": "debug:log", "input": {}},
+ ],
+ },
+ }
+ findings = schema_module.validate_structural(data)
+ assert any("Duplicate step id" in f["message"] for f in findings)
+
+ def test_detects_unknown_parameter_reference(self, schema_module) -> None:
+ data = {
+ "apiVersion": "scaffolder.backstage.io/v1beta3",
+ "kind": "Template",
+ "metadata": {"name": "ref"},
+ "spec": {
+ "type": "service",
+ "parameters": [
+ {
+ "title": "Details",
+ "properties": {"name": {"type": "string", "title": "Name"}},
+ }
+ ],
+ "steps": [
+ {
+ "id": "fetch",
+ "action": "fetch:template",
+ "input": {"url": "./s", "values": {"x": "${{ parameters.unknown }}"}},
+ }
+ ],
+ },
+ }
+ findings = schema_module.validate_cross_references(data)
+ assert any("unknown parameter" in f["message"] for f in findings)
+
+ def test_jsonschema_validates_good_example(self, schema_module) -> None:
+ template = BUNDLED_EXAMPLES / "nodejs-backend" / "template.yaml"
+ data = yaml.safe_load(template.read_text(encoding="utf-8"))
+ findings, note = schema_module.validate_with_jsonschema(data, SKILL_DIR)
+ if note and "not installed" in note:
+ pytest.skip(note)
+ critical = [f for f in findings if f["severity"] == "critical"]
+ assert critical == []
+
+
+class TestValidateScript:
+ def test_minimal_example_passes(self) -> None:
+ template = BUNDLED_EXAMPLES / "minimal-template"
+ result = run_script("validate.py", "--path", str(template), "--json")
+ assert result.returncode == 0
+ data = json.loads(result.stdout)
+ assert data["ok"] is True
+ assert data["critical_count"] == 0
+
+ def test_detects_bad_api_version(self, tmp_path: Path) -> None:
+ bad = tmp_path / "template.yaml"
+ bad.write_text(
+ "apiVersion: backstage.io/v1beta2\n"
+ "kind: Template\n"
+ "metadata:\n"
+ " name: bad\n"
+ "spec:\n"
+ " parameters: []\n"
+ " steps: []\n",
+ encoding="utf-8",
+ )
+ result = run_script("validate.py", "--path", str(bad), "--json")
+ assert result.returncode == 1
+ data = json.loads(result.stdout)
+ assert data["critical_count"] >= 1
+
+ def test_detects_missing_required_in_schema(self, tmp_path: Path) -> None:
+ bad = tmp_path / "template.yaml"
+ bad.write_text(
+ "apiVersion: scaffolder.backstage.io/v1beta3\n"
+ "kind: Template\n"
+ "metadata:\n"
+ " name: bad\n"
+ "spec:\n"
+ " type: service\n"
+ " parameters:\n"
+ " - title: Details\n"
+ " required:\n"
+ " - ghost\n"
+ " properties:\n"
+ " name:\n"
+ " type: string\n"
+ " steps:\n"
+ " - id: x\n"
+ " action: debug:log\n"
+ " input: {}\n",
+ encoding="utf-8",
+ )
+ result = run_script("validate.py", "--path", str(bad), "--json")
+ data = json.loads(result.stdout)
+ schema_findings = [f for f in data["findings"] if f["check"] == "json_schema"]
+ assert any("ghost" in f["message"] for f in schema_findings)
+
+ def test_repo_flag_checks_location(self, tmp_path: Path) -> None:
+ template_dir = tmp_path / "templates" / "demo"
+ template_dir.mkdir(parents=True)
+ (template_dir / "template.yaml").write_text(
+ (BUNDLED_EXAMPLES / "minimal-template" / "template.yaml").read_text(encoding="utf-8"),
+ encoding="utf-8",
+ )
+ result = run_script("validate.py", "--path", str(template_dir), "--repo", "--json")
+ data = json.loads(result.stdout)
+ location_findings = [f for f in data["findings"] if f["check"] == "location"]
+ assert any("location.yaml not found" in f["message"] for f in location_findings)
+
+
+class TestScaffolderApiHelpers:
+ def test_parse_template_ref(self) -> None:
+ sys.path.insert(0, str(SCRIPTS))
+ from scaffolder_api import parse_template_ref
+
+ kind, namespace, name = parse_template_ref("template:default/my-template")
+ assert (kind, namespace, name) == ("template", "default", "my-template")
+
+ def test_load_directory_contents(self, tmp_path: Path) -> None:
+ sys.path.insert(0, str(SCRIPTS))
+ from scaffolder_api import load_directory_contents
+
+ (tmp_path / "README.md").write_text("hello", encoding="utf-8")
+ nested = tmp_path / "nested"
+ nested.mkdir()
+ (nested / "file.txt").write_text("data", encoding="utf-8")
+
+ contents = load_directory_contents(tmp_path)
+ paths = {item["path"] for item in contents}
+ assert paths == {"README.md", "nested/file.txt"}
+ assert all("base64Content" in item for item in contents)
+
+
+class TestExplainActionScript:
+ def test_requires_exactly_one_mode(self) -> None:
+ result = run_script(
+ "explain_action.py",
+ "--rhdh-url",
+ "http://localhost:7007",
+ "--json",
+ )
+ assert result.returncode == 2
+ assert "exactly one" in result.stderr.lower()
+
+
+class TestListExamplesScript:
+ def test_help(self) -> None:
+ result = run_script("list_examples.py", "--help")
+ assert result.returncode == 0
+ assert "--match" in result.stdout
+
+ def test_recommended_backend_match(self) -> None:
+ result = run_script(
+ "list_examples.py",
+ "--match",
+ "spring boot backend with ci",
+ "--limit",
+ "3",
+ "--json",
+ )
+ assert result.returncode == 0
+ data = json.loads(result.stdout)
+ assert data["ok"] is True
+ assert data["count"] >= 1
+ ids = {item["id"] for item in data["examples"]}
+ assert "spring-boot-backend" in ids
+
+ def test_local_only_filter(self) -> None:
+ result = run_script("list_examples.py", "--local-only", "--json")
+ data = json.loads(result.stdout)
+ assert data["count"] >= 2
+ assert all(item.get("local_bundled") for item in data["examples"])
+
+ def test_catalog_file_exists(self) -> None:
+ catalog = SKILL_DIR / "assets" / "example-catalog.json"
+ assert catalog.is_file()
+ data = json.loads(catalog.read_text(encoding="utf-8"))
+ assert len(data["examples"]) >= 20
+
+
+class TestListActionsScript:
+ def test_help(self) -> None:
+ result = run_script("list_actions.py", "--help")
+ assert result.returncode == 0
+ assert "--rhdh-url" in result.stdout
+
+
+class TestDryRunScript:
+ def test_missing_template(self, tmp_path: Path) -> None:
+ result = run_script(
+ "dry_run.py",
+ "--rhdh-url",
+ "http://localhost:7007",
+ "--path",
+ str(tmp_path),
+ )
+ assert result.returncode != 0
diff --git a/uv.lock b/uv.lock
index 8d77fb0..cc39d30 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,10 +2,20 @@ version = 1
revision = 3
requires-python = ">=3.9"
resolution-markers = [
- "python_full_version >= '3.10'",
+ "python_full_version >= '3.11'",
+ "python_full_version == '3.10.*'",
"python_full_version < '3.10'",
]
+[[package]]
+name = "attrs"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
+]
+
[[package]]
name = "colorama"
version = "0.4.6"
@@ -44,20 +54,72 @@ name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.10'",
+ "python_full_version >= '3.11'",
+ "python_full_version == '3.10.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
+[[package]]
+name = "jsonschema"
+version = "4.25.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "attrs", marker = "python_full_version < '3.10'" },
+ { name = "jsonschema-specifications", marker = "python_full_version < '3.10'" },
+ { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.11'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "attrs", marker = "python_full_version >= '3.10'" },
+ { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" },
+ { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "rpds-py", version = "2026.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
[[package]]
name = "packaging"
-version = "26.0"
+version = "26.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
@@ -101,10 +163,11 @@ wheels = [
[[package]]
name = "pytest"
-version = "9.0.2"
+version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.10'",
+ "python_full_version >= '3.11'",
+ "python_full_version == '3.10.*'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
@@ -115,9 +178,9 @@ dependencies = [
{ name = "pygments", marker = "python_full_version >= '3.10'" },
{ name = "tomli", marker = "python_full_version == '3.10.*'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
@@ -193,104 +256,573 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" },
]
+[[package]]
+name = "referencing"
+version = "0.36.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "attrs", marker = "python_full_version < '3.10'" },
+ { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.11'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "attrs", marker = "python_full_version >= '3.10'" },
+ { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "rpds-py", version = "2026.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
[[package]]
name = "rhdh-skill"
-version = "0.5.0"
+version = "0.5.1"
source = { virtual = "." }
[package.optional-dependencies]
dev = [
+ { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "jsonschema", version = "4.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "pyyaml" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
+ { name = "jsonschema", marker = "extra == 'dev'", specifier = ">=4.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" },
{ name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4.0" },
]
provides-extras = ["dev"]
+[[package]]
+name = "rpds-py"
+version = "0.27.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" },
+ { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" },
+ { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" },
+ { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" },
+ { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" },
+ { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" },
+ { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" },
+ { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" },
+ { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" },
+ { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" },
+ { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" },
+ { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" },
+ { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" },
+ { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" },
+ { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" },
+ { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" },
+ { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" },
+ { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" },
+ { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" },
+ { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" },
+ { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" },
+ { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" },
+ { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" },
+ { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" },
+ { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" },
+ { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" },
+ { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" },
+ { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" },
+ { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" },
+ { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" },
+ { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" },
+ { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" },
+ { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" },
+ { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" },
+ { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/6c/252e83e1ce7583c81f26d1d884b2074d40a13977e1b6c9c50bbf9a7f1f5a/rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527", size = 372140, upload-time = "2025-08-27T12:15:05.441Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/71/949c195d927c5aeb0d0629d329a20de43a64c423a6aa53836290609ef7ec/rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d", size = 354086, upload-time = "2025-08-27T12:15:07.404Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/02/e43e332ad8ce4f6c4342d151a471a7f2900ed1d76901da62eb3762663a71/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8", size = 382117, upload-time = "2025-08-27T12:15:09.275Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/05/b0fdeb5b577197ad72812bbdfb72f9a08fa1e64539cc3940b1b781cd3596/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc", size = 394520, upload-time = "2025-08-27T12:15:10.727Z" },
+ { url = "https://files.pythonhosted.org/packages/67/1f/4cfef98b2349a7585181e99294fa2a13f0af06902048a5d70f431a66d0b9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1", size = 522657, upload-time = "2025-08-27T12:15:12.613Z" },
+ { url = "https://files.pythonhosted.org/packages/44/55/ccf37ddc4c6dce7437b335088b5ca18da864b334890e2fe9aa6ddc3f79a9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125", size = 402967, upload-time = "2025-08-27T12:15:14.113Z" },
+ { url = "https://files.pythonhosted.org/packages/74/e5/5903f92e41e293b07707d5bf00ef39a0eb2af7190aff4beaf581a6591510/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905", size = 384372, upload-time = "2025-08-27T12:15:15.842Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/e3/fbb409e18aeefc01e49f5922ac63d2d914328430e295c12183ce56ebf76b/rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e", size = 401264, upload-time = "2025-08-27T12:15:17.388Z" },
+ { url = "https://files.pythonhosted.org/packages/55/79/529ad07794e05cb0f38e2f965fc5bb20853d523976719400acecc447ec9d/rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e", size = 418691, upload-time = "2025-08-27T12:15:19.144Z" },
+ { url = "https://files.pythonhosted.org/packages/33/39/6554a7fd6d9906fda2521c6d52f5d723dca123529fb719a5b5e074c15e01/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786", size = 558989, upload-time = "2025-08-27T12:15:21.087Z" },
+ { url = "https://files.pythonhosted.org/packages/19/b2/76fa15173b6f9f445e5ef15120871b945fb8dd9044b6b8c7abe87e938416/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec", size = 589835, upload-time = "2025-08-27T12:15:22.696Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/9e/5560a4b39bab780405bed8a88ee85b30178061d189558a86003548dea045/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b", size = 555227, upload-time = "2025-08-27T12:15:24.278Z" },
+ { url = "https://files.pythonhosted.org/packages/52/d7/cd9c36215111aa65724c132bf709c6f35175973e90b32115dedc4ced09cb/rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52", size = 217899, upload-time = "2025-08-27T12:15:25.926Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e0/d75ab7b4dd8ba777f6b365adbdfc7614bbfe7c5f05703031dfa4b61c3d6c/rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab", size = 228725, upload-time = "2025-08-27T12:15:27.398Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" },
+ { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" },
+ { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" },
+ { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" },
+ { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" },
+ { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" },
+ { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" },
+ { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/ea/5463cd5048a7a2fcdae308b6e96432802132c141bfb9420260142632a0f1/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475", size = 371778, upload-time = "2025-08-27T12:16:13.851Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c8/f38c099db07f5114029c1467649d308543906933eebbc226d4527a5f4693/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f", size = 354394, upload-time = "2025-08-27T12:16:15.609Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/79/b76f97704d9dd8ddbd76fed4c4048153a847c5d6003afe20a6b5c3339065/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6", size = 382348, upload-time = "2025-08-27T12:16:17.251Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/3f/ef23d3c1be1b837b648a3016d5bbe7cfe711422ad110b4081c0a90ef5a53/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3", size = 394159, upload-time = "2025-08-27T12:16:19.251Z" },
+ { url = "https://files.pythonhosted.org/packages/74/8a/9e62693af1a34fd28b1a190d463d12407bd7cf561748cb4745845d9548d3/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3", size = 522775, upload-time = "2025-08-27T12:16:20.929Z" },
+ { url = "https://files.pythonhosted.org/packages/36/0d/8d5bb122bf7a60976b54c5c99a739a3819f49f02d69df3ea2ca2aff47d5c/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8", size = 402633, upload-time = "2025-08-27T12:16:22.548Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/0e/237948c1f425e23e0cf5a566d702652a6e55c6f8fbd332a1792eb7043daf/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400", size = 384867, upload-time = "2025-08-27T12:16:24.29Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/0a/da0813efcd998d260cbe876d97f55b0f469ada8ba9cbc47490a132554540/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485", size = 401791, upload-time = "2025-08-27T12:16:25.954Z" },
+ { url = "https://files.pythonhosted.org/packages/51/78/c6c9e8a8aaca416a6f0d1b6b4a6ee35b88fe2c5401d02235d0a056eceed2/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1", size = 419525, upload-time = "2025-08-27T12:16:27.659Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/69/5af37e1d71487cf6d56dd1420dc7e0c2732c1b6ff612aa7a88374061c0a8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5", size = 559255, upload-time = "2025-08-27T12:16:29.343Z" },
+ { url = "https://files.pythonhosted.org/packages/40/7f/8b7b136069ef7ac3960eda25d832639bdb163018a34c960ed042dd1707c8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4", size = 590384, upload-time = "2025-08-27T12:16:31.005Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/06/c316d3f6ff03f43ccb0eba7de61376f8ec4ea850067dddfafe98274ae13c/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c", size = 555959, upload-time = "2025-08-27T12:16:32.73Z" },
+ { url = "https://files.pythonhosted.org/packages/60/94/384cf54c430b9dac742bbd2ec26c23feb78ded0d43d6d78563a281aec017/rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859", size = 228784, upload-time = "2025-08-27T12:16:34.428Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" },
+ { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" },
+ { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" },
+ { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
+ { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
+ { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
+ { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
+ { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
+ { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
+ { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
+ { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
+ { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
+ { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
+ { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
+ { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
+ { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
+ { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
+ { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
+ { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
+ { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
+ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
+ { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
+ { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
+ { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
+ { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
+ { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
+ { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "2026.5.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.11'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4f/a0/acf8b6fc20bfdcd3a45bd3f57680fb198e157b7e997b9123b10763798bd2/rpds_py-2026.5.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3397a5ed7174dc2786bb214030232fc36fe8e5584fec43a9952cc542b1a12036", size = 355609, upload-time = "2026-05-28T11:58:50.78Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/95/f8203fd997484b1690a6869cd0e503b6c3c6be55b0ecc36d1a491fe742f0/rpds_py-2026.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc", size = 348460, upload-time = "2026-05-28T11:58:52.374Z" },
+ { url = "https://files.pythonhosted.org/packages/33/8c/b47326ad2f0be545a5e5c1a55937a12afaea7d392ba2837bb9680f57e6c9/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164", size = 381031, upload-time = "2026-05-28T11:58:53.775Z" },
+ { url = "https://files.pythonhosted.org/packages/22/0b/e83bbd97ffac6f6389b605cd4e1c8ac5761dc7e977769c9255d8c5adb7bd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead", size = 387121, upload-time = "2026-05-28T11:58:55.243Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/0e/d285d1bc8864245919c61e1ca82263e4a66d337759c3a4cef72766ff9afc/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece", size = 501026, upload-time = "2026-05-28T11:58:56.788Z" },
+ { url = "https://files.pythonhosted.org/packages/86/06/ccb2109a1e543437b5e43816f2b43b9554cc6783145528a4e3711e05c011/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb", size = 391865, upload-time = "2026-05-28T11:58:58.298Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/33/237173db1cfef10105b3839a24de00eb8d2a523711add4632447cdf0aedd/rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda", size = 378012, upload-time = "2026-05-28T11:58:59.589Z" },
+ { url = "https://files.pythonhosted.org/packages/97/64/1eae54e34d5161f9969295e80bd6b62a55f2b6ac5f2a5b60d02c2140e758/rpds_py-2026.5.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a", size = 391111, upload-time = "2026-05-28T11:59:01.104Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/34/5bb334a5a0f65d77869217c4654f34c78a7d11b93938a3c076a2edeafc52/rpds_py-2026.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe71bca7d547acb17027c7fd1624ff8aae623499c498d3e7011182c4de5c25e0", size = 409225, upload-time = "2026-05-28T11:59:02.433Z" },
+ { url = "https://files.pythonhosted.org/packages/16/0f/007ec21283b5b040b4ec3bd95e0402591e22bfa7d5c93dfe01c465c2d2d7/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a", size = 556487, upload-time = "2026-05-28T11:59:04.012Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/10/5437c94508169b6b22d8418fef7a66e9ffb5f3b9e9c94460f2eedafe06ff/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df1d2a1996755b24b9ecee92cb4d36c28f86f464a6a173349c26bab41e94b8c2", size = 620798, upload-time = "2026-05-28T11:59:05.485Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/d5/9937dce4d6bda74157b954e7d1460db05a22f5929dccfeeba1ed27a93df0/rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2", size = 584053, upload-time = "2026-05-28T11:59:06.837Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/31/750617dd0ae1752471bf43f9e41d263398fae7cde7849d23b8574a70e617/rpds_py-2026.5.1-cp311-cp311-win32.whl", hash = "sha256:3684a59b158a7683aaeb8e25352e9a9dd2122cec78f2d8530266e4f91b4c7b3f", size = 214390, upload-time = "2026-05-28T11:59:08.402Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/bb/3dcab0e1d9516303f2eb672a5d6f62eca5a69e2886301e9c8c54b520c39b/rpds_py-2026.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bd530e6a530bb3ea892f194fafa455f3516ac25ecf7143fd33c09be62b0470a", size = 231097, upload-time = "2026-05-28T11:59:09.786Z" },
+ { url = "https://files.pythonhosted.org/packages/49/d6/c6bbf5cb1cf12b9732df8074b57f6ef8341ba884c95d40632ae8bddb44e4/rpds_py-2026.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:0a5ae4dbe43c1076983b72616496919872ae7bbe7a1e21cc48336bc3154d130b", size = 226361, upload-time = "2026-05-28T11:59:11.079Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" },
+ { url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" },
+ { url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" },
+ { url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" },
+ { url = "https://files.pythonhosted.org/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" },
+ { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" },
+ { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" },
+ { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" },
+ { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" },
+ { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" },
+ { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" },
+ { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" },
+ { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" },
+ { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" },
+ { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" },
+ { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" },
+ { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" },
+ { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" },
+ { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" },
+ { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" },
+ { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" },
+ { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" },
+ { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" },
+ { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" },
+ { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" },
+ { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" },
+ { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" },
+ { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" },
+ { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" },
+ { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" },
+ { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" },
+ { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" },
+ { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" },
+ { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" },
+ { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" },
+ { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" },
+ { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" },
+ { url = "https://files.pythonhosted.org/packages/42/56/3fe0fb34820ff667be791b3a3c22b85e8bcba54e9c832f47438c191fa7be/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:edf2765d84e42447f112ad877af8fe1db0089aaec5b28e88d6eab45e7fe99cea", size = 357151, upload-time = "2026-05-28T12:01:53.43Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f2/3eb9ccdb9f143b8c9b003978898cb497f942a324c077401e6b8834238e63/rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb", size = 350195, upload-time = "2026-05-28T12:01:54.901Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/24/dbda232bc4f3ed732120692ab0d2c8402cb020516556d8bee622dcef2413/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df", size = 381850, upload-time = "2026-05-28T12:01:56.601Z" },
+ { url = "https://files.pythonhosted.org/packages/40/30/32e769839a358f78810c234f160f2cc21d1e4e47e1c0e0e0d535be5a0219/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7", size = 387899, upload-time = "2026-05-28T12:01:58.212Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/86/ec84d243aadb3b34b71dd26a010d0930b2d284ff5fc9a69fec53810ee6fd/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc", size = 501618, upload-time = "2026-05-28T12:01:59.888Z" },
+ { url = "https://files.pythonhosted.org/packages/74/25/b60e52686bbff777a64f9e4f4d3dd57980dc846913777177a2c92e4937aa/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162", size = 394003, upload-time = "2026-05-28T12:02:01.482Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/c7/b3a6a588cc2219510ef3f42e207483a93950bedd1e3a0fd4015c95cff9e5/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251", size = 379778, upload-time = "2026-05-28T12:02:03.197Z" },
+ { url = "https://files.pythonhosted.org/packages/31/00/c7dba3fc8a3da8cb3f6db1eb3386be4d79c2e97c6890d20eb9ac66ae8c43/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a", size = 392359, upload-time = "2026-05-28T12:02:04.817Z" },
+ { url = "https://files.pythonhosted.org/packages/93/dd/472ba494c70753f93745992c99855bee0636daf74e6984e5e003f150316f/rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4abbf391a70be864920858bf360f4fb380577c9a0f732438a1996726e2c195b", size = 412820, upload-time = "2026-05-28T12:02:06.401Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/6f/93831a3bfe789542ed0c1d0d74b78b440f055d6dc3ea4640eba2d95e6e23/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34", size = 557243, upload-time = "2026-05-28T12:02:08.013Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/ff/0b3d604614ffc77522c6b288fdbce68957eb583da1002aa65ba38ac0ee40/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:8213afbe8a3a906fb9acb2014423fe3359ee783d0bf90995f70623a3217bfa6c", size = 623541, upload-time = "2026-05-28T12:02:09.661Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/ea/e7b0251441da9adfeaebcf29601d10f2a1455fcf0772fae9e7e19032bd96/rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049", size = 586326, upload-time = "2026-05-28T12:02:11.47Z" },
+]
+
[[package]]
name = "ruff"
-version = "0.15.10"
+version = "0.15.18"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/74/98/1295ad5a5aa9bc85bdcdfa5d82fe7b49c61af5657df4f227637ff9de0da6/ruff-0.15.18.tar.gz", hash = "sha256:2698a964c70e8bf402dcb99c8810472d270d141e7aa8c4e13599fd52033a2f33", size = 4761437, upload-time = "2026-06-18T18:25:39.224Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" },
- { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" },
- { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" },
- { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" },
- { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" },
- { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" },
- { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" },
- { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" },
- { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" },
- { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" },
- { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" },
- { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" },
- { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" },
- { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" },
- { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" },
- { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" },
- { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/d0/686e984941269621e2be72612d5c1e461f8f7b38415a2a7d7a81c8ae6715/ruff-0.15.18-py3-none-linux_armv6l.whl", hash = "sha256:8b6850172348c8381b8b3084c5915a4393c2373b9b54cd5b5e1ea15812bc10df", size = 10887308, upload-time = "2026-06-18T18:25:03.062Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/21/bc4123e3f5515ee99f8ce1eb93a14a0628fe4d1678663cd08f933ac16931/ruff-0.15.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fccc153a85417dcd976883160cacce486997b0a0058dd18f54b8aaaac7d1ce2", size = 11281305, upload-time = "2026-06-18T18:25:30.026Z" },
+ { url = "https://files.pythonhosted.org/packages/51/93/4769464c25cf7ab2acb3c7dda9cad3d867eb41c59565b3e2a9d17249c90c/ruff-0.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08d4c86a68f2c3ec2c9d56380a71fb4a4f65373055cbb8caabd645e9102f38d4", size = 10641215, upload-time = "2026-06-18T18:25:15.802Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/42/56926d17120db2c208d76bf60a1a019644dd9e91dc27f0f95c9caddb1366/ruff-0.15.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e5108745c2c0705da916d7d4de533ddf547051ef45f62888c31bae73f66318", size = 10957224, upload-time = "2026-06-18T18:25:36.955Z" },
+ { url = "https://files.pythonhosted.org/packages/22/4f/d43fab8d8189afde803103022d000a8ef9f230616d436d52a8b2b8d63b50/ruff-0.15.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56949a6ce8b3abde54c0bcb22cebfe57e8771cadc84b407ae8b8eaf67ebdcd43", size = 10699024, upload-time = "2026-06-18T18:25:05.707Z" },
+ { url = "https://files.pythonhosted.org/packages/63/42/1e3e4c68bd408b9768cf3e439acbe2c78245225faef253f7028a0cdb63e0/ruff-0.15.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01a754cd6a1b630d3f97e33eb452cf7a98040482318e870f8bc52a5a30e62657", size = 11491458, upload-time = "2026-06-18T18:25:20.275Z" },
+ { url = "https://files.pythonhosted.org/packages/20/77/47a3484bea8521e14a203d98c389c5c97846675e4f02734672da4a69b52a/ruff-0.15.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ba7a07e03a44dbf10bb086ee06705b173625014ec99f73a7e6836a5e5590a0c", size = 12383752, upload-time = "2026-06-18T18:25:22.535Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/ca/054159590787023d83b658a1a1819c4c8910114e7015069340b71c0961cb/ruff-0.15.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2c40a41a4cadbcf5897b548ab29dfe248b20c540961c0247d98a3973c70403", size = 11577923, upload-time = "2026-06-18T18:25:10.702Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/ff/d353d6b7bbd73cc0ec37f4463d7540e45e894338abdd9964eee0de332708/ruff-0.15.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f0480ce690cbb6c4db6e5d08f19fce98e10ba131a8b60c1bcdac42771e3ae2d", size = 11583925, upload-time = "2026-06-18T18:25:32.391Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/4a/891f89b9c296ed3e5f3ece1a5629badc989d9a8fdaa30431aaf4774bc1c2/ruff-0.15.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2330215f1f393fa8733f55edce04fcf94c36a2c460fcde31f78cc84e4951e9b1", size = 11582834, upload-time = "2026-06-18T18:25:27.309Z" },
+ { url = "https://files.pythonhosted.org/packages/32/a3/ed9e370154bf85de360b93c03026157f02d4943b2d01ff4945f4429f8e8a/ruff-0.15.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6aa6a3d979e48ae617578183674bf264fbe7d0114a796a26bd678d67963c7ff", size = 10927328, upload-time = "2026-06-18T18:25:34.676Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/d1/5cf5909329fedb5d39d555ee818ba5cf4638e1a301b89785d34f2905bfcb/ruff-0.15.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a81beadbbff2c9c245561ae3f77b16709d87f35eec650d0501679239d3449b22", size = 10693187, upload-time = "2026-06-18T18:25:08.245Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/44/ff6c635cf2c4f4e7b618b6640da057376baa36014695487d88aed4794268/ruff-0.15.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2186d9e940ae332ab293623a75b5f4fe49565f449954d50a72a046683aa6b809", size = 11208721, upload-time = "2026-06-18T18:25:41.327Z" },
+ { url = "https://files.pythonhosted.org/packages/88/d9/5baa2a30861adfb7022cf33c1e35b2fc18085b08c16f83eff4c7b99a5f48/ruff-0.15.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c2abf140438032bc77b2284a6c9944ecd8a19e5f1c7b52b1b8e4a0a80d19a7a", size = 11678599, upload-time = "2026-06-18T18:25:13.607Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/1a/0725a7cfdc32ff769efb96ee782bec882e16448c5d9e3be947ec4c04ce27/ruff-0.15.18-py3-none-win32.whl", hash = "sha256:02299e6e9fa5b297a3f6d5d10d7bcd655c925b028bb8b9d4588214549c6b9ec4", size = 10901903, upload-time = "2026-06-18T18:25:24.755Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/51/805d9f6fb7970505c3504794a5ec350f605361b807fef4dcf214ebd35e72/ruff-0.15.18-py3-none-win_amd64.whl", hash = "sha256:dac80dc8d26b2257dbefabed62f5d255c3937b4ccb122da1fc634794fa3578b3", size = 12041189, upload-time = "2026-06-18T18:25:17.915Z" },
+ { url = "https://files.pythonhosted.org/packages/29/4c/67bb45e41609eb4726f1bfeb59e083cf91d14c696d4bd14c234a980be93d/ruff-0.15.18-py3-none-win_arm64.whl", hash = "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", size = 11329958, upload-time = "2026-06-18T18:25:43.686Z" },
]
[[package]]
name = "tomli"
-version = "2.4.0"
+version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
- { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
- { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
- { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
- { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
- { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
- { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
- { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
- { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
- { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
- { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
- { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
- { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
- { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
- { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
- { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
- { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
- { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
- { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
- { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
- { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
- { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
- { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
- { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
- { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
- { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
- { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
- { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
- { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
- { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
- { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
- { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
- { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
- { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
- { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
- { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
- { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
- { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
- { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
- { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
- { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
- { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
- { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
- { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
- { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
- { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
+ { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
+ { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
+ { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
+ { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
+ { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
+ { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
+ { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
+ { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
+ { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
+ { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
+ { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
+ { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
+ { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
+ { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
+ { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
]
[[package]]