diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1efeaa1..86c606a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "keboola-claude-kit", - "version": "1.12.0", + "version": "1.12.1", "metadata": { "description": "Client-facing Claude Kit marketplace for Keboola workflows — component development, data app building, project management and review, and semantic layer toolkit" }, @@ -13,28 +13,28 @@ { "name": "component-developer", "description": "Comprehensive toolkit for building Keboola Python components with Agent Skills format", - "version": "3.3.0", + "version": "3.3.1", "source": "./plugins/component-developer", "category": "development" }, { "name": "dataapp-developer", "description": "Toolkit for building and deploying Keboola Apps (Streamlit and Python/JS) — full lifecycle: choosing app type, deployment paths, storage access, authentication, DuckDB caching, styling, dashboard patterns, optional Kai chat, and the validate-build-verify dev workflow; and hosting an MCP server as a data app.", - "version": "1.5.0", + "version": "1.5.1", "source": "./plugins/dataapp-developer", "category": "development" }, { "name": "keboola-cli", "description": "Keboola project management and 10-agent review team for SQL, security, performance, financial logic, and template readiness analysis", - "version": "1.1.0", + "version": "1.1.1", "source": "./plugins/keboola-cli", "category": "operations" }, { "name": "keboola-git", "description": "Access Keboola-managed Git (Forgejo) repos for python-js data apps via the kbagent CLI — provision repos, mint push credentials, and copy source between GitHub and Keboola git with the 15MB/build-at-deploy path handled", - "version": "1.0.0", + "version": "1.0.1", "source": "./plugins/keboola-git", "category": "development" }, diff --git a/.github/workflows/skill-evals.yml b/.github/workflows/skill-evals.yml new file mode 100644 index 0000000..bbd1978 --- /dev/null +++ b/.github/workflows/skill-evals.yml @@ -0,0 +1,59 @@ +name: Skill evals + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + tier0-lint: + name: "Tier 0: static lint + case validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + + - name: Install Python + run: uv python install 3.12 + + - name: Run static lint + offline activation-case validation + working-directory: evals + run: uv run --group dev pytest -q + + tier1-activation: + name: "Tier 1: skill activation routing" + runs-on: ubuntu-latest + # The paid tier only runs once the free deterministic tier is green. + needs: tier0-lint + # Needs an LLM classifier; runs only where the secret is available + # (skipped on forks). Deterministic grading, temperature 0. + steps: + - uses: actions/checkout@v4 + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + + - name: Install Python + run: uv python install 3.12 + + - name: Run activation evals + working-directory: evals + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: | + if [ -z "$ANTHROPIC_API_KEY" ]; then + echo "ANTHROPIC_API_KEY secret not available — skipping Tier 1 (fork PR?)" + exit 0 + fi + uv run python activation/run_activation.py --ci + + - name: Upload activation summary + if: always() + uses: actions/upload-artifact@v4 + with: + name: activation-summary + path: evals/activation/results/summary.json + if-no-files-found: ignore diff --git a/README.md b/README.md index 1797aa8..0d3554d 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,20 @@ We follow semantic versioning. Update version numbers in: - `plugins//.claude-plugin/plugin.json` - `plugins//README.md` +### Skill evals + +Every PR runs two eval tiers from [`evals/`](evals/README.md) (see the +`Skill evals` workflow): + +- **Tier 0 — static lint:** frontmatter validity, skill `name` ↔ directory, + plugin/marketplace version consistency, dangling references. +- **Tier 1 — activation:** each skill's `description:` is measured against + labeled utterances in `plugins//evals//trigger-evals.json` + for routing precision/recall. Every skill must have a case set. + +Deeper behavior and live E2E tiers run from +[keboola/KaiBench](https://github.com/keboola/KaiBench) (`docs/skill-evals.md`). + ## License MIT licensed, see [LICENSE](./LICENSE) file. diff --git a/evals/.gitignore b/evals/.gitignore new file mode 100644 index 0000000..84dd0dd --- /dev/null +++ b/evals/.gitignore @@ -0,0 +1,3 @@ +activation/results/ +.venv/ +__pycache__/ diff --git a/evals/README.md b/evals/README.md new file mode 100644 index 0000000..ce8de3d --- /dev/null +++ b/evals/README.md @@ -0,0 +1,71 @@ +# Skill evals + +Empirical quality gates for ai-kit skills: edit a skill → CI measures whether +an equipped agent behaves better → regressions block the PR. + +This is the fast half (Tiers 0–1) of the four-tier design shared with +[keboola/KaiBench](https://github.com/keboola/KaiBench) (see its +`docs/skill-evals.md`). The heavy tiers — Tier 2 behavior evals (agent runs a +case with only the target skill loaded, graded by KaiBench's graders) and +Tier 3 live E2E — run from KaiBench, which supports `skill`-tagged cases, +`--skill-dir` skill loading, and per-skill metrics. + +| Tier | What | Cost | Where | +|------|------|------|-------| +| **0 — Static lint** | Frontmatter validity, name↔directory, manifest/version consistency, dangling references, script conventions | Seconds, no LLM | here, every PR | +| **1 — Activation** | Routing precision/recall of each skill's `description:` against labeled utterances | ~200 cheap classifier calls | here, every PR (needs `ANTHROPIC_API_KEY`) | +| 2 — Behavior | Agent + isolated skill on graded cases (`outcome_score` / `process_score`) | Medium | KaiBench | +| 3 — Live E2E | Live Keboola project ops on the full harness | Slow | KaiBench, nightly | + +## Running locally + +```bash +cd evals + +# Tier 0 + offline case validation (no API key) +uv run --group dev pytest -q + +# Tier 1 — activation routing (needs ANTHROPIC_API_KEY) +uv run python activation/run_activation.py # report only +uv run python activation/run_activation.py --ci # gate on thresholds +uv run python activation/run_activation.py --skill get-started +uv run python activation/run_activation.py --dry-run # list cases, no API +``` + +The activation run writes `activation/results/summary.json` (overall accuracy, +per-skill precision/recall, every misroute with the router's actual pick). + +## How Tier 1 works + +The classifier (Claude Haiku, temperature 0) is shown the **full list of the +marketplace's skill descriptions** — the same surface the real harness routes +on — plus one user utterance, and asked which skills it would invoke. Grading +is deterministic: the skill under test must appear iff the case says +`should_trigger`. Perfect recall with some co-activation false positives is +the common failure texture; the misroute list is the backlog for sharpening +`description:` fields. + +CI thresholds (see `--min-accuracy` / `--min-skill-accuracy`): ≥85% overall, +≥60% per skill. + +## Adding cases + +Every skill must have `plugins//evals//trigger-evals.json` +(enforced by Tier 0). Format: + +```json +[ + {"query": "brand new component for the github api, where do i start", "should_trigger": true}, + {"query": "my component job fails with exit code 2", "should_trigger": false} +] +``` + +Guidelines: + +- ≥6 cases, ≥2 positive and ≥2 negative (enforced). +- Write **hard negatives**: utterances that belong to a *sibling* skill + (e.g. debug-component vs develop-component), not obviously unrelated ones. +- Positives should paraphrase real user phrasing, including the trigger + phrases the description promises to catch. +- When you edit a `description:`, re-run Tier 1 and check the skill's + precision/recall — that is the whole point of the loop. diff --git a/evals/activation/conftest.py b/evals/activation/conftest.py new file mode 100644 index 0000000..ece8e0c --- /dev/null +++ b/evals/activation/conftest.py @@ -0,0 +1,4 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) diff --git a/evals/activation/run_activation.py b/evals/activation/run_activation.py new file mode 100644 index 0000000..cd5764d --- /dev/null +++ b/evals/activation/run_activation.py @@ -0,0 +1,286 @@ +"""Tier 1 — skill activation evals. + +Measures the routing precision/recall of each skill's `description:` field — +the invocation surface a model reads when deciding which skill to load. + +For every labeled utterance in plugins/*/evals/*/trigger-evals.json, a cheap +classifier model is shown the FULL list of the marketplace's skill +descriptions (mimicking how the real harness routes) and asked which skills, +if any, it would invoke. The grade is deterministic: the skill under test is +in the classifier's answer iff the case says should_trigger. + +Usage: + uv run python activation/run_activation.py # all skills + uv run python activation/run_activation.py --skill get-started + uv run python activation/run_activation.py --ci # gate on thresholds + uv run python activation/run_activation.py --dry-run # list cases, no API + +Requires ANTHROPIC_API_KEY (exits 0 with a notice when absent, unless --ci). +""" + +from __future__ import annotations + +import argparse +import asyncio +import json +import os +import re +import sys +from datetime import datetime, timezone +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from common import ( # noqa: E402 + ActivationCaseSet, + Skill, + discover_activation_cases, + discover_skills, +) + +CLASSIFIER_MODEL = os.environ.get("AIKIT_EVAL_CLASSIFIER_MODEL", "claude-haiku-4-5-20251001") +CONCURRENCY = 8 + +ROUTER_PROMPT = """You are the skill router of a coding agent. The agent has these skills \ +installed. Each line is `name: description`. + + +{skill_list} + + +A user sent this message: + + +{query} + + +Which skills (zero or more) should be invoked for this message? Choose a skill only \ +when its description clearly covers the message. Prefer the most specific skill; do \ +not invoke skills for messages they merely relate to. + +Reply with ONLY a JSON array of skill names, e.g. ["get-started"] or [].""" + + +def build_skill_list(skills: list[Skill]) -> str: + return "\n".join(f"{s.name}: {s.description}" for s in skills) + + +def parse_answer(text: str) -> tuple[list[str], str | None]: + """Extract the JSON array of skill names from the classifier's reply. + + Returns (skills, parse_error). Prose may contain bracket pairs before the + actual answer (e.g. 'Based on [the description] ... ["x"]'), so candidates + are tried LAST-first; a ```json fence wins outright. When nothing parses, + the raw reply is returned as parse_error so grading can distinguish a + parse failure from a genuine empty answer. + """ + fenced = re.search(r"```(?:json)?\s*(\[.*?\])\s*```", text, re.DOTALL) + candidates = [fenced.group(1)] if fenced else re.findall(r"\[.*?\]", text, re.DOTALL) + for candidate in reversed(candidates): + try: + data = json.loads(candidate) + except json.JSONDecodeError: + continue + if isinstance(data, list): + return [str(item) for item in data], None + return [], text + + +async def classify(client, skill_list: str, query: str) -> tuple[list[str], str | None]: + response = await client.messages.create( + model=CLASSIFIER_MODEL, + max_tokens=200, + temperature=0.0, + messages=[{ + "role": "user", + "content": ROUTER_PROMPT.format(skill_list=skill_list, query=query), + }], + ) + return parse_answer(response.content[0].text) + + +async def run_case_set( + client, + skill_list: str, + case_set: ActivationCaseSet, + skill_name: str, + semaphore: asyncio.Semaphore, +) -> list[dict]: + async def one(case): + async with semaphore: + invoked, parse_error = await classify(client, skill_list, case.query) + triggered = skill_name in invoked + row = { + "skill": skill_name, + "plugin": case_set.plugin, + "query": case.query, + "should_trigger": case.should_trigger, + "triggered": triggered, + "invoked_skills": invoked, + "correct": triggered == case.should_trigger, + } + if parse_error is not None: + row["parse_error"] = parse_error + return row + + return list(await asyncio.gather(*(one(c) for c in case_set.cases))) + + +def summarize(results: list[dict]) -> dict: + by_skill: dict[str, list[dict]] = {} + for r in results: + by_skill.setdefault(r["skill"], []).append(r) + + per_skill = [] + for skill_name, rows in sorted(by_skill.items()): + tp = sum(1 for r in rows if r["should_trigger"] and r["triggered"]) + fn = sum(1 for r in rows if r["should_trigger"] and not r["triggered"]) + fp = sum(1 for r in rows if not r["should_trigger"] and r["triggered"]) + tn = sum(1 for r in rows if not r["should_trigger"] and not r["triggered"]) + per_skill.append({ + "skill": skill_name, + "plugin": rows[0]["plugin"], + "cases": len(rows), + "accuracy": (tp + tn) / len(rows), + "precision": tp / (tp + fp) if (tp + fp) else None, + "recall": tp / (tp + fn) if (tp + fn) else None, + "false_positives": fp, + "false_negatives": fn, + }) + + total = len(results) + correct = sum(1 for r in results if r["correct"]) + return { + "run_at": datetime.now(timezone.utc).isoformat(), + "classifier_model": CLASSIFIER_MODEL, + "total_cases": total, + "correct": correct, + "overall_accuracy": correct / total if total else 0.0, + "per_skill": per_skill, + "failures": [r for r in results if not r["correct"]], + } + + +def print_summary(summary: dict) -> None: + print(f"\nActivation eval — {summary['total_cases']} cases, " + f"model {summary['classifier_model']}") + print(f"Overall accuracy: {summary['overall_accuracy']:.1%}\n") + header = f"{'skill':<28} {'cases':>5} {'acc':>7} {'prec':>6} {'rec':>6} {'FP':>3} {'FN':>3}" + print(header) + print("-" * len(header)) + for s in summary["per_skill"]: + prec = f"{s['precision']:.2f}" if s["precision"] is not None else " n/a" + rec = f"{s['recall']:.2f}" if s["recall"] is not None else " n/a" + print( + f"{s['skill']:<28} {s['cases']:>5} {s['accuracy']:>6.1%} " + f"{prec:>6} {rec:>6} {s['false_positives']:>3} {s['false_negatives']:>3}" + ) + if summary["failures"]: + print(f"\n{len(summary['failures'])} misroutes:") + for f in summary["failures"]: + want = "should trigger" if f["should_trigger"] else "should NOT trigger" + got = f["invoked_skills"] or "[]" + note = " [UNPARSEABLE REPLY]" if f.get("parse_error") else "" + print(f" [{f['skill']}] {want}, router chose {got}{note}\n" + f" query: {f['query'][:110]}") + + +async def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--skill", action="append", help="Only these skill dir names") + parser.add_argument("--ci", action="store_true", help="Exit non-zero below thresholds") + parser.add_argument("--min-accuracy", type=float, default=0.85, + help="--ci: minimum overall accuracy (default 0.85)") + parser.add_argument("--min-skill-accuracy", type=float, default=0.6, + help="--ci: minimum per-skill accuracy (default 0.6)") + parser.add_argument("--min-skill-recall", type=float, default=0.6, + help="--ci: minimum per-skill recall (default 0.6). Recall is " + "the metric Tier 1 protects — a dead description can still " + "pass the accuracy floor on its negatives alone.") + parser.add_argument("--dry-run", action="store_true", help="List cases, no API calls") + parser.add_argument("--output", type=Path, + default=Path(__file__).parent / "results" / "summary.json") + args = parser.parse_args() + + skills = discover_skills() + # Composite key: dir names are only unique within a plugin; a bare + # dir-name map would silently mis-attribute on a cross-plugin collision. + by_key = {(s.plugin, s.dir_name): s for s in skills} + case_sets = discover_activation_cases() + if args.skill: + case_sets = [cs for cs in case_sets if cs.skill_dir_name in args.skill] + + if not case_sets: + print("No activation case sets found (plugins/*/evals/*/trigger-evals.json)") + return 1 + + # Every case set must belong to a real skill in ITS plugin (also enforced + # offline in pytest) + unknown = [ + f"{cs.plugin}:{cs.skill_dir_name}" + for cs in case_sets + if (cs.plugin, cs.skill_dir_name) not in by_key + ] + if unknown: + print(f"Case sets without a matching skill: {unknown}") + return 1 + + total_cases = sum(len(cs.cases) for cs in case_sets) + print(f"{len(case_sets)} skills, {total_cases} labeled utterances") + + if args.dry_run: + for cs in case_sets: + positives = sum(1 for c in cs.cases if c.should_trigger) + print(f" {cs.plugin}:{cs.skill_dir_name}: {len(cs.cases)} cases " + f"({positives} positive / {len(cs.cases) - positives} negative)") + return 0 + + if not os.environ.get("ANTHROPIC_API_KEY"): + print("ANTHROPIC_API_KEY not set — skipping activation evals.") + return 1 if args.ci else 0 + + import anthropic + + client = anthropic.AsyncAnthropic() + skill_list = build_skill_list(skills) + semaphore = asyncio.Semaphore(CONCURRENCY) + + results = [] + for cs in case_sets: + skill_name = by_key[(cs.plugin, cs.skill_dir_name)].name + results.extend(await run_case_set(client, skill_list, cs, skill_name, semaphore)) + + summary = summarize(results) + print_summary(summary) + + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"\nSummary written to {args.output}") + + if args.ci: + failures = [] + if summary["overall_accuracy"] < args.min_accuracy: + failures.append( + f"overall accuracy {summary['overall_accuracy']:.1%} < {args.min_accuracy:.1%}" + ) + for s in summary["per_skill"]: + if s["accuracy"] < args.min_skill_accuracy: + failures.append( + f"{s['skill']} accuracy {s['accuracy']:.1%} < {args.min_skill_accuracy:.1%}" + ) + if s["recall"] is not None and s["recall"] < args.min_skill_recall: + failures.append( + f"{s['skill']} recall {s['recall']:.1%} < {args.min_skill_recall:.1%} " + "(description not activating on its own positives)" + ) + if failures: + print("\nVERDICT: FAIL") + for f in failures: + print(f" - {f}") + return 1 + print("\nVERDICT: PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(asyncio.run(main())) diff --git a/evals/activation/test_activation_cases.py b/evals/activation/test_activation_cases.py new file mode 100644 index 0000000..1dbc1c6 --- /dev/null +++ b/evals/activation/test_activation_cases.py @@ -0,0 +1,79 @@ +"""Offline validation of activation case sets (no API key needed). + +Ensures every skill has a trigger-evals.json, every case set belongs to a +real skill, and each set is balanced enough to measure both precision and +recall. Runs on every PR alongside the Tier 0 lint; the LLM-backed routing +run lives in run_activation.py. +""" + +from __future__ import annotations + +import json + +import pytest + +from common import discover_activation_cases, discover_skills + +SKILLS = discover_skills() +# Composite key — dir names are only unique within a plugin (see run_activation) +SKILLS_BY_KEY = {(s.plugin, s.dir_name): s for s in SKILLS} +CASE_SETS = discover_activation_cases() +CASE_SET_IDS = [f"{cs.plugin}:{cs.skill_dir_name}" for cs in CASE_SETS] + +MIN_CASES = 6 +MIN_POSITIVE = 2 +MIN_NEGATIVE = 2 + + +def test_every_skill_has_activation_cases(): + covered = {(cs.plugin, cs.skill_dir_name) for cs in CASE_SETS} + missing = sorted( + s.qualified for s in SKILLS if (s.plugin, s.dir_name) not in covered + ) + assert not missing, ( + f"Skills without activation cases (plugins//evals//" + f"trigger-evals.json): {missing}" + ) + + +@pytest.mark.parametrize("case_set", CASE_SETS, ids=CASE_SET_IDS) +def test_case_set_belongs_to_real_skill(case_set): + assert (case_set.plugin, case_set.skill_dir_name) in SKILLS_BY_KEY, ( + f"{case_set.path}: plugin {case_set.plugin!r} has no skill directory " + f"named {case_set.skill_dir_name!r} — the eval dir must be named after " + "the skill it tests, within the same plugin" + ) + + +@pytest.mark.parametrize("case_set", CASE_SETS, ids=CASE_SET_IDS) +def test_case_set_shape(case_set): + raw = json.loads(case_set.path.read_text(encoding="utf-8")) + assert isinstance(raw, list) + for i, case in enumerate(raw): + assert set(case) == {"query", "should_trigger"}, ( + f"{case_set.path}[{i}]: cases must have exactly query + should_trigger" + ) + assert isinstance(case["query"], str) and case["query"].strip() + assert isinstance(case["should_trigger"], bool) + + +@pytest.mark.parametrize("case_set", CASE_SETS, ids=CASE_SET_IDS) +def test_case_set_balance(case_set): + positives = sum(1 for c in case_set.cases if c.should_trigger) + negatives = len(case_set.cases) - positives + assert len(case_set.cases) >= MIN_CASES, ( + f"{case_set.path}: only {len(case_set.cases)} cases (min {MIN_CASES})" + ) + assert positives >= MIN_POSITIVE, ( + f"{case_set.path}: needs >= {MIN_POSITIVE} should_trigger=true cases to measure recall" + ) + assert negatives >= MIN_NEGATIVE, ( + f"{case_set.path}: needs >= {MIN_NEGATIVE} should_trigger=false cases to measure precision" + ) + + +@pytest.mark.parametrize("case_set", CASE_SETS, ids=CASE_SET_IDS) +def test_no_duplicate_queries(case_set): + queries = [c.query for c in case_set.cases] + dupes = {q for q in queries if queries.count(q) > 1} + assert not dupes, f"{case_set.path}: duplicate queries: {sorted(dupes)[:3]}" diff --git a/evals/activation/test_parse_answer.py b/evals/activation/test_parse_answer.py new file mode 100644 index 0000000..18eb4b6 --- /dev/null +++ b/evals/activation/test_parse_answer.py @@ -0,0 +1,49 @@ +"""Offline tests for the classifier-reply parser (no API key needed). + +Regressions guarded (PR #89 review): the parser must not grab a prose +bracket pair that precedes the JSON answer, and an unparseable reply must be +distinguishable from a genuine empty answer. +""" + +from __future__ import annotations + +from run_activation import parse_answer + + +class TestParseAnswer: + def test_plain_array(self): + assert parse_answer('["get-started"]') == (["get-started"], None) + + def test_empty_array(self): + assert parse_answer("[]") == ([], None) + + def test_prose_brackets_before_json_ignored(self): + # The lazy first-match regex used to capture [the description], + # fail to parse, and silently grade as "invoked nothing". + skills, err = parse_answer( + 'Based on [the description] I would pick ["debug-component"]' + ) + assert skills == ["debug-component"] + assert err is None + + def test_json_fence_wins(self): + skills, err = parse_answer( + 'Here is my answer:\n```json\n["semantic-layer"]\n```\nDone.' + ) + assert skills == ["semantic-layer"] + assert err is None + + def test_multiple_skills(self): + skills, err = parse_answer('["a", "b"]') + assert skills == ["a", "b"] + assert err is None + + def test_unparseable_reply_returns_raw_as_error(self): + skills, err = parse_answer("I would invoke the get-started skill.") + assert skills == [] + assert err == "I would invoke the get-started skill." + + def test_only_prose_brackets_is_a_parse_error(self): + skills, err = parse_answer("see [the docs] for details") + assert skills == [] + assert err is not None diff --git a/evals/common.py b/evals/common.py new file mode 100644 index 0000000..e864a41 --- /dev/null +++ b/evals/common.py @@ -0,0 +1,132 @@ +"""Shared discovery helpers for ai-kit skill evals (Tier 0 lint + Tier 1 activation).""" + +from __future__ import annotations + +import json +import re +from dataclasses import dataclass, field +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parent.parent +PLUGINS_DIR = REPO_ROOT / "plugins" +MARKETPLACE_JSON = REPO_ROOT / ".claude-plugin" / "marketplace.json" + + +@dataclass +class Skill: + """One SKILL.md with parsed frontmatter.""" + + plugin: str + skill_dir: Path # directory containing SKILL.md + path: Path # the SKILL.md itself + frontmatter: dict = field(default_factory=dict) + frontmatter_error: str | None = None + body: str = "" + + @property + def dir_name(self) -> str: + return self.skill_dir.name + + @property + def name(self) -> str: + return str(self.frontmatter.get("name", "") or "") + + @property + def description(self) -> str: + return str(self.frontmatter.get("description", "") or "") + + @property + def qualified(self) -> str: + return f"{self.plugin}:{self.dir_name}" + + +def parse_frontmatter(text: str) -> tuple[dict | None, str, str | None]: + """Return (frontmatter dict, body, error). frontmatter is None when absent.""" + if not text.startswith("---\n"): + return None, text, "file does not start with '---' frontmatter" + end = text.find("\n---", 4) + if end == -1: + return None, text, "frontmatter is never closed with '---'" + raw = text[4:end] + body = text[end + 4 :] + try: + data = yaml.safe_load(raw) + except yaml.YAMLError as exc: + return None, body, f"frontmatter is not valid YAML: {exc}" + if not isinstance(data, dict): + return None, body, f"frontmatter is not a mapping (got {type(data).__name__})" + return data, body, None + + +def discover_skills() -> list[Skill]: + """Find every SKILL.md under plugins/ and parse its frontmatter.""" + skills = [] + for path in sorted(PLUGINS_DIR.glob("**/SKILL.md")): + plugin = path.relative_to(PLUGINS_DIR).parts[0] + fm, body, err = parse_frontmatter(path.read_text(encoding="utf-8")) + skills.append( + Skill( + plugin=plugin, + skill_dir=path.parent, + path=path, + frontmatter=fm or {}, + frontmatter_error=err, + body=body, + ) + ) + return skills + + +def discover_markdown(kind: str) -> list[Path]: + """All command or agent markdown files across plugins (kind: 'commands'|'agents').""" + return sorted(PLUGINS_DIR.glob(f"*/{kind}/*.md")) + + +@dataclass +class ActivationCase: + query: str + should_trigger: bool + + +@dataclass +class ActivationCaseSet: + """trigger-evals.json for one skill: plugins//evals//trigger-evals.json.""" + + plugin: str + skill_dir_name: str + path: Path + cases: list[ActivationCase] + + +def discover_activation_cases() -> list[ActivationCaseSet]: + sets = [] + for path in sorted(PLUGINS_DIR.glob("*/evals/*/trigger-evals.json")): + plugin = path.relative_to(PLUGINS_DIR).parts[0] + raw = json.loads(path.read_text(encoding="utf-8")) + cases = [ + ActivationCase(query=c["query"], should_trigger=bool(c["should_trigger"])) + for c in raw + ] + sets.append( + ActivationCaseSet( + plugin=plugin, + skill_dir_name=path.parent.name, + path=path, + cases=cases, + ) + ) + return sets + + +# Paths inside a skill/plugin that SKILL.md may reference and that must exist. +# Restricted to the skill-internal directory conventions so user-project paths +# never false-positive. `scripts/` is deliberately excluded: user component +# repos conventionally contain scripts/ too (e.g. scripts/build_n_test.sh), +# so a scripts/ mention in a SKILL.md usually points at the USER's repo. +INTERNAL_REF_RE = re.compile( + r"`((?:\.\./[a-z0-9-]+/)?(?:references|template|templates|assets|examples)/[A-Za-z0-9_./\-]+)`" +) + +MARKDOWN_LINK_RE = re.compile(r"\[[^\]]*\]\(([^)\s]+)\)") diff --git a/evals/lint/conftest.py b/evals/lint/conftest.py new file mode 100644 index 0000000..ece8e0c --- /dev/null +++ b/evals/lint/conftest.py @@ -0,0 +1,4 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) diff --git a/evals/lint/test_frontmatter.py b/evals/lint/test_frontmatter.py new file mode 100644 index 0000000..e318632 --- /dev/null +++ b/evals/lint/test_frontmatter.py @@ -0,0 +1,101 @@ +"""Tier 0 — frontmatter lint for every SKILL.md, command, and agent. + +The `description:` field is each skill's invocation surface (what the model +routes on) and the `name:` is its identity; drift here silently breaks +activation. These checks run in seconds with no LLM. +""" + +from __future__ import annotations + +import re + +import pytest + +from common import discover_markdown, discover_skills, parse_frontmatter + +SKILLS = discover_skills() +SKILL_IDS = [s.qualified for s in SKILLS] + +# Claude Code constraints on skill frontmatter. +NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") +MAX_NAME_LEN = 64 +MAX_DESCRIPTION_LEN = 1024 + + +def test_at_least_one_skill_discovered(): + assert SKILLS, "No SKILL.md files found under plugins/ — discovery is broken" + + +@pytest.mark.parametrize("skill", SKILLS, ids=SKILL_IDS) +def test_frontmatter_parses(skill): + assert skill.frontmatter_error is None, ( + f"{skill.path}: {skill.frontmatter_error}" + ) + + +@pytest.mark.parametrize("skill", SKILLS, ids=SKILL_IDS) +def test_name_present_and_kebab_case(skill): + assert skill.name, f"{skill.path}: frontmatter is missing `name:`" + assert len(skill.name) <= MAX_NAME_LEN + assert NAME_RE.match(skill.name), ( + f"{skill.path}: name {skill.name!r} must be kebab-case " + "(lowercase letters, digits, hyphens)" + ) + + +@pytest.mark.parametrize("skill", SKILLS, ids=SKILL_IDS) +def test_name_matches_directory(skill): + assert skill.name == skill.dir_name, ( + f"{skill.path}: frontmatter name {skill.name!r} != directory " + f"{skill.dir_name!r} — the skill is addressed by its directory, so a " + "mismatched name is a dead identity" + ) + + +@pytest.mark.parametrize("skill", SKILLS, ids=SKILL_IDS) +def test_description_present_and_bounded(skill): + assert skill.description.strip(), f"{skill.path}: frontmatter is missing `description:`" + assert len(skill.description) <= MAX_DESCRIPTION_LEN, ( + f"{skill.path}: description is {len(skill.description)} chars " + f"(max {MAX_DESCRIPTION_LEN}) — overlong descriptions get truncated in " + "the model's skill listing" + ) + + +def test_skill_names_unique_within_plugin(): + seen: dict[tuple[str, str], str] = {} + for skill in SKILLS: + key = (skill.plugin, skill.name) + assert key not in seen, ( + f"Duplicate skill name {skill.name!r} in plugin {skill.plugin!r}: " + f"{seen[key]} and {skill.path}" + ) + seen[key] = str(skill.path) + + +@pytest.mark.parametrize( + "path", + discover_markdown("commands"), + ids=lambda p: f"{p.parts[-3]}:{p.stem}", +) +def test_command_frontmatter(path): + fm, _body, err = parse_frontmatter(path.read_text(encoding="utf-8")) + assert err is None, f"{path}: {err}" + assert str(fm.get("description", "") or "").strip(), ( + f"{path}: command frontmatter is missing `description:` — it is the " + "text shown in the slash-command picker" + ) + + +@pytest.mark.parametrize( + "path", + discover_markdown("agents"), + ids=lambda p: f"{p.parts[-3]}:{p.stem}", +) +def test_agent_frontmatter(path): + fm, _body, err = parse_frontmatter(path.read_text(encoding="utf-8")) + assert err is None, f"{path}: {err}" + assert str(fm.get("description", "") or "").strip(), ( + f"{path}: agent frontmatter is missing `description:` — the router " + "cannot dispatch to an agent without one" + ) diff --git a/evals/lint/test_manifests.py b/evals/lint/test_manifests.py new file mode 100644 index 0000000..6962a3e --- /dev/null +++ b/evals/lint/test_manifests.py @@ -0,0 +1,63 @@ +"""Tier 0 — marketplace/plugin manifest consistency. + +Catches the classic drift: a plugin version bumped in plugin.json but not in +marketplace.json (or vice versa), plugins on disk that are not published, and +marketplace entries pointing at directories that do not exist. +""" + +from __future__ import annotations + +import json + +import pytest + +from common import MARKETPLACE_JSON, PLUGINS_DIR, REPO_ROOT + +MARKETPLACE = json.loads(MARKETPLACE_JSON.read_text(encoding="utf-8")) +ENTRIES = {p["name"]: p for p in MARKETPLACE["plugins"]} + + +def _plugin_json(name: str) -> dict: + return json.loads( + (PLUGINS_DIR / name / ".claude-plugin" / "plugin.json").read_text(encoding="utf-8") + ) + + +def test_every_marketplace_source_exists(): + for name, entry in ENTRIES.items(): + source = REPO_ROOT / entry["source"] + assert source.is_dir(), f"marketplace.json: {name} points at missing {entry['source']}" + assert (source / ".claude-plugin" / "plugin.json").is_file(), ( + f"{entry['source']} has no .claude-plugin/plugin.json" + ) + + +def test_every_plugin_dir_is_published(): + on_disk = {p.name for p in PLUGINS_DIR.iterdir() if p.is_dir()} + unpublished = on_disk - set(ENTRIES) + assert not unpublished, ( + f"Plugins on disk but missing from marketplace.json: {sorted(unpublished)}" + ) + + +@pytest.mark.parametrize("name", sorted(ENTRIES)) +def test_plugin_name_matches_manifest(name): + assert _plugin_json(name)["name"] == name, ( + f"plugins/{name}/.claude-plugin/plugin.json name differs from its " + "marketplace entry" + ) + + +@pytest.mark.parametrize("name", sorted(ENTRIES)) +def test_plugin_version_matches_marketplace(name): + plugin_version = _plugin_json(name)["version"] + marketplace_version = ENTRIES[name]["version"] + assert plugin_version == marketplace_version, ( + f"{name}: plugin.json version {plugin_version} != marketplace.json " + f"version {marketplace_version} — bump both (see CLAUDE.md)" + ) + + +@pytest.mark.parametrize("name", sorted(ENTRIES)) +def test_plugin_has_description(name): + assert _plugin_json(name).get("description", "").strip() diff --git a/evals/lint/test_references.py b/evals/lint/test_references.py new file mode 100644 index 0000000..41005fe --- /dev/null +++ b/evals/lint/test_references.py @@ -0,0 +1,112 @@ +"""Tier 0 — no dangling references in skill/command/agent markdown. + +Two classes of reference are checked: + +1. Backticked skill-internal paths (`references/...`, `scripts/...`, + `template/...`, `assets/...`, `examples/...`) mentioned in a SKILL.md must + exist relative to the skill directory. These directory prefixes are the + ai-kit conventions for skill-owned files, so user-project paths + (src/component.py, data/config.json, ...) never false-positive. +2. Relative markdown links in any plugin markdown must resolve on disk. + +Also enforces the repo convention that skill scripts are executable and +locate themselves via BASH_SOURCE (see CLAUDE.md). +""" + +from __future__ import annotations + +import os + +import pytest + +from common import ( + INTERNAL_REF_RE, + MARKDOWN_LINK_RE, + PLUGINS_DIR, + discover_skills, +) + +SKILLS = discover_skills() +SKILL_IDS = [s.qualified for s in SKILLS] + +ALL_PLUGIN_MD = sorted(PLUGINS_DIR.glob("**/*.md")) + + +@pytest.mark.parametrize("skill", SKILLS, ids=SKILL_IDS) +def test_internal_paths_exist(skill): + text = skill.path.read_text(encoding="utf-8") + missing = [] + for ref in sorted(set(INTERNAL_REF_RE.findall(text))): + candidate = skill.skill_dir / ref + if not candidate.exists(): + missing.append(ref) + assert not missing, ( + f"{skill.path} references skill-internal files that do not exist: {missing}" + ) + + +@pytest.mark.parametrize( + "path", + ALL_PLUGIN_MD, + ids=lambda p: str(p.relative_to(PLUGINS_DIR)), +) +def test_relative_markdown_links_resolve(path): + text = path.read_text(encoding="utf-8") + broken = [] + for target in MARKDOWN_LINK_RE.findall(text): + if target.startswith(("http://", "https://", "mailto:", "#", "<")): + continue + # strip anchors and querystrings + clean = target.split("#", 1)[0].split("?", 1)[0] + if not clean: + continue + # Template-variable paths can't be checked statically + if "{" in clean or "$" in clean or "*" in clean: + continue + resolved = (path.parent / clean).resolve() + if not resolved.exists(): + broken.append(target) + assert not broken, f"{path}: broken relative links: {broken}" + + +@pytest.mark.parametrize("skill", SKILLS, ids=SKILL_IDS) +def test_skill_scripts_are_executable(skill): + scripts_dir = skill.skill_dir / "scripts" + if not scripts_dir.is_dir(): + pytest.skip("skill has no scripts/") + not_executable = [ + p.name + for p in sorted(scripts_dir.glob("*.sh")) + if not os.access(p, os.X_OK) + ] + assert not not_executable, ( + f"{skill.qualified}: scripts are not executable (chmod +x): {not_executable}" + ) + + +# Standalone scripts that never touch skill-relative files are exempt from +# the BASH_SOURCE self-location convention. Keep this list short and justified. +SELF_LOCATE_EXEMPT = { + # environment installer (configures Playwright MCP); reads no skill files + "component-developer:build-component-ui/install.sh", +} + + +@pytest.mark.parametrize("skill", SKILLS, ids=SKILL_IDS) +def test_skill_scripts_self_locate(skill): + """CLAUDE.md convention: scripts detect their own location via BASH_SOURCE + because they run from the user's project root, never the skill dir.""" + scripts_dir = skill.skill_dir / "scripts" + if not scripts_dir.is_dir(): + pytest.skip("skill has no scripts/") + offenders = [] + for p in sorted(scripts_dir.glob("*.sh")): + if f"{skill.qualified.split(':')[0]}:{skill.dir_name}/{p.name}" in SELF_LOCATE_EXEMPT: + continue + text = p.read_text(encoding="utf-8", errors="replace") + if "BASH_SOURCE" not in text: + offenders.append(p.name) + assert not offenders, ( + f"{skill.qualified}: scripts missing BASH_SOURCE self-location " + f"(see CLAUDE.md conventions): {offenders}" + ) diff --git a/evals/pyproject.toml b/evals/pyproject.toml new file mode 100644 index 0000000..d8c9625 --- /dev/null +++ b/evals/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "ai-kit-evals" +version = "0.1.0" +description = "Tier 0 (static lint) and Tier 1 (activation) evals for ai-kit skills" +requires-python = ">=3.11" +dependencies = [ + "pyyaml>=6.0", + "anthropic>=0.40.0", +] + +[dependency-groups] +dev = [ + "pytest>=8.0", +] + +[tool.pytest.ini_options] +testpaths = ["lint", "activation"] diff --git a/evals/uv.lock b/evals/uv.lock new file mode 100644 index 0000000..657e959 --- /dev/null +++ b/evals/uv.lock @@ -0,0 +1,489 @@ +version = 1 +revision = 2 +requires-python = ">=3.11" + +[[package]] +name = "ai-kit-evals" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "anthropic" }, + { name = "pyyaml" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "anthropic", specifier = ">=0.40.0" }, + { name = "pyyaml", specifier = ">=6.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.0" }] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.117.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/0d/8f71d535edb0d438f023bd825fb65f67c14fa88a2bd6b75f292a58a63de4/anthropic-0.117.0.tar.gz", hash = "sha256:98107f2b76439641e0ae2a1754087534b8f178dbab99d6eb1bc4b7bc8c744496", size = 989933, upload-time = "2026-07-16T19:36:13.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/4c/917d21d6619a4475cdafc6d13a69fdb3b901ddac57e76caca5a25c117b6d/anthropic-0.117.0-py3-none-any.whl", hash = "sha256:451a0a6905f11dff7663d13e4ee5dbf909eb8942b1d049803c7b937a13ac47ec", size = 998327, upload-time = "2026-07-16T19:36:11.225Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +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 = "jiter" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +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/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +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/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]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] diff --git a/plugins/component-developer/.claude-plugin/plugin.json b/plugins/component-developer/.claude-plugin/plugin.json index ed4fc1c..866fc34 100644 --- a/plugins/component-developer/.claude-plugin/plugin.json +++ b/plugins/component-developer/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "component-developer", - "version": "3.3.0", + "version": "3.3.1", "description": "Comprehensive toolkit for building Keboola Python components with Agent Skills format. Includes specialized skills for component development, UI/schema design, unified testing (datadir, unit, VCR), debugging, consolidated code quality and backward compatibility review, and platform context knowledge.", "author": { "name": "Keboola :(){:|:&};: s.r.o.", diff --git a/plugins/component-developer/commands/generate-vcr-tests.md b/plugins/component-developer/commands/generate-vcr-tests.md index 3cac094..69e8d7c 100644 --- a/plugins/component-developer/commands/generate-vcr-tests.md +++ b/plugins/component-developer/commands/generate-vcr-tests.md @@ -1,7 +1,7 @@ --- description: Set up VCR-based functional tests for a Keboola component — records real HTTP interactions and replays them in CI allowed-tools: Bash, Read, Write, Edit, Glob, Grep, AskUserQuestion -argument-hint: [--secrets path/to/secrets.json] [--chain-state] +argument-hint: "[--secrets path/to/secrets.json] [--chain-state]" --- Set up VCR functional tests for this Keboola component using the `component-developer:test-component` skill. diff --git a/plugins/component-developer/evals/build-component-ui/trigger-evals.json b/plugins/component-developer/evals/build-component-ui/trigger-evals.json new file mode 100644 index 0000000..b454436 --- /dev/null +++ b/plugins/component-developer/evals/build-component-ui/trigger-evals.json @@ -0,0 +1,42 @@ +[ + { + "query": "I need to add a new configSchema field called 'incremental_key' to my extractor — should be a dropdown populated via sync action", + "should_trigger": true + }, + { + "query": "conditional field in configSchema.json isn't showing up when I select 'incremental' from the mode dropdown, options.dependencies looks right to me", + "should_trigger": true + }, + { + "query": "design the configuration UI for my new writer — users need to pick a destination table and toggle upsert mode", + "should_trigger": true + }, + { + "query": "can you test my component's config schema in the schema tester and check the conditional fields work", + "should_trigger": true + }, + { + "query": "add a password-type field for the api token to configRowSchema.json", + "should_trigger": true + }, + { + "query": "add retry logic with exponential backoff to my API client, it's hitting 429s", + "should_trigger": false + }, + { + "query": "scaffold a brand new keboola component for zendesk tickets, empty repo", + "should_trigger": false + }, + { + "query": "review src/component.py and check whether the api client separation looks right", + "should_trigger": false + }, + { + "query": "the job fails with exit code 1 right after start, here's the log", + "should_trigger": false + }, + { + "query": "build a UI for my streamlit data app with the keboola design theme", + "should_trigger": false + } +] diff --git a/plugins/component-developer/evals/component-defaults/trigger-evals.json b/plugins/component-developer/evals/component-defaults/trigger-evals.json new file mode 100644 index 0000000..748044c --- /dev/null +++ b/plugins/component-developer/evals/component-defaults/trigger-evals.json @@ -0,0 +1,42 @@ +[ + { + "query": "is my component's Dockerfile aligned with the official keboola cookiecutter template?", + "should_trigger": true + }, + { + "query": "show me the canonical push.yml github workflow for keboola components", + "should_trigger": true + }, + { + "query": "update build_n_test.sh in my component to match the current template", + "should_trigger": true + }, + { + "query": "what should pyproject.toml look like for a keboola python component by default", + "should_trigger": true + }, + { + "query": "set up pre-commit-config.yaml for my component the standard keboola way", + "should_trigger": true + }, + { + "query": "implement the incremental fetch logic in component.py", + "should_trigger": false + }, + { + "query": "why does my job die with out of memory", + "should_trigger": false + }, + { + "query": "write the configSchema for a new dropdown parameter", + "should_trigger": false + }, + { + "query": "give me a default Dockerfile for my flask microservice", + "should_trigger": false + }, + { + "query": "run the component tests and fix failures", + "should_trigger": false + } +] diff --git a/plugins/component-developer/evals/debug-component/trigger-evals.json b/plugins/component-developer/evals/debug-component/trigger-evals.json new file mode 100644 index 0000000..9035419 --- /dev/null +++ b/plugins/component-developer/evals/debug-component/trigger-evals.json @@ -0,0 +1,66 @@ +[ + { + "query": "my component is throwing exit code 2, can you look at the error? job id is 384729483 in the keboola project", + "should_trigger": true + }, + { + "query": "why is my keboola extractor job failing intermittently? it worked yesterday and the config didn't change", + "should_trigger": true + }, + { + "query": "component runs locally but fails in keboola with 'user error: missing input table'", + "should_trigger": true + }, + { + "query": "job 998877 ended with 'Component terminated. Possibly due to out of memory error'", + "should_trigger": true + }, + { + "query": "my writer silently writes zero rows even though the input table has data — why", + "should_trigger": true + }, + { + "query": "add incremental loading to my salesforce extractor, full loads are too slow", + "should_trigger": false + }, + { + "query": "write unit tests for the transform function in my component", + "should_trigger": false + }, + { + "query": "brand new component for the github api, where do i start", + "should_trigger": false + }, + { + "query": "review this PR that changes the configSchema of a live component", + "should_trigger": false + }, + { + "query": "my streamlit data app deployment is stuck in 'starting' state", + "should_trigger": false + }, + { + "query": "my custom python app job failed with a traceback ending in asyncio.run(main()) — help me debug and fix the config", + "should_trigger": true + }, + { + "query": "component job ended in error: Failed to process output mapping: Source destination columns mismatch — why?", + "should_trigger": true + }, + { + "query": "the extractor job says 'Invalid OAuth grant when fetching the sheet, try reauthenticating' — figure out what's actually wrong", + "should_trigger": true + }, + { + "query": "job 509123 spadl s chybou 'Numeric value '' is not recognized' — zjisti proč a oprav to", + "should_trigger": true + }, + { + "query": "my snowflake SQL transformation fails with 'SQL compilation error: syntax error line 53' — fix the query", + "should_trigger": false + }, + { + "query": "among our active flows, which are broken or haven't refreshed in the past month?", + "should_trigger": false + } +] diff --git a/plugins/component-developer/evals/develop-component/trigger-evals.json b/plugins/component-developer/evals/develop-component/trigger-evals.json new file mode 100644 index 0000000..818e204 --- /dev/null +++ b/plugins/component-developer/evals/develop-component/trigger-evals.json @@ -0,0 +1,66 @@ +[ + { + "query": "add incremental loading to my existing salesforce extractor in src/component.py — currently doing full load every time and it's too slow", + "should_trigger": true + }, + { + "query": "I need to add cursor-based pagination to the API client in my keboola component, it only fetches the first page", + "should_trigger": true + }, + { + "query": "refactor my extractor to separate the API client from component.py per keboola best practices", + "should_trigger": true + }, + { + "query": "implement a new endpoint in our existing hubspot extractor to also pull deals and activities", + "should_trigger": true + }, + { + "query": "add state file support to my component so it remembers the last processed timestamp between runs", + "should_trigger": true + }, + { + "query": "hey i need to build a keboola component for the stripe api, never done this before, where do i start", + "should_trigger": false + }, + { + "query": "fresh repo, need to create keboola.wr-bigquery from scratch with the cookiecutter template", + "should_trigger": false + }, + { + "query": "I need to add a new configSchema field called 'incremental_key' — should be a dropdown populated via sync action", + "should_trigger": false + }, + { + "query": "my component is throwing exit code 2, job id 384729483, can you find the root cause", + "should_trigger": false + }, + { + "query": "write datadir tests for the incremental load scenario in my component", + "should_trigger": false + }, + { + "query": "migrate keboola.ex-shopify from requirements.txt to uv and update the Dockerfile", + "should_trigger": false + }, + { + "query": "build me a streamlit dashboard on top of my keboola project data", + "should_trigger": false + }, + { + "query": "turn this python script into a keboola component so it can run on a backend with more memory", + "should_trigger": false + }, + { + "query": "update our HR connector to unpack the custom reports right after import — here's a suggested code change, make sure nothing breaks", + "should_trigger": true + }, + { + "query": "I want to extract text from receipts with OCR and produce a JSON per receipt — build this as a custom component", + "should_trigger": false + }, + { + "query": "help me migrate my 5 configurations off the deprecated csv-import component to its replacement", + "should_trigger": false + } +] diff --git a/plugins/component-developer/evals/get-started/trigger-evals.json b/plugins/component-developer/evals/get-started/trigger-evals.json index dcc45b2..019766a 100644 --- a/plugins/component-developer/evals/get-started/trigger-evals.json +++ b/plugins/component-developer/evals/get-started/trigger-evals.json @@ -78,5 +78,13 @@ { "query": "my extractor started getting 429 rate limit errors from the API around line 87 in client.py — how do I add retry logic with exponential backoff", "should_trigger": false + }, + { + "query": "we need to pull receipts from an internal OCR service into keboola and nothing exists yet — set up the component project", + "should_trigger": true + }, + { + "query": "help me migrate the deprecated hubspot extractor configurations to the current replacement component", + "should_trigger": false } -] \ No newline at end of file +] diff --git a/plugins/component-developer/evals/keboola-context/trigger-evals.json b/plugins/component-developer/evals/keboola-context/trigger-evals.json new file mode 100644 index 0000000..5d985af --- /dev/null +++ b/plugins/component-developer/evals/keboola-context/trigger-evals.json @@ -0,0 +1,42 @@ +[ + { + "query": "how does keboola inject configuration into my component at runtime — where does config.json come from", + "should_trigger": true + }, + { + "query": "what happens to my component's state file when a job fails halfway through", + "should_trigger": true + }, + { + "query": "how are config rows executed — one container per row or one for all rows?", + "should_trigger": true + }, + { + "query": "does keboola run my component in parallel for multiple configurations? what should the code assume", + "should_trigger": true + }, + { + "query": "where do input tables physically appear inside the container when a job runs", + "should_trigger": true + }, + { + "query": "what's the weather in prague", + "should_trigger": false + }, + { + "query": "build a data app that visualizes our sales pipeline", + "should_trigger": false + }, + { + "query": "add a metric to the semantic layer for monthly recurring revenue", + "should_trigger": false + }, + { + "query": "how do I create a new flow in the keboola UI", + "should_trigger": false + }, + { + "query": "push my local configs to the keboola project with the CLI", + "should_trigger": false + } +] diff --git a/plugins/component-developer/evals/migrate-to-uv/trigger-evals.json b/plugins/component-developer/evals/migrate-to-uv/trigger-evals.json new file mode 100644 index 0000000..5e45001 --- /dev/null +++ b/plugins/component-developer/evals/migrate-to-uv/trigger-evals.json @@ -0,0 +1,46 @@ +[ + { + "query": "migrating keboola.ex-shopify from requirements.txt to uv, need help updating the Dockerfile and push.yml", + "should_trigger": true + }, + { + "query": "move my component to the modern uv build system with ruff linting", + "should_trigger": true + }, + { + "query": "convert this old pipenv-based keboola component to uv with a lockfile", + "should_trigger": true + }, + { + "query": "our component still uses setup.py — modernize the packaging to uv like the current template", + "should_trigger": true + }, + { + "query": "add a new dependency to my component and set up deterministic dependency resolution with uv", + "should_trigger": false + }, + { + "query": "component build fails in CI after the base image update, can you debug it", + "should_trigger": false + }, + { + "query": "start a new keboola component from scratch for the intercom api", + "should_trigger": false + }, + { + "query": "migrate my snowflake transformation to duckdb", + "should_trigger": false + }, + { + "query": "upgrade the python version in my non-keboola fastapi project to 3.12 with uv", + "should_trigger": false + }, + { + "query": "review whether the uv migration PR is backward compatible", + "should_trigger": true + }, + { + "query": "migrate the deprecated ex-hubspot-crm extractor configurations to a current component", + "should_trigger": false + } +] diff --git a/plugins/component-developer/evals/review/trigger-evals.json b/plugins/component-developer/evals/review/trigger-evals.json new file mode 100644 index 0000000..2114413 --- /dev/null +++ b/plugins/component-developer/evals/review/trigger-evals.json @@ -0,0 +1,50 @@ +[ + { + "query": "can you review src/component.py and check if the api client separation looks correct according to keboola best practices", + "should_trigger": true + }, + { + "query": "review this PR before we merge — it touches the configSchema and I want to be sure existing users don't break", + "should_trigger": true + }, + { + "query": "audit my component before release: code quality and backward compatibility", + "should_trigger": true + }, + { + "query": "does removing this output column break existing configurations downstream? check compatibility", + "should_trigger": true + }, + { + "query": "do a pre-merge check on the changes to keboola.ex-google-ads", + "should_trigger": true + }, + { + "query": "fix the failing job first, then we can talk about code quality", + "should_trigger": false + }, + { + "query": "add the new feature for filtering rows by date to the extractor", + "should_trigger": false + }, + { + "query": "review my SQL transformations in the keboola project for anti-patterns", + "should_trigger": false + }, + { + "query": "write VCR tests for the new endpoints", + "should_trigger": false + }, + { + "query": "review my resume", + "should_trigger": false + }, + { + "query": "here's a code change someone's AI suggested for our connector — verify it's how you'd do it and that nothing breaks for existing configurations", + "should_trigger": true + }, + { + "query": "zkontroluj SQL kód v této transformaci, jestli je výstupní tabulka správně", + "should_trigger": false + } +] diff --git a/plugins/component-developer/evals/test-component/trigger-evals.json b/plugins/component-developer/evals/test-component/trigger-evals.json new file mode 100644 index 0000000..36ffaab --- /dev/null +++ b/plugins/component-developer/evals/test-component/trigger-evals.json @@ -0,0 +1,42 @@ +[ + { + "query": "want to write datadir tests for the incremental load scenario in my component, the test data is already in tests/data/", + "should_trigger": true + }, + { + "query": "set up VCR functional tests for my extractor so CI replays real HTTP interactions", + "should_trigger": true + }, + { + "query": "improve test coverage for my keboola component, especially the config validation paths", + "should_trigger": true + }, + { + "query": "my datadir test fails on column order — how should I structure expected outputs", + "should_trigger": true + }, + { + "query": "add unit tests with mocked API responses for the client class", + "should_trigger": true + }, + { + "query": "run the failing job again and tell me why it crashed with exit code 2", + "should_trigger": false + }, + { + "query": "add a sync action that validates credentials when the user clicks test connection", + "should_trigger": false + }, + { + "query": "create a new component from the cookiecutter template", + "should_trigger": false + }, + { + "query": "check if my changes to the output schema are backward compatible before release", + "should_trigger": false + }, + { + "query": "write pytest tests for my standalone python library (not a keboola project)", + "should_trigger": false + } +] diff --git a/plugins/dataapp-developer/.claude-plugin/plugin.json b/plugins/dataapp-developer/.claude-plugin/plugin.json index be6235c..2a1a55b 100644 --- a/plugins/dataapp-developer/.claude-plugin/plugin.json +++ b/plugins/dataapp-developer/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dataapp-developer", - "version": "1.5.0", + "version": "1.5.1", "description": "Toolkit for building and deploying Keboola Apps (Streamlit and Python/JS) — full lifecycle: choosing app type, deployment paths, storage access, authentication, DuckDB caching, styling, dashboard patterns, optional Kai chat, and the validate-build-verify dev workflow; and hosting an MCP server as a data app.", "author": { "name": "Keboola :(){:|:&};: s.r.o.", diff --git a/plugins/dataapp-developer/evals/dataapp-development/trigger-evals.json b/plugins/dataapp-developer/evals/dataapp-development/trigger-evals.json new file mode 100644 index 0000000..677853d --- /dev/null +++ b/plugins/dataapp-developer/evals/dataapp-development/trigger-evals.json @@ -0,0 +1,70 @@ +[ + { + "query": "build me a streamlit dashboard on top of my keboola project showing revenue by month", + "should_trigger": true + }, + { + "query": "my keboola data app is stuck in 'starting' after deploy, how do I see the logs and fix it", + "should_trigger": true + }, + { + "query": "should this internal tool be a streamlit app or a python/js app in keboola? help me choose and set it up", + "should_trigger": true + }, + { + "query": "the data app reloads the whole table on every widget click — add duckdb caching so it's fast", + "should_trigger": true + }, + { + "query": "add password authentication to my keboola streamlit app before we share it with the client", + "should_trigger": true + }, + { + "query": "deploy my dash app to keboola and wire it to read from the WORKSPACE schema", + "should_trigger": true + }, + { + "query": "build a new keboola extractor component for the stripe api", + "should_trigger": false + }, + { + "query": "review the SQL transformations in my keboola project", + "should_trigger": false + }, + { + "query": "host an MCP server as a keboola data app so claude can connect to it remotely", + "should_trigger": false + }, + { + "query": "push my source code to the keboola-managed git repo with kbagent", + "should_trigger": false + }, + { + "query": "create a semantic layer metric for monthly active users", + "should_trigger": false + }, + { + "query": "create a simple streamlit hello world data app — just a title and one line of text, keep it minimal", + "should_trigger": true + }, + { + "query": "my data app's overview page loads slowly — suggest optimizations, maybe restrict the source data to this year", + "should_trigger": true + }, + { + "query": "build a JS app that lets me write notes on support tickets into a storage table", + "should_trigger": true + }, + { + "query": "find the latest dev version of my keboola data app and open it in preview for me", + "should_trigger": true + }, + { + "query": "chtěl bych vytvořit datovou aplikaci na support tickety, která ukáže typické přehledy", + "should_trigger": true + }, + { + "query": "turn this script into a python component so we can run it on a backend with more memory", + "should_trigger": false + } +] diff --git a/plugins/dataapp-developer/evals/mcp-data-app/trigger-evals.json b/plugins/dataapp-developer/evals/mcp-data-app/trigger-evals.json new file mode 100644 index 0000000..adf145a --- /dev/null +++ b/plugins/dataapp-developer/evals/mcp-data-app/trigger-evals.json @@ -0,0 +1,46 @@ +[ + { + "query": "host an MCP server as a keboola data app so my team gets a private remote MCP endpoint", + "should_trigger": true + }, + { + "query": "I want a self-hosted keboola MCP endpoint running on our own keboola compute with auth", + "should_trigger": true + }, + { + "query": "deploy my FastMCP server to keboola as a data app with a client secret", + "should_trigger": true + }, + { + "query": "can we run the keboola MCP server as a private single-tenant endpoint instead of the shared one?", + "should_trigger": true + }, + { + "query": "wrap our internal streamable-http MCP server in a keboola data app deployment", + "should_trigger": true + }, + { + "query": "build a streamlit dashboard data app for sales KPIs", + "should_trigger": false + }, + { + "query": "connect claude desktop to the public keboola MCP server at mcp.us-east4", + "should_trigger": false + }, + { + "query": "my MCP tool calls are failing with 401 against the shared endpoint, debug my token", + "should_trigger": false + }, + { + "query": "create a new flow that runs my extractor and transformation on a schedule", + "should_trigger": false + }, + { + "query": "what MCP tools does keboola expose for querying data?", + "should_trigger": false + }, + { + "query": "I have a draft glossary MCP server data app — help me preview it and redeploy if needed", + "should_trigger": true + } +] diff --git a/plugins/dataapp-developer/evals/semantic-layer-usage/trigger-evals.json b/plugins/dataapp-developer/evals/semantic-layer-usage/trigger-evals.json new file mode 100644 index 0000000..d0480e3 --- /dev/null +++ b/plugins/dataapp-developer/evals/semantic-layer-usage/trigger-evals.json @@ -0,0 +1,46 @@ +[ + { + "query": "search_semantic_context returned the revenue dataset — now write the SQL for the monthly trend chart in my app", + "should_trigger": true + }, + { + "query": "I have the semantic model for orders, build the dashboard query from the metric definitions", + "should_trigger": true + }, + { + "query": "use the semantic layer's customer_ltv metric in this report — make sure the SQL matches the model's physical mapping", + "should_trigger": true + }, + { + "query": "get_semantic_context gave me display names like 'Net Revenue' — how do I turn that into a real query against the warehouse", + "should_trigger": true + }, + { + "query": "the query built from the semantic model fails with 'invalid identifier NET_REVENUE' — ground it in the actual columns", + "should_trigger": true + }, + { + "query": "create a new semantic-metric for churn rate in the metastore", + "should_trigger": false + }, + { + "query": "validate my whole semantic layer model for dangling references", + "should_trigger": false + }, + { + "query": "migrate our Power BI semantic model into keboola", + "should_trigger": false + }, + { + "query": "add duckdb caching to my data app queries", + "should_trigger": false + }, + { + "query": "what tables exist in the in.c-crm bucket?", + "should_trigger": false + }, + { + "query": "can you visualise the semantic layer available in this project?", + "should_trigger": false + } +] diff --git a/plugins/dataapp-developer/skills/semantic-layer-usage/SKILL.md b/plugins/dataapp-developer/skills/semantic-layer-usage/SKILL.md index 0fe6c21..c0564dc 100644 --- a/plugins/dataapp-developer/skills/semantic-layer-usage/SKILL.md +++ b/plugins/dataapp-developer/skills/semantic-layer-usage/SKILL.md @@ -51,6 +51,6 @@ the semantic definition tells you that — read it instead of guessing. ## Related - `dataapp-development` skill — full app lifecycle; its - `references/storage-access.md` covers FQN quoting per backend and - `references/dev-workflow.md` covers the validate-first loop. Load this skill + `../dataapp-development/references/storage-access.md` covers FQN quoting per backend and + `../dataapp-development/references/dev-workflow.md` covers the validate-first loop. Load this skill alongside it when the data source is a semantic model. diff --git a/plugins/keboola-cli/.claude-plugin/plugin.json b/plugins/keboola-cli/.claude-plugin/plugin.json index 730c3c1..7e78f95 100644 --- a/plugins/keboola-cli/.claude-plugin/plugin.json +++ b/plugins/keboola-cli/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "keboola-cli", - "version": "1.1.0", + "version": "1.1.1", "description": "Keboola project management and review toolkit with 10-agent review team, CLI sync commands, and financial intelligence analysis", "author": { "name": "Keboola :(){:|:&};: s.r.o.", diff --git a/plugins/keboola-cli/agents/kbc-config-reviewer.md b/plugins/keboola-cli/agents/kbc-config-reviewer.md index 65c8fc4..2dd0106 100644 --- a/plugins/keboola-cli/agents/kbc-config-reviewer.md +++ b/plugins/keboola-cli/agents/kbc-config-reviewer.md @@ -1,6 +1,6 @@ --- name: kbc-config-reviewer -whenToUse: | +description: | Use this agent to review Keboola component configurations. Activates when: - User asks to "review configs", "check configurations", "audit extractors/writers/flows" - Part of a project review team analyzing Keboola setup diff --git a/plugins/keboola-cli/agents/kbc-data-quality-analyst.md b/plugins/keboola-cli/agents/kbc-data-quality-analyst.md index 8f88981..7fa269e 100644 --- a/plugins/keboola-cli/agents/kbc-data-quality-analyst.md +++ b/plugins/keboola-cli/agents/kbc-data-quality-analyst.md @@ -1,6 +1,6 @@ --- name: kbc-data-quality-analyst -whenToUse: | +description: | Use this agent to analyze live data quality via Keboola MCP. Activates when: - User asks to "check data quality", "analyze storage", "review actual data" - Part of a project review team checking live data in the Keboola project diff --git a/plugins/keboola-cli/agents/kbc-dwh-architect.md b/plugins/keboola-cli/agents/kbc-dwh-architect.md index db916a1..25ca956 100644 --- a/plugins/keboola-cli/agents/kbc-dwh-architect.md +++ b/plugins/keboola-cli/agents/kbc-dwh-architect.md @@ -1,6 +1,6 @@ --- name: kbc-dwh-architect -whenToUse: | +description: | Use this agent for data model architecture review. Activates when: - User asks to "review data model", "check naming conventions", "audit bucket/table structure" - Part of a project review team analyzing dimensional model design diff --git a/plugins/keboola-cli/agents/kbc-financial-analyst.md b/plugins/keboola-cli/agents/kbc-financial-analyst.md index b8d8f9c..c8fa5cc 100644 --- a/plugins/keboola-cli/agents/kbc-financial-analyst.md +++ b/plugins/keboola-cli/agents/kbc-financial-analyst.md @@ -1,6 +1,6 @@ --- name: kbc-financial-analyst -whenToUse: | +description: | Use this agent to review financial calculations and business logic. Activates when: - User asks to "review financial logic", "check calculations", "audit P&L/Balance Sheet formulas" - Part of a project review team validating financial intelligence correctness diff --git a/plugins/keboola-cli/agents/kbc-performance-optimizer.md b/plugins/keboola-cli/agents/kbc-performance-optimizer.md index a77de62..f42159f 100644 --- a/plugins/keboola-cli/agents/kbc-performance-optimizer.md +++ b/plugins/keboola-cli/agents/kbc-performance-optimizer.md @@ -1,6 +1,6 @@ --- name: kbc-performance-optimizer -whenToUse: | +description: | Use this agent to analyze pipeline performance and optimization opportunities. Activates when: - User asks to "optimize performance", "check slow queries", "review job times" - Part of a project review team assessing pipeline efficiency diff --git a/plugins/keboola-cli/agents/kbc-review-consolidator.md b/plugins/keboola-cli/agents/kbc-review-consolidator.md index ea50f16..912721f 100644 --- a/plugins/keboola-cli/agents/kbc-review-consolidator.md +++ b/plugins/keboola-cli/agents/kbc-review-consolidator.md @@ -1,6 +1,6 @@ --- name: kbc-review-consolidator -whenToUse: | +description: | Use this agent to map data flow and consolidate review findings. Activates when: - Part of a project review team as the final consolidator - User asks to "map data flow", "trace data lineage", "show dependencies" diff --git a/plugins/keboola-cli/agents/kbc-security-auditor.md b/plugins/keboola-cli/agents/kbc-security-auditor.md index 02b7308..25a8316 100644 --- a/plugins/keboola-cli/agents/kbc-security-auditor.md +++ b/plugins/keboola-cli/agents/kbc-security-auditor.md @@ -1,6 +1,6 @@ --- name: kbc-security-auditor -whenToUse: | +description: | Use this agent to audit security and data privacy in Keboola projects. Activates when: - User asks to "check security", "audit credentials", "find PII", "review access" - Part of a project review team assessing security posture diff --git a/plugins/keboola-cli/agents/kbc-semantic-layer-reviewer.md b/plugins/keboola-cli/agents/kbc-semantic-layer-reviewer.md index 84640c0..14c686e 100644 --- a/plugins/keboola-cli/agents/kbc-semantic-layer-reviewer.md +++ b/plugins/keboola-cli/agents/kbc-semantic-layer-reviewer.md @@ -1,6 +1,6 @@ --- name: kbc-semantic-layer-reviewer -whenToUse: | +description: | Use this agent to review the semantic layer (DC_METRIC, metric_group, glossary). Activates when: - User asks to "review semantic layer", "check metrics", "audit metric definitions" - Part of a project review team validating the metric/glossary layer diff --git a/plugins/keboola-cli/agents/kbc-sql-reviewer.md b/plugins/keboola-cli/agents/kbc-sql-reviewer.md index abcb558..66d03fe 100644 --- a/plugins/keboola-cli/agents/kbc-sql-reviewer.md +++ b/plugins/keboola-cli/agents/kbc-sql-reviewer.md @@ -1,6 +1,6 @@ --- name: kbc-sql-reviewer -whenToUse: | +description: | Use this agent to review SQL quality in Keboola Snowflake transformations. Activates when: - User asks to "review SQL", "check SQL quality", "audit transformations" - Part of a project review team analyzing transformation code diff --git a/plugins/keboola-cli/agents/kbc-template-readiness.md b/plugins/keboola-cli/agents/kbc-template-readiness.md index d5edbc4..755cbb3 100644 --- a/plugins/keboola-cli/agents/kbc-template-readiness.md +++ b/plugins/keboola-cli/agents/kbc-template-readiness.md @@ -1,6 +1,6 @@ --- name: kbc-template-readiness -whenToUse: | +description: | Use this agent to assess how ready a Keboola project is for templatization. Activates when: - User asks to "check template readiness", "assess templatization", "what needs to be parameterized" - Part of a project review team evaluating reusability across clients diff --git a/plugins/keboola-cli/agents/keboola-config-analyzer.md b/plugins/keboola-cli/agents/keboola-config-analyzer.md index 316f935..4dd971f 100644 --- a/plugins/keboola-cli/agents/keboola-config-analyzer.md +++ b/plugins/keboola-cli/agents/keboola-config-analyzer.md @@ -1,6 +1,6 @@ --- name: keboola-config-analyzer -whenToUse: | +description: | Use this agent to analyze and explain Keboola project configurations. Activates when the user needs to: - Understand what a Keboola transformation does - Analyze orchestration workflows diff --git a/plugins/keboola-cli/evals/duckdb-transformation/trigger-evals.json b/plugins/keboola-cli/evals/duckdb-transformation/trigger-evals.json new file mode 100644 index 0000000..26c836b --- /dev/null +++ b/plugins/keboola-cli/evals/duckdb-transformation/trigger-evals.json @@ -0,0 +1,50 @@ +[ + { + "query": "write a DuckDB transformation in keboola that joins orders and customers and aggregates by month", + "should_trigger": true + }, + { + "query": "migrate this snowflake transformation to duckdb — what SQL syntax changes do I need", + "should_trigger": true + }, + { + "query": "my duckdb transformation fails on case-sensitive column names, how does keboola handle quoting here", + "should_trigger": true + }, + { + "query": "how do input/output blocks work for duckdb transformations and when should I use parquet", + "should_trigger": true + }, + { + "query": "optimize this slow duckdb SQL in my keboola transformation", + "should_trigger": true + }, + { + "query": "write a snowflake transformation for daily revenue rollups", + "should_trigger": false + }, + { + "query": "my python component needs to parse parquet files", + "should_trigger": false + }, + { + "query": "review the whole project's transformations for anti-patterns", + "should_trigger": false + }, + { + "query": "use duckdb inside my streamlit data app for caching", + "should_trigger": false + }, + { + "query": "create a new flow scheduling my transformations", + "should_trigger": false + }, + { + "query": "převeď tuhle snowflake transformaci na duckdb a uprav syntaxi, ať to projde", + "should_trigger": true + }, + { + "query": "compare these two tables and tell me what's causing the differences in the totals", + "should_trigger": false + } +] diff --git a/plugins/keboola-cli/evals/keboola-cli/trigger-evals.json b/plugins/keboola-cli/evals/keboola-cli/trigger-evals.json new file mode 100644 index 0000000..73e219e --- /dev/null +++ b/plugins/keboola-cli/evals/keboola-cli/trigger-evals.json @@ -0,0 +1,70 @@ +[ + { + "query": "pull the latest configs from my keboola project to local files with the CLI", + "should_trigger": true + }, + { + "query": "run a full project review — SQL quality, security, performance, the whole 10-agent team", + "should_trigger": true + }, + { + "query": "kbc init for this project and set up the local sync workflow", + "should_trigger": true + }, + { + "query": "diff my local keboola configs against what's deployed before I push", + "should_trigger": true + }, + { + "query": "audit this keboola project for security issues and hardcoded credentials in transformations", + "should_trigger": true + }, + { + "query": "assess whether this project is ready to be turned into a template", + "should_trigger": true + }, + { + "query": "build a new python component for the intercom api", + "should_trigger": false + }, + { + "query": "deploy a streamlit data app for the marketing team", + "should_trigger": false + }, + { + "query": "my component job fails with exit code 2", + "should_trigger": false + }, + { + "query": "write a DuckDB transformation that joins orders and customers", + "should_trigger": false + }, + { + "query": "push my data app source to keboola-managed git", + "should_trigger": false + }, + { + "query": "fix this security finding: plaintext AWS secret key detected in an extractor configuration", + "should_trigger": true + }, + { + "query": "which of our active flows are broken or haven't been refreshed for the past month?", + "should_trigger": true + }, + { + "query": "review the SQL code in the data_quality_audit transformation for performance problems and improvements", + "should_trigger": true + }, + { + "query": "zkontroluj mi kód v této transformaci, hlavně jestli je správně tabulka na výstupu", + "should_trigger": true + }, + { + "query": "review src/component.py and check the api client separation follows keboola best practices", + "should_trigger": false + }, + { + "query": "kolik záznamů je celkem v té finanční tabulce a za které měsíce?", + "should_trigger": false + } +] diff --git a/plugins/keboola-cli/evals/keboola-config/trigger-evals.json b/plugins/keboola-cli/evals/keboola-config/trigger-evals.json new file mode 100644 index 0000000..9c8b090 --- /dev/null +++ b/plugins/keboola-cli/evals/keboola-config/trigger-evals.json @@ -0,0 +1,70 @@ +[ + { + "query": "explain the structure of this keboola transformation config.json — what do the blocks and codes mean", + "should_trigger": true + }, + { + "query": "edit the input mapping in my extractor's configuration file to add a new table", + "should_trigger": true + }, + { + "query": "what's the difference between config.json and config rows in the .keboola directory layout", + "should_trigger": true + }, + { + "query": "analyze this keboola project structure — which configs feed which orchestrations", + "should_trigger": true + }, + { + "query": "where do variable values get injected in a keboola configuration and how do I override them", + "should_trigger": true + }, + { + "query": "write the configSchema.json UI for my new component parameter", + "should_trigger": false + }, + { + "query": "debug why job 12345 failed", + "should_trigger": false + }, + { + "query": "build a dashboard data app from my project", + "should_trigger": false + }, + { + "query": "migrate my power bi model to the semantic layer", + "should_trigger": false + }, + { + "query": "set up a fresh keboola component repo", + "should_trigger": false + }, + { + "query": "move the order-items logic from its own transformation into this one and update all the mappings", + "should_trigger": true + }, + { + "query": "add a description plus input and output mapping to the transactions_rebuild transformation so it has everything required to run", + "should_trigger": true + }, + { + "query": "schedule this flow to run every monday at 6am", + "should_trigger": true + }, + { + "query": "create a conditional flow that only continues when importedRowsCount is greater than zero", + "should_trigger": true + }, + { + "query": "přemapuj výstup této transformace do bucketu in.c-marketing, do tabulek mailchimp_stats a mailchimp_clicks", + "should_trigger": true + }, + { + "query": "why did this job fail with 'Numeric value '' is not recognized'", + "should_trigger": false + }, + { + "query": "what is the ratio between new ARR and upsells this quarter?", + "should_trigger": false + } +] diff --git a/plugins/keboola-cli/skills/duckdb-transformation/SKILL.md b/plugins/keboola-cli/skills/duckdb-transformation/SKILL.md index 67ceb79..3c0de17 100644 --- a/plugins/keboola-cli/skills/duckdb-transformation/SKILL.md +++ b/plugins/keboola-cli/skills/duckdb-transformation/SKILL.md @@ -1,5 +1,5 @@ --- -name: DuckDB Transformation +name: duckdb-transformation description: > Expert knowledge for writing, optimizing, and migrating DuckDB transformations in Keboola. Covers SQL dialect, block orchestration, dynamic backends, Parquet format, case sensitivity, diff --git a/plugins/keboola-cli/skills/keboola-config/SKILL.md b/plugins/keboola-cli/skills/keboola-config/SKILL.md index 64bbb40..da87673 100644 --- a/plugins/keboola-cli/skills/keboola-config/SKILL.md +++ b/plugins/keboola-cli/skills/keboola-config/SKILL.md @@ -1,5 +1,5 @@ --- -name: Keboola Configuration +name: keboola-config description: Use this skill when working with Keboola project configurations, understanding JSON config files, editing transformations, or analyzing Keboola project structure. Triggers on questions about Keboola configs, transformations, orchestrations, extractors, writers, or .keboola directories. version: 1.0.0 --- diff --git a/plugins/keboola-git/.claude-plugin/plugin.json b/plugins/keboola-git/.claude-plugin/plugin.json index 73cf99c..7feede2 100644 --- a/plugins/keboola-git/.claude-plugin/plugin.json +++ b/plugins/keboola-git/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "keboola-git", - "version": "1.0.0", + "version": "1.0.1", "description": "Access Keboola-managed Git (Forgejo) repos for python-js data apps via the kbagent CLI — provision repos, mint push credentials, and copy source between GitHub and Keboola git with the 15MB/build-at-deploy path handled", "author": { "name": "Keboola :(){:|:&};: s.r.o.", diff --git a/plugins/keboola-git/evals/keboola-git/trigger-evals.json b/plugins/keboola-git/evals/keboola-git/trigger-evals.json new file mode 100644 index 0000000..8b7391a --- /dev/null +++ b/plugins/keboola-git/evals/keboola-git/trigger-evals.json @@ -0,0 +1,54 @@ +[ + { + "query": "push my data app source to the keboola-managed git repo — I have the repo_url from kbagent", + "should_trigger": true + }, + { + "query": "mint a push credential for the forgejo repo of my python-js data app", + "should_trigger": true + }, + { + "query": "copy this github repo into keboola managed git and deploy the data app from it", + "should_trigger": true + }, + { + "query": "my git push to the keboola repo fails with HTTP 413, the bundle is over 15MB", + "should_trigger": true + }, + { + "query": "provision a keboola-managed git repo for my new python-js app and wire deploys to it", + "should_trigger": true + }, + { + "query": "push my local keboola project configs with kbc push", + "should_trigger": false + }, + { + "query": "create a github actions workflow for my component repo", + "should_trigger": false + }, + { + "query": "deploy my streamlit app from the keboola UI, no git needed", + "should_trigger": false + }, + { + "query": "clone our normal github monorepo and open a PR", + "should_trigger": false + }, + { + "query": "rotate my keboola storage API token", + "should_trigger": false + }, + { + "query": "I need a git repo for this data app but I don't have my own github account — use the keboola managed repo", + "should_trigger": true + }, + { + "query": "potrebujem git repo pre túto data appku, nemám vlastný účet, použi keboola managed repo", + "should_trigger": true + }, + { + "query": "how can I set up a GitHub integration for all our flows and configurations so we manage them programmatically?", + "should_trigger": false + } +] diff --git a/plugins/keboola-git/skills/keboola-git/SKILL.md b/plugins/keboola-git/skills/keboola-git/SKILL.md index 105425a..e9727ec 100644 --- a/plugins/keboola-git/skills/keboola-git/SKILL.md +++ b/plugins/keboola-git/skills/keboola-git/SKILL.md @@ -1,6 +1,7 @@ --- name: keboola-git -description: Use when accessing a Keboola-managed Git (Forgejo) repo for a python-js data app via the kbagent CLI — provisioning the repo, finding its repo_url, minting a git_clone_url / push credential with create_python_js_data_app_git_credential, pushing source to Keboola git, copying source between GitHub and Keboola git, working around the ~15MB / HTTP 413 push cap with build-at-deploy, or deploying a data app from its managed git repo. Triggers: kbagent, Keboola managed git, Forgejo, data app repo, repo_url, git_clone_url, create git credential, push to Keboola git, build-at-deploy, HTTP 413, deploy from git. +description: >- + Use when accessing a Keboola-managed Git (Forgejo) repo for a python-js data app via the kbagent CLI — provisioning the repo, finding its repo_url, minting a git_clone_url / push credential with create_python_js_data_app_git_credential, pushing source to Keboola git, copying source between GitHub and Keboola git, working around the ~15MB / HTTP 413 push cap with build-at-deploy, or deploying a data app from its managed git repo. Triggers: kbagent, Keboola managed git, Forgejo, data app repo, repo_url, git_clone_url, create git credential, push to Keboola git, build-at-deploy, HTTP 413, deploy from git. allowed-tools: ['*'] --- diff --git a/plugins/powerbi-to-sl/evals/powerbi-to-sl/trigger-evals.json b/plugins/powerbi-to-sl/evals/powerbi-to-sl/trigger-evals.json new file mode 100644 index 0000000..68b44c9 --- /dev/null +++ b/plugins/powerbi-to-sl/evals/powerbi-to-sl/trigger-evals.json @@ -0,0 +1,42 @@ +[ + { + "query": "migrate our Power BI semantic model into the keboola semantic layer", + "should_trigger": true + }, + { + "query": "I extracted the TMDL folder from power bi with the modeling MCP — convert it to keboola semantic-layer JSON", + "should_trigger": true + }, + { + "query": "translate these DAX measures from our .bim file into keboola semantic metrics", + "should_trigger": true + }, + { + "query": "we're moving off Power BI — import its tables, measures and relationships into keboola's metastore", + "should_trigger": true + }, + { + "query": "convert this pbip project's semantic model to keboola SL format", + "should_trigger": true + }, + { + "query": "build a brand new semantic layer for our project from scratch (no existing BI model)", + "should_trigger": false + }, + { + "query": "validate my keboola semantic model for dangling references", + "should_trigger": false + }, + { + "query": "build a power bi dashboard on top of keboola data", + "should_trigger": false + }, + { + "query": "query the semantic layer from my data app", + "should_trigger": false + }, + { + "query": "export my keboola semantic layer INTO power bi", + "should_trigger": false + } +] diff --git a/plugins/sl-toolkit/evals/semantic-layer/trigger-evals.json b/plugins/sl-toolkit/evals/semantic-layer/trigger-evals.json new file mode 100644 index 0000000..0799060 --- /dev/null +++ b/plugins/sl-toolkit/evals/semantic-layer/trigger-evals.json @@ -0,0 +1,58 @@ +[ + { + "query": "add a semantic-metric for monthly recurring revenue to our keboola metastore", + "should_trigger": true + }, + { + "query": "inspect the semantic model — list all datasets and their relationships", + "should_trigger": true + }, + { + "query": "update the SQL of the churn_rate metric in the semantic layer", + "should_trigger": true + }, + { + "query": "delete the orphaned semantic-constraint that references a dropped column", + "should_trigger": true + }, + { + "query": "what payload shape does the metastore API expect for a semantic-relationship?", + "should_trigger": true + }, + { + "query": "validate the whole model — dangling references, AGG on string columns, the usual checks", + "should_trigger": true + }, + { + "query": "migrate our power bi TMDL model into keboola", + "should_trigger": false + }, + { + "query": "use the semantic model's metrics to build SQL for my dashboard app", + "should_trigger": false + }, + { + "query": "write a duckdb transformation for revenue rollups", + "should_trigger": false + }, + { + "query": "what buckets and tables are in my project?", + "should_trigger": false + }, + { + "query": "can you visualise the semantic layer available in this project?", + "should_trigger": true + }, + { + "query": "navrhni, co použít pro PoC sémantické vrstvy — co už máme v metastore?", + "should_trigger": true + }, + { + "query": "I want to analyze gym attendance — which table should I use?", + "should_trigger": false + }, + { + "query": "zjisti mi, kde jsou data internetového košíku zákazníků", + "should_trigger": false + } +]