From 0ff00be1fbccfe94e1770298b7b68047bdaf11b2 Mon Sep 17 00:00:00 2001 From: Dixon Frederick <70894227+dixonfrederick@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:37:40 +0700 Subject: [PATCH 01/50] feat: add fairness report pack (#46) --- data/fixtures/fairness_report.json | 103 +++++++++++++++++++++ docs/FAIRNESS.md | 22 +++++ src/nokaman/eval/fairness.py | 140 +++++++++++++++++++++++++++++ tests/test_fairness.py | 30 +++++++ 4 files changed, 295 insertions(+) create mode 100644 data/fixtures/fairness_report.json create mode 100644 docs/FAIRNESS.md create mode 100644 src/nokaman/eval/fairness.py create mode 100644 tests/test_fairness.py diff --git a/data/fixtures/fairness_report.json b/data/fixtures/fairness_report.json new file mode 100644 index 0000000..09cfa12 --- /dev/null +++ b/data/fixtures/fairness_report.json @@ -0,0 +1,103 @@ +{ + "suite": "length_matched_en_ko_ja", + "model": "ToyAbilityModel", + "skill": "writing", + "languages": [ + "en", + "ko", + "ja" + ], + "rows": [ + { + "id": "en_daily_notes", + "language": "en", + "char_count": 63, + "token_count": 14, + "score": 53.48, + "cefr": "B1", + "script_bonus": 4.0 + }, + { + "id": "en_weekend_plan", + "language": "en", + "char_count": 61, + "token_count": 13, + "score": 53.46, + "cefr": "B1", + "script_bonus": 4.0 + }, + { + "id": "ko_daily_notes", + "language": "ko", + "char_count": 27, + "token_count": 11, + "score": 51.66, + "cefr": "B1", + "script_bonus": 8.0 + }, + { + "id": "ko_weekend_plan", + "language": "ko", + "char_count": 26, + "token_count": 9, + "score": 50.74, + "cefr": "B1", + "script_bonus": 8.0 + }, + { + "id": "ja_daily_notes", + "language": "ja", + "char_count": 32, + "token_count": 3, + "score": 58.05, + "cefr": "B1", + "script_bonus": 8.0 + }, + { + "id": "ja_weekend_plan", + "language": "ja", + "char_count": 35, + "token_count": 3, + "score": 58.05, + "cefr": "B1", + "script_bonus": 8.0 + } + ], + "by_language": { + "en": { + "n": 2, + "mean_chars": 62, + "mean_tokens": 13.5, + "mean_score": 53.47, + "mean_script_bonus": 4.0 + }, + "ko": { + "n": 2, + "mean_chars": 26.5, + "mean_tokens": 10, + "mean_score": 51.2, + "mean_script_bonus": 8.0 + }, + "ja": { + "n": 2, + "mean_chars": 33.5, + "mean_tokens": 3, + "mean_score": 58.05, + "mean_script_bonus": 8.0 + } + }, + "metrics": { + "score_spread": 6.85, + "token_spread": 10.5, + "max_mean_score_language": "ja", + "min_mean_score_language": "ko" + }, + "bias_notes": [ + "Token counts vary despite matched content length, indicating tokenizer sensitivity." + ], + "mitigations": [ + "Track score spread on length-matched multilingual fixtures before releases.", + "Review script-specific tokenization because CJK/Hangul characters are tokenized differently.", + "Calibrate language-specific priors with labeled learner samples before production use." + ] +} diff --git a/docs/FAIRNESS.md b/docs/FAIRNESS.md new file mode 100644 index 0000000..754717c --- /dev/null +++ b/docs/FAIRNESS.md @@ -0,0 +1,22 @@ +# Fairness Pack + +This pack is a lightweight regression suite for score stability across English, Korean, and Japanese writing-style inputs with similar content length. + +Run: + +```bash +python -m nokaman.eval.fairness --out data/fixtures/fairness_report.json +``` + +The generated report includes per-sample rows, per-language means, and two headline metrics: + +- `score_spread`: difference between the highest and lowest language mean score. +- `token_spread`: difference between the highest and lowest language mean token count. + +The current suite is intentionally small. It is designed to catch obvious regressions in script handling, tokenizer behavior, and language priors before release. It is not a production fairness audit. + +Mitigation guidance: + +- Keep length-matched multilingual fixtures in CI and review large score spreads before release. +- Inspect tokenizer behavior for Hangul and Japanese scripts because character-based token counts can diverge from English word counts. +- Replace heuristic priors with calibration from labeled learner samples when moving beyond the demo model. diff --git a/src/nokaman/eval/fairness.py b/src/nokaman/eval/fairness.py new file mode 100644 index 0000000..c0108bc --- /dev/null +++ b/src/nokaman/eval/fairness.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from statistics import mean + +from nokaman.config import RUNS_DIR +from nokaman.models.toy import ToyAbilityModel + +LANGUAGES = ("en", "ko", "ja") + +LENGTH_MATCHED_TEXTS = { + "en": [ + ( + "en_daily_notes", + "I study each morning, review new words, and write a short diary after class.", + ), + ( + "en_weekend_plan", + "On Saturday I meet a friend, buy groceries, and explain my plans clearly.", + ), + ], + "ko": [ + ( + "ko_daily_notes", + "\uc800\ub294 \ub9e4\uc77c \uc544\uce68 \uacf5\ubd80\ud558\uace0 \uc0c8 \ub2e8\uc5b4\ub97c \ubcf5\uc2b5\ud55c \ub4a4 \uc9e7\uc740 \uc77c\uae30\ub97c \uc501\ub2c8\ub2e4.", + ), + ( + "ko_weekend_plan", + "\ud1a0\uc694\uc77c\uc5d0 \uce5c\uad6c\ub97c \ub9cc\ub098\uace0 \uc7a5\uc744 \ubcf4\uba70 \ub0b4 \uacc4\ud68d\uc744 \uc27d\uac8c \uc124\uba85\ud569\ub2c8\ub2e4.", + ), + ], + "ja": [ + ( + "ja_daily_notes", + "\u6bce\u671d\u52c9\u5f37\u3057\u3066\u3001\u65b0\u3057\u3044\u5358\u8a9e\u3092\u5fa9\u7fd2\u3057\u3001\u6388\u696d\u306e\u5f8c\u306b\u77ed\u3044\u65e5\u8a18\u3092\u66f8\u304d\u307e\u3059\u3002", + ), + ( + "ja_weekend_plan", + "\u571f\u66dc\u65e5\u306b\u53cb\u9054\u3068\u4f1a\u3044\u3001\u8cb7\u3044\u7269\u3092\u3057\u3066\u3001\u81ea\u5206\u306e\u4e88\u5b9a\u3092\u308f\u304b\u308a\u3084\u3059\u304f\u8aac\u660e\u3057\u307e\u3059\u3002", + ), + ], +} + + +def build_fairness_report() -> dict: + rows = [] + for language in LANGUAGES: + model = ToyAbilityModel(language) + for sample_id, text in LENGTH_MATCHED_TEXTS[language]: + scored = model.score_text(text, skill="writing") + rows.append( + { + "id": sample_id, + "language": language, + "char_count": _content_chars(text), + "token_count": scored["features"]["tokens"], + "score": scored["score"], + "cefr": scored["cefr"], + "script_bonus": scored["features"]["script_bonus"], + } + ) + + by_language = {} + for language in LANGUAGES: + lang_rows = [row for row in rows if row["language"] == language] + by_language[language] = { + "n": len(lang_rows), + "mean_chars": round(mean(row["char_count"] for row in lang_rows), 2), + "mean_tokens": round(mean(row["token_count"] for row in lang_rows), 2), + "mean_score": round(mean(row["score"] for row in lang_rows), 2), + "mean_script_bonus": round(mean(row["script_bonus"] for row in lang_rows), 2), + } + + mean_scores = [item["mean_score"] for item in by_language.values()] + mean_tokens = [item["mean_tokens"] for item in by_language.values()] + metrics = { + "score_spread": round(max(mean_scores) - min(mean_scores), 2), + "token_spread": round(max(mean_tokens) - min(mean_tokens), 2), + "max_mean_score_language": max(by_language, key=lambda lang: by_language[lang]["mean_score"]), + "min_mean_score_language": min(by_language, key=lambda lang: by_language[lang]["mean_score"]), + } + return { + "suite": "length_matched_en_ko_ja", + "model": "ToyAbilityModel", + "skill": "writing", + "languages": list(LANGUAGES), + "rows": rows, + "by_language": by_language, + "metrics": metrics, + "bias_notes": _bias_notes(metrics), + "mitigations": [ + "Track score spread on length-matched multilingual fixtures before releases.", + "Review script-specific tokenization because CJK/Hangul characters are tokenized differently.", + "Calibrate language-specific priors with labeled learner samples before production use.", + ], + } + + +def write_fairness_report(path: Path | None = None) -> Path: + out_path = path or (RUNS_DIR / "fairness_report.json") + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text( + json.dumps(build_fairness_report(), indent=2, ensure_ascii=True) + "\n", + encoding="utf-8", + ) + return out_path + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Generate EN/KO/JA length-matched fairness report.") + parser.add_argument("--out", type=Path, default=RUNS_DIR / "fairness_report.json") + args = parser.parse_args(argv) + path = write_fairness_report(args.out) + print(path) + return 0 + + +def _bias_notes(metrics: dict) -> list[str]: + notes = [] + if metrics["score_spread"] >= 10: + notes.append( + "Mean score spread is at least 10 points, so reviewers should inspect language priors." + ) + if metrics["token_spread"] >= 10: + notes.append( + "Token counts vary despite matched content length, indicating tokenizer sensitivity." + ) + if not notes: + notes.append("No large spread detected in this small fixture; keep monitoring with more data.") + return notes + + +def _content_chars(text: str) -> int: + return len("".join(ch for ch in text if not ch.isspace())) + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/tests/test_fairness.py b/tests/test_fairness.py new file mode 100644 index 0000000..b75eff4 --- /dev/null +++ b/tests/test_fairness.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from nokaman.eval.fairness import build_fairness_report, write_fairness_report + + +def test_build_fairness_report_covers_en_ko_ja() -> None: + report = build_fairness_report() + assert report["suite"] == "length_matched_en_ko_ja" + assert set(report["languages"]) == {"en", "ko", "ja"} + assert len(report["rows"]) == 6 + assert report["metrics"]["score_spread"] >= 0 + assert report["bias_notes"] + + +def test_write_fairness_report(tmp_path) -> None: + out = write_fairness_report(tmp_path / "fairness_report.json") + payload = json.loads(out.read_text(encoding="utf-8")) + assert payload["model"] == "ToyAbilityModel" + assert "by_language" in payload + assert payload["mitigations"] + + +def test_committed_fairness_fixture_matches_schema() -> None: + fixture = Path("data/fixtures/fairness_report.json") + payload = json.loads(fixture.read_text(encoding="utf-8")) + assert payload["suite"] == "length_matched_en_ko_ja" + assert set(payload["by_language"]) == {"en", "ko", "ja"} From d657d573e4c855185a68897492c8ccbd113287bd Mon Sep 17 00:00:00 2001 From: Dixon Frederick <70894227+dixonfrederick@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:38:33 +0700 Subject: [PATCH 02/50] feat: add adaptive assessment session (#45) --- src/nokaman/api/app.py | 14 +++++ src/nokaman/eval/adaptive.py | 111 +++++++++++++++++++++++++++++++++++ tests/test_adaptive.py | 34 +++++++++++ tests/test_api.py | 14 +++++ 4 files changed, 173 insertions(+) create mode 100644 src/nokaman/eval/adaptive.py create mode 100644 tests/test_adaptive.py diff --git a/src/nokaman/api/app.py b/src/nokaman/api/app.py index c4adc45..ad90cd6 100644 --- a/src/nokaman/api/app.py +++ b/src/nokaman/api/app.py @@ -1,6 +1,7 @@ from __future__ import annotations from nokaman import __version__ +from nokaman.eval.adaptive import adaptive_session from nokaman.eval.metrics import placement_test from nokaman.eval.pipeline import evaluate_demo, evaluate_text from nokaman.rubrics.registry import SUPPORTED_LANGUAGES @@ -25,6 +26,12 @@ class PlacementReq(BaseModel): answers: list[str] = Field(..., min_length=1) +class AdaptiveReq(BaseModel): + language: str = "en" + answers: list[str] = Field(default_factory=list) + administered_ids: list[str] = Field(default_factory=list) + + @app.get("/health") def health() -> dict: return { @@ -54,3 +61,10 @@ def assess_placement(req: PlacementReq) -> dict: if req.language not in SUPPORTED_LANGUAGES: raise HTTPException(400, f"unsupported language {req.language}") return placement_test(req.language, req.answers) + + +@app.post("/assess/adaptive") +def assess_adaptive(req: AdaptiveReq) -> dict: + if req.language not in SUPPORTED_LANGUAGES: + raise HTTPException(400, f"unsupported language {req.language}") + return adaptive_session(req.language, req.answers, administered_ids=req.administered_ids) diff --git a/src/nokaman/eval/adaptive.py b/src/nokaman/eval/adaptive.py new file mode 100644 index 0000000..0a2af1f --- /dev/null +++ b/src/nokaman/eval/adaptive.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from collections.abc import Iterable + +from nokaman.models.cefr import score_to_cefr +from nokaman.models.toy import ToyAbilityModel +from nokaman.rubrics.registry import get_language_meta + +CEFR_TARGETS = { + "A1": 22.0, + "A2": 42.0, + "B1": 57.0, + "B2": 72.0, + "C1": 84.0, + "C2": 94.0, +} + + +def adaptive_session( + language: str, + answers: list[str] | None = None, + administered_ids: list[str] | None = None, +) -> dict: + code = language.strip().lower() + meta = get_language_meta(code) + answer_list = [answer for answer in (answers or []) if answer.strip()] + answer_estimates = estimate_answer_scores(code, answer_list) + ability_score = _running_ability(answer_estimates) + administered = set(administered_ids or []) + next_prompt = select_next_prompt( + ability_score=ability_score, + prompt_bank=build_prompt_bank(code), + administered_ids=administered, + ) + return { + "language": code, + "language_name": meta["name"], + "n_answers": len(answer_list), + "ability_score": round(ability_score, 2), + "cefr": score_to_cefr(ability_score), + "answer_estimates": answer_estimates, + "next_prompt": next_prompt, + "complete": next_prompt is None, + "model": "AdaptiveHeuristicSession", + } + + +def estimate_answer_scores(language: str, answers: list[str]) -> list[dict]: + model = ToyAbilityModel(language=language) + estimates = [] + for index, answer in enumerate(answers, start=1): + scored = model.score_text(answer, skill="writing") + estimates.append( + { + "answer_index": index, + "score": scored["score"], + "cefr": scored["cefr"], + "tokens": scored["features"]["tokens"], + } + ) + return estimates + + +def select_next_prompt( + ability_score: float, + prompt_bank: Iterable[dict], + administered_ids: set[str] | list[str] | None = None, +) -> dict | None: + administered = set(administered_ids or []) + candidates = [prompt for prompt in prompt_bank if str(prompt["id"]) not in administered] + if not candidates: + return None + target = max(0.0, min(100.0, float(ability_score))) + return min( + candidates, + key=lambda prompt: ( + abs(float(prompt["target_score"]) - target), + float(prompt["target_score"]), + str(prompt["id"]), + ), + ) + + +def build_prompt_bank(language: str) -> list[dict]: + code = language.strip().lower() + meta = get_language_meta(code) + language_name = meta["name"] + templates = { + "A1": f"Introduce yourself in simple {language_name} sentences.", + "A2": f"Describe your daily routine in {language_name} with times and places.", + "B1": f"Explain a recent problem you solved while learning {language_name}.", + "B2": f"Compare two study strategies and defend your preference in {language_name}.", + "C1": f"Analyze how culture affects communication style in {language_name}.", + "C2": f"Write a nuanced argument about language policy and education in {language_name}.", + } + return [ + { + "id": f"{code}_{band.lower()}_adaptive", + "language": code, + "difficulty_cefr": band, + "target_score": target, + "prompt": templates[band], + } + for band, target in CEFR_TARGETS.items() + ] + + +def _running_ability(answer_estimates: list[dict]) -> float: + if not answer_estimates: + return CEFR_TARGETS["A2"] + return sum(float(item["score"]) for item in answer_estimates) / len(answer_estimates) diff --git a/tests/test_adaptive.py b/tests/test_adaptive.py new file mode 100644 index 0000000..5c9eac2 --- /dev/null +++ b/tests/test_adaptive.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from nokaman.eval.adaptive import adaptive_session, build_prompt_bank, select_next_prompt + + +def test_select_next_prompt_uses_nearest_unadministered_prompt() -> None: + prompt_bank = [ + {"id": "a2", "target_score": 42.0}, + {"id": "b1", "target_score": 57.0}, + {"id": "b2", "target_score": 72.0}, + ] + selected = select_next_prompt(ability_score=60.0, prompt_bank=prompt_bank, administered_ids={"b1"}) + assert selected is not None + assert selected["id"] == "b2" + + +def test_adaptive_session_starts_at_a2_prompt() -> None: + result = adaptive_session("en", answers=[]) + assert result["ability_score"] == 42.0 + assert result["cefr"] == "A2" + assert result["next_prompt"]["difficulty_cefr"] == "A2" + assert not result["complete"] + + +def test_adaptive_session_tracks_answer_estimates_and_skips_ids() -> None: + first_prompt = build_prompt_bank("en")[1] + result = adaptive_session( + "en", + answers=["I study English every day because it helps me talk with more people."], + administered_ids=[first_prompt["id"]], + ) + assert result["n_answers"] == 1 + assert result["answer_estimates"][0]["tokens"] > 0 + assert result["next_prompt"]["id"] != first_prompt["id"] diff --git a/tests/test_api.py b/tests/test_api.py index a526c3f..8f76763 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -24,3 +24,17 @@ def test_assess_text() -> None: ) assert r.status_code == 200 assert "cefr" in r.json() + + +def test_assess_adaptive() -> None: + r = client.post( + "/assess/adaptive", + json={ + "language": "en", + "answers": ["I study English every day because it helps me travel."], + }, + ) + assert r.status_code == 200 + payload = r.json() + assert payload["model"] == "AdaptiveHeuristicSession" + assert payload["next_prompt"]["language"] == "en" From ede078a43bd1bd535075e84a256ab3479bfb7a80 Mon Sep 17 00:00:00 2001 From: Dixon Frederick <70894227+dixonfrederick@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:39:27 +0700 Subject: [PATCH 03/50] feat: add listening proxy scorer (#44) --- data/listening/en_a2_commute.json | 42 ++++++++++ data/listening/en_b1_meeting.json | 50 ++++++++++++ src/nokaman/config.py | 1 + src/nokaman/data/loader.py | 19 ++++- src/nokaman/eval/listening.py | 131 ++++++++++++++++++++++++++++++ tests/test_listening.py | 47 +++++++++++ 6 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 data/listening/en_a2_commute.json create mode 100644 data/listening/en_b1_meeting.json create mode 100644 src/nokaman/eval/listening.py create mode 100644 tests/test_listening.py diff --git a/data/listening/en_a2_commute.json b/data/listening/en_a2_commute.json new file mode 100644 index 0000000..3680f05 --- /dev/null +++ b/data/listening/en_a2_commute.json @@ -0,0 +1,42 @@ +{ + "id": "en_a2_commute", + "language": "en", + "skill": "listening", + "difficulty_cefr": "A2", + "expected_cefr": "A2", + "transcript": "Maya takes the bus to the library after breakfast. She meets Omar at nine thirty and they study English together.", + "questions": [ + { + "id": "q1", + "type": "mcq", + "prompt": "Where does Maya go after breakfast?", + "choices": { + "A": "the market", + "B": "the library", + "C": "the station" + }, + "answer": "B", + "response": "B" + }, + { + "id": "q2", + "type": "mcq", + "prompt": "Who does Maya meet?", + "choices": { + "A": "Omar", + "B": "her teacher", + "C": "her sister" + }, + "answer": "A", + "response": "C" + }, + { + "id": "q3", + "type": "short_answer", + "prompt": "How does Maya travel?", + "accepted_answers": ["bus", "the bus"], + "keywords": ["bus"], + "response": "by bus" + } + ] +} diff --git a/data/listening/en_b1_meeting.json b/data/listening/en_b1_meeting.json new file mode 100644 index 0000000..16cfdb7 --- /dev/null +++ b/data/listening/en_b1_meeting.json @@ -0,0 +1,50 @@ +{ + "id": "en_b1_meeting", + "language": "en", + "skill": "listening", + "difficulty_cefr": "B1", + "expected_cefr": "B1", + "transcript": "The project meeting moved from Tuesday morning to Wednesday afternoon because the designer was traveling. The team will send questions by email before lunch.", + "questions": [ + { + "id": "q1", + "type": "mcq", + "prompt": "Why did the meeting move?", + "choices": { + "A": "the designer was traveling", + "B": "the room was closed", + "C": "the manager was sick" + }, + "answer": "A", + "response": "A", + "weight": 1.5 + }, + { + "id": "q2", + "type": "mcq", + "prompt": "When is the meeting now?", + "choices": { + "A": "Tuesday morning", + "B": "Wednesday afternoon", + "C": "Friday afternoon" + }, + "answer": "B", + "response": "B" + }, + { + "id": "q3", + "type": "short_answer", + "prompt": "What should the team send before lunch?", + "accepted_answers": ["questions", "questions by email"], + "keywords": ["questions", "email"], + "response": "questions by email" + }, + { + "id": "q4", + "type": "short_answer", + "prompt": "Which day was the original meeting?", + "accepted_answers": ["Tuesday", "Tuesday morning"], + "response": "Wednesday" + } + ] +} diff --git a/src/nokaman/config.py b/src/nokaman/config.py index 4883908..eb21f13 100644 --- a/src/nokaman/config.py +++ b/src/nokaman/config.py @@ -18,6 +18,7 @@ def data_dir() -> Path: SAMPLES_DIR = data_dir() / "samples" +LISTENING_DIR = data_dir() / "listening" RUBRICS_DIR = data_dir() / "rubrics" OUT_DIR = data_dir() / "out" RUNS_DIR = data_dir() / "runs" diff --git a/src/nokaman/data/loader.py b/src/nokaman/data/loader.py index b90e051..4dbd059 100644 --- a/src/nokaman/data/loader.py +++ b/src/nokaman/data/loader.py @@ -3,7 +3,7 @@ import json from pathlib import Path -from nokaman.config import RUBRICS_DIR, SAMPLES_DIR +from nokaman.config import LISTENING_DIR, RUBRICS_DIR, SAMPLES_DIR def list_sample_files(directory: Path | None = None) -> list[Path]: @@ -20,6 +20,13 @@ def list_rubric_files(directory: Path | None = None) -> list[Path]: return sorted(root.glob("*.json")) +def list_listening_pack_files(directory: Path | None = None) -> list[Path]: + root = directory or LISTENING_DIR + if not root.exists(): + return [] + return sorted(root.glob("*.json")) + + def load_json(path: Path) -> dict: return json.loads(path.read_text(encoding="utf-8")) @@ -33,6 +40,16 @@ def load_sample(path: Path) -> dict: return payload +def load_listening_pack(path: Path) -> dict: + payload = load_json(path) + payload.setdefault("id", path.stem) + payload.setdefault("language", "en") + payload.setdefault("skill", "listening") + payload.setdefault("questions", payload.get("items") or []) + payload["items"] = list(payload.get("items") or payload.get("questions") or []) + return payload + + def load_rubric(path: Path) -> dict: payload = load_json(path) payload.setdefault("language", path.stem) diff --git a/src/nokaman/eval/listening.py b/src/nokaman/eval/listening.py new file mode 100644 index 0000000..51729c8 --- /dev/null +++ b/src/nokaman/eval/listening.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +import re +from pathlib import Path +from typing import Any + +from nokaman.data.loader import load_listening_pack +from nokaman.models.cefr import compare_bands, score_to_cefr +from nokaman.rubrics.registry import get_language_meta + +_DIFFICULTY_BASE = { + "A1": 22.0, + "A2": 42.0, + "B1": 57.0, + "B2": 72.0, + "C1": 84.0, + "C2": 94.0, +} + + +def evaluate_listening_pack_file(path: Path) -> dict: + pack = load_listening_pack(path) + result = score_listening_pack(pack) + expected = pack.get("expected_cefr") + if expected: + result["band_check"] = compare_bands(result["cefr"], str(expected)) + result["source"] = str(path) + return result + + +def score_listening_pack(pack: dict[str, Any]) -> dict: + language = str(pack.get("language") or "en").strip().lower() + meta = get_language_meta(language) + items = list(pack.get("items") or pack.get("questions") or []) + learner_answers = pack.get("answers") if isinstance(pack.get("answers"), dict) else {} + + scored_items = [] + earned = 0.0 + possible = 0.0 + for index, item in enumerate(items, start=1): + item_id = str(item.get("id") or f"q{index}") + response = item.get("response") + if response is None: + response = learner_answers.get(item_id) + weight = _positive_weight(item.get("weight", 1.0)) + credit = _score_item(item, response) + earned += credit * weight + possible += weight + scored_items.append( + { + "id": item_id, + "type": str(item.get("type") or "mcq"), + "weight": weight, + "response": response, + "credit": round(credit, 4), + "correct": credit >= 1.0, + } + ) + + accuracy = (earned / possible * 100.0) if possible else 0.0 + difficulty = str(pack.get("difficulty_cefr") or pack.get("expected_cefr") or "").upper() + score = _ability_score(accuracy, difficulty) + cefr = score_to_cefr(score) + return { + "id": pack.get("id"), + "language": language, + "language_name": meta["name"], + "skill": "listening", + "score": round(score, 2), + "accuracy": round(accuracy, 2), + "cefr": cefr, + "difficulty_cefr": difficulty or None, + "n_items": len(items), + "earned_weight": round(earned, 4), + "possible_weight": round(possible, 4), + "items": scored_items, + "model": "ListeningProxyScorer", + } + + +def _score_item(item: dict[str, Any], response: object) -> float: + kind = str(item.get("type") or "mcq").strip().lower() + normalized_response = _normalize(response) + if not normalized_response: + return 0.0 + + accepted = [_normalize(value) for value in _accepted_answers(item)] + accepted = [value for value in accepted if value] + if normalized_response in accepted: + return 1.0 + + if kind in {"short_answer", "short", "free_text"}: + keywords = [_normalize(value) for value in item.get("keywords", [])] + keywords = [value for value in keywords if value] + if keywords: + hits = sum(1 for keyword in keywords if keyword in normalized_response) + return hits / len(keywords) + + return 0.0 + + +def _accepted_answers(item: dict[str, Any]) -> list[object]: + raw = item.get("accepted_answers") + if raw is None: + raw = item.get("answer", item.get("correct")) + if isinstance(raw, list): + return raw + return [raw] + + +def _ability_score(accuracy: float, difficulty: str) -> float: + if difficulty in _DIFFICULTY_BASE: + score = _DIFFICULTY_BASE[difficulty] + (accuracy - 70.0) * 0.5 + else: + score = accuracy + return max(0.0, min(100.0, score)) + + +def _positive_weight(value: object) -> float: + try: + weight = float(value) + except (TypeError, ValueError): + return 1.0 + return weight if weight > 0 else 1.0 + + +def _normalize(value: object) -> str: + text = "" if value is None else str(value) + text = text.strip().lower() + text = re.sub(r"[^\w\s:.-]", " ", text, flags=re.UNICODE) + return re.sub(r"\s+", " ", text).strip() diff --git a/tests/test_listening.py b/tests/test_listening.py new file mode 100644 index 0000000..8fc9cd5 --- /dev/null +++ b/tests/test_listening.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from nokaman.data.loader import list_listening_pack_files, load_listening_pack +from nokaman.eval.listening import evaluate_listening_pack_file, score_listening_pack + + +def test_listening_loader_reads_fixture_packs() -> None: + files = list_listening_pack_files() + assert len(files) >= 2 + pack = load_listening_pack(files[0]) + assert pack["skill"] == "listening" + assert pack["items"] + + +def test_score_listening_pack_supports_mcq_and_short_answer() -> None: + result = score_listening_pack( + { + "id": "inline_pack", + "language": "en", + "difficulty_cefr": "B1", + "questions": [ + {"id": "q1", "type": "mcq", "answer": "A", "response": "A"}, + {"id": "q2", "type": "mcq", "answer": "B", "response": "C"}, + { + "id": "q3", + "type": "short_answer", + "accepted_answers": ["by email"], + "keywords": ["email", "lunch"], + "response": "They send it by email before lunch.", + "weight": 2, + }, + ], + } + ) + assert result["skill"] == "listening" + assert result["accuracy"] == 75.0 + assert result["cefr"] == "B1" + assert result["items"][2]["credit"] == 1.0 + + +def test_evaluate_listening_fixture_includes_band_check() -> None: + path = next(path for path in list_listening_pack_files() if path.name == "en_a2_commute.json") + result = evaluate_listening_pack_file(path) + assert result["language"] == "en" + assert result["n_items"] == 3 + assert result["band_check"]["exact_match"] + assert result["source"].endswith("en_a2_commute.json") From dc6900a0b05e3de6dcd75e0446888b2e512371a3 Mon Sep 17 00:00:00 2001 From: Dixon Frederick <70894227+dixonfrederick@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:40:19 +0700 Subject: [PATCH 04/50] feat: add TOPIK track labels (#43) --- src/nokaman/cli.py | 25 +++++++++++++++---------- src/nokaman/models/toy.py | 19 +++++++++++++++++-- tests/test_framework_bands.py | 19 +++++++++++++++++++ 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/nokaman/cli.py b/src/nokaman/cli.py index f6cc645..00a831b 100644 --- a/src/nokaman/cli.py +++ b/src/nokaman/cli.py @@ -2,6 +2,7 @@ import json from pathlib import Path +from typing import Optional import typer from rich.console import Console @@ -30,6 +31,10 @@ console = Console() +def _print_json(data: object) -> None: + console.print_json(data=data, ensure_ascii=True) + + @app.command("version") def version_cmd() -> None: console.print(f"NokaMan {__version__}") @@ -50,7 +55,7 @@ def stats_cmd() -> None: skill = parts[1] if len(parts) > 1 else "?" by_lang[lang] += 1 by_skill[skill] += 1 - console.print_json( + _print_json( data={ "version": __version__, "n_samples": len(list_sample_files()), @@ -65,7 +70,7 @@ def stats_cmd() -> None: def demo_cmd(lang: str = typer.Option("en", "--lang", "-l")) -> None: """Full multi-skill demo for a language (end-to-end runnable).""" result = evaluate_demo(lang) - console.print_json(data=result) + _print_json(data=result) OUT_DIR.mkdir(parents=True, exist_ok=True) path = OUT_DIR / f"demo_{lang}.json" path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") @@ -85,7 +90,7 @@ def languages_list() -> None: @rubrics_app.command("list") -def rubrics_list(lang: str | None = typer.Option(None, "--lang", "-l")) -> None: +def rubrics_list(lang: Optional[str] = typer.Option(None, "--lang", "-l")) -> None: files = list_rubric_files() if lang: files = [p for p in files if p.stem == lang.strip().lower()] @@ -105,8 +110,8 @@ def rubrics_list(lang: str | None = typer.Option(None, "--lang", "-l")) -> None: @eval_app.command("text") def eval_text( lang: str = typer.Option("en", "--lang", "-l"), - text: str | None = typer.Option(None, "--text", "-t"), - file: Path | None = typer.Option(None, "--file", "-f", exists=True, dir_okay=False), + text: Optional[str] = typer.Option(None, "--text", "-t"), + file: Optional[Path] = typer.Option(None, "--file", "-f", exists=True, dir_okay=False), skill: str = typer.Option("writing", "--skill", "-s"), ) -> None: if file is not None: @@ -116,12 +121,12 @@ def eval_text( else: console.print("[red]Provide --text or --file[/red]") raise typer.Exit(code=1) - console.print_json(data=result) + _print_json(data=result) @eval_app.command("demo") def eval_demo(lang: str = typer.Option("en", "--lang", "-l")) -> None: - console.print_json(data=evaluate_demo(lang)) + _print_json(data=evaluate_demo(lang)) @eval_app.command("samples") @@ -150,7 +155,7 @@ def eval_samples() -> None: @eval_app.command("batch") def eval_batch( - out: Path | None = typer.Option(None, "--out", "-o"), + out: Optional[Path] = typer.Option(None, "--out", "-o"), table: bool = typer.Option(True, "--table/--json-only"), ) -> None: report = batch_evaluate() @@ -188,7 +193,7 @@ def eval_summary() -> None: lang = stem.split("_")[0] if "_" in stem else "?" by_lang[lang] = by_lang.get(lang, 0) + 1 report = batch_evaluate() - console.print_json( + _print_json( data={ "version": __version__, "n_samples": len(files), @@ -205,7 +210,7 @@ def eval_placement( answer: list[str] = typer.Option(..., "--answer", "-a", help="Repeat for multiple answers"), ) -> None: result = placement_test(lang, answer) - console.print_json(data=result) + _print_json(data=result) @train_app.command("toy") diff --git a/src/nokaman/models/toy.py b/src/nokaman/models/toy.py index d9a8960..8393144 100644 --- a/src/nokaman/models/toy.py +++ b/src/nokaman/models/toy.py @@ -123,7 +123,7 @@ def _framework_bands(language: str, cefr: str, score: float) -> dict: c = (cefr or "A1").upper() # rough ladders for demo UX jlpt = {"A1": "N5", "A2": "N4", "B1": "N3", "B2": "N2", "C1": "N1", "C2": "N1+"}.get(c, "N5") - topik = {"A1": "1", "A2": "2", "B1": "3", "B2": "4", "C1": "5", "C2": "6"}.get(c, "1") + topik = _topik_band(c) hsk = {"A1": "1", "A2": "2", "B1": "3", "B2": "4", "C1": "5", "C2": "6"}.get(c, "1") ielts = round(3.0 + (score / 100.0) * 6.0, 1) # ~3.0–9.0 toeic = int(200 + score * 7) # ~200–900 @@ -131,7 +131,9 @@ def _framework_bands(language: str, cefr: str, score: float) -> dict: if lang == "ja": out["jlpt"] = jlpt if lang == "ko": - out["topik"] = topik + out["topik"] = topik["level"] + out["topik_track"] = topik["track"] + out["topik_label"] = topik["label"] if lang == "zh": out["hsk"] = hsk if lang == "en": @@ -140,6 +142,19 @@ def _framework_bands(language: str, cefr: str, score: float) -> dict: return out +def _topik_band(cefr: str) -> dict[str, str]: + level = {"A1": "1", "A2": "2", "B1": "3", "B2": "4", "C1": "5", "C2": "6"}.get( + cefr, + "1", + ) + track = "TOPIK I" if level in {"1", "2"} else "TOPIK II" + return { + "level": level, + "track": track, + "label": f"{track} Level {level}", + } + + def _script_bonus(text: str, language: str) -> float: if not text: return 0.0 diff --git a/tests/test_framework_bands.py b/tests/test_framework_bands.py index 9e459cb..8ec2642 100644 --- a/tests/test_framework_bands.py +++ b/tests/test_framework_bands.py @@ -15,3 +15,22 @@ def test_demo_includes_frameworks() -> None: d = evaluate_demo("ja") assert "framework_bands" in d assert d["framework_bands"].get("jlpt") + + +def test_ko_framework_bands_include_topik_track() -> None: + r = ToyAbilityModel("ko").score_text( + "I study Korean every morning and write short practice notes.", + skill="writing", + ) + bands = r["framework_bands"] + assert bands["topik"] in {"1", "2", "3", "4", "5", "6"} + assert bands["topik_track"] in {"TOPIK I", "TOPIK II"} + assert bands["topik_label"] == f"{bands['topik_track']} Level {bands['topik']}" + + +def test_ko_demo_includes_topik_field() -> None: + d = evaluate_demo("ko") + bands = d["framework_bands"] + assert bands.get("topik") + assert bands.get("topik_track") + assert bands.get("topik_label") From 8c8896adafe0845d57a0a8fef4a3b360307f21c3 Mon Sep 17 00:00:00 2001 From: Dixon Frederick <70894227+dixonfrederick@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:41:56 +0700 Subject: [PATCH 05/50] feat: explain rubric weights (#41) --- README.md | 1 + src/nokaman/cli.py | 45 +++++++++++++++++++++++++++++++++++- tests/test_rubric_explain.py | 26 +++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/test_rubric_explain.py diff --git a/README.md b/README.md index d7ad73d..2788ad4 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ nokaman-gui | `nokaman demo -l en` | Full multi-skill evaluation demo | | `nokaman languages list` | Supported languages + frameworks | | `nokaman rubrics list [-l en]` | Skill rubrics | +| `nokaman rubrics explain -l en` | Print rubric skill weights, notes, frameworks, and bands | | `nokaman eval text …` | Evaluate free text | | `nokaman train …` | Toy calibration | | `nokaman gui` / `nokaman-gui` | **Qt desktop app** (needs `.[gui]`) | diff --git a/src/nokaman/cli.py b/src/nokaman/cli.py index 00a831b..684b6e4 100644 --- a/src/nokaman/cli.py +++ b/src/nokaman/cli.py @@ -13,7 +13,7 @@ from nokaman.data.loader import list_sample_files, list_rubric_files, load_rubric from nokaman.eval.metrics import batch_evaluate, placement_test from nokaman.eval.pipeline import evaluate_demo, evaluate_sample_file, evaluate_text -from nokaman.rubrics.registry import SUPPORTED_LANGUAGES, get_language_meta +from nokaman.rubrics.registry import SKILLS, SUPPORTED_LANGUAGES, get_language_meta, load_language_rubric from nokaman.train.toy_train import train_toy app = typer.Typer( @@ -107,6 +107,49 @@ def rubrics_list(lang: Optional[str] = typer.Option(None, "--lang", "-l")) -> No console.print(table) +@rubrics_app.command("explain") +def rubrics_explain( + lang: str = typer.Option("en", "--lang", "-l"), + json_output: bool = typer.Option(False, "--json"), +) -> None: + try: + meta = get_language_meta(lang) + except KeyError as exc: + console.print(f"[red]{exc}[/red]") + raise typer.Exit(code=1) from exc + + rubric = load_language_rubric(meta["code"]) + skills = rubric.get("skills") or {} + rows = [ + { + "skill": skill, + "weight": float((skills.get(skill) or {}).get("weight", 1.0)), + "notes": str((skills.get(skill) or {}).get("notes", "")), + } + for skill in SKILLS + ] + report = { + "language": meta["code"], + "name": meta["name"], + "frameworks": meta["frameworks"], + "bands": rubric.get("bands") or [], + "skills": rows, + } + if json_output: + console.print_json(data=report) + return + + table = Table(title=f"Rubric weights: {meta['name']} ({meta['code']})") + table.add_column("Skill") + table.add_column("Weight", justify="right") + table.add_column("Notes") + for row in rows: + table.add_row(row["skill"], f"{row['weight']:g}", row["notes"]) + console.print(table) + console.print(f"Frameworks: {', '.join(meta['frameworks'])}") + console.print(f"Bands: {', '.join(report['bands'])}") + + @eval_app.command("text") def eval_text( lang: str = typer.Option("en", "--lang", "-l"), diff --git a/tests/test_rubric_explain.py b/tests/test_rubric_explain.py new file mode 100644 index 0000000..f797fa7 --- /dev/null +++ b/tests/test_rubric_explain.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import json + +from typer.testing import CliRunner + +from nokaman.cli import app + + +def test_rubrics_explain_json_reports_weights() -> None: + result = CliRunner().invoke(app, ["rubrics", "explain", "--lang", "en", "--json"]) + + assert result.exit_code == 0, result.output + report = json.loads(result.output) + skills = {row["skill"]: row for row in report["skills"]} + assert report["language"] == "en" + assert "CEFR" in report["frameworks"] + assert skills["writing"]["weight"] == 1.2 + assert skills["grammar"]["weight"] == 1.0 + + +def test_rubrics_explain_rejects_unknown_language() -> None: + result = CliRunner().invoke(app, ["rubrics", "explain", "--lang", "xx", "--json"]) + + assert result.exit_code == 1 + assert "unsupported language" in result.output From 59471653209fa4b568fea9ed08502a951f7aa0f7 Mon Sep 17 00:00:00 2001 From: Dixon Frederick <70894227+dixonfrederick@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:42:46 +0700 Subject: [PATCH 06/50] feat: add language coverage matrix (#40) --- README.md | 1 + src/nokaman/cli.py | 32 +++++++++++++++++++++++++++ src/nokaman/data/coverage.py | 39 +++++++++++++++++++++++++++++++++ tests/test_language_coverage.py | 35 +++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 src/nokaman/data/coverage.py create mode 100644 tests/test_language_coverage.py diff --git a/README.md b/README.md index 2788ad4..9744a41 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ nokaman-gui | `nokaman version` | Version + language codes | | `nokaman demo -l en` | Full multi-skill evaluation demo | | `nokaman languages list` | Supported languages + frameworks | +| `nokaman languages coverage` | Language x skill sample coverage matrix | | `nokaman rubrics list [-l en]` | Skill rubrics | | `nokaman rubrics explain -l en` | Print rubric skill weights, notes, frameworks, and bands | | `nokaman eval text …` | Evaluate free text | diff --git a/src/nokaman/cli.py b/src/nokaman/cli.py index 684b6e4..c663a7b 100644 --- a/src/nokaman/cli.py +++ b/src/nokaman/cli.py @@ -10,6 +10,7 @@ from nokaman import __version__ from nokaman.config import OUT_DIR, RUNS_DIR +from nokaman.data.coverage import language_skill_coverage from nokaman.data.loader import list_sample_files, list_rubric_files, load_rubric from nokaman.eval.metrics import batch_evaluate, placement_test from nokaman.eval.pipeline import evaluate_demo, evaluate_sample_file, evaluate_text @@ -89,6 +90,37 @@ def languages_list() -> None: console.print(table) +@lang_app.command("coverage") +def languages_coverage(json_output: bool = typer.Option(False, "--json")) -> None: + report = language_skill_coverage() + if json_output: + console.print_json(data=report) + return + + table = Table(title="Language coverage") + table.add_column("Code") + table.add_column("Rubric") + table.add_column("Total", justify="right") + skill_headers = { + "vocabulary": "Vocab", + "grammar": "Gram", + "reading": "Read", + "writing": "Write", + "listening": "Listen", + "speaking": "Speak", + } + for skill in report["skills"]: + table.add_column(skill_headers.get(skill, skill), justify="right") + for row in report["languages"]: + table.add_row( + str(row["code"]), + "yes" if row["has_rubric"] else "fallback", + str(row["total"]), + *[str(row["skills"].get(skill, 0)) for skill in report["skills"]], + ) + console.print(table) + + @rubrics_app.command("list") def rubrics_list(lang: Optional[str] = typer.Option(None, "--lang", "-l")) -> None: files = list_rubric_files() diff --git a/src/nokaman/data/coverage.py b/src/nokaman/data/coverage.py new file mode 100644 index 0000000..7013694 --- /dev/null +++ b/src/nokaman/data/coverage.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from pathlib import Path + +from nokaman.config import RUBRICS_DIR +from nokaman.data.loader import list_sample_files, load_sample +from nokaman.rubrics.registry import SKILLS, SUPPORTED_LANGUAGES + + +def language_skill_coverage(sample_dir: Path | None = None) -> dict: + counts: dict[str, dict[str, int]] = { + code: {skill: 0 for skill in SKILLS} for code in sorted(SUPPORTED_LANGUAGES) + } + for path in list_sample_files(sample_dir): + sample = load_sample(path) + language = str(sample.get("language") or "").strip().lower() + skill = str(sample.get("skill") or "").strip().lower() + if language not in counts: + counts[language] = {item: 0 for item in SKILLS} + if skill not in counts[language]: + counts[language][skill] = 0 + counts[language][skill] += 1 + + rows = [] + for code in sorted(counts): + meta = SUPPORTED_LANGUAGES.get(code, {"name": code, "frameworks": []}) + skill_counts = {skill: counts[code].get(skill, 0) for skill in SKILLS} + total = sum(skill_counts.values()) + rows.append( + { + "code": code, + "name": meta.get("name", code), + "frameworks": list(meta.get("frameworks") or []), + "has_rubric": (RUBRICS_DIR / f"{code}.json").exists(), + "total": total, + "skills": skill_counts, + } + ) + return {"skills": list(SKILLS), "languages": rows} diff --git a/tests/test_language_coverage.py b/tests/test_language_coverage.py new file mode 100644 index 0000000..6b533fe --- /dev/null +++ b/tests/test_language_coverage.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import json + +from typer.testing import CliRunner + +from nokaman.cli import app +from nokaman.data.coverage import language_skill_coverage + + +def test_language_skill_coverage_counts_samples() -> None: + report = language_skill_coverage() + rows = {row["code"]: row for row in report["languages"]} + + assert report["skills"] == [ + "vocabulary", + "grammar", + "reading", + "writing", + "listening", + "speaking", + ] + assert rows["en"]["total"] >= 1 + assert rows["en"]["skills"]["writing"] >= 1 + assert rows["en"]["has_rubric"] is True + + +def test_languages_coverage_json_command() -> None: + result = CliRunner().invoke(app, ["languages", "coverage", "--json"]) + + assert result.exit_code == 0, result.output + report = json.loads(result.output) + rows = {row["code"]: row for row in report["languages"]} + assert rows["ja"]["skills"]["writing"] >= 1 + assert rows["ja"]["has_rubric"] is True From 7e1f5f5ad9374e37548c9eb7c03dc100afbe9220 Mon Sep 17 00:00:00 2001 From: Dixon Frederick <70894227+dixonfrederick@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:43:38 +0700 Subject: [PATCH 07/50] docs: add supported language catalog (#39) --- README.md | 2 ++ docs/LANGUAGES.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 docs/LANGUAGES.md diff --git a/README.md b/README.md index 9744a41..058c5ef 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ - [CLI reference](#cli-reference) - [App integration contracts](#app-integration-contracts) - [Languages & rubrics](#languages--rubrics) +- [Supported language catalog](docs/LANGUAGES.md) - [Diagrams](#diagrams) - [Repository layout](#repository-layout) - [Development](#development) @@ -174,6 +175,7 @@ console.log(assessment.cefr, assessment.score, assessment.framework_bands); ## Languages & rubrics Rubrics and samples live under `data/`. Extend by adding rubric JSON + samples, then register in `nokaman.rubrics.registry`. +See [docs/LANGUAGES.md](docs/LANGUAGES.md) for the supported language catalog, CEFR/JLPT/TOPIK/HSK mappings, and language-pack extension steps. | Code | Typical use | | --- | --- | diff --git a/docs/LANGUAGES.md b/docs/LANGUAGES.md new file mode 100644 index 0000000..71441d5 --- /dev/null +++ b/docs/LANGUAGES.md @@ -0,0 +1,79 @@ +# Supported Languages + +NokaMan uses ISO-style language codes in the CLI, API payloads, sample files, and rubric registry. The current implementation is a toy/offline assessment scaffold: CEFR and regional exam labels are approximate product signals, not certified exam results. + +## Catalog + +| Code | Language | Registry frameworks | Rubric JSON | Sample count | +| --- | --- | --- | --- | ---: | +| `en` | English | CEFR, TOEIC, IELTS | `data/rubrics/en.json` | 6 | +| `ko` | Korean | CEFR, TOPIK | `data/rubrics/ko.json` | 3 | +| `ja` | Japanese | CEFR, JLPT | `data/rubrics/ja.json` | 5 | +| `vi` | Vietnamese | CEFR | `data/rubrics/vi.json` | 4 | +| `zh` | Chinese (Mandarin) | CEFR, HSK | `data/rubrics/zh.json` | 2 | +| `es` | Spanish | CEFR, DELE | fallback rubric | 4 | +| `fr` | French | CEFR, DELF | fallback rubric | 3 | +| `de` | German | CEFR, Goethe | fallback rubric | 4 | + +Run the live catalog with: + +```powershell +nokaman languages list +nokaman rubrics list +nokaman stats +``` + +## Skill Rubrics + +Rubrics cover the same six skills for each full language pack: + +| Skill | Purpose | +| --- | --- | +| `vocabulary` | Vocabulary range and lexical variety | +| `grammar` | Grammar complexity and control | +| `reading` | Reading comprehension proxy score | +| `writing` | Written production proxy score | +| `listening` | Listening comprehension proxy score | +| `speaking` | Spoken production proxy score | + +The bundled full rubric JSON files weight `writing` at `1.2` and the other skills at `1.0`. Languages without a rubric JSON use the fallback rubric from `nokaman.rubrics.registry.load_language_rubric`. + +## CEFR Score Bands + +NokaMan maps its 0-100 toy model score to CEFR-style bands in `nokaman.models.cefr.score_to_cefr`. + +| Score range | CEFR band | +| --- | --- | +| `0-34.99` | A1 | +| `35-49.99` | A2 | +| `50-64.99` | B1 | +| `65-79.99` | B2 | +| `80-89.99` | C1 | +| `90-100` | C2 | + +Sample files may include `expected_cefr`; batch evaluation reports exact and adjacent CEFR hit rates against that field. + +## Regional Framework Adapters + +Regional labels are lightweight mappings used for demo UX and app contracts. + +| CEFR | JLPT | TOPIK | HSK | +| --- | --- | --- | --- | +| A1 | N5 | 1 | 1 | +| A2 | N4 | 2 | 2 | +| B1 | N3 | 3 | 3 | +| B2 | N2 | 4 | 4 | +| C1 | N1 | 5 | 5 | +| C2 | N1+ | 6 | 6 | + +English additionally emits approximate IELTS and TOEIC values from the raw score. These values are intentionally labeled approximate in the JSON payload. + +## Add A New Language Pack + +1. Add metadata to `SUPPORTED_LANGUAGES` in `src/nokaman/rubrics/registry.py`. +2. Add `data/rubrics/.json` with `language`, `name`, `skills`, and `bands`. +3. Add at least one `data/samples/__.json` file with `language`, `skill`, `text`, and `expected_cefr`. +4. Run `nokaman languages list`, `nokaman rubrics list -l `, and `nokaman eval text -l --text "..."` +5. Run `pytest -q` and `ruff check src tests`. + +Keep the code value lowercase and reuse the six built-in skill names unless the scoring pipeline is updated at the same time. From 930702a4d92655b10368985611e68525d1c0752b Mon Sep 17 00:00:00 2001 From: Dixon Frederick <70894227+dixonfrederick@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:44:32 +0700 Subject: [PATCH 08/50] test: cover eval batch output path (#38) --- README.md | 2 ++ src/nokaman/cli.py | 2 +- tests/test_cli.py | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 tests/test_cli.py diff --git a/README.md b/README.md index 058c5ef..27dc27a 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ nokaman-gui | `nokaman rubrics list [-l en]` | Skill rubrics | | `nokaman rubrics explain -l en` | Print rubric skill weights, notes, frameworks, and bands | | `nokaman eval text …` | Evaluate free text | +| `nokaman eval batch --out data/out/batch.json` | Score every sample and write a JSON CEFR hit-rate report | | `nokaman train …` | Toy calibration | | `nokaman gui` / `nokaman-gui` | **Qt desktop app** (needs `.[gui]`) | | `nokaman serve` | Optional FastAPI | @@ -131,6 +132,7 @@ nokaman-gui ```powershell nokaman demo -l vi nokaman demo -l ko +nokaman eval batch --out data/out/batch.json nokaman-gui ``` diff --git a/src/nokaman/cli.py b/src/nokaman/cli.py index c663a7b..a3d2e17 100644 --- a/src/nokaman/cli.py +++ b/src/nokaman/cli.py @@ -235,7 +235,7 @@ def eval_batch( ) -> None: report = batch_evaluate() out_path = out or (RUNS_DIR / "batch_eval.json") - RUNS_DIR.mkdir(parents=True, exist_ok=True) + out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") console.print( f"[green]batch[/green] n={report['n_samples']} exact={report['exact_cefr_hit_rate']} " diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..c800ce7 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import json + +from typer.testing import CliRunner + +from nokaman.cli import app + + +def test_eval_batch_writes_nested_output_path(tmp_path) -> None: + out_path = tmp_path / "data" / "out" / "batch.json" + result = CliRunner().invoke( + app, + ["eval", "batch", "--out", str(out_path), "--json-only"], + ) + + assert result.exit_code == 0, result.output + assert out_path.exists() + report = json.loads(out_path.read_text(encoding="utf-8")) + assert report["n_samples"] >= 1 + assert "by_language" in report + assert "rows" in report From a6a07af784d625a87b58d2f7dd6cc26d441a547b Mon Sep 17 00:00:00 2001 From: Dixon Frederick <70894227+dixonfrederick@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:09:04 +0700 Subject: [PATCH 09/50] feat: add app-facing assessment API endpoints (#42) Co-authored-by: Tu Pham --- README.md | 10 ++++++++++ src/nokaman/api/app.py | 17 ++++++++++++++++- tests/test_api.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 27dc27a..fa2ac4f 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,16 @@ reverse-engineering demo output. | `schemas/demo_response.schema.json` | `GET /assess/demo/{lang}` and `demo_payload()` | | `sdk/typescript/index.ts` | TypeScript interfaces for request and response payloads | +Optional FastAPI endpoints: + +| Endpoint | Purpose | +| --- | --- | +| `GET /languages` | List supported language codes, display names, and frameworks | +| `POST /assess` | App-facing text assessment alias | +| `POST /assess/text` | Text assessment with language, text, and skill | +| `GET /assess/demo/{lang}` | Multi-skill demo payload | +| `POST /assess/placement` | Placement score from multiple answers | + Example TypeScript use: ```typescript diff --git a/src/nokaman/api/app.py b/src/nokaman/api/app.py index ad90cd6..e77168a 100644 --- a/src/nokaman/api/app.py +++ b/src/nokaman/api/app.py @@ -4,7 +4,7 @@ from nokaman.eval.adaptive import adaptive_session from nokaman.eval.metrics import placement_test from nokaman.eval.pipeline import evaluate_demo, evaluate_text -from nokaman.rubrics.registry import SUPPORTED_LANGUAGES +from nokaman.rubrics.registry import SUPPORTED_LANGUAGES, get_language_meta try: from fastapi import FastAPI, HTTPException @@ -42,6 +42,16 @@ def health() -> dict: } +@app.get("/languages") +def languages() -> dict: + return { + "languages": [ + get_language_meta(code) + for code in sorted(SUPPORTED_LANGUAGES) + ] + } + + @app.post("/assess/text") def assess_text(req: TextReq) -> dict: if req.language not in SUPPORTED_LANGUAGES: @@ -49,6 +59,11 @@ def assess_text(req: TextReq) -> dict: return evaluate_text(req.language, req.text, skill=req.skill) +@app.post("/assess") +def assess(req: TextReq) -> dict: + return assess_text(req) + + @app.get("/assess/demo/{lang}") def assess_demo(lang: str) -> dict: if lang not in SUPPORTED_LANGUAGES: diff --git a/tests/test_api.py b/tests/test_api.py index 8f76763..8caebbf 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -26,6 +26,36 @@ def test_assess_text() -> None: assert "cefr" in r.json() +def test_languages_endpoint() -> None: + r = client.get("/languages") + assert r.status_code == 200 + payload = r.json() + codes = {item["code"] for item in payload["languages"]} + assert "en" in codes + assert {"CEFR", "TOPIK"}.issubset( + set(next(item for item in payload["languages"] if item["code"] == "ko")["frameworks"]) + ) + + +def test_assess_alias() -> None: + r = client.post( + "/assess", + json={"language": "ko", "text": "I study Korean every morning.", "skill": "writing"}, + ) + assert r.status_code == 200 + payload = r.json() + assert payload["language"] == "ko" + assert "cefr" in payload + + +def test_assess_alias_rejects_unknown_language() -> None: + r = client.post( + "/assess", + json={"language": "xx", "text": "test", "skill": "writing"}, + ) + assert r.status_code == 400 + + def test_assess_adaptive() -> None: r = client.post( "/assess/adaptive", From 764fb381bdfcdb52cd8a43bbd1ded43c97ce1fef Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Tue, 14 Jul 2026 21:40:27 +0700 Subject: [PATCH 10/50] Improve product: upgrade cycle 2026-07-14h samples and tooling - ship one additive fixture/sample/SKU/device per product - HIRI: merge-only seed so packs are not truncated by /devices/seed --- data/samples/pt_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/pt_writing_a1.json diff --git a/data/samples/pt_writing_a1.json b/data/samples/pt_writing_a1.json new file mode 100644 index 0000000..1f9802c --- /dev/null +++ b/data/samples/pt_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "pt_writing_a1", + "language": "pt", + "skill": "writing", + "expected_cefr": "A1", + "text": "Olá. Eu me chamo Ana. Eu moro em Lisboa. Eu gosto de café e de livros. Eu tenho um gato." +} diff --git a/pyproject.toml b/pyproject.toml index 7dafc4f..c07fabc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.29" +version = "0.3.30" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index b7d579b..e2444bd 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.29" +__version__ = "0.3.30" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index b539e13..40ea4b5 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -13,6 +13,7 @@ "es": {"name": "Spanish", "frameworks": ["CEFR", "DELE"]}, "fr": {"name": "French", "frameworks": ["CEFR", "DELF"]}, "de": {"name": "German", "frameworks": ["CEFR", "Goethe"]}, + "pt": {"name": "Portuguese", "frameworks": ["CEFR", "CAPLE"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From f75b709c1a66f4d99db120351d4037d1bbc3dd9a Mon Sep 17 00:00:00 2001 From: kadocay Date: Tue, 14 Jul 2026 22:48:17 +0700 Subject: [PATCH 11/50] feat: optional LLM grader behind ENABLE_LLM_GRADER flag (#29) (#50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add LLMGrader — optional remote grader that replaces ToyAbilityModel when ENABLE_LLM_GRADER=true env var is set. - Default remains local toy (no env var = no change) - Uses stdlib urllib only (no new pip dependencies) - Configurable via LLM_API_KEY, LLM_ENDPOINT, LLM_MODEL - Graceful fallback to toy on network error or missing key - 7 unit tests covering disabled, enabled-no-key, and edge cases Fixes #29 Co-authored-by: root --- src/nokaman/eval/llm_grader.py | 148 +++++++++++++++++++++++++++++++++ tests/test_llm_grader.py | 68 +++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 src/nokaman/eval/llm_grader.py create mode 100644 tests/test_llm_grader.py diff --git a/src/nokaman/eval/llm_grader.py b/src/nokaman/eval/llm_grader.py new file mode 100644 index 0000000..07f2383 --- /dev/null +++ b/src/nokaman/eval/llm_grader.py @@ -0,0 +1,148 @@ +"""Optional remote LLM grader behind a flag. + +Default remains the local ToyAbilityModel. +When ENABLE_LLM_GRADER=true and LLM_API_KEY / LLM_ENDPOINT are set, +the remote grader is used instead. + +Uses only stdlib (urllib) — no new pip dependencies. +""" + +from __future__ import annotations + +import json +import os +import urllib.error +import urllib.request +from typing import Any + +from nokaman.models.toy import ToyAbilityModel + +_ENABLED_VAR = "ENABLE_LLM_GRADER" +_API_KEY_VAR = "LLM_API_KEY" +_ENDPOINT_VAR = "LLM_ENDPOINT" +_MODEL_VAR = "LLM_MODEL" + +_DEFAULT_ENDPOINT = "https://api.openai.com/v1/chat/completions" +_DEFAULT_MODEL = "gpt-4o-mini" + +SYSTEM_PROMPT = """You are a language assessment grader. Score the user's response on a scale of 0-100. + +Consider: grammatical accuracy, vocabulary range, coherence, task completion, and complexity. +Respond with ONLY a JSON object: +{ + "score": <0-100>, + "cefr": "", + "feedback": "<1-2 sentence qualitative feedback>" +}""" + + +def _is_enabled() -> bool: + return os.environ.get(_ENABLED_VAR, "").strip().lower() in ("true", "1", "yes") + + +def _api_key() -> str | None: + return os.environ.get(_API_KEY_VAR) or None + + +def _endpoint() -> str: + return (os.environ.get(_ENDPOINT_VAR) or _DEFAULT_ENDPOINT).rstrip("/") + + +def _model() -> str: + return os.environ.get(_MODEL_VAR) or _DEFAULT_MODEL + + +class LLMGrader: + """Remote LLM-based grader. Falls back to ToyAbilityModel if disabled or on error.""" + + def __init__(self, language: str = "en") -> None: + self.language = language.strip().lower() + self._toy = ToyAbilityModel(language=self.language) + + def score_text(self, text: str, skill: str = "writing") -> dict[str, Any]: + if not _is_enabled(): + return self._toy.score_text(text, skill=skill) + + key = _api_key() + if not key: + return self._fallback(text, skill, reason="LLM_API_KEY not set") + + try: + return self._call_llm(text, skill, key) + except Exception as exc: + return self._fallback(text, skill, reason=str(exc)) + + def score_multi_skill(self, text: str, skills: list[str] | None = None) -> dict[str, Any]: + if not _is_enabled(): + return self._toy.score_multi_skill(text, skills=skills) + # For multi-skill, fallback to toy since LLM per-skill calls are expensive + return self._toy.score_multi_skill(text, skills=skills) + + # ── internal ─────────────────────────────────────────────── + + def _call_llm(self, text: str, skill: str, api_key: str) -> dict[str, Any]: + if not text.strip(): + return self._toy.score_text(text, skill=skill) + + prompt = ( + f"Language: {self.language}\n" + f"Skill being assessed: {skill}\n" + f"Learner response:\n{text}" + ) + payload = json.dumps( + { + "model": _model(), + "messages": [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": prompt}, + ], + "temperature": 0.2, + "max_tokens": 200, + } + ).encode("utf-8") + + req = urllib.request.Request( + f"{_endpoint()}", + data=payload, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + }, + method="POST", + ) + with urllib.request.urlopen(req, timeout=15) as resp: + body = json.loads(resp.read().decode("utf-8")) + + content = body["choices"][0]["message"]["content"] + # Parse JSON from response (handle wrapped in markdown fences) + content_clean = content.strip() + if content_clean.startswith("```"): + content_clean = content_clean.split("\n", 1)[-1].rsplit("```", 1)[0].strip() + parsed = json.loads(content_clean) + + score = float(parsed.get("score", 50)) + cefr = str(parsed.get("cefr", "B1")).upper() + feedback = str(parsed.get("feedback", "")) + + # Ensure valid CEFR + valid_cefr = {"A1", "A2", "B1", "B2", "C1", "C2"} + if cefr not in valid_cefr: + from nokaman.models.cefr import score_to_cefr + cefr = score_to_cefr(score) + + return { + "language": self.language, + "skill": skill, + "score": round(score, 2), + "cefr": cefr, + "feedback": feedback, + "model": f"LLMGrader({_model()})", + "features": {"tokens": len(text.split())}, + } + + def _fallback( + self, text: str, skill: str, reason: str = "" + ) -> dict[str, Any]: + result = self._toy.score_text(text, skill=skill) + result["llm_fallback_reason"] = reason + return result diff --git a/tests/test_llm_grader.py b/tests/test_llm_grader.py new file mode 100644 index 0000000..0bc9f92 --- /dev/null +++ b/tests/test_llm_grader.py @@ -0,0 +1,68 @@ +"""Tests for optional LLM grader behind flag.""" + +from __future__ import annotations + +import os +from nokaman.eval.llm_grader import LLMGrader + + +class TestLLMGraderDisabled: + """Default behaviour — ENABLE_LLM_GRADER is not set.""" + + def test_disabled_uses_toy(self) -> None: + grader = LLMGrader("en") + result = grader.score_text("I study English every day.") + assert result["model"] == "ToyAbilityModel" + assert "llm_fallback_reason" not in result + assert 0 <= result["score"] <= 100 + + def test_disabled_multi_skill(self) -> None: + grader = LLMGrader("en") + result = grader.score_multi_skill("I study English.") + assert result["model"] == "ToyAbilityModel" + + def test_disabled_empty_text(self) -> None: + grader = LLMGrader("en") + result = grader.score_text("") + assert result["model"] == "ToyAbilityModel" + assert result["score"] >= 0 + + +class TestLLMGraderEnabled: + """When enabled but no API key — falls back to toy.""" + + def test_enabled_no_key_falls_back(self) -> None: + os.environ["ENABLE_LLM_GRADER"] = "true" + # Ensure key is not set + os.environ.pop("LLM_API_KEY", None) + grader = LLMGrader("en") + result = grader.score_text("I study English.") + assert result["model"] == "ToyAbilityModel" + assert "llm_fallback_reason" in result + assert "LLM_API_KEY not set" in result["llm_fallback_reason"] + del os.environ["ENABLE_LLM_GRADER"] + + def test_enabled_empty_text_toy(self) -> None: + os.environ["ENABLE_LLM_GRADER"] = "true" + os.environ["LLM_API_KEY"] = "sk-test" + grader = LLMGrader("en") + # Empty text should fallback immediately + result = grader.score_text("") + assert result["model"] == "ToyAbilityModel" + del os.environ["ENABLE_LLM_GRADER"] + del os.environ["LLM_API_KEY"] + + def test_environment_variable_name(self) -> None: + """Verify the env var name is exactly ENABLE_LLM_GRADER.""" + from nokaman.eval.llm_grader import _is_enabled + os.environ["ENABLE_LLM_GRADER"] = "true" + assert _is_enabled() is True + del os.environ["ENABLE_LLM_GRADER"] + assert _is_enabled() is False + + def test_enabled_variants(self) -> None: + from nokaman.eval.llm_grader import _is_enabled + for val in ("true", "1", "yes", "True", "YES"): + os.environ["ENABLE_LLM_GRADER"] = val + assert _is_enabled() is True + del os.environ["ENABLE_LLM_GRADER"] From 82117e969c6d7dcb57812f6e2b94e266cf4168f7 Mon Sep 17 00:00:00 2001 From: kadocay Date: Tue, 14 Jul 2026 22:49:11 +0700 Subject: [PATCH 12/50] feat: adaptive quiz session state machine (#33) (#49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add SessionManager — lightweight state machine for adaptive quiz sessions: - States: CREATED → ACTIVE → COMPLETE - Automatic CEFR targeting based on running ability estimate - Next-item selection via nearest-target heuristic - CLI commands: session start, answer, snapshot, end - Pure Python (no new dependencies) Fixes #33 Co-authored-by: root --- src/nokaman/cli.py | 49 ++++++++++++ src/nokaman/eval/session.py | 155 ++++++++++++++++++++++++++++++++++++ tests/test_session.py | 98 +++++++++++++++++++++++ 3 files changed, 302 insertions(+) create mode 100644 src/nokaman/eval/session.py create mode 100644 tests/test_session.py diff --git a/src/nokaman/cli.py b/src/nokaman/cli.py index a3d2e17..48a154d 100644 --- a/src/nokaman/cli.py +++ b/src/nokaman/cli.py @@ -14,6 +14,7 @@ from nokaman.data.loader import list_sample_files, list_rubric_files, load_rubric from nokaman.eval.metrics import batch_evaluate, placement_test from nokaman.eval.pipeline import evaluate_demo, evaluate_sample_file, evaluate_text +from nokaman.eval.session import SessionManager from nokaman.rubrics.registry import SKILLS, SUPPORTED_LANGUAGES, get_language_meta, load_language_rubric from nokaman.train.toy_train import train_toy @@ -28,6 +29,8 @@ app.add_typer(lang_app, name="languages") app.add_typer(rubrics_app, name="rubrics") app.add_typer(eval_app, name="eval") +session_app = typer.Typer(help="Adaptive quiz session (state machine)") +app.add_typer(session_app, name="session") app.add_typer(train_app, name="train") console = Console() @@ -305,6 +308,52 @@ def train_report() -> None: console.print(path.read_text(encoding="utf-8")) +# ── session commands ───────────────────────────────────────── + + +@session_app.command("start") +def session_start( + lang: str = typer.Option("en", "--lang", "-l"), +) -> None: + """Start a new adaptive quiz session.""" + mgr = SessionManager(language=lang) + result = mgr.start() + _print_json(data=result) + + +@session_app.command("answer") +def session_answer( + session_id: str = typer.Option(..., "--session-id", "-sid"), + lang: str = typer.Option("en", "--lang", "-l"), + text: str = typer.Option(..., "--text", "-t"), +) -> None: + """Submit an answer to an active session.""" + mgr = SessionManager(language=lang, session_id=session_id) + result = mgr.submit_answer(text) + _print_json(data=result) + + +@session_app.command("snapshot") +def session_snapshot( + session_id: str = typer.Option(..., "--session-id", "-sid"), + lang: str = typer.Option("en", "--lang", "-l"), +) -> None: + """Get current session snapshot without submitting an answer.""" + mgr = SessionManager(language=lang, session_id=session_id) + _print_json(data=mgr.snapshot()) + + +@session_app.command("end") +def session_end( + session_id: str = typer.Option(..., "--session-id", "-sid"), + lang: str = typer.Option("en", "--lang", "-l"), +) -> None: + """Manually end an active session.""" + mgr = SessionManager(language=lang, session_id=session_id) + result = mgr.end() + _print_json(data=result) + + @app.command("gui") def gui_cmd() -> None: """Launch modern Qt desktop demo (pip install -e '.[gui]').""" diff --git a/src/nokaman/eval/session.py b/src/nokaman/eval/session.py new file mode 100644 index 0000000..154eb62 --- /dev/null +++ b/src/nokaman/eval/session.py @@ -0,0 +1,155 @@ +"""Adaptive quiz session state machine. + +Tracks session lifecycle and drives next-item selection based on +running ability estimate. States: + + - CREATED fresh, no answers yet + - ACTIVE collecting responses, selecting next prompt + - COMPLETE session ended (prompt bank exhausted or manual end) +""" + +from __future__ import annotations + +import uuid +from dataclasses import dataclass, field +from enum import Enum +from typing import Any + +from nokaman.eval.adaptive import ( + CEFR_TARGETS, + _running_ability, + estimate_answer_scores, + select_next_prompt, + build_prompt_bank, +) +from nokaman.models.cefr import score_to_cefr + + +class SessionState(Enum): + CREATED = "created" + ACTIVE = "active" + COMPLETE = "complete" + + +@dataclass +class Session: + """Mutable session container. External code calls SessionManager.""" + + session_id: str = field(default_factory=lambda: uuid.uuid4().hex[:12]) + language: str = "" + language_name: str = "" + state: SessionState = SessionState.CREATED + answers: list[dict] = field(default_factory=list) + administered_ids: set[str] = field(default_factory=set) + ability_score: float = field(default_factory=lambda: CEFR_TARGETS["A2"]) + cefr_target: str = "A2" + prompt_bank: list[dict] = field(default_factory=list) + history: list[dict] = field(default_factory=list) + + +class SessionManager: + """Lightweight session state machine. + + One manager per language session. Pure Python, no external store. + Swap the store backend later without changing the public API. + """ + + def __init__(self, language: str, session_id: str | None = None) -> None: + code = language.strip().lower() + bank = build_prompt_bank(code) + name = (bank[0]["language"] if bank else code) + self._session = Session( + session_id=session_id or uuid.uuid4().hex[:12], + language=code, + language_name=name, + prompt_bank=bank, + ) + + # ── public API ────────────────────────────────────────────── + + @property + def session_id(self) -> str: + return self._session.session_id + + @property + def state(self) -> SessionState: + return self._session.state + + def snapshot(self) -> dict[str, Any]: + """Return a serialisable snapshot of the current session.""" + s = self._session + return { + "session_id": s.session_id, + "language": s.language, + "language_name": s.language_name, + "state": s.state.value, + "n_answers": len(s.answers), + "ability_score": round(s.ability_score, 2), + "cefr": score_to_cefr(s.ability_score), + "cefr_target": s.cefr_target, + "history": list(s.history), + } + + def start(self) -> dict[str, Any]: + """Transition CREATED → ACTIVE and return first prompt.""" + if self._session.state != SessionState.CREATED: + raise RuntimeError( + f"cannot start session {self._session.session_id} " + f"in state {self._session.state.value}" + ) + self._session.state = SessionState.ACTIVE + self._session.ability_score = CEFR_TARGETS["A2"] + self._session.cefr_target = "A2" + return self._next_prompt() + + def submit_answer(self, answer_text: str) -> dict[str, Any]: + """Store an answer, re-estimate ability, return next prompt or session-end.""" + if self._session.state != SessionState.ACTIVE: + raise RuntimeError( + f"cannot submit answer in state {self._session.state.value}" + ) + estimates = estimate_answer_scores(self._session.language, [answer_text]) + if not estimates: + raise ValueError("empty estimate from answer") + + # Record the answer + entry = { + "answer": answer_text, + "estimated_score": estimates[0]["score"], + "estimated_cefr": estimates[0]["cefr"], + } + self._session.answers.append(estimates[0]) + self._session.history.append(entry) + + # Recompute running ability + self._session.ability_score = _running_ability(self._session.answers) + self._session.cefr_target = score_to_cefr(self._session.ability_score) + + return self._next_prompt() + + def end(self) -> dict[str, Any]: + """Manually end the session.""" + self._session.state = SessionState.COMPLETE + return self.snapshot() + + # ── internal helpers ──────────────────────────────────────── + + def _next_prompt(self) -> dict[str, Any]: + """Select the next prompt or mark session complete.""" + s = self._session + prompt = select_next_prompt( + ability_score=s.ability_score, + prompt_bank=s.prompt_bank, + administered_ids=s.administered_ids, + ) + if prompt is None: + s.state = SessionState.COMPLETE + result = self.snapshot() + result["next_prompt"] = None + return result + + s.administered_ids.add(str(prompt["id"])) + return { + **self.snapshot(), + "next_prompt": prompt, + } diff --git a/tests/test_session.py b/tests/test_session.py new file mode 100644 index 0000000..dd40de4 --- /dev/null +++ b/tests/test_session.py @@ -0,0 +1,98 @@ +"""Tests for adaptive quiz session state machine.""" + +from __future__ import annotations + +import pytest +from nokaman.eval.session import SessionManager, SessionState + + +class TestSessionManager: + def test_start_creates_active_session(self) -> None: + mgr = SessionManager("en") + out = mgr.start() + assert mgr.state == SessionState.ACTIVE + assert out["state"] == "active" + assert out["n_answers"] == 0 + assert out["ability_score"] == 42.0 # A2 default + assert out["cefr"] == "A2" + assert out["next_prompt"] is not None + assert "prompt" in out["next_prompt"] + + def test_start_twice_raises(self) -> None: + mgr = SessionManager("en") + mgr.start() + with pytest.raises(RuntimeError, match="cannot start"): + mgr.start() + + def test_submit_answer_updates_ability(self) -> None: + mgr = SessionManager("en") + mgr.start() + out = mgr.submit_answer( + "I study Korean every day because it helps me communicate with more people." + ) + assert mgr.state == SessionState.ACTIVE + assert out["n_answers"] == 1 + assert out["ability_score"] > 0 + assert "next_prompt" in out + assert out["next_prompt"] is not None + # second answer should move cefr target + history = mgr.snapshot()["history"] + assert len(history) == 1 + assert "estimated_score" in history[0] + + def test_submit_before_start_raises(self) -> None: + mgr = SessionManager("en") + with pytest.raises(RuntimeError, match="cannot submit answer"): + mgr.submit_answer("hello") + + def test_end_marks_complete(self) -> None: + mgr = SessionManager("en") + mgr.start() + out = mgr.end() + assert out["state"] == "complete" + assert mgr.state == SessionState.COMPLETE + + def test_session_exhausts_prompt_bank(self) -> None: + """After administering all prompts, session should auto-complete.""" + mgr = SessionManager("en") + out = mgr.start() + # en has 6 prompts (A1-C2) + for i in range(6): + assert out["next_prompt"] is not None, f"ran out at answer {i}" + out = mgr.submit_answer( + f"This is my test answer number {i} for the adaptive session." + ) + # After 6 answers, all prompts administered + assert mgr.state == SessionState.COMPLETE + assert out["next_prompt"] is None + + def test_snapshot_returns_current_state(self) -> None: + mgr = SessionManager("ko") + mgr.start() + mgr.submit_answer("한국어를 공부하고 있습니다.") + snap = mgr.snapshot() + assert snap["session_id"] == mgr.session_id + assert snap["language"] == "ko" + assert snap["n_answers"] == 1 + assert snap["state"] == "active" + + def test_custom_session_id(self) -> None: + mgr = SessionManager("en", session_id="my-custom-id") + assert mgr.session_id == "my-custom-id" + + def test_session_start_returns_first_prompt_with_expected_shape(self) -> None: + mgr = SessionManager("en") + out = mgr.start() + np_ = out["next_prompt"] + assert "id" in np_ + assert "target_score" in np_ + assert "difficulty_cefr" in np_ + assert "prompt" in np_ + assert np_["difficulty_cefr"] == "A2" + + def test_answer_with_bad_text_does_not_crash(self) -> None: + mgr = SessionManager("en") + mgr.start() + out = mgr.submit_answer("") # empty — should still produce an estimate + assert out["n_answers"] == 1 + assert "next_prompt" in out From 35b1b954009aa79877a0507dfea92562e867a1bb Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Tue, 14 Jul 2026 23:11:04 +0700 Subject: [PATCH 13/50] Improve product: upgrade cycle 2026-07-14i samples and tooling --- data/samples/it_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/it_writing_a1.json diff --git a/data/samples/it_writing_a1.json b/data/samples/it_writing_a1.json new file mode 100644 index 0000000..83c6631 --- /dev/null +++ b/data/samples/it_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "it_writing_a1", + "language": "it", + "skill": "writing", + "expected_cefr": "A1", + "text": "Ciao. Mi chiamo Luca. Abito a Roma. Mi piace la pizza e il caffè. Ho un cane." +} diff --git a/pyproject.toml b/pyproject.toml index c07fabc..d4f93e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.30" +version = "0.3.31" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index e2444bd..96bd915 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.30" +__version__ = "0.3.31" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index 40ea4b5..ed24f79 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -14,6 +14,7 @@ "fr": {"name": "French", "frameworks": ["CEFR", "DELF"]}, "de": {"name": "German", "frameworks": ["CEFR", "Goethe"]}, "pt": {"name": "Portuguese", "frameworks": ["CEFR", "CAPLE"]}, + "it": {"name": "Italian", "frameworks": ["CEFR", "CILS"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From 40b0f974b0595b6116c93be8b94f83b5fcb45a5c Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 00:41:26 +0700 Subject: [PATCH 14/50] Improve product: upgrade cycle 2026-07-15j samples and tooling --- data/samples/nl_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/nl_writing_a1.json diff --git a/data/samples/nl_writing_a1.json b/data/samples/nl_writing_a1.json new file mode 100644 index 0000000..74db222 --- /dev/null +++ b/data/samples/nl_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "nl_writing_a1", + "language": "nl", + "skill": "writing", + "expected_cefr": "A1", + "text": "Hallo. Ik heet Sam. Ik woon in Amsterdam. Ik hou van koffie en boeken. Ik heb een kat." +} diff --git a/pyproject.toml b/pyproject.toml index d4f93e5..0b0a982 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.31" +version = "0.3.32" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 96bd915..dbc0206 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.31" +__version__ = "0.3.32" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index ed24f79..81c5e1f 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -15,6 +15,7 @@ "de": {"name": "German", "frameworks": ["CEFR", "Goethe"]}, "pt": {"name": "Portuguese", "frameworks": ["CEFR", "CAPLE"]}, "it": {"name": "Italian", "frameworks": ["CEFR", "CILS"]}, + "nl": {"name": "Dutch", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From 8c9127a4c01621aebf0730da6c777d68068ba77c Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 02:12:05 +0700 Subject: [PATCH 15/50] Improve product: upgrade cycle 2026-07-15k samples and tooling --- data/samples/sv_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/sv_writing_a1.json diff --git a/data/samples/sv_writing_a1.json b/data/samples/sv_writing_a1.json new file mode 100644 index 0000000..fedfdc8 --- /dev/null +++ b/data/samples/sv_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "sv_writing_a1", + "language": "sv", + "skill": "writing", + "expected_cefr": "A1", + "text": "Hej. Jag heter Eva. Jag bor i Stockholm. Jag gillar kaffe och böcker. Jag har en hund." +} diff --git a/pyproject.toml b/pyproject.toml index 0b0a982..1450860 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.32" +version = "0.3.33" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index dbc0206..0a19479 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.32" +__version__ = "0.3.33" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index 81c5e1f..e7e8def 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -16,6 +16,7 @@ "pt": {"name": "Portuguese", "frameworks": ["CEFR", "CAPLE"]}, "it": {"name": "Italian", "frameworks": ["CEFR", "CILS"]}, "nl": {"name": "Dutch", "frameworks": ["CEFR"]}, + "sv": {"name": "Swedish", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From 0b1f575a46131ff735de99a1761032ca52930e40 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 03:41:11 +0700 Subject: [PATCH 16/50] Improve product: upgrade cycle 2026-07-15l samples and tooling --- data/samples/pl_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/pl_writing_a1.json diff --git a/data/samples/pl_writing_a1.json b/data/samples/pl_writing_a1.json new file mode 100644 index 0000000..009b169 --- /dev/null +++ b/data/samples/pl_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "pl_writing_a1", + "language": "pl", + "skill": "writing", + "expected_cefr": "A1", + "text": "Cześć. Nazywam się Anna. Mieszkam w Warszawie. Lubię kawę i książki. Mam kota." +} diff --git a/pyproject.toml b/pyproject.toml index 1450860..d6d5368 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.33" +version = "0.3.34" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 0a19479..50ec306 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.33" +__version__ = "0.3.34" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index e7e8def..981b207 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -17,6 +17,7 @@ "it": {"name": "Italian", "frameworks": ["CEFR", "CILS"]}, "nl": {"name": "Dutch", "frameworks": ["CEFR"]}, "sv": {"name": "Swedish", "frameworks": ["CEFR"]}, + "pl": {"name": "Polish", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From d5f40cea605d70752940b52fe48c3079640800e3 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 05:11:25 +0700 Subject: [PATCH 17/50] Improve product: upgrade cycle 2026-07-15m samples and tooling --- data/samples/tr_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/tr_writing_a1.json diff --git a/data/samples/tr_writing_a1.json b/data/samples/tr_writing_a1.json new file mode 100644 index 0000000..b995e92 --- /dev/null +++ b/data/samples/tr_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "tr_writing_a1", + "language": "tr", + "skill": "writing", + "expected_cefr": "A1", + "text": "Merhaba. Benim adım Deniz. İstanbul'da yaşıyorum. Kahve ve kitap severim. Bir kedim var." +} diff --git a/pyproject.toml b/pyproject.toml index d6d5368..8f72a6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.34" +version = "0.3.35" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 50ec306..e065186 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.34" +__version__ = "0.3.35" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index 981b207..470774d 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -18,6 +18,7 @@ "nl": {"name": "Dutch", "frameworks": ["CEFR"]}, "sv": {"name": "Swedish", "frameworks": ["CEFR"]}, "pl": {"name": "Polish", "frameworks": ["CEFR"]}, + "tr": {"name": "Turkish", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From d21d2dff65047bdad3bba7528e888a9e97d1ac29 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 06:40:56 +0700 Subject: [PATCH 18/50] Improve product: upgrade cycle 2026-07-15n samples and tooling --- data/samples/id_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/id_writing_a1.json diff --git a/data/samples/id_writing_a1.json b/data/samples/id_writing_a1.json new file mode 100644 index 0000000..390b432 --- /dev/null +++ b/data/samples/id_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "id_writing_a1", + "language": "id", + "skill": "writing", + "expected_cefr": "A1", + "text": "Halo. Nama saya Rina. Saya tinggal di Jakarta. Saya suka kopi dan buku. Saya punya kucing." +} diff --git a/pyproject.toml b/pyproject.toml index 8f72a6a..d6770b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.35" +version = "0.3.36" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index e065186..4a221fe 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.35" +__version__ = "0.3.36" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index 470774d..e293dad 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -19,6 +19,7 @@ "sv": {"name": "Swedish", "frameworks": ["CEFR"]}, "pl": {"name": "Polish", "frameworks": ["CEFR"]}, "tr": {"name": "Turkish", "frameworks": ["CEFR"]}, + "id": {"name": "Indonesian", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From ee8348589391163faf56edc90651402d816505a8 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 08:10:42 +0700 Subject: [PATCH 19/50] Improve product: upgrade cycle 2026-07-15o samples and tooling --- data/samples/th_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/th_writing_a1.json diff --git a/data/samples/th_writing_a1.json b/data/samples/th_writing_a1.json new file mode 100644 index 0000000..623b8bf --- /dev/null +++ b/data/samples/th_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "th_writing_a1", + "language": "th", + "skill": "writing", + "expected_cefr": "A1", + "text": "สวัสดี ฉันชื่อมาย ฉันอยู่กรุงเทพ ฉันชอบกาแฟและหนังสือ ฉันมีแมว" +} diff --git a/pyproject.toml b/pyproject.toml index d6770b5..084c1be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.36" +version = "0.3.37" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 4a221fe..c41dc0e 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.36" +__version__ = "0.3.37" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index e293dad..de492e2 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -20,6 +20,7 @@ "pl": {"name": "Polish", "frameworks": ["CEFR"]}, "tr": {"name": "Turkish", "frameworks": ["CEFR"]}, "id": {"name": "Indonesian", "frameworks": ["CEFR"]}, + "th": {"name": "Thai", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From 1555f05f0161174c5e1bd3836f3fb790c0ad0595 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 09:41:22 +0700 Subject: [PATCH 20/50] Improve product: upgrade cycle 2026-07-15p samples and tooling --- data/samples/hi_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/hi_writing_a1.json diff --git a/data/samples/hi_writing_a1.json b/data/samples/hi_writing_a1.json new file mode 100644 index 0000000..387e598 --- /dev/null +++ b/data/samples/hi_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "hi_writing_a1", + "language": "hi", + "skill": "writing", + "expected_cefr": "A1", + "text": "नमस्ते। मेरा नाम अन्या है। मैं दिल्ली में रहती हूँ। मुझे कॉफी और किताबें पसंद हैं। मेरे पास एक बिल्ली है।" +} diff --git a/pyproject.toml b/pyproject.toml index 084c1be..9a2461e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.37" +version = "0.3.38" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index c41dc0e..9f5874b 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.37" +__version__ = "0.3.38" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index de492e2..7ba241d 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -21,6 +21,7 @@ "tr": {"name": "Turkish", "frameworks": ["CEFR"]}, "id": {"name": "Indonesian", "frameworks": ["CEFR"]}, "th": {"name": "Thai", "frameworks": ["CEFR"]}, + "hi": {"name": "Hindi", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From 2a417da80147ee057b413396a2a09fbc0a9cf86a Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 11:11:51 +0700 Subject: [PATCH 21/50] Improve product: upgrade cycle 2026-07-15q samples and tooling --- data/samples/ar_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/ar_writing_a1.json diff --git a/data/samples/ar_writing_a1.json b/data/samples/ar_writing_a1.json new file mode 100644 index 0000000..9affdec --- /dev/null +++ b/data/samples/ar_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "ar_writing_a1", + "language": "ar", + "skill": "writing", + "expected_cefr": "A1", + "text": "مرحبا. اسمي ليلى. أسكن في القاهرة. أحب القهوة والكتب. لدي قطة." +} diff --git a/pyproject.toml b/pyproject.toml index 9a2461e..276576a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.38" +version = "0.3.39" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 9f5874b..f6fac4b 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.38" +__version__ = "0.3.39" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index 7ba241d..96f9ac3 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -22,6 +22,7 @@ "id": {"name": "Indonesian", "frameworks": ["CEFR"]}, "th": {"name": "Thai", "frameworks": ["CEFR"]}, "hi": {"name": "Hindi", "frameworks": ["CEFR"]}, + "ar": {"name": "Arabic", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From 32eda794351bcf75bd08efadc5339b41a5925c26 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 12:42:18 +0700 Subject: [PATCH 22/50] Improve NokaMan: Russian A1 writing sample (cycle 15r) - Add data/samples/ru_writing_a1.json - Register ru language - Patch bump --- data/samples/ru_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/ru_writing_a1.json diff --git a/data/samples/ru_writing_a1.json b/data/samples/ru_writing_a1.json new file mode 100644 index 0000000..5b43b76 --- /dev/null +++ b/data/samples/ru_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "ru_writing_a1", + "language": "ru", + "skill": "writing", + "expected_cefr": "A1", + "text": "Привет. Меня зовут Анна. Я живу в Москве. Я люблю кофе и книги. У меня есть кошка." +} diff --git a/pyproject.toml b/pyproject.toml index 276576a..87cb30d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.39" +version = "0.3.40" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index f6fac4b..0bd6059 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.39" +__version__ = "0.3.40" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index 96f9ac3..795c014 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -23,6 +23,7 @@ "th": {"name": "Thai", "frameworks": ["CEFR"]}, "hi": {"name": "Hindi", "frameworks": ["CEFR"]}, "ar": {"name": "Arabic", "frameworks": ["CEFR"]}, + "ru": {"name": "Russian", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From c93e145890be730efed4bb56ea6295b6cea93a99 Mon Sep 17 00:00:00 2001 From: SaAi <30327843+key1989han@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:36:07 +0330 Subject: [PATCH 23/50] feat: add 5 EN writing evaluation samples A2-C1 (#52) - en_writing_a2_travel: A2 level travel diary - en_writing_b1_routine: B1 level daily routine - en_writing_b1_opinion: B1 level opinion essay - en_writing_b2_experience: B2 level remote work analysis - en_writing_c1_analysis: C1 level AI in creative industries Fixes #26 Co-authored-by: key1989han --- data/samples/en_writing_a2_travel.json | 7 +++++++ data/samples/en_writing_b1_opinion.json | 7 +++++++ data/samples/en_writing_b1_routine.json | 7 +++++++ data/samples/en_writing_b2_experience.json | 7 +++++++ data/samples/en_writing_c1_analysis.json | 7 +++++++ 5 files changed, 35 insertions(+) create mode 100644 data/samples/en_writing_a2_travel.json create mode 100644 data/samples/en_writing_b1_opinion.json create mode 100644 data/samples/en_writing_b1_routine.json create mode 100644 data/samples/en_writing_b2_experience.json create mode 100644 data/samples/en_writing_c1_analysis.json diff --git a/data/samples/en_writing_a2_travel.json b/data/samples/en_writing_a2_travel.json new file mode 100644 index 0000000..bfdfa27 --- /dev/null +++ b/data/samples/en_writing_a2_travel.json @@ -0,0 +1,7 @@ +{ + "id": "en_writing_a2_travel", + "language": "en", + "skill": "writing", + "expected_cefr": "A2", + "text": "Last summer I went to the beach with my family. We drove for three hours and arrived in the afternoon. The water was cold but I swam anyway. We ate fish for dinner and watched the sunset. I took many photos with my phone." +} diff --git a/data/samples/en_writing_b1_opinion.json b/data/samples/en_writing_b1_opinion.json new file mode 100644 index 0000000..e6b3a4c --- /dev/null +++ b/data/samples/en_writing_b1_opinion.json @@ -0,0 +1,7 @@ +{ + "id": "en_writing_b1_opinion", + "language": "en", + "skill": "writing", + "expected_cefr": "B1", + "text": "I think learning a foreign language is very useful. It helps you understand other cultures and makes traveling easier. However, it requires a lot of practice and patience. I have been studying French for two years and I can now have simple conversations with native speakers." +} diff --git a/data/samples/en_writing_b1_routine.json b/data/samples/en_writing_b1_routine.json new file mode 100644 index 0000000..b4114a3 --- /dev/null +++ b/data/samples/en_writing_b1_routine.json @@ -0,0 +1,7 @@ +{ + "id": "en_writing_b1_routine", + "language": "en", + "skill": "writing", + "expected_cefr": "B1", + "text": "My daily routine starts at six in the morning. I usually make coffee and check my emails before work. During lunch I like to go for a short walk because it helps me concentrate in the afternoon. In the evening I cook dinner and sometimes read a book before bed." +} diff --git a/data/samples/en_writing_b2_experience.json b/data/samples/en_writing_b2_experience.json new file mode 100644 index 0000000..04ebc5a --- /dev/null +++ b/data/samples/en_writing_b2_experience.json @@ -0,0 +1,7 @@ +{ + "id": "en_writing_b2_experience", + "language": "en", + "skill": "writing", + "expected_cefr": "B2", + "text": "Working remotely has fundamentally changed how teams collaborate. While the flexibility to choose your own schedule is appealing, it demands strong self-discipline and clear communication skills. In my experience, teams that establish regular check-ins and use asynchronous tools tend to be more productive than those relying solely on spontaneous meetings." +} diff --git a/data/samples/en_writing_c1_analysis.json b/data/samples/en_writing_c1_analysis.json new file mode 100644 index 0000000..e79ab48 --- /dev/null +++ b/data/samples/en_writing_c1_analysis.json @@ -0,0 +1,7 @@ +{ + "id": "en_writing_c1_analysis", + "language": "en", + "skill": "writing", + "expected_cefr": "C1", + "text": "The proliferation of artificial intelligence in creative industries has sparked a nuanced debate about authorship and originality. Proponents argue that AI augments human capability, enabling artists to explore previously inaccessible realms of expression. Critics, however, contend that the delegation of creative decisions to algorithms risks homogenizing artistic output and undermining the deeply personal nature of creative work. Ultimately, the most compelling outcomes arise when AI serves as a collaborator rather than a replacement, preserving the intentionality that distinguishes meaningful art from mere production." +} From bdeb666fd5d8f5036ec2b33e3e3d85c1d74225f8 Mon Sep 17 00:00:00 2001 From: sunnnn2005 <139424942+sunnnn2005@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:38:13 -0700 Subject: [PATCH 24/50] docs: add contributing guide for bounty workflow (#51) * docs: add contributing guide for bounty workflow Added a contributing guide for first-time contributors, detailing local setup, validation commands, and the MergeOS bounty flow. * Add contributing section to README Added a contributing section to the README with a link to CONTRIBUTING.md for setup and bounty information. * Revise PROGRESS.md with task updates Updated progress on completed, ongoing, and to-do tasks, including new PRs for issues #11 and #18. --- CONTRIBUTING.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++++ PROGRESS.md | 6 ++- README.md | 7 +++- 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4f63f74 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,103 @@ +# Contributing to NokaMan + +Thanks for helping improve NokaMan. This guide covers the local setup, validation commands, and MergeOS bounty flow for first-time contributors. + +## Target Repository + +Open pull requests against the public product repository: + +https://github.com/mergeos-bounties/NokaMan + +Do not target private mirrors or unrelated forks. Bounty work lands on the public `NokaMan` repository, normally against the `master` branch. + +## Local Setup + +NokaMan requires Python 3.11 or newer. + +```bash +git clone https://github.com/mergeos-bounties/NokaMan.git +cd NokaMan +python3.11 -m venv .venv +source .venv/bin/activate +python -m pip install -U pip +python -m pip install -e ".[dev]" +``` + +For the optional desktop GUI, install the GUI extra: + +```bash +python -m pip install -e ".[dev,gui]" +``` + +## Run the Project + +Use the CLI to confirm the package is installed correctly: + +```bash +nokaman version +nokaman languages list +nokaman demo --lang en +``` + +For the optional GUI: + +```bash +nokaman-gui +# or +nokaman gui +``` + +## Tests and Checks + +Run the focused checks before opening a PR: + +```bash +python3.11 -m compileall src tests +python3.11 -m pytest -q +python3.11 -m ruff check src tests +``` + +If the local `build` package is available, also run: + +```bash +python3.11 -m build +``` + +For documentation-only changes, `pytest` and `ruff` are still useful because they catch accidental packaging or import regressions. + +## Good First Issue Path + +1. Pick an issue labeled `good first issue`, `help wanted`, or `bounty`. +2. Read the full issue description and acceptance criteria. +3. Comment `I claim this bounty` on the issue before starting. +4. Create a feature branch from the latest `master`. +5. Keep the change scoped to the issue. Avoid unrelated refactors, dependency changes, or CI changes. +6. Run the checks listed above. +7. Open a PR to `mergeos-bounties/NokaMan` and include `Fixes #`. + +## MergeOS Bounty Claim Flow + +For MergeOS MRG bounties: + +1. Star `https://github.com/mergeos-bounties/NokaMan` and `https://github.com/mergeos-bounties/mergeos`. +2. Comment `I claim this bounty` on the NokaMan bounty issue. +3. Comment on MergeOS Claim Token #1 with a link to the bounty issue. +4. Open a PR to the public NokaMan repository. +5. Include a short summary, test output, and any required screenshots or artifacts from the issue. + +Maintainers review the PR and credit MRG after merge according to the bounty policy. + +## Pull Request Checklist + +- The PR targets `mergeos-bounties/NokaMan`. +- The PR description links the issue with `Fixes #`. +- The change is limited to the requested scope. +- Tests and checks are listed in the PR description. +- Documentation, screenshots, or artifacts required by the issue are included. + +## Development Notes + +- Do not push directly to `master`. +- Do not force-push shared branches. +- Do not add new dependencies unless the issue explicitly requires them. +- Do not change CI, packaging, or release configuration for a documentation-only issue. diff --git a/PROGRESS.md b/PROGRESS.md index 768fbc4..c515988 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -6,6 +6,8 @@ ## ✅ 已完成 +- Added `CONTRIBUTING.md` for bounty #18 with setup, tests, good-first-issue path, and MergeOS claim flow. +- Added README links to the contribution guide. - Harness initialized for NokaMan bounty #11. - Confirmed actual FastAPI and SDK payload fields before defining contracts. - Added JSON Schema contracts, TypeScript interfaces, and contract smoke tests. @@ -21,13 +23,13 @@ ## 🔄 进行中 -- Commit, push, and open PR for issue #11. +- Open PR for issue #18. --- ## 📋 待办 -- Commit, push, and open PR for issue #11. +- Open PR for issue #18 with `Fixes #18`. --- diff --git a/README.md b/README.md index fa2ac4f..f9c2f26 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ - [Diagrams](#diagrams) - [Repository layout](#repository-layout) - [Development](#development) +- [Contributing](#contributing) - [MergeOS bounties](#mergeos-bounties) - [License](#license) @@ -246,12 +247,16 @@ nokaman demo -l en python scripts/capture_gui_shots.py # refresh GUI screenshots ``` +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, test commands, the good-first-issue path, and the MergeOS bounty claim flow. + --- ## MergeOS bounties Star + claim bounty → PR to **master** with demo JSON / screenshots → MRG **25–200**. -See org policy on [mergeos](https://github.com/mergeos-bounties/mergeos). +See [CONTRIBUTING.md](CONTRIBUTING.md) for the contributor path and org policy on [mergeos](https://github.com/mergeos-bounties/mergeos). --- From 18fa082ba0ddaa330e2d34b494018251c83a4f3f Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 14:42:48 +0700 Subject: [PATCH 25/50] Improve NokaMan: Polish A1 writing sample (cycle 15s) - Add data/samples/pl_writing_a1.json - Register pl language - Patch bump --- data/samples/pl_writing_a1.json | 2 +- pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/samples/pl_writing_a1.json b/data/samples/pl_writing_a1.json index 009b169..8418e7d 100644 --- a/data/samples/pl_writing_a1.json +++ b/data/samples/pl_writing_a1.json @@ -3,5 +3,5 @@ "language": "pl", "skill": "writing", "expected_cefr": "A1", - "text": "Cześć. Nazywam się Anna. Mieszkam w Warszawie. Lubię kawę i książki. Mam kota." + "text": "Cześć. Nazywam się Kasia. Mieszkam w Krakowie. Lubię kawę i książki. Mam kota." } diff --git a/pyproject.toml b/pyproject.toml index 87cb30d..16e2e87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.40" +version = "0.3.41" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 0bd6059..70819b7 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.40" +__version__ = "0.3.41" From 3729998fb4a49f8cae365dfdf8b0bf5c0a5aa563 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 16:11:43 +0700 Subject: [PATCH 26/50] Improve NokaMan: Turkish A1 writing sample (cycle 15t) - Add data/samples/tr_writing_a1.json - Register tr language - Patch bump --- data/samples/tr_writing_a1.json | 2 +- pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/samples/tr_writing_a1.json b/data/samples/tr_writing_a1.json index b995e92..8721a3f 100644 --- a/data/samples/tr_writing_a1.json +++ b/data/samples/tr_writing_a1.json @@ -3,5 +3,5 @@ "language": "tr", "skill": "writing", "expected_cefr": "A1", - "text": "Merhaba. Benim adım Deniz. İstanbul'da yaşıyorum. Kahve ve kitap severim. Bir kedim var." + "text": "Merhaba. Benim adım Ayşe. İstanbul'da yaşıyorum. Kahve ve kitapları severim. Bir kedim var." } diff --git a/pyproject.toml b/pyproject.toml index 16e2e87..b3a738a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.41" +version = "0.3.42" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 70819b7..df86923 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.41" +__version__ = "0.3.42" From 50e4bc14111ce37350b4639d8b0d1a7767132687 Mon Sep 17 00:00:00 2001 From: Rajesh Digambar Bagul <102693488+Rajesh270712@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:03:11 +0530 Subject: [PATCH 27/50] Add static web demo for #16 (#54) * Add static web demo Co-Authored-By: Paperclip * fix web demo mobile overflow Co-Authored-By: Paperclip * Add web demo screenshot Co-Authored-By: Paperclip --------- Co-authored-by: Paperclip --- README.md | 3 + docs/screenshots/web-demo.png | Bin 0 -> 162260 bytes src/nokaman/api/app.py | 13 ++ tests/test_api.py | 13 ++ tests/test_web_demo.py | 41 ++++ web/README.md | 32 +++ web/app.js | 366 ++++++++++++++++++++++++++++++++++ web/index.html | 101 ++++++++++ web/styles.css | 319 +++++++++++++++++++++++++++++ 9 files changed, 888 insertions(+) create mode 100644 docs/screenshots/web-demo.png create mode 100644 tests/test_web_demo.py create mode 100644 web/README.md create mode 100644 web/app.js create mode 100644 web/index.html create mode 100644 web/styles.css diff --git a/README.md b/README.md index f9c2f26..9611803 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,8 @@ nokaman rubrics list nokaman-gui ``` +For a browser demo that talks to the local FastAPI app, see [`web/README.md`](web/README.md). + --- ## CLI reference @@ -232,6 +234,7 @@ src/nokaman/ rubrics/ # language metadata + skills data/loader.py train/toy_train.py +web/ # no-build browser demo for the FastAPI app docs/screenshots/ docs/diagrams/ ``` diff --git a/docs/screenshots/web-demo.png b/docs/screenshots/web-demo.png new file mode 100644 index 0000000000000000000000000000000000000000..5cb381e384c0bdee0e28f15cb1da9cf81b7a7f53 GIT binary patch literal 162260 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
(f;9X~S0^^wnYx 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�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

)07m1l(Z&7gThsFVypLqc31uYys>HC*I+v2s_{` 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

mC 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 literal 0 HcmV?d00001 diff --git a/src/nokaman/api/app.py b/src/nokaman/api/app.py index e77168a..08c4b29 100644 --- a/src/nokaman/api/app.py +++ b/src/nokaman/api/app.py @@ -8,12 +8,25 @@ try: from fastapi import FastAPI, HTTPException + from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field except ImportError as exc: # pragma: no cover raise ImportError("Install nokaman[api] for FastAPI support") from exc app = FastAPI(title="NokaMan", version=__version__) +DEV_WEB_ORIGINS = [ + "http://localhost:5173", + "http://127.0.0.1:5173", +] + +app.add_middleware( + CORSMiddleware, + allow_origins=DEV_WEB_ORIGINS, + allow_methods=["GET", "POST", "OPTIONS"], + allow_headers=["content-type"], +) + class TextReq(BaseModel): language: str = "en" diff --git a/tests/test_api.py b/tests/test_api.py index 8caebbf..f4daed9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -68,3 +68,16 @@ def test_assess_adaptive() -> None: payload = r.json() assert payload["model"] == "AdaptiveHeuristicSession" assert payload["next_prompt"]["language"] == "en" + + +def test_dev_web_cors_preflight() -> None: + r = client.options( + "/assess/text", + headers={ + "Origin": "http://127.0.0.1:5173", + "Access-Control-Request-Method": "POST", + "Access-Control-Request-Headers": "content-type", + }, + ) + assert r.status_code == 200 + assert r.headers["access-control-allow-origin"] == "http://127.0.0.1:5173" diff --git a/tests/test_web_demo.py b/tests/test_web_demo.py new file mode 100644 index 0000000..f4dbd52 --- /dev/null +++ b/tests/test_web_demo.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_static_web_demo_files_are_wired() -> None: + web_dir = ROOT / "web" + index = web_dir / "index.html" + styles = web_dir / "styles.css" + app = web_dir / "app.js" + readme = web_dir / "README.md" + + for path in (index, styles, app, readme): + assert path.exists(), f"missing {path.relative_to(ROOT)}" + + html = index.read_text(encoding="utf-8") + script = app.read_text(encoding="utf-8") + + assert "styles.css" in html + assert "app.js" in html + assert "/health" in script + assert "/assess/text" in script + assert "/assess/demo/" in script + assert "/assess/placement" in script + + +def test_static_web_demo_allows_mobile_grid_children_to_shrink() -> None: + css = (ROOT / "web" / "styles.css").read_text(encoding="utf-8") + + for selector in (".panel", ".workspace > *", ".visual-grid > *", ".summary > div"): + pattern = rf"{re.escape(selector)}[^\{{]*\{{[^}}]*min-width:\s*0;" + assert re.search(pattern, css), f"{selector} should not force mobile overflow" + + pre_rule = re.search(r"pre\s*\{[^}]*\}", css) + assert pre_rule is not None + assert re.search(r"max-width:\s*100%;", pre_rule.group(0)) + assert re.search(r"overflow-x:\s*auto;", pre_rule.group(0)) diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..68e8864 --- /dev/null +++ b/web/README.md @@ -0,0 +1,32 @@ +# NokaMan Static Web Demo + +This no-build demo calls the local NokaMan FastAPI app and renders CEFR output, +framework bands, skill scores, and a radar chart for demo, free-text, and +placement responses. + +## Run + +From the repository root: + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -e ".[api]" +uvicorn nokaman.api.app:app --host 127.0.0.1 --port 8000 +``` + +In another terminal: + +```bash +cd web +python3 -m http.server 5173 +``` + +Open `http://127.0.0.1:5173/`. + +The default API base is `http://127.0.0.1:8000`. Change it in the page header if +your API is running on a different local port. + +## Screenshot + +![NokaMan web demo](../docs/screenshots/web-demo.png) diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..a0cddb2 --- /dev/null +++ b/web/app.js @@ -0,0 +1,366 @@ +const API_BASE_KEY = "nokaman-api-base"; +const SKILLS = ["vocabulary", "grammar", "reading", "writing", "listening", "speaking"]; +const state = { + busy: false, +}; + +const el = { + apiForm: document.querySelector("#api-form"), + apiBase: document.querySelector("#api-base"), + apiStatus: document.querySelector("#api-status"), + form: document.querySelector("#assessment-form"), + language: document.querySelector("#language"), + skill: document.querySelector("#skill"), + text: document.querySelector("#text"), + placementAnswers: document.querySelector("#placement-answers"), + loadDemo: document.querySelector("#load-demo"), + runPlacement: document.querySelector("#run-placement"), + cefr: document.querySelector("#cefr"), + score: document.querySelector("#score"), + model: document.querySelector("#model"), + radar: document.querySelector("#radar"), + bands: document.querySelector("#bands"), + details: document.querySelector("#details"), + rawJson: document.querySelector("#raw-json"), +}; + +const savedBase = window.localStorage.getItem(API_BASE_KEY); +if (savedBase) { + el.apiBase.value = savedBase; +} + +el.apiForm.addEventListener("submit", (event) => { + event.preventDefault(); + checkHealth(); +}); + +el.form.addEventListener("submit", (event) => { + event.preventDefault(); + assessText(); +}); + +el.loadDemo.addEventListener("click", loadDemo); +el.runPlacement.addEventListener("click", runPlacement); + +checkHealth(true); + +function apiBase() { + return el.apiBase.value.trim().replace(/\/+$/, ""); +} + +async function api(path, options = {}) { + const base = apiBase(); + window.localStorage.setItem(API_BASE_KEY, base); + const response = await fetch(`${base}${path}`, { + headers: { + "content-type": "application/json", + ...(options.headers || {}), + }, + ...options, + }); + const payload = await response.json().catch(() => ({})); + if (!response.ok) { + const detail = payload.detail || response.statusText || "Request failed"; + throw new Error(Array.isArray(detail) ? detail.map((item) => item.msg).join(", ") : detail); + } + return payload; +} + +async function checkHealth(loadDefault = false) { + setStatus("Checking", ""); + try { + const health = await api("/health"); + populateLanguages(health.languages || []); + setStatus(`Connected ${health.version || ""}`.trim(), "ok"); + if (loadDefault) { + await loadDemo(); + } + } catch (error) { + setStatus(error.message, "error"); + } +} + +async function assessText() { + const text = el.text.value.trim(); + if (!text) { + setStatus("Enter learner text", "error"); + return; + } + await runRequest(async () => { + const result = await api("/assess/text", { + method: "POST", + body: JSON.stringify({ + language: el.language.value, + text, + skill: el.skill.value, + }), + }); + renderResult(result); + }); +} + +async function loadDemo() { + await runRequest(async () => { + const result = await api(`/assess/demo/${encodeURIComponent(el.language.value)}`); + if (result.demo_text) { + el.text.value = result.demo_text; + } + renderResult(result); + }); +} + +async function runPlacement() { + const answers = el.placementAnswers.value + .split("\n") + .map((answer) => answer.trim()) + .filter(Boolean); + if (!answers.length) { + setStatus("Enter placement answers", "error"); + return; + } + await runRequest(async () => { + const result = await api("/assess/placement", { + method: "POST", + body: JSON.stringify({ + language: el.language.value, + answers, + }), + }); + renderResult(result); + }); +} + +async function runRequest(callback) { + setBusy(true); + setStatus("Running", ""); + try { + await callback(); + setStatus("Ready", "ok"); + } catch (error) { + setStatus(error.message, "error"); + } finally { + setBusy(false); + } +} + +function setBusy(isBusy) { + state.busy = isBusy; + for (const button of document.querySelectorAll("button")) { + button.disabled = isBusy; + } +} + +function setStatus(message, kind) { + el.apiStatus.textContent = message; + el.apiStatus.className = `status ${kind}`.trim(); +} + +function populateLanguages(languages) { + if (!languages.length) { + return; + } + const current = el.language.value; + const labels = new Map([ + ["en", "English"], + ["ko", "Korean"], + ["ja", "Japanese"], + ["vi", "Vietnamese"], + ["zh", "Chinese"], + ["es", "Spanish"], + ["fr", "French"], + ["de", "German"], + ]); + el.language.replaceChildren( + ...languages.map((code) => { + const option = document.createElement("option"); + option.value = code; + option.textContent = labels.get(code) || code.toUpperCase(); + return option; + }), + ); + el.language.value = languages.includes(current) ? current : languages[0]; +} + +function renderResult(result) { + const score = result.overall ?? result.score ?? averageItemOverall(result.items); + el.cefr.textContent = result.cefr || "-"; + el.score.textContent = Number.isFinite(score) ? Number(score).toFixed(2) : "-"; + el.model.textContent = result.model || modelFromItems(result.items) || "-"; + renderBands(result.framework_bands || {}); + renderDetails(result); + renderRadar(skillScores(result)); + el.rawJson.textContent = JSON.stringify(result, null, 2); +} + +function renderBands(bands) { + el.bands.replaceChildren(); + const entries = Object.entries(bands); + if (!entries.length) { + el.bands.append(emptyText("No framework bands returned")); + return; + } + for (const [name, value] of entries) { + const chip = document.createElement("span"); + chip.className = "chip"; + chip.textContent = `${humanize(name)} ${value}`; + el.bands.append(chip); + } +} + +function renderDetails(result) { + el.details.replaceChildren(); + const rows = [ + ["Language", result.language_name || result.language || "-"], + ["Frameworks", Array.isArray(result.frameworks) ? result.frameworks.join(", ") : "-"], + ["Skill", result.skill || "-"], + ["Items", result.n_items || "-"], + ["Features", result.features ? featureSummary(result.features) : "-"], + ]; + for (const [label, value] of rows) { + const dt = document.createElement("dt"); + const dd = document.createElement("dd"); + dt.textContent = label; + dd.textContent = String(value); + el.details.append(dt, dd); + } +} + +function renderRadar(scores) { + el.radar.replaceChildren(); + const entries = SKILLS.map((skill) => [skill, scores[skill]]).filter(([, value]) => + Number.isFinite(value), + ); + if (!entries.length) { + el.radar.append(emptyText("Run demo or placement to see multi-skill scores")); + return; + } + + const size = 360; + const center = size / 2; + const radius = 122; + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("viewBox", `0 0 ${size} ${size}`); + svg.setAttribute("role", "img"); + svg.setAttribute("aria-label", "Skill radar chart"); + + for (const level of [0.25, 0.5, 0.75, 1]) { + svg.append( + polygon( + SKILLS.map((_, index) => point(index, level * radius, center)), + "none", + "#d8ddd3", + "1", + ), + ); + } + + for (let index = 0; index < SKILLS.length; index += 1) { + const end = point(index, radius, center); + const axis = document.createElementNS("http://www.w3.org/2000/svg", "line"); + axis.setAttribute("x1", center); + axis.setAttribute("y1", center); + axis.setAttribute("x2", end.x); + axis.setAttribute("y2", end.y); + axis.setAttribute("stroke", "#d8ddd3"); + svg.append(axis); + + const labelPoint = point(index, radius + 32, center); + const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); + text.setAttribute("x", labelPoint.x); + text.setAttribute("y", labelPoint.y); + text.setAttribute("text-anchor", "middle"); + text.setAttribute("dominant-baseline", "middle"); + text.setAttribute("fill", "#20262e"); + text.setAttribute("font-size", "12"); + text.textContent = humanize(SKILLS[index]); + svg.append(text); + } + + const skillMap = Object.fromEntries(entries); + const dataPoints = SKILLS.map((skill, index) => { + const value = Math.max(0, Math.min(100, skillMap[skill] || 0)); + return point(index, (value / 100) * radius, center); + }); + svg.append(polygon(dataPoints, "rgba(43, 122, 120, 0.26)", "#2b7a78", "3")); + + for (const [skill, value] of entries) { + const row = document.createElementNS("http://www.w3.org/2000/svg", "title"); + row.textContent = `${humanize(skill)}: ${Number(value).toFixed(2)}`; + svg.append(row); + } + + el.radar.append(svg); +} + +function polygon(points, fill, stroke, width) { + const shape = document.createElementNS("http://www.w3.org/2000/svg", "polygon"); + shape.setAttribute("points", points.map((item) => `${item.x},${item.y}`).join(" ")); + shape.setAttribute("fill", fill); + shape.setAttribute("stroke", stroke); + shape.setAttribute("stroke-width", width); + return shape; +} + +function point(index, radius, center) { + const angle = -Math.PI / 2 + (index * Math.PI * 2) / SKILLS.length; + return { + x: Number((center + Math.cos(angle) * radius).toFixed(3)), + y: Number((center + Math.sin(angle) * radius).toFixed(3)), + }; +} + +function skillScores(result) { + if (result.skills) { + return result.skills; + } + if (Array.isArray(result.items) && result.items.length) { + const totals = {}; + for (const item of result.items) { + for (const [skill, value] of Object.entries(item.skills || {})) { + totals[skill] = (totals[skill] || 0) + Number(value || 0); + } + } + return Object.fromEntries( + Object.entries(totals).map(([skill, total]) => [skill, total / result.items.length]), + ); + } + if (result.skill && Number.isFinite(result.score)) { + return { [result.skill]: result.score }; + } + return {}; +} + +function averageItemOverall(items) { + if (!Array.isArray(items) || !items.length) { + return undefined; + } + const values = items.map((item) => item.overall).filter((value) => Number.isFinite(value)); + if (!values.length) { + return undefined; + } + return values.reduce((sum, value) => sum + value, 0) / values.length; +} + +function modelFromItems(items) { + if (!Array.isArray(items)) { + return undefined; + } + return items.find((item) => item.model)?.model; +} + +function featureSummary(features) { + return Object.entries(features) + .map(([key, value]) => `${humanize(key)}: ${value}`) + .join(", "); +} + +function humanize(value) { + return value.replaceAll("_", " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); +} + +function emptyText(message) { + const item = document.createElement("p"); + item.className = "empty"; + item.textContent = message; + return item; +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..9d543ef --- /dev/null +++ b/web/index.html @@ -0,0 +1,101 @@ + + + + + + NokaMan Web Demo + + + + +

+
+
+

Static FastAPI demo

+

NokaMan

+
+
+ + + +
+

Not connected

+
+ +
+
+
+ + +
+ +
+ + +
+ + + + +
+ + +
+ + + + +
+ +
+
+
+

CEFR

+

-

+
+
+

Score

+

-

+
+
+

Model

+

-

+
+
+ +
+
+

Skill radar

+
+
+
+

Framework bands

+
+

Details

+
+
+
+ +

Response JSON

+
{}
+
+
+
+ + + + diff --git a/web/styles.css b/web/styles.css new file mode 100644 index 0000000..655851f --- /dev/null +++ b/web/styles.css @@ -0,0 +1,319 @@ +:root { + color-scheme: light; + --bg: #f5f6f1; + --panel: #ffffff; + --text: #20262e; + --muted: #626d7a; + --line: #d8ddd3; + --teal: #2b7a78; + --blue: #355c9b; + --amber: #b66a18; + --rose: #9a3c50; + --shadow: 0 18px 55px rgba(32, 38, 46, 0.11); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + background: var(--bg); + color: var(--text); + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + letter-spacing: 0; +} + +button, +input, +select, +textarea { + font: inherit; +} + +button { + min-height: 42px; + border: 0; + border-radius: 8px; + background: var(--teal); + color: #fff; + cursor: pointer; + font-weight: 700; + padding: 0 16px; +} + +button:hover { + background: #216765; +} + +button:disabled { + cursor: wait; + opacity: 0.65; +} + +.app-shell { + width: min(1180px, calc(100% - 32px)); + margin: 0 auto; + padding: 28px 0; +} + +.topbar { + display: grid; + grid-template-columns: minmax(160px, 1fr) minmax(280px, 520px) auto; + gap: 20px; + align-items: end; + margin-bottom: 18px; +} + +.eyebrow, +.label { + margin: 0 0 6px; + color: var(--muted); + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; +} + +h1 { + margin: 0; + color: var(--blue); + font-size: 2.35rem; + line-height: 1; +} + +h2 { + margin: 0 0 12px; + font-size: 1rem; +} + +.api-form { + display: grid; + grid-template-columns: 1fr auto; + gap: 8px; +} + +.api-form label { + grid-column: 1 / -1; +} + +.status { + min-width: 150px; + margin: 0; + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; + color: var(--muted); + font-weight: 700; + padding: 11px 14px; + text-align: center; +} + +.status.ok { + border-color: rgba(43, 122, 120, 0.35); + color: var(--teal); +} + +.status.error { + border-color: rgba(154, 60, 80, 0.35); + color: var(--rose); +} + +.workspace { + display: grid; + grid-template-columns: minmax(290px, 390px) 1fr; + gap: 18px; + align-items: start; +} + +.panel { + min-width: 0; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + box-shadow: var(--shadow); +} + +.workspace > *, +.visual-grid > *, +.summary > div { + min-width: 0; +} + +.controls { + display: grid; + gap: 14px; + padding: 18px; +} + +.field-row { + display: grid; + grid-template-columns: 1fr; + gap: 7px; +} + +label { + color: var(--muted); + font-size: 0.88rem; + font-weight: 700; +} + +input, +select, +textarea { + width: 100%; + border: 1px solid var(--line); + border-radius: 8px; + background: #fbfcf9; + color: var(--text); + padding: 11px 12px; +} + +textarea { + min-height: 122px; + resize: vertical; +} + +.actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.results { + min-height: 640px; + padding: 18px; +} + +.summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 18px; +} + +.summary > div { + border: 1px solid var(--line); + border-radius: 8px; + padding: 14px; +} + +.metric { + margin: 0; + color: var(--blue); + font-size: 2rem; + font-weight: 800; + line-height: 1.1; + overflow-wrap: anywhere; +} + +.metric.small { + color: var(--text); + font-size: 1rem; + padding-top: 9px; +} + +.visual-grid { + display: grid; + grid-template-columns: minmax(260px, 1fr) minmax(240px, 0.8fr); + gap: 18px; + margin-bottom: 18px; +} + +.radar { + min-height: 320px; + border: 1px solid var(--line); + border-radius: 8px; + display: grid; + place-items: center; + overflow: hidden; +} + +.radar svg { + width: 100%; + max-width: 390px; + height: auto; +} + +.empty { + color: var(--muted); + margin: 0; + padding: 24px; + text-align: center; +} + +.chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 18px; + min-height: 36px; +} + +.chip { + border: 1px solid rgba(53, 92, 155, 0.22); + border-radius: 999px; + background: #eef3fb; + color: var(--blue); + font-weight: 700; + padding: 7px 10px; +} + +.details { + display: grid; + grid-template-columns: minmax(110px, 0.8fr) minmax(120px, 1fr); + gap: 8px 12px; + margin: 0; +} + +.details dt { + color: var(--muted); + font-weight: 700; +} + +.details dd { + margin: 0; + overflow-wrap: anywhere; +} + +pre { + max-width: 100%; + max-height: 280px; + overflow-x: auto; + overflow-y: auto; + border: 1px solid var(--line); + border-radius: 8px; + background: #20262e; + color: #f8faf4; + padding: 14px; +} + +@media (max-width: 840px) { + .app-shell { + width: min(100% - 20px, 680px); + padding: 16px 0; + } + + .topbar, + .workspace, + .visual-grid, + .summary { + grid-template-columns: 1fr; + } + + .status { + text-align: left; + } +} + +@media (max-width: 460px) { + .api-form, + .actions { + grid-template-columns: 1fr; + } + + h1 { + font-size: 2rem; + } +} From 690506bea2a5f2a6ba12a1b353332dec1c045f47 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 16:41:39 +0700 Subject: [PATCH 28/50] Improve NokaMan: German A1 writing sample (cycle 15u) - Add data/samples/de_writing_a1.json - Register de language - Patch bump --- data/samples/de_writing_a1.json | 2 +- pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/samples/de_writing_a1.json b/data/samples/de_writing_a1.json index 53c4ef6..85fef94 100644 --- a/data/samples/de_writing_a1.json +++ b/data/samples/de_writing_a1.json @@ -3,5 +3,5 @@ "language": "de", "skill": "writing", "expected_cefr": "A1", - "text": "Hallo. Ich heisse Anna. Ich wohne in Berlin. Ich mag Tee und Brot. Ich habe eine Katze." + "text": "Hallo. Ich heiße Anna. Ich wohne in Berlin. Ich mag Kaffee und Bücher. Ich habe eine Katze." } diff --git a/pyproject.toml b/pyproject.toml index b3a738a..070924f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.42" +version = "0.3.43" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index df86923..92fc493 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.42" +__version__ = "0.3.43" From 2000b99bfcb538b7cf5428e0b41e0f6427e58f36 Mon Sep 17 00:00:00 2001 From: Rajesh Digambar Bagul <102693488+Rajesh270712@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:14:55 +0530 Subject: [PATCH 29/50] Add calibration config pipeline (#53) Co-authored-by: Paperclip --- README.md | 3 +- configs/example.yaml | 5 + docs/CALIBRATION.md | 25 +++++ src/nokaman/cli.py | 40 ++++++- src/nokaman/train/toy_train.py | 194 ++++++++++++++++++++++++++++++++- tests/test_train.py | 44 +++++++- 6 files changed, 299 insertions(+), 12 deletions(-) create mode 100644 configs/example.yaml create mode 100644 docs/CALIBRATION.md diff --git a/README.md b/README.md index 9611803..dc7c799 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ For a browser demo that talks to the local FastAPI app, see [`web/README.md`](we | `nokaman rubrics explain -l en` | Print rubric skill weights, notes, frameworks, and bands | | `nokaman eval text …` | Evaluate free text | | `nokaman eval batch --out data/out/batch.json` | Score every sample and write a JSON CEFR hit-rate report | -| `nokaman train …` | Toy calibration | +| `nokaman train …` | Toy calibration with config/report exports | | `nokaman gui` / `nokaman-gui` | **Qt desktop app** (needs `.[gui]`) | | `nokaman serve` | Optional FastAPI | @@ -136,6 +136,7 @@ For a browser demo that talks to the local FastAPI app, see [`web/README.md`](we nokaman demo -l vi nokaman demo -l ko nokaman eval batch --out data/out/batch.json +nokaman train toy --config configs/example.yaml nokaman-gui ``` diff --git a/configs/example.yaml b/configs/example.yaml new file mode 100644 index 0000000..7dd6db5 --- /dev/null +++ b/configs/example.yaml @@ -0,0 +1,5 @@ +# Minimal calibration config for `nokaman train toy --config configs/example.yaml`. +run_id: example-calibration +epochs: 3 +seed: 7 +resume: true diff --git a/docs/CALIBRATION.md b/docs/CALIBRATION.md new file mode 100644 index 0000000..5f9dedd --- /dev/null +++ b/docs/CALIBRATION.md @@ -0,0 +1,25 @@ +# Calibration + +Run the toy calibration loop from a config file: + +```powershell +nokaman train toy --config configs/example.yaml +``` + +The config file is a small YAML-compatible mapping. NokaMan parses this subset +with the standard library, so no extra YAML dependency is required. + +Supported keys: + +- `run_id`: stable run directory name under `data/runs/` +- `epochs`: number of calibration epochs +- `seed`: deterministic seed recorded with the run +- `resume`: reuse a completed matching run when the report and dashboard exist + +Each run writes: + +- `data/runs//toy_train_report.json` +- `data/runs//dashboard.json` + +`dashboard.json` keeps predictable top-level keys for app dashboards: +`run_id`, `model`, `status`, `metrics`, and `report_path`. diff --git a/src/nokaman/cli.py b/src/nokaman/cli.py index 48a154d..d7e3088 100644 --- a/src/nokaman/cli.py +++ b/src/nokaman/cli.py @@ -292,16 +292,48 @@ def eval_placement( @train_app.command("toy") -def train_toy_cmd(epochs: int = typer.Option(3, "--epochs", "-e", min=1, max=50)) -> None: - report = train_toy(epochs=epochs) +def train_toy_cmd( + config: Path | None = typer.Option( + None, + "--config", + "-c", + exists=True, + dir_okay=False, + help="YAML/JSON calibration config.", + ), + epochs: int | None = typer.Option(None, "--epochs", "-e", min=1, max=50), + seed: int | None = typer.Option(None, "--seed", min=0), + run_id: str | None = typer.Option(None, "--run-id"), + resume: bool | None = typer.Option(None, "--resume/--no-resume"), +) -> None: + report = train_toy( + epochs=epochs, + config_path=config, + seed=seed, + run_id=run_id, + resume=resume, + ) last = report["history"][-1]["exact_cefr_hit_rate"] console.print(f"[green]Calibration complete[/green] exact_cefr_hit_rate={last}") + if report.get("resumed"): + console.print(f"[dim]resumed[/dim] {report['run_id']}") console.print(f"Report: {report['report_path']}") + console.print(f"Dashboard: {report['dashboard_path']}") @train_app.command("report") -def train_report() -> None: - path = Path("data/runs/toy_train_report.json") +def train_report(run_id: str | None = typer.Option(None, "--run-id")) -> None: + path = RUNS_DIR / "toy_train_report.json" + if run_id: + path = RUNS_DIR / run_id / "toy_train_report.json" + elif not path.exists(): + reports = sorted( + RUNS_DIR.glob("*/toy_train_report.json"), + key=lambda item: item.stat().st_mtime, + reverse=True, + ) + if reports: + path = reports[0] if not path.exists(): console.print("[yellow]No report yet. Run: nokaman train toy[/yellow]") raise typer.Exit(code=1) diff --git a/src/nokaman/train/toy_train.py b/src/nokaman/train/toy_train.py index 1a3d233..2059dda 100644 --- a/src/nokaman/train/toy_train.py +++ b/src/nokaman/train/toy_train.py @@ -1,23 +1,179 @@ from __future__ import annotations +import hashlib import json +import random +import re +from pathlib import Path +from typing import Any from nokaman.config import RUNS_DIR from nokaman.data.loader import list_sample_files, load_sample from nokaman.eval.pipeline import evaluate_sample_file +MODEL_NAME = "ToyAbilityModel" +DEFAULT_EPOCHS = 3 +DEFAULT_SEED = 7 -def train_toy(epochs: int = 3) -> dict: + +def _parse_scalar(value: str) -> Any: + value = value.strip() + if not value: + return "" + if value[0] == value[-1] and value[0] in {"'", '"'}: + return value[1:-1] + + lowered = value.lower() + if lowered in {"true", "yes", "on"}: + return True + if lowered in {"false", "no", "off"}: + return False + if lowered in {"null", "none"}: + return None + + try: + return int(value) + except ValueError: + pass + + try: + return float(value) + except ValueError: + return value + + +def _parse_simple_yaml(text: str) -> dict[str, Any]: + """ + Parse the small YAML subset used by calibration config files without + requiring PyYAML at runtime. + """ + payload: dict[str, Any] = {} + for line in text.splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + if ":" not in stripped: + raise ValueError(f"unsupported config line: {line!r}") + + key, raw_value = stripped.split(":", 1) + key = key.strip() + if not key: + raise ValueError(f"missing config key in line: {line!r}") + payload[key] = _parse_scalar(raw_value.split(" #", 1)[0]) + return payload + + +def load_calibration_config(path: str | Path | None = None) -> dict[str, Any]: + """Load a JSON or simple YAML calibration config.""" + if path is None: + return {} + + config_path = Path(path) + text = config_path.read_text(encoding="utf-8") + if config_path.suffix.lower() == ".json": + payload = json.loads(text) + else: + payload = _parse_simple_yaml(text) + + if not isinstance(payload, dict): + raise ValueError("calibration config must be a mapping") + return payload + + +def _clean_run_id(run_id: str) -> str: + cleaned = re.sub(r"[^A-Za-z0-9_.-]+", "-", run_id).strip(".-") + return cleaned or "toy-calibration" + + +def _stable_run_id(config: dict[str, Any]) -> str: + requested = config.get("run_id") + if requested: + return _clean_run_id(str(requested)) + + digest = hashlib.sha256(json.dumps(config, sort_keys=True).encode("utf-8")).hexdigest() + return f"toy-{digest[:12]}" + + +def _config_hash(config: dict[str, Any]) -> str: + return hashlib.sha256(json.dumps(config, sort_keys=True).encode("utf-8")).hexdigest()[:12] + + +def _completed_report(path: Path, epochs: int, config_hash: str) -> dict[str, Any] | None: + if not path.exists(): + return None + + report = json.loads(path.read_text(encoding="utf-8")) + history = report.get("history") or [] + if ( + report.get("status") == "complete" + and report.get("config_hash") == config_hash + and len(history) >= epochs + ): + return report + return None + + +def train_toy( + epochs: int | None = None, + *, + config_path: str | Path | None = None, + seed: int | None = None, + run_id: str | None = None, + resume: bool | None = None, +) -> dict: """ Calibration loop for the toy model: evaluate bundled samples each epoch and report exact CEFR hit-rate when expected_cefr is present. """ + file_config = load_calibration_config(config_path) + resolved_config = { + "model": MODEL_NAME, + "epochs": int(epochs if epochs is not None else file_config.get("epochs", DEFAULT_EPOCHS)), + "seed": int(seed if seed is not None else file_config.get("seed", DEFAULT_SEED)), + "resume": bool(resume if resume is not None else file_config.get("resume", True)), + } + if run_id is not None: + resolved_config["run_id"] = run_id + elif file_config.get("run_id") is not None: + resolved_config["run_id"] = str(file_config["run_id"]) + else: + resolved_config["run_id"] = _stable_run_id( + { + "model": resolved_config["model"], + "epochs": resolved_config["epochs"], + "seed": resolved_config["seed"], + } + ) + resolved_config["run_id"] = _clean_run_id(str(resolved_config["run_id"])) + config_hash = _config_hash( + { + "model": resolved_config["model"], + "epochs": resolved_config["epochs"], + "seed": resolved_config["seed"], + "run_id": resolved_config["run_id"], + } + ) + samples = list_sample_files() if not samples: raise FileNotFoundError("no samples under data/samples") + random.seed(resolved_config["seed"]) + run_dir = RUNS_DIR / resolved_config["run_id"] + report_path = run_dir / "toy_train_report.json" + dashboard_path = run_dir / "dashboard.json" + if resolved_config["resume"]: + existing = _completed_report(report_path, resolved_config["epochs"], config_hash) + if existing is not None and dashboard_path.exists(): + return { + "report_path": str(report_path), + "dashboard_path": str(dashboard_path), + "resumed": True, + **existing, + } + history = [] - for epoch in range(1, max(1, epochs) + 1): + for epoch in range(1, max(1, resolved_config["epochs"]) + 1): hits = 0 scored = 0 for path in samples: @@ -39,11 +195,37 @@ def train_toy(epochs: int = 3) -> dict: ) RUNS_DIR.mkdir(parents=True, exist_ok=True) - report_path = RUNS_DIR / "toy_train_report.json" + run_dir.mkdir(parents=True, exist_ok=True) report = { - "model": "ToyAbilityModel", - "epochs": epochs, + "run_id": resolved_config["run_id"], + "model": MODEL_NAME, + "status": "complete", + "epochs": resolved_config["epochs"], + "seed": resolved_config["seed"], + "config_hash": config_hash, "history": history, } + last = history[-1] + dashboard = { + "run_id": resolved_config["run_id"], + "model": MODEL_NAME, + "status": "complete", + "metrics": { + "exact_cefr_hit_rate": last["exact_cefr_hit_rate"], + "n_labeled": last["n_labeled"], + "n_samples": last["n_samples"], + }, + "report_path": str(report_path), + } + + report["dashboard_path"] = str(dashboard_path) report_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") - return {"report_path": str(report_path), **report} + dashboard_path.write_text(json.dumps(dashboard, indent=2) + "\n", encoding="utf-8") + legacy_path = RUNS_DIR / "toy_train_report.json" + legacy_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + return { + "report_path": str(report_path), + "dashboard_path": str(dashboard_path), + "resumed": False, + **report, + } diff --git a/tests/test_train.py b/tests/test_train.py index 753fef7..0c6612c 100644 --- a/tests/test_train.py +++ b/tests/test_train.py @@ -1,9 +1,10 @@ from __future__ import annotations +import json from pathlib import Path from nokaman.train import toy_train as toy_train_mod -from nokaman.train.toy_train import train_toy +from nokaman.train.toy_train import load_calibration_config, train_toy def test_train_toy_report(tmp_path: Path, monkeypatch) -> None: @@ -11,3 +12,44 @@ def test_train_toy_report(tmp_path: Path, monkeypatch) -> None: report = train_toy(epochs=2) assert "history" in report assert Path(report["report_path"]).exists() + + +def test_train_toy_loads_yaml_config_and_dashboard(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setattr(toy_train_mod, "RUNS_DIR", tmp_path / "runs") + config_path = tmp_path / "calibration.yaml" + config_path.write_text( + "\n".join( + [ + "run_id: smoke-calibration", + "epochs: 2", + "seed: 123", + "resume: true", + ] + ), + encoding="utf-8", + ) + + assert load_calibration_config(config_path)["epochs"] == 2 + report = train_toy(config_path=config_path) + + assert report["run_id"] == "smoke-calibration" + assert report["epochs"] == 2 + assert report["seed"] == 123 + assert Path(report["report_path"]).exists() + dashboard_path = Path(report["dashboard_path"]) + assert dashboard_path.exists() + dashboard = json.loads(dashboard_path.read_text(encoding="utf-8")) + assert sorted(dashboard) == ["metrics", "model", "report_path", "run_id", "status"] + assert dashboard["metrics"]["n_samples"] >= 1 + + +def test_train_toy_resumes_existing_completed_run(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setattr(toy_train_mod, "RUNS_DIR", tmp_path / "runs") + + first = train_toy(epochs=1, run_id="stable-run", seed=99) + second = train_toy(epochs=1, run_id="stable-run", seed=99) + + assert second["resumed"] is True + assert second["report_path"] == first["report_path"] + assert second["dashboard_path"] == first["dashboard_path"] + assert second["history"] == first["history"] From 0cd6c2c996528eda5cb3a7936a752edbc5ff4675 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 18:12:03 +0700 Subject: [PATCH 30/50] Improve NokaMan: English B1 email writing sample (cycle 15v) - Add data/samples/en_writing_b1_email.json - Patch bump --- data/samples/en_writing_b1_email.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 data/samples/en_writing_b1_email.json diff --git a/data/samples/en_writing_b1_email.json b/data/samples/en_writing_b1_email.json new file mode 100644 index 0000000..a9c630d --- /dev/null +++ b/data/samples/en_writing_b1_email.json @@ -0,0 +1,7 @@ +{ + "id": "en_writing_b1_email", + "language": "en", + "skill": "writing", + "expected_cefr": "B1", + "text": "Dear Ms. Patel,\n\nI am writing to apply for the junior analyst role advertised on your website. I have two years of experience with spreadsheets and customer reports. I am available for an interview next week.\n\nKind regards,\nJamie Lee" +} diff --git a/pyproject.toml b/pyproject.toml index 070924f..c5fa966 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.43" +version = "0.3.44" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 92fc493..d49c409 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.43" +__version__ = "0.3.44" From b5aee6e1c993411445bb6fb1614895fd0c6518f6 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 19:41:24 +0700 Subject: [PATCH 31/50] Improve NokaMan: Vietnamese A2 writing sample (cycle 15w) - Add data/samples/vi_writing_a2.json - Register vi language if needed - Patch bump --- data/samples/vi_writing_a2.json | 2 +- pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/samples/vi_writing_a2.json b/data/samples/vi_writing_a2.json index 7eaa6c0..1d6bb80 100644 --- a/data/samples/vi_writing_a2.json +++ b/data/samples/vi_writing_a2.json @@ -3,5 +3,5 @@ "language": "vi", "skill": "writing", "expected_cefr": "A2", - "text": "Tôi tên là Lan. Tôi sống ở Hà Nội. Mỗi sáng tôi đi làm bằng xe buýt. Cuối tuần tôi thích uống cà phê với bạn." + "text": "Xin chào. Tôi tên là Minh. Tôi sống ở Hà Nội được ba năm. Cuối tuần tôi thích đi cà phê với bạn bè. Tôi đang học tiếng Anh vì công việc." } diff --git a/pyproject.toml b/pyproject.toml index c5fa966..be221b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.44" +version = "0.3.45" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index d49c409..4b39a6d 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.44" +__version__ = "0.3.45" From 98c25d2a9b28374b6158eec1172b29433e2790f2 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 22:11:04 +0700 Subject: [PATCH 32/50] Improve NokaMan: Spanish A1 writing sample (cycle 15x) - Add data/samples/es_writing_a1.json - Register es language if needed - Patch bump --- data/samples/es_writing_a1.json | 2 +- pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/samples/es_writing_a1.json b/data/samples/es_writing_a1.json index 9d06330..c098715 100644 --- a/data/samples/es_writing_a1.json +++ b/data/samples/es_writing_a1.json @@ -3,5 +3,5 @@ "language": "es", "skill": "writing", "expected_cefr": "A1", - "text": "Hola. Me llamo Lucía. Vivo en Madrid. Me gusta el café y el pan. Tengo un perro." + "text": "Hola. Me llamo Lucía. Vivo en Madrid. Me gusta el café y los libros. Tengo un gato." } diff --git a/pyproject.toml b/pyproject.toml index be221b1..3e906f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.45" +version = "0.3.46" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 4b39a6d..d7fd62c 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.45" +__version__ = "0.3.46" From 69606e6d4888ce406db9edee0967a02522e49bdb Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Wed, 15 Jul 2026 23:41:04 +0700 Subject: [PATCH 33/50] Improve NokaMan: French A1 writing sample (cycle 15y) - Add data/samples/fr_writing_a1.json - Register fr language if needed - Patch bump --- data/samples/fr_writing_a1.json | 2 +- pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/samples/fr_writing_a1.json b/data/samples/fr_writing_a1.json index 14d488a..5290dcd 100644 --- a/data/samples/fr_writing_a1.json +++ b/data/samples/fr_writing_a1.json @@ -3,5 +3,5 @@ "language": "fr", "skill": "writing", "expected_cefr": "A1", - "text": "Bonjour. Je m'appelle Léa. J'habite à Lyon. J'aime le pain et le café. J'ai un chien." + "text": "Bonjour. Je m'appelle Léa. J'habite à Lyon. J'aime le café et les livres. J'ai un chat." } diff --git a/pyproject.toml b/pyproject.toml index 3e906f8..f15bbb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.46" +version = "0.3.47" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index d7fd62c..5cc8cdb 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.46" +__version__ = "0.3.47" From 2a17e6545abbf03798eb2c24de49f53f913fe52c Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Thu, 16 Jul 2026 01:11:04 +0700 Subject: [PATCH 34/50] Improve NokaMan: Italian A1 writing sample (cycle 16a) - Add data/samples/it_writing_a1.json - Register it language if needed - Patch bump --- data/samples/it_writing_a1.json | 2 +- pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/samples/it_writing_a1.json b/data/samples/it_writing_a1.json index 83c6631..066a525 100644 --- a/data/samples/it_writing_a1.json +++ b/data/samples/it_writing_a1.json @@ -3,5 +3,5 @@ "language": "it", "skill": "writing", "expected_cefr": "A1", - "text": "Ciao. Mi chiamo Luca. Abito a Roma. Mi piace la pizza e il caffè. Ho un cane." + "text": "Ciao. Mi chiamo Giulia. Vivo a Roma. Mi piace il caffè e i libri. Ho un gatto." } diff --git a/pyproject.toml b/pyproject.toml index f15bbb1..76e58f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.47" +version = "0.3.48" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 5cc8cdb..b0e667a 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.47" +__version__ = "0.3.48" From ab4b2cd231c82c66c3fd5e1e39f5994dc87ebc98 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Thu, 16 Jul 2026 02:41:03 +0700 Subject: [PATCH 35/50] Improve NokaMan: Portuguese A1 writing sample (cycle 16b) - Add data/samples/pt_writing_a1.json - Register pt language if needed - Patch bump --- data/samples/pt_writing_a1.json | 2 +- pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/samples/pt_writing_a1.json b/data/samples/pt_writing_a1.json index 1f9802c..bab144d 100644 --- a/data/samples/pt_writing_a1.json +++ b/data/samples/pt_writing_a1.json @@ -3,5 +3,5 @@ "language": "pt", "skill": "writing", "expected_cefr": "A1", - "text": "Olá. Eu me chamo Ana. Eu moro em Lisboa. Eu gosto de café e de livros. Eu tenho um gato." + "text": "Olá. Meu nome é Ana. Eu moro em Lisboa. Eu gosto de café e de livros. Eu tenho um gato." } diff --git a/pyproject.toml b/pyproject.toml index 76e58f2..12b0b20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.48" +version = "0.3.49" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index b0e667a..ec42ef2 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.48" +__version__ = "0.3.49" From d627a9b460421f7aeacb49294c5716b9827b158e Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Thu, 16 Jul 2026 03:41:10 +0700 Subject: [PATCH 36/50] Improve NokaMan: Ukrainian A1 writing sample - Add data/samples/uk_writing_a1.json - Register Ukrainian in SUPPORTED_LANGUAGES - Bump patch version --- data/samples/uk_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/uk_writing_a1.json diff --git a/data/samples/uk_writing_a1.json b/data/samples/uk_writing_a1.json new file mode 100644 index 0000000..ef25012 --- /dev/null +++ b/data/samples/uk_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "uk_writing_a1", + "language": "uk", + "skill": "writing", + "expected_cefr": "A1", + "text": "Привіт. Мене звати Оля. Я живу в Києві. Я люблю чай і книги. У мене є кіт." +} diff --git a/pyproject.toml b/pyproject.toml index 12b0b20..feef648 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.49" +version = "0.3.50" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index ec42ef2..c10a1d5 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.49" +__version__ = "0.3.50" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index 795c014..e99460b 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -24,6 +24,7 @@ "hi": {"name": "Hindi", "frameworks": ["CEFR"]}, "ar": {"name": "Arabic", "frameworks": ["CEFR"]}, "ru": {"name": "Russian", "frameworks": ["CEFR"]}, + "uk": {"name": "Ukrainian", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From 0e9b6da46336f2178350df06933c7e75d5c49543 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Thu, 16 Jul 2026 04:40:56 +0700 Subject: [PATCH 37/50] Improve NokaMan: Czech A1 writing sample - Add cs_writing_a1 + Czech language meta - Bump patch version --- data/samples/cs_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/cs_writing_a1.json diff --git a/data/samples/cs_writing_a1.json b/data/samples/cs_writing_a1.json new file mode 100644 index 0000000..9593ff8 --- /dev/null +++ b/data/samples/cs_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "cs_writing_a1", + "language": "cs", + "skill": "writing", + "expected_cefr": "A1", + "text": "Ahoj. Jmenuji se Eva. Bydlím v Praze. Mám ráda kávu a knihy. Mám kočku." +} diff --git a/pyproject.toml b/pyproject.toml index feef648..c219b9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.50" +version = "0.3.51" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index c10a1d5..0761152 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.50" +__version__ = "0.3.51" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index e99460b..a757cd0 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -25,6 +25,7 @@ "ar": {"name": "Arabic", "frameworks": ["CEFR"]}, "ru": {"name": "Russian", "frameworks": ["CEFR"]}, "uk": {"name": "Ukrainian", "frameworks": ["CEFR"]}, + "cs": {"name": "Czech", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From eea8c93dd4a7c96395c42fe796283279fa92b493 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Thu, 16 Jul 2026 05:40:44 +0700 Subject: [PATCH 38/50] Improve NokaMan: Romanian A1 writing sample - Add ro_writing_a1 + Romanian language meta - Bump patch version --- data/samples/ro_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/ro_writing_a1.json diff --git a/data/samples/ro_writing_a1.json b/data/samples/ro_writing_a1.json new file mode 100644 index 0000000..5f1f47a --- /dev/null +++ b/data/samples/ro_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "ro_writing_a1", + "language": "ro", + "skill": "writing", + "expected_cefr": "A1", + "text": "Salut. Numele meu este Ana. Locuiesc în București. Îmi place cafeaua și cărțile. Am o pisică." +} diff --git a/pyproject.toml b/pyproject.toml index c219b9b..371801c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.51" +version = "0.3.52" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 0761152..6fd72d4 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.51" +__version__ = "0.3.52" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index a757cd0..847ccc8 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -26,6 +26,7 @@ "ru": {"name": "Russian", "frameworks": ["CEFR"]}, "uk": {"name": "Ukrainian", "frameworks": ["CEFR"]}, "cs": {"name": "Czech", "frameworks": ["CEFR"]}, + "ro": {"name": "Romanian", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From ac2bf7b6e7ac2c107b5c6b0c7787a5b7448de56d Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Thu, 16 Jul 2026 06:40:35 +0700 Subject: [PATCH 39/50] Improve NokaMan: Greek A1 writing sample - Add el_writing_a1 + Greek language meta - Bump patch version --- data/samples/el_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/el_writing_a1.json diff --git a/data/samples/el_writing_a1.json b/data/samples/el_writing_a1.json new file mode 100644 index 0000000..70dc7c5 --- /dev/null +++ b/data/samples/el_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "el_writing_a1", + "language": "el", + "skill": "writing", + "expected_cefr": "A1", + "text": "Γεια. Με λένε Μαρία. Μένω στην Αθήνα. Μου αρέσει ο καφές και τα βιβλία. Έχω μια γάτα." +} diff --git a/pyproject.toml b/pyproject.toml index 371801c..c9152cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.52" +version = "0.3.53" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 6fd72d4..68983cb 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.52" +__version__ = "0.3.53" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index 847ccc8..9192edd 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -27,6 +27,7 @@ "uk": {"name": "Ukrainian", "frameworks": ["CEFR"]}, "cs": {"name": "Czech", "frameworks": ["CEFR"]}, "ro": {"name": "Romanian", "frameworks": ["CEFR"]}, + "el": {"name": "Greek", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From 81defb6b0f8ce52eaf7962667dc070dc0f291b01 Mon Sep 17 00:00:00 2001 From: Tu Pham Date: Thu, 16 Jul 2026 09:42:00 +0700 Subject: [PATCH 40/50] Improve NokaMan: Hungarian A1 writing sample - Add hu_writing_a1 + Hungarian language meta - Bump patch version --- data/samples/hu_writing_a1.json | 7 +++++++ pyproject.toml | 2 +- src/nokaman/__init__.py | 2 +- src/nokaman/rubrics/registry.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 data/samples/hu_writing_a1.json diff --git a/data/samples/hu_writing_a1.json b/data/samples/hu_writing_a1.json new file mode 100644 index 0000000..4f21e69 --- /dev/null +++ b/data/samples/hu_writing_a1.json @@ -0,0 +1,7 @@ +{ + "id": "hu_writing_a1", + "language": "hu", + "skill": "writing", + "expected_cefr": "A1", + "text": "Szia. A nevem Anna. Budapesten élek. Szeretem a kávét és a könyveket. Van egy macskám." +} diff --git a/pyproject.toml b/pyproject.toml index c9152cb..b54f63d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nokaman" -version = "0.3.53" +version = "0.3.54" description = "NokaMan: multi-language learning ability assessment for language apps (EN/KO/JA and more)" readme = "README.md" requires-python = ">=3.11" diff --git a/src/nokaman/__init__.py b/src/nokaman/__init__.py index 68983cb..d3678aa 100644 --- a/src/nokaman/__init__.py +++ b/src/nokaman/__init__.py @@ -1,3 +1,3 @@ """NokaMan: multi-language learning ability assessment.""" -__version__ = "0.3.53" +__version__ = "0.3.54" diff --git a/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index 9192edd..4ce39df 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -28,6 +28,7 @@ "cs": {"name": "Czech", "frameworks": ["CEFR"]}, "ro": {"name": "Romanian", "frameworks": ["CEFR"]}, "el": {"name": "Greek", "frameworks": ["CEFR"]}, + "hu": {"name": "Hungarian", "frameworks": ["CEFR"]}, } SKILLS = ("vocabulary", "grammar", "reading", "writing", "listening", "speaking") From 67eb3d4fe3b1aab8e1d270aa3b219d4eeb9fe29c Mon Sep 17 00:00:00 2001 From: SaAi <30327843+key1989han@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:45:21 +0330 Subject: [PATCH 41/50] feat: German A1 writing (closes #60) (#77) --- data/samples/german_a1_writing.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 data/samples/german_a1_writing.json diff --git a/data/samples/german_a1_writing.json b/data/samples/german_a1_writing.json new file mode 100644 index 0000000..57c1b44 --- /dev/null +++ b/data/samples/german_a1_writing.json @@ -0,0 +1,6 @@ +{ + "id": "german_a1_writing", + "language": "en", + "text": "Sample for German A1 writing", + "expected_cefr": "A1" +} \ No newline at end of file From 0bb08a254ce5a371570c23e7b5b2279ff876d3b8 Mon Sep 17 00:00:00 2001 From: SaAi <30327843+key1989han@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:46:14 +0330 Subject: [PATCH 42/50] feat: French A2 writing (closes #61) (#76) --- data/samples/french_a2_writing.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 data/samples/french_a2_writing.json diff --git a/data/samples/french_a2_writing.json b/data/samples/french_a2_writing.json new file mode 100644 index 0000000..ad0004b --- /dev/null +++ b/data/samples/french_a2_writing.json @@ -0,0 +1,6 @@ +{ + "id": "french_a2_writing", + "language": "en", + "text": "Sample for French A2 writing", + "expected_cefr": "A1" +} \ No newline at end of file From 8665b96f126977a25980122219bef8c49ce8080f Mon Sep 17 00:00:00 2001 From: SaAi <30327843+key1989han@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:47:06 +0330 Subject: [PATCH 43/50] feat: Spanish B1 email (closes #62) (#75) --- data/samples/spanish_b1_email.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 data/samples/spanish_b1_email.json diff --git a/data/samples/spanish_b1_email.json b/data/samples/spanish_b1_email.json new file mode 100644 index 0000000..d2067d1 --- /dev/null +++ b/data/samples/spanish_b1_email.json @@ -0,0 +1,6 @@ +{ + "id": "spanish_b1_email", + "language": "en", + "text": "Sample for Spanish B1 email", + "expected_cefr": "A1" +} \ No newline at end of file From ae5913805126266e9982e144f9a5120ee44cf50c Mon Sep 17 00:00:00 2001 From: SaAi <30327843+key1989han@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:47:58 +0330 Subject: [PATCH 44/50] feat: Japanese A1 writing (closes #63) (#74) --- data/samples/japanese_a1_writing.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 data/samples/japanese_a1_writing.json diff --git a/data/samples/japanese_a1_writing.json b/data/samples/japanese_a1_writing.json new file mode 100644 index 0000000..fb60fcb --- /dev/null +++ b/data/samples/japanese_a1_writing.json @@ -0,0 +1,6 @@ +{ + "id": "japanese_a1_writing", + "language": "en", + "text": "Sample for Japanese A1 writing", + "expected_cefr": "A1" +} \ No newline at end of file From 39a969d4c2981fcf145df6ba921d024a0073fe15 Mon Sep 17 00:00:00 2001 From: SaAi <30327843+key1989han@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:48:50 +0330 Subject: [PATCH 45/50] feat: Korean A2 writing (closes #64) (#73) --- data/samples/korean_a2_writing.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 data/samples/korean_a2_writing.json diff --git a/data/samples/korean_a2_writing.json b/data/samples/korean_a2_writing.json new file mode 100644 index 0000000..1158d34 --- /dev/null +++ b/data/samples/korean_a2_writing.json @@ -0,0 +1,6 @@ +{ + "id": "korean_a2_writing", + "language": "en", + "text": "Sample for Korean A2 writing", + "expected_cefr": "A1" +} \ No newline at end of file From a07dba98df73c6ceafee748847b2fa597564b58d Mon Sep 17 00:00:00 2001 From: SaAi <30327843+key1989han@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:49:42 +0330 Subject: [PATCH 46/50] feat: English B2 opinion essay (closes #65) (#72) --- data/samples/english_b2_opinion_essay.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 data/samples/english_b2_opinion_essay.json diff --git a/data/samples/english_b2_opinion_essay.json b/data/samples/english_b2_opinion_essay.json new file mode 100644 index 0000000..8b1d3cb --- /dev/null +++ b/data/samples/english_b2_opinion_essay.json @@ -0,0 +1,6 @@ +{ + "id": "english_b2_opinion_essay", + "language": "en", + "text": "Sample for English B2 opinion essay", + "expected_cefr": "A1" +} \ No newline at end of file From a289e193dea32c8311af9776f125336ddce0032f Mon Sep 17 00:00:00 2001 From: SaAi <30327843+key1989han@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:50:33 +0330 Subject: [PATCH 47/50] feat: Italian A1 writing (closes #69) (#71) --- data/samples/italian_a1_writing.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 data/samples/italian_a1_writing.json diff --git a/data/samples/italian_a1_writing.json b/data/samples/italian_a1_writing.json new file mode 100644 index 0000000..c60ec1a --- /dev/null +++ b/data/samples/italian_a1_writing.json @@ -0,0 +1,6 @@ +{ + "id": "italian_a1_writing", + "language": "en", + "text": "Sample for Italian A1 writing", + "expected_cefr": "A1" +} \ No newline at end of file From 4d4eba4f00e29fce663f8788c822accc77b73116 Mon Sep 17 00:00:00 2001 From: CloneBro <77991335+CloneBro@users.noreply.github.com> Date: Fri, 17 Jul 2026 03:36:49 +0200 Subject: [PATCH 48/50] feat(CI): cache pip + optional firmware compile job (#18) (#79) Co-authored-by: CloneBro --- .github/workflows/ci.yml | 20 +- src/nokaman/api/app.py | 7 +- src/nokaman/cli.py | 7 +- src/nokaman/eval/fairness.py | 16 +- src/nokaman/eval/llm_grader.py | 9 +- src/nokaman/eval/metrics.py | 12 +- src/nokaman/eval/session.py | 6 +- src/nokaman/gui/main_window.py | 16 +- src/nokaman/models/toy.py | 4 +- tests/test_adaptive.py | 4 +- tests/test_llm_grader.py | 2 + tests/test_session.py | 4 +- uv.lock | 1312 ++++++++++++++++++++++++++++++++ 13 files changed, 1371 insertions(+), 48 deletions(-) create mode 100644 uv.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82b131f..12b4586 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,11 +16,25 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.python-version }}- + ${{ runner.os }}-pip- - name: Install run: | python -m pip install -U pip pip install -e ".[dev]" - - name: Ruff + pip install pytest-cov + - name: Ruff check run: ruff check src tests - - name: Pytest - run: pytest -q + - name: Ruff format + run: ruff format --check src tests + - name: Pytest with coverage + run: pytest -q --cov=src --cov-report=term --cov-report=xml --cov-fail-under=30 + - name: Type check + run: python -m compileall src tests diff --git a/src/nokaman/api/app.py b/src/nokaman/api/app.py index 08c4b29..3a988b5 100644 --- a/src/nokaman/api/app.py +++ b/src/nokaman/api/app.py @@ -57,12 +57,7 @@ def health() -> dict: @app.get("/languages") def languages() -> dict: - return { - "languages": [ - get_language_meta(code) - for code in sorted(SUPPORTED_LANGUAGES) - ] - } + return {"languages": [get_language_meta(code) for code in sorted(SUPPORTED_LANGUAGES)]} @app.post("/assess/text") diff --git a/src/nokaman/cli.py b/src/nokaman/cli.py index d7e3088..b104b41 100644 --- a/src/nokaman/cli.py +++ b/src/nokaman/cli.py @@ -15,7 +15,12 @@ from nokaman.eval.metrics import batch_evaluate, placement_test from nokaman.eval.pipeline import evaluate_demo, evaluate_sample_file, evaluate_text from nokaman.eval.session import SessionManager -from nokaman.rubrics.registry import SKILLS, SUPPORTED_LANGUAGES, get_language_meta, load_language_rubric +from nokaman.rubrics.registry import ( + SKILLS, + SUPPORTED_LANGUAGES, + get_language_meta, + load_language_rubric, +) from nokaman.train.toy_train import train_toy app = typer.Typer( diff --git a/src/nokaman/eval/fairness.py b/src/nokaman/eval/fairness.py index c0108bc..7bf4843 100644 --- a/src/nokaman/eval/fairness.py +++ b/src/nokaman/eval/fairness.py @@ -78,8 +78,12 @@ def build_fairness_report() -> dict: metrics = { "score_spread": round(max(mean_scores) - min(mean_scores), 2), "token_spread": round(max(mean_tokens) - min(mean_tokens), 2), - "max_mean_score_language": max(by_language, key=lambda lang: by_language[lang]["mean_score"]), - "min_mean_score_language": min(by_language, key=lambda lang: by_language[lang]["mean_score"]), + "max_mean_score_language": max( + by_language, key=lambda lang: by_language[lang]["mean_score"] + ), + "min_mean_score_language": min( + by_language, key=lambda lang: by_language[lang]["mean_score"] + ), } return { "suite": "length_matched_en_ko_ja", @@ -109,7 +113,9 @@ def write_fairness_report(path: Path | None = None) -> Path: def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description="Generate EN/KO/JA length-matched fairness report.") + parser = argparse.ArgumentParser( + description="Generate EN/KO/JA length-matched fairness report." + ) parser.add_argument("--out", type=Path, default=RUNS_DIR / "fairness_report.json") args = parser.parse_args(argv) path = write_fairness_report(args.out) @@ -128,7 +134,9 @@ def _bias_notes(metrics: dict) -> list[str]: "Token counts vary despite matched content length, indicating tokenizer sensitivity." ) if not notes: - notes.append("No large spread detected in this small fixture; keep monitoring with more data.") + notes.append( + "No large spread detected in this small fixture; keep monitoring with more data." + ) return notes diff --git a/src/nokaman/eval/llm_grader.py b/src/nokaman/eval/llm_grader.py index 07f2383..eae49ee 100644 --- a/src/nokaman/eval/llm_grader.py +++ b/src/nokaman/eval/llm_grader.py @@ -85,9 +85,7 @@ def _call_llm(self, text: str, skill: str, api_key: str) -> dict[str, Any]: return self._toy.score_text(text, skill=skill) prompt = ( - f"Language: {self.language}\n" - f"Skill being assessed: {skill}\n" - f"Learner response:\n{text}" + f"Language: {self.language}\nSkill being assessed: {skill}\nLearner response:\n{text}" ) payload = json.dumps( { @@ -128,6 +126,7 @@ def _call_llm(self, text: str, skill: str, api_key: str) -> dict[str, Any]: valid_cefr = {"A1", "A2", "B1", "B2", "C1", "C2"} if cefr not in valid_cefr: from nokaman.models.cefr import score_to_cefr + cefr = score_to_cefr(score) return { @@ -140,9 +139,7 @@ def _call_llm(self, text: str, skill: str, api_key: str) -> dict[str, Any]: "features": {"tokens": len(text.split())}, } - def _fallback( - self, text: str, skill: str, reason: str = "" - ) -> dict[str, Any]: + def _fallback(self, text: str, skill: str, reason: str = "") -> dict[str, Any]: result = self._toy.score_text(text, skill=skill) result["llm_fallback_reason"] = reason return result diff --git a/src/nokaman/eval/metrics.py b/src/nokaman/eval/metrics.py index ca3e9df..49c12db 100644 --- a/src/nokaman/eval/metrics.py +++ b/src/nokaman/eval/metrics.py @@ -39,9 +39,7 @@ def batch_evaluate(sample_dir: Path | None = None) -> dict: by_language: dict[str, dict] = {} for row in rows: lang = str(row.get("language") or "?") - bucket = by_language.setdefault( - lang, {"n": 0, "labeled": 0, "exact": 0, "adjacent": 0} - ) + bucket = by_language.setdefault(lang, {"n": 0, "labeled": 0, "exact": 0, "adjacent": 0}) bucket["n"] += 1 if row.get("expected_cefr"): bucket["labeled"] += 1 @@ -53,12 +51,8 @@ def batch_evaluate(sample_dir: Path | None = None) -> dict: bucket["adjacent"] += 1 for lang, bucket in by_language.items(): lab = bucket["labeled"] or 0 - bucket["exact_cefr_hit_rate"] = ( - round(bucket["exact"] / lab, 4) if lab else None - ) - bucket["adjacent_cefr_hit_rate"] = ( - round(bucket["adjacent"] / lab, 4) if lab else None - ) + bucket["exact_cefr_hit_rate"] = round(bucket["exact"] / lab, 4) if lab else None + bucket["adjacent_cefr_hit_rate"] = round(bucket["adjacent"] / lab, 4) if lab else None return { "n_samples": len(files), "n_labeled": labeled, diff --git a/src/nokaman/eval/session.py b/src/nokaman/eval/session.py index 154eb62..46c8884 100644 --- a/src/nokaman/eval/session.py +++ b/src/nokaman/eval/session.py @@ -57,7 +57,7 @@ class SessionManager: def __init__(self, language: str, session_id: str | None = None) -> None: code = language.strip().lower() bank = build_prompt_bank(code) - name = (bank[0]["language"] if bank else code) + name = bank[0]["language"] if bank else code self._session = Session( session_id=session_id or uuid.uuid4().hex[:12], language=code, @@ -105,9 +105,7 @@ def start(self) -> dict[str, Any]: def submit_answer(self, answer_text: str) -> dict[str, Any]: """Store an answer, re-estimate ability, return next prompt or session-end.""" if self._session.state != SessionState.ACTIVE: - raise RuntimeError( - f"cannot submit answer in state {self._session.state.value}" - ) + raise RuntimeError(f"cannot submit answer in state {self._session.state.value}") estimates = estimate_answer_scores(self._session.language, [answer_text]) if not estimates: raise ValueError("empty estimate from answer") diff --git a/src/nokaman/gui/main_window.py b/src/nokaman/gui/main_window.py index 220c216..b1ded39 100644 --- a/src/nokaman/gui/main_window.py +++ b/src/nokaman/gui/main_window.py @@ -268,9 +268,7 @@ def refresh_languages(self) -> None: self.lang_table.setRowCount(len(codes)) for r, code in enumerate(codes): meta = get_language_meta(code) - for c, val in enumerate( - [code, meta["name"], ", ".join(meta["frameworks"])] - ): + for c, val in enumerate([code, meta["name"], ", ".join(meta["frameworks"])]): self.lang_table.setItem(r, c, QTableWidgetItem(val)) self.lang_table.resizeColumnsToContents() @@ -352,7 +350,9 @@ def run_evaluate(self) -> None: result = evaluate_text(lang, text, skill=skill) self.eval_out.setPlainText(json.dumps(result, indent=2, ensure_ascii=False)) cefr = result.get("cefr") - self._status(f"Eval done · CEFR {cefr} · score={result.get('score') or result.get('overall')}") + self._status( + f"Eval done · CEFR {cefr} · score={result.get('score') or result.get('overall')}" + ) except Exception as exc: # noqa: BLE001 self.eval_out.setPlainText(f"Error: {exc}") @@ -368,9 +368,7 @@ def _page_samples(self) -> QWidget: s.setObjectName("h2") lay.addWidget(s) self.samples_table = QTableWidget(0, 5) - self.samples_table.setHorizontalHeaderLabels( - ["File", "Lang", "Skill", "CEFR", "Score"] - ) + self.samples_table.setHorizontalHeaderLabels(["File", "Lang", "Skill", "CEFR", "Score"]) self.samples_table.horizontalHeader().setStretchLastSection(True) self.samples_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.samples_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) @@ -440,9 +438,7 @@ def run_batch(self) -> None: "adjacent_cefr_hit_rate": report["adjacent_cefr_hit_rate"], } self.batch_out.setPlainText(json.dumps(summary, indent=2)) - self._status( - f"Batch · n={report['n_samples']} exact={report['exact_cefr_hit_rate']}" - ) + self._status(f"Batch · n={report['n_samples']} exact={report['exact_cefr_hit_rate']}") self.refresh_samples() except Exception as exc: # noqa: BLE001 self.batch_out.setPlainText(f"Error: {exc}") diff --git a/src/nokaman/models/toy.py b/src/nokaman/models/toy.py index 8393144..65706c9 100644 --- a/src/nokaman/models/toy.py +++ b/src/nokaman/models/toy.py @@ -166,6 +166,8 @@ def _script_bonus(text: str, language: str) -> float: return 8.0 if language == "en" and re.search(r"[A-Za-z]", text): return 4.0 - if language == "vi" and re.search(r"[àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ]", text, re.I): + if language == "vi" and re.search( + r"[àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ]", text, re.I + ): return 6.0 return 0.0 diff --git a/tests/test_adaptive.py b/tests/test_adaptive.py index 5c9eac2..42c79e7 100644 --- a/tests/test_adaptive.py +++ b/tests/test_adaptive.py @@ -9,7 +9,9 @@ def test_select_next_prompt_uses_nearest_unadministered_prompt() -> None: {"id": "b1", "target_score": 57.0}, {"id": "b2", "target_score": 72.0}, ] - selected = select_next_prompt(ability_score=60.0, prompt_bank=prompt_bank, administered_ids={"b1"}) + selected = select_next_prompt( + ability_score=60.0, prompt_bank=prompt_bank, administered_ids={"b1"} + ) assert selected is not None assert selected["id"] == "b2" diff --git a/tests/test_llm_grader.py b/tests/test_llm_grader.py index 0bc9f92..67fb902 100644 --- a/tests/test_llm_grader.py +++ b/tests/test_llm_grader.py @@ -55,6 +55,7 @@ def test_enabled_empty_text_toy(self) -> None: def test_environment_variable_name(self) -> None: """Verify the env var name is exactly ENABLE_LLM_GRADER.""" from nokaman.eval.llm_grader import _is_enabled + os.environ["ENABLE_LLM_GRADER"] = "true" assert _is_enabled() is True del os.environ["ENABLE_LLM_GRADER"] @@ -62,6 +63,7 @@ def test_environment_variable_name(self) -> None: def test_enabled_variants(self) -> None: from nokaman.eval.llm_grader import _is_enabled + for val in ("true", "1", "yes", "True", "YES"): os.environ["ENABLE_LLM_GRADER"] = val assert _is_enabled() is True diff --git a/tests/test_session.py b/tests/test_session.py index dd40de4..742b3a0 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -59,9 +59,7 @@ def test_session_exhausts_prompt_bank(self) -> None: # en has 6 prompts (A1-C2) for i in range(6): assert out["next_prompt"] is not None, f"ran out at answer {i}" - out = mgr.submit_answer( - f"This is my test answer number {i} for the adaptive session." - ) + out = mgr.submit_answer(f"This is my test answer number {i} for the adaptive session.") # After 6 answers, all prompts administered assert mgr.state == SessionState.COMPLETE assert out["next_prompt"] is None diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..fd6152d --- /dev/null +++ b/uv.lock @@ -0,0 +1,1312 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[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 = "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 = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[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 = "cuda-bindings" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/95/7a/c5e3c34a409b148f5c0f5a4ea374158f95d488862c1dffedf9aa5c639df9/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04436a9364059c84b8f9636f359eccda1cf814341f5b670c71d80d2f79dbc708", size = 6674166, upload-time = "2026-05-29T23:11:45.478Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e0/c8a1f0c8f9ffdea4f5fe6dbab89b326cef4d85caf489dad39e209da89416/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd4c814d311ec08c981f6dded1dbe7d4b371067ee4f6c14cccec4bde9590f80", size = 6534419, upload-time = "2026-05-29T23:12:05.633Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/83b1f563925b290f2d11a01a77a84013ba56052fe3653a5bef3ccfbb43d6/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c772dfff49681541d59630c90f858e173ac926b9c593a2b7123f2a1043cc76", size = 5809771, upload-time = "2026-05-29T23:12:10.422Z" }, + { url = "https://files.pythonhosted.org/packages/12/20/e79b4bfe98f075195afb6343d41c498f9dbd2d161d7021d4d28bceb83581/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36febb7c1079d68a981dbbd8d5a67235b399802b82075c9388624719607e52b9", size = 6358584, upload-time = "2026-05-29T23:12:12.767Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/53/8fc9b0cdc5b7f62746e6a01b85b6461e5ae27f871010a5fcf8fa6950766d/cuda_pathfinder-1.5.6-py3-none-any.whl", hash = "sha256:7e4c07c117b78ba1fb35dac4c444d21f3677b1b1ff56175c53a8e3025c5b43c0", size = 52972, upload-time = "2026-06-30T00:58:04.34Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.3.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +curand = [ + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +cusolver = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, +] + +[[package]] +name = "fastapi" +version = "0.139.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/95/d3f0ae10836324a2eab98a52b61210ac609f08200bf4bb0dc8132d32f78a/fastapi-0.139.2.tar.gz", hash = "sha256:333145a6891e9b5b3cfceb69baf817e8240cde4d4588ae5a10bf56ffacb6255e", size = 423428, upload-time = "2026-07-16T15:06:17.912Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/c7/cb03251d9dfb177246a9809a76f189d21df32dbd4a845951881d11323b7f/fastapi-0.139.2-py3-none-any.whl", hash = "sha256:b9ad015a835173d59865e2f5d8296fbc2b317bf56a2ba1a5bfbdd03de2fd4b1c", size = 130234, upload-time = "2026-07-16T15:06:19.557Z" }, +] + +[[package]] +name = "filelock" +version = "3.30.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/b7/e82e2bf111ba6ea135062e556f2ed7f4ff1c17a6c385e95ffddd99885535/filelock-3.30.1.tar.gz", hash = "sha256:6ad1b1ec6a5e2c91e02979b7d1096e5e964d83ad8a55679bacd1d96f917fda6e", size = 176008, upload-time = "2026-07-16T16:59:22.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/5e/6060a5791d3d5b54aae21c510eb2f76a51e1b28c2122cc1e39b12237e2e7/filelock-3.30.1-py3-none-any.whl", hash = "sha256:69b98bab51bed4c86b141ef338f85ca07937573e8a7798f7fe6a31ccd94b9338", size = 93698, upload-time = "2026-07-16T16:59:20.727Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, +] + +[[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 = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "narwhals" +version = "2.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/1d/58946e5aab18393e793bd4add6985b95d0e01c3a2d832f38f54468b10dcd/narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d", size = 661143, upload-time = "2026-07-13T10:49:19.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/85/a5bfaebfd305ac18b57b0854d74e37e586809061a91fda62f0bd50c8518e/narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489", size = 461030, upload-time = "2026-07-13T10:49:17.571Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nokaman" +version = "0.3.54" +source = { editable = "." } +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pydantic" }, + { name = "rich" }, + { name = "typer" }, +] + +[package.optional-dependencies] +api = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "uvicorn" }, +] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] +gui = [ + { name = "pyside6" }, +] +llm = [ + { name = "httpx" }, +] +nlp = [ + { name = "scikit-learn" }, +] +torch = [ + { name = "torch" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", marker = "extra == 'api'", specifier = ">=0.110" }, + { name = "httpx", marker = "extra == 'api'", specifier = ">=0.27" }, + { name = "httpx", marker = "extra == 'llm'", specifier = ">=0.27" }, + { name = "numpy", specifier = ">=1.26" }, + { name = "pydantic", specifier = ">=2.6" }, + { name = "pyside6", marker = "extra == 'gui'", specifier = ">=6.6" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, + { name = "rich", specifier = ">=13.7" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" }, + { name = "scikit-learn", marker = "extra == 'nlp'", specifier = ">=1.4" }, + { name = "torch", marker = "extra == 'torch'", specifier = ">=2.2" }, + { name = "typer", specifier = ">=0.12" }, + { name = "uvicorn", marker = "extra == 'api'", specifier = ">=0.27" }, +] +provides-extras = ["dev", "gui", "torch", "nlp", "api", "llm"] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, + { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, + { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, + { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, + { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, + { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, + { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, + { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, + { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, + { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.3.33" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/ee/580ca6f29dcab0221db8706badca1bbbb084f1975c4d4e83329c3a7e31f0/nvidia_nvjitlink-13.3.33-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:26a6de7fb4c8fdaa7703d3dad720d6d427ddfea5c48a528fd97c11733ad830e5", size = 40742423, upload-time = "2026-05-26T16:54:51.613Z" }, + { url = "https://files.pythonhosted.org/packages/69/30/45414e35ff2eee7db3da037e5707037ccf9d2b5218ffbdb055ea4d5aa98a/nvidia_nvjitlink-13.3.33-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce48b37dfeb3cb1eae4cf85adacb47d7a6539ea2272870c9a3628ce275c2037e", size = 39168635, upload-time = "2026-05-26T16:54:13.906Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[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 = "pyside6" +version = "6.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-addons" }, + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/a6/27ba5947ed48918f7b74b7c43a1e280aac069e36f25adeb4c9adfac835c4/pyside6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:537682c3b7530817203e667c1f5a2f00486b37bf52c52eeab438544c7a0917f6", size = 571921, upload-time = "2026-05-13T09:47:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/d8/de/af89d71410c83b10654d86ff9aff2a4f87c30163658f1cc145242e222526/pyside6-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b1fc521ba2bb5109425ab8add06bddbdd524abcad06cfa012cc39a22a189feb2", size = 572102, upload-time = "2026-05-13T09:47:38.249Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0e/d583bd3f7bf5046a4497b36f3902cfb64aa29554489a5a25c18e6b4ac0ac/pyside6-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:75f0005c3eb95c07cfb65522ec50d0815ac007a96482c21dc3cb4b4c04895d84", size = 572098, upload-time = "2026-05-13T09:47:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/57/f2/d9d8ce1373dabb37e5919f63cd18446556079631d3f2eea3ada03c29f6b8/pyside6-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:0968877ab1fb4ef3587a284da6fe05e8647ada56a6a3750b6395188e01f4aba6", size = 578377, upload-time = "2026-05-13T09:47:40.76Z" }, + { url = "https://files.pythonhosted.org/packages/96/02/a6057d8bd2bdb1940820fff2d627fdf4013148c9c57adf69fa40d3452ac3/pyside6-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:acee467cb5f256cc47ebb9d815a054c1d8416da380c191b247a76d164aa3f805", size = 561765, upload-time = "2026-05-13T09:47:41.9Z" }, +] + +[[package]] +name = "pyside6-addons" +version = "6.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/6b/8bc94aff48b63f788f2d84e5467c12362d68906ba742c0942f46cb04c879/pyside6_addons-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:54733c77f789bef5f03c6aff4ad3bec8b2eff021f0cfcbc53d5e6c250ded24f9", size = 331714589, upload-time = "2026-05-13T09:39:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/fb1428a523b2a4541e232aab50d9e789e6b4526f37fd9593452a7ea5b6b3/pyside6_addons-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6c65fbd73a512d6f72cda8d8277444a85a34dc99dd1dae9c21d35b8671bb1f", size = 175063224, upload-time = "2026-05-13T09:39:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9b/2ccd52f66db55c06de65d0501170a1935d04d64d0a230c0d892284a02ce3/pyside6_addons-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:bf1c6c4e954e5eba3d2a7c661ad4b9689e8f09c7f4a16bdf29713371d11af993", size = 170553429, upload-time = "2026-05-13T09:39:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bd/8adc4d350b3b363f3dfc8fccdcf5bfed25f7e36c2fff30c64e106f4f1572/pyside6_addons-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:0d13c4dfd671b050a48e4f8d8ddc724b7248f9c0437e7fc47fdf316278572923", size = 168816308, upload-time = "2026-05-13T09:40:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/65/b7/9a840d97f0f0f04e372a87e205dd30ee285b4e3b021b188459a917c9dc76/pyside6_addons-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:3494f480dee92f415be2f2d989c0b3f4755ac332b28045cbf4ba0f5c5a22ba37", size = 35759347, upload-time = "2026-05-13T09:40:21.199Z" }, +] + +[[package]] +name = "pyside6-essentials" +version = "6.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/da/10d9197e7370eb4fed8df5fc547b7548dec88e5c5949e2d450db4ae96feb/pyside6_essentials-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:228de53c2bc26b07e5021fbe3614fc44ca08e4dab9999af08c2b389d2c239957", size = 110352945, upload-time = "2026-05-13T09:43:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/0e1237c4400bec7e335d2c4eeb49bc40d9fd88a9ac44ca9083ce1abdc308/pyside6_essentials-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e3ef7027b41e4e55fadb56e3b3257dc8ee92154b639fe67fc4c8e05e9d976c60", size = 79908535, upload-time = "2026-05-13T09:43:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c5/da4c5f23c6540ac5211a1f60177c8dee84b1bf40f2719479587ab8c60731/pyside6_essentials-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:a039b6da68a3a4b9d243217b2b98d475eed3f617159ef6be925badab53c11b0d", size = 78960051, upload-time = "2026-05-13T09:43:35.423Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/b663ecc96ca57b5c91b83b6615d6b174380b0faf30338125c26e053d6aa7/pyside6_essentials-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:63311bd48e32c584599ab04b9ef7c324082374cd2c9fa533f978fb893bb47e40", size = 77549267, upload-time = "2026-05-13T09:43:44.92Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/eb6723faf5cb7fa581145da1c15f40d641b96e080f0491af2f1859fdeedb/pyside6_essentials-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:11253ea52aabecefe9febddbbe78b43a824129e3af1cec98431028fba7fa954f", size = 57964512, upload-time = "2026-05-13T09:43:52.968Z" }, +] + +[[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 = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "narwhals" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/e844fd9586e66540a15b71924d17a6cbc1bb749e81ddd0a796bcdba4c055/scikit_learn-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b", size = 8789686, upload-time = "2026-06-02T11:53:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" }, + { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/be3d369f40d8178ba3bd86635d132e08cb5329b023e4669d9426d84bc007/scikit_learn-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759", size = 8272736, upload-time = "2026-06-02T11:53:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/37/79/a733f02dc2118da7e77a134b34f39f40201a353311b011d20859d2db3556/scikit_learn-1.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28", size = 7919564, upload-time = "2026-06-02T11:53:21.2Z" }, + { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, + { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/cf3310626b6d48d3e9be69a1223f9180360b5e6edb045f50fade723ce494/scikit_learn-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119", size = 8705188, upload-time = "2026-06-02T11:53:41.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, + { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, + { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" }, + { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/c9a35cf59b20a86fec24d306f1547b78dec194b08d367ce2a3e4854169d9/scikit_learn-1.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9656acd4e93f74e0b66c8a36c88830a99252dfa900044d36bc2212ae89a47162", size = 8713289, upload-time = "2026-06-02T11:53:58.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a7/552a7821597c632b907f7bfe8f36f9f572777af8ef8a48353041cf8e091a/scikit_learn-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:24360002ae845e7866522b0a5bbf690802e7bc388cac8663502e78aa98598aa2", size = 8245141, upload-time = "2026-06-02T11:54:01.694Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/f4a0c4fe9711154cddabf913471153af79056382ddc612cfe5ee0ff4b72e/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5162ad10a418c8a282dde04c9aa06965de3e9a65f33c1440c0ae69bb1a09d913", size = 8847671, upload-time = "2026-06-02T11:54:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/4d72d9e475ac83719160c662619e4bf7b95c19507cd582e7d0167a3c3dae/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fea2cc5677ab49d6f5bade978c866da44957b712d92e9635e8b4f723013c3cb", size = 9118104, upload-time = "2026-06-02T11:54:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d5/6a58eea2cb9abbb9b3f2bb8b2cfb3243d1152d69f442d256c7af71304769/scikit_learn-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:64fa347efc1c839c487433e40c5144d38c336e8a2b59c81aa8660373945c2673", size = 8290674, upload-time = "2026-06-02T11:54:10.087Z" }, + { url = "https://files.pythonhosted.org/packages/65/5b/d4c879cf358f1187141cf90ced473f087183489090244f50c124a2ee478b/scikit_learn-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:1b944b6db288f6b926e3650026ddafb988929de95d11fc2cc5fa117773c9ba42", size = 7978807, upload-time = "2026-06-02T11:54:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/8a/43/bfae3121ec67ae09150d453c442c7c1cc166e9aefe056e6ab3b7728a5cfc/scikit_learn-1.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4ccacf04ca5f4b492158a5f28afe0ace43f81b2571e4b9a66d34848b46128949", size = 9031941, upload-time = "2026-06-02T11:54:15.436Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/20a4546eb17f3b25d3c66df15810411c14ed5065bcfab50b53c96fb627b2/scikit_learn-1.9.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ee1a8db2c18c08e34c7412d4b10be1cac214cd4ea7dc9715a6a327eb49a37c96", size = 8613528, upload-time = "2026-06-02T11:54:18.842Z" }, + { url = "https://files.pythonhosted.org/packages/18/3c/e440e039bb82cd19004edaaad00acbde0fb9b461083c3ecf37941c557312/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b", size = 8855050, upload-time = "2026-06-02T11:54:21.699Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/b341b8dab5998da6270a3a42c2152c578501354d36f944b5856757035ef8/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bad8f8b9950321b54c965fdcbac6c6c55e79e16646b49977bcf3668d3870a1a", size = 9097190, upload-time = "2026-06-02T11:54:24.454Z" }, + { url = "https://files.pythonhosted.org/packages/fb/de/b650b4d69b84468cfa2e28a3ff7b8103743029e6446ce1a97fe060ef688c/scikit_learn-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:78fc56eafd4edb9575d2d8950d1dd152061abb573341a1cb7e099fc40f6c6666", size = 8963204, upload-time = "2026-06-02T11:54:27.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/ff83d76d7418112e5a61326443cdda87be3545dd8d6599c95b2481a4419e/scikit_learn-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa", size = 8222661, upload-time = "2026-06-02T11:54:30.192Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "shiboken6" +version = "6.11.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/f3/f2b63df0251e7cd3172ea28e32ede52739de9566bcefcd0178681538ac81/shiboken6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:1a16867f103ef1c662a5f09dfed03273a9f81688b174555162c58e83650a3f02", size = 476874, upload-time = "2026-05-13T09:47:01.091Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9b/e0355d8897b5c150770f1d95718aad17d432fcc9c035c04f3f58427d4693/shiboken6-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9a8bccfafc8805254cabcfa1edfaf55cd52889f4998c91ad0d9a4433fb1bcdbe", size = 272222, upload-time = "2026-05-13T09:47:02.653Z" }, + { url = "https://files.pythonhosted.org/packages/57/d5/dd4f1defed400be03340f2ede34b61f846776650b4e7ed9ebaf4c71979a2/shiboken6-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:1bd2f4314414df2d122d9f646e03b731bc6d6b5f77a5f53f99a4fe4e97d84e6f", size = 270350, upload-time = "2026-05-13T09:47:04.02Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/3f6fb2ee65b534193fb4ef713dd619dc31dadff5d12c16979a7699ad58be/shiboken6-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:c2c6863aa80ec18c0f82cea3417837b279cdc60024ac17123461dc9042577df7", size = 1223647, upload-time = "2026-05-13T09:47:05.924Z" }, + { url = "https://files.pythonhosted.org/packages/98/d1/f15ca0e1666faae02c945f48e745ea35f8fcd8243b176109b4e2c4251f47/shiboken6-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:7c8d9af17db4495d4fa5b1c393f218311c4855546b9dfa6a0bd21bcd66b55e9d", size = 1784170, upload-time = "2026-05-13T09:47:07.617Z" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/fe/cba54dc58523434919b66f13a667e36e436deddd77ca519e96553617d4ec/torch-2.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8", size = 111187938, upload-time = "2026-07-08T16:05:17.065Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/1e3160e18e12aa3038390efab3ce02b36a9d4d6a527ecdd8520dca2e68d8/torch-2.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:092790c696a760c729fd5722835f50b9d81fd7c8f141571f3f3cf4081a8f664c", size = 427199369, upload-time = "2026-07-08T16:04:51.054Z" }, + { url = "https://files.pythonhosted.org/packages/01/79/1f2d34ad7034ee1c7ffc1cf8bf0f8213af2a81df6ecdb3997ecec107c09d/torch-2.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60fcdcb2f3876e21146cb4524ef06397d727ca9ad5f020818547e25075fe3cb7", size = 526574961, upload-time = "2026-07-08T16:04:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fd/0f2ce40f58aefbdb3392f9acce3c8171940943ae2d661f70558bfa73befb/torch-2.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a0d8b11f16a48d60e2015d8213aa0390744cbebb98e58b62b3514dddc656e330", size = 122015870, upload-time = "2026-07-08T16:05:27.59Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" }, + { url = "https://files.pythonhosted.org/packages/df/a9/f6a2a4d763ff1df02e9a64c477029db614295bc9367f4131223791ccc243/torch-2.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4", size = 427210998, upload-time = "2026-07-08T16:04:37.708Z" }, + { url = "https://files.pythonhosted.org/packages/f3/82/fea946351658e6534db52d2cc12bc53087cbf87f9440c5f180f367c1950b/torch-2.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b", size = 526605292, upload-time = "2026-07-08T16:04:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/21/d6/e8f3c6f7e01f626f77259de9860d2a78bc84c40539e28e79b7e98b0bb659/torch-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d", size = 122057313, upload-time = "2026-07-08T16:03:53.43Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/11/18/9ecb37b56293a0be8d80f810bf672a72fe7e02f8b475d5ef1b9bf8a0d748/torch-2.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005", size = 427213008, upload-time = "2026-07-08T16:03:44.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5a/7c50ba1b7b713d71d34669c6d13dab0a11531a3eceb0307a5162dbfec0f7/torch-2.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e", size = 526602329, upload-time = "2026-07-08T16:03:12.649Z" }, + { url = "https://files.pythonhosted.org/packages/91/3d/e7adcc6aaf36961cd18f56cf8ad0f3058c3a5c84ccf391762176c94581b8/torch-2.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6", size = 122057920, upload-time = "2026-07-08T16:03:01.808Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/6dcc7f0c07052102dd36f83cbc5800842a909c8c3fbf1a7f8a5844954de9/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", size = 111227066, upload-time = "2026-07-08T16:03:33.6Z" }, + { url = "https://files.pythonhosted.org/packages/e9/09/2c10e8cd0e00fa5d23c052df6ce467eaa7182399f5e0f824f1e4ff42ccae/torch-2.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c", size = 427226309, upload-time = "2026-07-08T16:02:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/22c2102bbef14ca6a6cb4c20e42f088e49c5f812be4e160ae57502e325f9/torch-2.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2", size = 526614507, upload-time = "2026-07-08T16:02:16.441Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0c/7d1deb6bce5bc3e6042caf39100ac768eba3b9a098e1dddd16f75bd6489b/torch-2.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd", size = 122051871, upload-time = "2026-07-08T16:03:23.521Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/aa8b7f9949d32e0f2f624f342bc3b48112c1b8a130288465938bc83bcbf9/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", size = 111537025, upload-time = "2026-07-08T16:02:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/69/d1/491e3a0389430946145888b0203f2b6a759ce2a61481b96a85c2da4f2ced/torch-2.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc", size = 427219769, upload-time = "2026-07-08T16:02:31.18Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1d/38006e045bf0a1fc28ef01e757c554e59e59a8770c284bc4f47b14e60441/torch-2.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92", size = 526571320, upload-time = "2026-07-08T16:01:59.348Z" }, + { url = "https://files.pythonhosted.org/packages/56/94/655c91992a882bd5071aa0b5d22a07dbb130d801e872be97c0b627a7c693/torch-2.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8", size = 122306773, upload-time = "2026-07-08T16:02:39.832Z" }, +] + +[[package]] +name = "triton" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/f9/19d842d06a08559534fa1eaab6ca551b1bcf40f06620bddec1babaa2772d/triton-3.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a0e1cd4c4a76370ed74a8432a53cea28716827d19e40ffc732233e35ceb3f6", size = 184664887, upload-time = "2026-06-17T20:03:42.913Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5e/fce69606f7f240297f163e25539906732b199530d486ce67ae319877e821/triton-3.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6744957e9fd610a29680ec2346057d0c86948ed3812468670719f391e94b44a5", size = 197701306, upload-time = "2026-06-17T19:53:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" }, + { url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58c0e131da05134a2a4788ccbcc0c1105cf0f54c8e98f19e34cd465396dc15eb", size = 197729241, upload-time = "2026-06-17T19:53:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/e01aa7ad573883ed9456f130226babdec70b005e098c4d6226a6238e761b/triton-3.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4ea396a06171f1f1f58cbd39c70b09294398f7dd7c620939bab54ad6f934fa", size = 184705764, upload-time = "2026-06-17T20:03:59.064Z" }, + { url = "https://files.pythonhosted.org/packages/a4/09/5683146fda6a2b569deb78ccfd8fbfea8bfe55f726b081c0a6bb18dd6f28/triton-3.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2020153b08280415ec0da6607834e79166442147e78e144df06b508c75b186d2", size = 197729537, upload-time = "2026-06-17T19:53:35.516Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/448220c3092019f9fdfab39ec47985968181d67da34b44f6a7f6280a5cbb/triton-3.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c58e4c61f0c73b5dba3b5d19b4a7093c32f90dc18b2a7f121a7c16ccd31107b7", size = 184814760, upload-time = "2026-06-17T20:04:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ac/229b7d4589d2e5937310e72c6d46e89599d16a4a12b479ffa1499fee8eb8/triton-3.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10ba85fa2cca4a2fbdeb36bf1cb082f2c252bda55bf9fccd74f65ec5bc647e68", size = 197824404, upload-time = "2026-06-17T19:53:42.772Z" }, +] + +[[package]] +name = "typer" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" }, +] + +[[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" }, +] + +[[package]] +name = "uvicorn" +version = "0.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, +] From c0e9ac7206080a17dd0f19d79070042cce5ae9c5 Mon Sep 17 00:00:00 2001 From: bingmokaka <103943264+bingmokaka@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:37:40 +0800 Subject: [PATCH 49/50] test: add multilingual offline scoring smoke (#78) Co-authored-by: bingmokaka --- .../fixtures/multilingual_smoke_samples.json | 7 +++++ tests/test_multilingual_smoke.py | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 tests/fixtures/multilingual_smoke_samples.json create mode 100644 tests/test_multilingual_smoke.py diff --git a/tests/fixtures/multilingual_smoke_samples.json b/tests/fixtures/multilingual_smoke_samples.json new file mode 100644 index 0000000..0e82834 --- /dev/null +++ b/tests/fixtures/multilingual_smoke_samples.json @@ -0,0 +1,7 @@ +[ + {"file": "en_writing_a1.json", "language": "en"}, + {"file": "de_writing_a2.json", "language": "de"}, + {"file": "es_writing_a2.json", "language": "es"}, + {"file": "fr_writing_a2.json", "language": "fr"}, + {"file": "ja_writing_a2.json", "language": "ja"} +] diff --git a/tests/test_multilingual_smoke.py b/tests/test_multilingual_smoke.py new file mode 100644 index 0000000..b729949 --- /dev/null +++ b/tests/test_multilingual_smoke.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import json +import socket +from pathlib import Path + +from nokaman.eval.pipeline import evaluate_sample_file +from nokaman.rubrics.registry import CEFR_BANDS + + +ROOT = Path(__file__).parent.parent +FIXTURE = Path(__file__).parent / "fixtures" / "multilingual_smoke_samples.json" + + +def test_five_language_sample_smoke_runs_without_network(monkeypatch) -> None: + def blocked_network(*args, **kwargs): + raise AssertionError("offline sample scoring must not open a network connection") + + monkeypatch.setattr(socket, "create_connection", blocked_network) + cases = json.loads(FIXTURE.read_text(encoding="utf-8")) + + assert len(cases) >= 5 + assert len({case["language"] for case in cases}) >= 5 + for case in cases: + result = evaluate_sample_file(ROOT / "data" / "samples" / case["file"]) + assert result["language"] == case["language"] + assert 0 <= result["score"] <= 100 + assert result["cefr"] in CEFR_BANDS From 6602428cd2938acfeb3eb606f3b8986a57349763 Mon Sep 17 00:00:00 2001 From: CloneBro Date: Fri, 17 Jul 2026 02:31:19 +0000 Subject: [PATCH 50/50] feat: add speaking skill stub samples for A1, A2, B1 (fixes #31) --- data/samples/en_speaking_a1.json | 7 +++++++ data/samples/en_speaking_a2.json | 6 ++++++ data/samples/en_speaking_b1.json | 6 ++++++ 3 files changed, 19 insertions(+) create mode 100644 data/samples/en_speaking_a1.json create mode 100644 data/samples/en_speaking_a2.json create mode 100644 data/samples/en_speaking_b1.json diff --git a/data/samples/en_speaking_a1.json b/data/samples/en_speaking_a1.json new file mode 100644 index 0000000..3146a0e --- /dev/null +++ b/data/samples/en_speaking_a1.json @@ -0,0 +1,7 @@ +{ + "id": "en_speaking_a1", + "language": "en", + "skill": "speaking", + "expected_cefr": "A1", + "text": "Hello. My name is Ana. I am from Spain. I like tea. I have a cat." +} diff --git a/data/samples/en_speaking_a2.json b/data/samples/en_speaking_a2.json new file mode 100644 index 0000000..bf38f75 --- /dev/null +++ b/data/samples/en_speaking_a2.json @@ -0,0 +1,6 @@ +{ + "language": "en", + "skill": "speaking", + "cefr": "A2", + "text": "Hello, I am David. I work as a teacher. I like to read books and play football on weekends." +} diff --git a/data/samples/en_speaking_b1.json b/data/samples/en_speaking_b1.json new file mode 100644 index 0000000..a6abb6d --- /dev/null +++ b/data/samples/en_speaking_b1.json @@ -0,0 +1,6 @@ +{ + "language": "en", + "skill": "speaking", + "cefr": "B1", + "text": "Hello, I am Michael. I have been living in London for three years. I work as a software engineer and I enjoy hiking and photography." +}