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/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 d7ad73d..dc7c799 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,11 @@ - [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) +- [Contributing](#contributing) - [MergeOS bounties](#mergeos-bounties) - [License](#license) @@ -110,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 @@ -119,15 +123,20 @@ 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 | -| `nokaman train …` | Toy calibration | +| `nokaman eval batch --out data/out/batch.json` | Score every sample and write a JSON CEFR hit-rate report | +| `nokaman train …` | Toy calibration with config/report exports | | `nokaman gui` / `nokaman-gui` | **Qt desktop app** (needs `.[gui]`) | | `nokaman serve` | Optional FastAPI | ```powershell 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 ``` @@ -146,6 +155,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 @@ -172,6 +191,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 | | --- | --- | @@ -215,6 +235,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/ ``` @@ -230,12 +251,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). --- 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/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/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/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/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/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/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/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." +} 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_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/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." +} 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 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/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/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 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 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/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/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/data/samples/it_writing_a1.json b/data/samples/it_writing_a1.json new file mode 100644 index 0000000..066a525 --- /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 Giulia. Vivo a Roma. Mi piace il caffè e i libri. Ho un gatto." +} 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 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 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 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/data/samples/pl_writing_a1.json b/data/samples/pl_writing_a1.json new file mode 100644 index 0000000..8418e7d --- /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ę Kasia. Mieszkam w Krakowie. Lubię kawę i książki. Mam kota." +} diff --git a/data/samples/pt_writing_a1.json b/data/samples/pt_writing_a1.json new file mode 100644 index 0000000..bab144d --- /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á. Meu nome é Ana. Eu moro em Lisboa. Eu gosto de café e de livros. Eu tenho um gato." +} 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/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/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 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/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/data/samples/tr_writing_a1.json b/data/samples/tr_writing_a1.json new file mode 100644 index 0000000..8721a3f --- /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 Ayşe. İstanbul'da yaşıyorum. Kahve ve kitapları severim. Bir kedim var." +} 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/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/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/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/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. diff --git a/docs/screenshots/web-demo.png b/docs/screenshots/web-demo.png new file mode 100644 index 0000000..5cb381e Binary files /dev/null and b/docs/screenshots/web-demo.png differ diff --git a/pyproject.toml b/pyproject.toml index 7dafc4f..b54f63d 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.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 b7d579b..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.29" +__version__ = "0.3.54" diff --git a/src/nokaman/api/app.py b/src/nokaman/api/app.py index c4adc45..3a988b5 100644 --- a/src/nokaman/api/app.py +++ b/src/nokaman/api/app.py @@ -1,18 +1,32 @@ 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 +from nokaman.rubrics.registry import SUPPORTED_LANGUAGES, get_language_meta 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" @@ -25,6 +39,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 { @@ -35,6 +55,11 @@ 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: @@ -42,6 +67,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: @@ -54,3 +84,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/cli.py b/src/nokaman/cli.py index f6cc645..b104b41 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 @@ -9,10 +10,17 @@ 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 -from nokaman.rubrics.registry import SUPPORTED_LANGUAGES, get_language_meta +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 app = typer.Typer( @@ -26,10 +34,16 @@ 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() +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 +64,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 +79,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") @@ -84,8 +98,39 @@ 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: 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()] @@ -102,11 +147,54 @@ def rubrics_list(lang: str | None = typer.Option(None, "--lang", "-l")) -> None: 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"), - 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 +204,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,12 +238,12 @@ 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() 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']} " @@ -188,7 +276,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,26 +293,104 @@ 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") -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) 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/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/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/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/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/src/nokaman/eval/fairness.py b/src/nokaman/eval/fairness.py new file mode 100644 index 0000000..7bf4843 --- /dev/null +++ b/src/nokaman/eval/fairness.py @@ -0,0 +1,148 @@ +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/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/src/nokaman/eval/llm_grader.py b/src/nokaman/eval/llm_grader.py new file mode 100644 index 0000000..eae49ee --- /dev/null +++ b/src/nokaman/eval/llm_grader.py @@ -0,0 +1,145 @@ +"""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}\nSkill being assessed: {skill}\nLearner 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/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 new file mode 100644 index 0000000..46c8884 --- /dev/null +++ b/src/nokaman/eval/session.py @@ -0,0 +1,153 @@ +"""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/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 d9a8960..65706c9 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 @@ -151,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/src/nokaman/rubrics/registry.py b/src/nokaman/rubrics/registry.py index b539e13..4ce39df 100644 --- a/src/nokaman/rubrics/registry.py +++ b/src/nokaman/rubrics/registry.py @@ -13,6 +13,22 @@ "es": {"name": "Spanish", "frameworks": ["CEFR", "DELE"]}, "fr": {"name": "French", "frameworks": ["CEFR", "DELF"]}, "de": {"name": "German", "frameworks": ["CEFR", "Goethe"]}, + "pt": {"name": "Portuguese", "frameworks": ["CEFR", "CAPLE"]}, + "it": {"name": "Italian", "frameworks": ["CEFR", "CILS"]}, + "nl": {"name": "Dutch", "frameworks": ["CEFR"]}, + "sv": {"name": "Swedish", "frameworks": ["CEFR"]}, + "pl": {"name": "Polish", "frameworks": ["CEFR"]}, + "tr": {"name": "Turkish", "frameworks": ["CEFR"]}, + "id": {"name": "Indonesian", "frameworks": ["CEFR"]}, + "th": {"name": "Thai", "frameworks": ["CEFR"]}, + "hi": {"name": "Hindi", "frameworks": ["CEFR"]}, + "ar": {"name": "Arabic", "frameworks": ["CEFR"]}, + "ru": {"name": "Russian", "frameworks": ["CEFR"]}, + "uk": {"name": "Ukrainian", "frameworks": ["CEFR"]}, + "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") 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/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_adaptive.py b/tests/test_adaptive.py new file mode 100644 index 0000000..42c79e7 --- /dev/null +++ b/tests/test_adaptive.py @@ -0,0 +1,36 @@ +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..f4daed9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -24,3 +24,60 @@ def test_assess_text() -> None: ) assert r.status_code == 200 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", + 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" + + +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_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 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"} 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") 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 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") diff --git a/tests/test_llm_grader.py b/tests/test_llm_grader.py new file mode 100644 index 0000000..67fb902 --- /dev/null +++ b/tests/test_llm_grader.py @@ -0,0 +1,70 @@ +"""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"] 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 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 diff --git a/tests/test_session.py b/tests/test_session.py new file mode 100644 index 0000000..742b3a0 --- /dev/null +++ b/tests/test_session.py @@ -0,0 +1,96 @@ +"""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 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"] 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/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" }, +] 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; + } +}