commit 7b2bb49e3c56c72b3a3cc5571a08214da2b9ad1d Author: emfurst Date: Tue Sep 29 20:52:52 2026 -0400 Art 0.1.0 Built from art-dev 78fb10b. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eea7c9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.venv/ +__pycache__/ +*.pyc +*.egg-info/ +data/cache/ +data/corpus/ +digests/ +.DS_Store +models/ +data/art.sqlite* +user/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ebb13eb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Eric M. Furst + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..521587d --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# Art + +Art is a small, locally run LLM toolchain for rheology research. It accompanies the talk "Art: An LLM toolchain for rheology research" (Society of Rheology Annual Meeting, Boston, October 2026, session "AI and ML in Rheology"), and is meant as a starting point that other rheologists can read in one sitting, adopt, and adapt. + +Everything runs on your own machine: embedding and re-ranking models are cached locally from HuggingFace, and text generation (coming in the next stage) goes to [Ollama](https://ollama.com) on `localhost`. No cloud APIs. + +Art has two parts: + +1. **bingham**, a literature screener. It pulls new abstracts from the *Journal of Rheology* and arXiv, screens them against your interest profiles, and writes a daily Markdown digest of the papers worth reading. A Bingham plastic does not flow until the stress exceeds a yield threshold; most abstracts stay put, and only those that clear a calibrated threshold come through. +2. **Corpus search** (planned), hybrid retrieval over a locally indexed collection of rheology papers, with raw-evidence inspection and grounded synthesis with citations. + +## Status + +bingham fetches, screens, and writes digests. The LLM passes (a short summary, a topic label, and a one-sentence match reason for each highlighted paper) are next; until then, each digest entry shows the opening sentences of the abstract and the screening scores. Thresholds are provisional until calibrated on a hand-labeled set. + +## How bingham works + +``` +fetch -> store -> embed -> screen -> re-rank -> digest +(code) (SQLite) (bge) (cosine (cross- (code) + + BM25) encoder) +``` + +- **Fetch.** arXiv announcements from the per-category RSS feeds at `rss.arxiv.org`; *J. Rheol.* from Crossref (by ISSN) merged by DOI with the AIP current-issue feeds, which carry abstracts but no authors. Backfills and skipped days come from arXiv's OAI-PMH interface. +- **Store.** One SQLite file holds papers, embeddings, scores, and a record of every run, so no stage is recomputed unless its model or profile has changed. +- **Screen.** Each paper (title and abstract) is embedded once with `BAAI/bge-large-en-v1.5`. For each profile it gets a cosine similarity (the best match over the profile description and its exemplar papers) and a BM25 score against the profile's keywords, which catches abbreviations such as SAOS, LAOS, and DST and names that embeddings blur. The top candidates by either score, plus any paper by a tracked author, go on. +- **Re-rank.** `BAAI/bge-reranker-v2-m3` reads each (profile description, paper) pair together and scores it from 0 to 1. A paper is highlighted when its score clears the profile's threshold. +- **Digest.** One Markdown file per day in `digests/`, grouped by profile and ordered by score, with a footer recording counts, models, host, device, and time per stage. + +Retrieval models decide relevance; the LLM, when added, only writes about papers that have already passed the screen, and never produces identifiers (DOIs, arXiv IDs, titles, authors, and dates come straight from the feed metadata). + +## Getting started + +### Requirements + +- Python 3.12 and [uv](https://docs.astral.sh/uv/) +- About 3.5 GB of disk for the two HuggingFace models +- A GPU is optional. Tested on an NVIDIA RTX 4090 (CUDA) under Linux and an Apple M3 Pro (MPS) under macOS; the device is chosen automatically (`cuda`, then `mps`, then `cpu`). +- [Ollama](https://ollama.com), for the LLM passes once they are added (not needed yet) + +### Install + +``` +git clone https://lem.che.udel.edu/git/furst/art.git +cd art +uv sync +uv run bingham download-models # once, with network access +``` + +`download-models` fetches the embedder and re-ranker into `./models/`. Every other command runs with HuggingFace in offline mode. + +On Linux with an NVIDIA GPU, use a driver from the 580 series or later (CUDA 13). With older drivers, the standard PyTorch wheels cannot use the GPU, and Ollama 0.34 falls back to the CPU without an error (it still reports "100% GPU" in `ollama ps`, while generation slows to minutes per call). If you cannot update the driver, route `torch` to the CUDA 12.6 index in `pyproject.toml` (`[tool.uv.sources]`, index `https://download.pytorch.org/whl/cu126`) and use an Ollama release that supports your driver. + +### First run + +``` +uv run bingham fetch # J. Rheol. back to jor.backfill_from, plus today's arXiv +uv run bingham fetch --source arxiv --since 2026-09-21 # optional: a week of arXiv to screen +uv run bingham screen +uv run bingham digest --since 2026-09-21 +``` + +The first `fetch` backfills *J. Rheol.* from Crossref (about 145 papers since January 2025). arXiv RSS is empty on Saturdays and Sundays, so a weekend first run fetches no arXiv papers unless you add `--since`. Backfilled papers stay out of the default daily digest; `digest --since DATE` includes them. + +## Running + +``` +uv run bingham daily # fetch + screen + digest; the usual command +uv run bingham fetch [--source arxiv|jor|all] [--since DATE] [--refresh] +uv run bingham screen +uv run bingham digest [--since DATE] +uv run bingham topic "shear thickening and frictional contacts" [-n 15] +``` + +- **`daily`** writes `digests/YYYY-MM-DD.md` covering everything fetched since the previous digest. It is safe to run by hand at irregular intervals: if a weekday's arXiv announcement was missed, the next run fills the gap from OAI-PMH and says so in the digest footer. Gaps longer than `arxiv.max_catchup_days` are stored as backfill instead. +- **`fetch --since DATE`** backfills arXiv (OAI-PMH) and *J. Rheol.* (Crossref) from a date. Requests to arXiv are spaced at least three seconds apart. +- **`topic`** ranks every stored paper against a free-text query, with no profile and no LLM. +- Raw feed responses are cached for `feed_cache_ttl_hours` (6); `--refresh` ignores the cache. + +### Measured run times + +| Machine | Screening a week (about 850 papers, 8 profiles) | A typical day | +|---|---|---| +| RTX 4090 workstation | 37 s, including model loading | a few seconds | +| M3 Pro laptop | about 20 s per profile | under a minute (estimated) | + +## Adapting it to your own interests + +Your research-specific files live in a `user/` directory inside the checkout. It is ignored by this repository, so your profiles, the people you track, and your contact address never end up in the code. Art reads each file from `user/` first and falls back to the generic versions in `data/` (or the examples in `data/examples/`), and merges `user/config.yaml` over `config.yaml`. + +``` +mkdir user +cp data/examples/profiles.yaml data/examples/config.yaml data/examples/interest_profile.md user/ +``` + +- **`user/profiles.yaml`**: one entry per topic, each with a `description` (two or three sentences in the field's vocabulary, used for embedding and re-ranking), `keywords` for BM25, and optional `exemplars` (DOIs or arXiv IDs of papers that typify the topic; abstracts are fetched once, or can be pasted in). `tracked_authors` lists people whose papers are always shown. Editing a profile re-screens against that profile only. +- **`user/config.yaml`**: your contact email (sent to arXiv and Crossref), arXiv categories, and per-profile thresholds. Anything in `config.yaml` can be overridden here; nested keys override and lists replace. +- **`user/interest_profile.md`**: a prose version of your interests, used only by the LLM-as-classifier baseline. +- **`data/vocabulary.md`** and **`data/feeds.yaml`**: the rheology term list that keywords are drawn from, and journal RSS feeds (*Rheologica Acta*, *Soft Matter*, *JNNFM*, and *Macromolecules* are listed but disabled). Copy either into `user/` to customize it. + +Re-ranker scores depend on how broad a profile's description is, so thresholds are set per profile. The digest applies the current thresholds, so changing one needs no re-screen. + +**Drafting a profile from your own papers.** Instead of writing `user/profiles.yaml` from scratch, let `bingham init` draft it from papers you already have: + +``` +uv run bingham fetch # first, so there is a background corpus to compare against +uv run bingham init --orcid 0000-0002-1825-0097 # your ORCID iD +uv run bingham init --ids my_papers.txt # or one DOI or arXiv ID per line +uv run bingham init --csl zotero.json --bib refs.bib # or Zotero exports (CSL JSON, BibTeX); sources can be combined +``` + +It fetches abstracts from Crossref and arXiv (cached in `user/seed/`), groups the papers into topics (`--topics`, default 5) by embedding similarity, picks each topic's most central papers as exemplars, suggests keywords from phrases that recur in the topic and from `data/vocabulary.md`, and suggests tracked authors from coauthors who appear on several of the papers. The LLM writes only each topic's name and description, as a draft. The results go to `user/profiles.draft.yaml`, `user/interest_profile.draft.md`, and `user/vocabulary.draft.md` (abbreviations defined in your papers but missing from the vocabulary); existing files are never overwritten. Papers too unlike the rest to form a topic are listed at the end of the draft rather than forced into one. With fewer than about 30 seed papers, expect to merge and edit topics by hand. + +Then check what each profile would pick up from the stored papers, and choose thresholds: + +``` +uv run bingham profile-test --file user/profiles.draft.yaml +``` + +When you are satisfied, save the draft as `user/profiles.yaml`. + +**Using Art on more than one machine.** Make `user/` a small private git repository of its own and clone it into each checkout (`git clone user`). Sync with `git pull` and `git push` inside `user/`. The SQLite store, digests, and models stay local to each machine. To keep `user/` elsewhere, set `ART_USER_DIR`. + +## Repository layout + +``` +config.yaml base settings (paths, arXiv, J. Rheol., screening, LLM) +data/examples/ example profiles, user config, and interest profile +data/vocabulary.md rheology vocabulary +data/feeds.yaml journal RSS feeds +user/ your own files (not tracked; see above) +modelfiles/ Ollama Modelfile for the researcher-tone model +docs/failure-modes.md curated log of small-model failure modes +art/ + cli.py the bingham command + env.py offline HuggingFace settings, config, device, host + store.py SQLite schema and access + feeds/fetch.py arXiv RSS, OAI-PMH, and API; journal RSS; Crossref + feeds/collect.py daily and backfill fetch logic, arXiv catch-up + feeds/cache.py raw-fetch cache with a TTL + profile/profiles.py load and validate profiles.yaml + screen/ embed.py, bm25.py, rerank.py, screen.py + digest.py Markdown digest + cluster.py average-linkage clustering (field pulse, profile drafts) + toolbox/ seeds.py (ORCID, ID lists, BibTeX, CSL JSON), draft.py (bingham init) + summarize/summarize.py LLM summary and topic label (being wired in) +tests/ uv run pytest +``` + +Generated files (`data/art.sqlite`, `data/cache/`, `digests/`, `models/`) are not tracked. + +## Tests + +``` +uv run pytest +``` + +The tests cover the deterministic parts: feed parsing (against a saved arXiv RSS snapshot), merging papers from several sources, author matching, and the arXiv catch-up logic. They need no network and no models. + +## Related work + +Art copies patterns, with attribution in the code, from: + +- [llm-workshop](https://lem.che.udel.edu/git/furst/llm-workshop), sections `02-ollama`, `03-rag`, and `04-semantic-search` +- [ssearch](https://lem.che.udel.edu/git/furst/ssearch), the hybrid retrieval stack (bge embeddings, bge re-ranker, BM25, offline model cache) +- [Stan](https://github.com/EntropicLearners/stan) ([arXiv:2603.04657](https://arxiv.org/abs/2603.04657)), for its taxonomy of small-model failure modes and its practice of splitting composite LLM tasks + +## License + +MIT; see [LICENSE](LICENSE). Copyright (c) 2026 Eric M. Furst. diff --git a/art/__init__.py b/art/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/art/_paths.py b/art/_paths.py new file mode 100644 index 0000000..fa72755 --- /dev/null +++ b/art/_paths.py @@ -0,0 +1,3 @@ +from pathlib import Path + +ROOT = Path(__file__).parent.parent diff --git a/art/baselines/__init__.py b/art/baselines/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/art/baselines/llm_score.py b/art/baselines/llm_score.py new file mode 100644 index 0000000..81832e7 --- /dev/null +++ b/art/baselines/llm_score.py @@ -0,0 +1,40 @@ +"""LLM-as-classifier baseline: the model rates each paper 0-3 against the whole profile. + +Formerly art/profile/score.py, which scored every paper in the main pipeline. +It is kept only for comparison with the retrieval screen (evaluation step 3). +Two changes from the original: + +- Input is the abstract, not an LLM summary, so the baseline and the screen + see the same text. +- The original took the first digit anywhere in the response and fell back to + 0 on a parse failure, which hid drift. Here a response that is not a single + digit is recorded as parse_fail with its raw text (in llm_calls), and the + score is None rather than 0. +""" + +import re + +from art import env, llm + + +def load_profile() -> str: + """The prose interest profile, user/interest_profile.md (or the example).""" + return env.data_file("interest_profile.md").read_text() + + +def _parse(raw: str) -> tuple[str, int | None, dict]: + text = raw.strip() + if re.fullmatch(r"[0-3]", text): + return "ok", int(text), {} + digit = re.search(r"[0-3]", text) + # What the original lenient parser would have returned, recorded for comparison. + return "parse_fail", None, {"lenient": int(digit.group()) if digit else 0} + + +def score_relevance(conn, item: dict, profile_text: str, run_id: int | None = None) -> int | None: + """0-3 relevance rating, or None if the response is not a single digit.""" + r = llm.generate(conn, "llm_score", + {"profile_text": profile_text, "title": item["title"], + "abstract": item["abstract"] or "(no abstract)"}, + parse=_parse, item_id=item["id"], run_id=run_id) + return r.parsed if r.outcome == "ok" else None diff --git a/art/cli.py b/art/cli.py new file mode 100644 index 0000000..fd169bf --- /dev/null +++ b/art/cli.py @@ -0,0 +1,237 @@ +"""bingham: the Art literature screener. + + uv run bingham fetch [--source arxiv|jor|all] [--since DATE] [--refresh] + uv run bingham screen + uv run bingham digest [--since DATE] [--no-llm] + uv run bingham topic "suspension rheology" + uv run bingham daily # fetch + screen + digest, for launchd + uv run bingham download-models # once, with network access + uv run bingham init --orcid 0000-0000-0000-0000 [--ids FILE] [--bib FILE] [--csl FILE] + uv run bingham profile-test [--file user/profiles.draft.yaml] +""" + +import os +import sys + +# download-models is the one command that may reach HuggingFace; env.py reads +# this before huggingface_hub is imported. +if sys.argv[1:2] == ["download-models"]: + os.environ["ART_ONLINE"] = "1" + +from art import env # noqa: E402 (must precede HuggingFace imports) + +import argparse # noqa: E402 +import time # noqa: E402 + +from art import store # noqa: E402 + + +def cmd_fetch(conn, args) -> None: + from art.feeds import collect + run_id = store.start_run(conn, "fetch", vars(args)) + t = time.perf_counter() + stats = collect.run(conn, args.source, args.since, args.refresh) + store.finish_run(conn, run_id, {"fetch": round(time.perf_counter() - t, 2)}, stats) + print(f"fetched {stats['fetched']} items, {stats['new']} new") + + +def cmd_screen(conn, args) -> None: + from art.screen import screen + run_id = store.start_run(conn, "screen", vars(args), env.device()) + stats, stages = screen.run(conn, run_id) + store.finish_run(conn, run_id, stages, stats) + print(f"screened {stats['screened_pairs']} (paper, profile) pairs, " + f"re-ranked {stats['reranked_pairs']}, highlighted {stats['highlighted']}") + + +def cmd_digest(conn, args, run_id: int | None = None) -> None: + from art import digest + own = run_id is None + if own: + run_id = store.start_run(conn, "digest", vars(args)) + t = time.perf_counter() + path, stats = digest.write(conn, args.since, run_id, use_llm=not args.no_llm) + if own: + store.finish_run(conn, run_id, {"digest": round(time.perf_counter() - t, 2)}, stats) + print(f"digest: {path} ({stats['highlighted']} highlighted of {stats['in_window']})") + return stats + + +def cmd_daily(conn, args) -> None: + """fetch + screen + digest, recorded as one run with per-stage times.""" + from art import digest + from art.feeds import collect + from art.screen import screen + run_id = store.start_run(conn, "daily", vars(args), env.device()) + stages, stats = {}, {} + t = time.perf_counter() + stats["fetch"] = collect.run(conn, "all") + stages["fetch"] = round(time.perf_counter() - t, 2) + stats["screen"], screen_stages = screen.run(conn, run_id) + stages.update(screen_stages) + t = time.perf_counter() + path, stats["digest"] = digest.write(conn, None, run_id, stages, use_llm=not args.no_llm) + stages["digest"] = round(time.perf_counter() - t, 2) + store.finish_run(conn, run_id, stages, stats) + print(f"digest: {path}") + + +def cmd_topic(conn, args) -> None: + """Rank stored papers against a free-text query (an ad hoc profile; no LLM).""" + import numpy as np + from art.profile import profiles as prof + from art.screen import bm25, embed, rerank, screen + p = prof.adhoc(args.query) + items = screen.load_items(conn) + if not items: + print("no papers in the store; run `bingham fetch` first") + return + vecs = embed.embed(conn, {it["id"]: embed.paper_text(it) for it in items}) + qvec = embed.model().encode([args.query], normalize_embeddings=True)[0] + cos = np.stack([vecs[it["id"]] for it in items]) @ qvec + bm = bm25.Index([it["id"] for it in items], [bm25.paper_text(it) for it in items]).scores(args.query) + cand = sorted(set(np.argsort(-cos)[:20]) | {i for i in np.argsort(-bm)[:20] if bm[i] > 0}) + scores = rerank.score(p.description, [embed.paper_text(items[i]) for i in cand]) + for s, i in sorted(zip(scores, cand), reverse=True)[:args.n]: + it = items[i] + print(f"{s:.2f} {it['date']} {it['source']:9s} {it['title'][:90]}\n {it['link']}") + + +def cmd_init(conn, args) -> None: + """Draft profiles, an interest profile, and vocabulary candidates from seed papers.""" + import json + from pathlib import Path + from art.toolbox import draft, seeds as sd + found, owner = [], args.name + if args.orcid: + s, name = sd.from_orcid(args.orcid) + print(f" ORCID {args.orcid}: {len(s)} works with a DOI or arXiv ID" + (f" ({name})" if name else "")) + found += s + owner = owner or name + for path, reader, label in ((args.ids, sd.from_id_list, "identifier list"), + (args.bib, sd.from_bibtex, "BibTeX"), (args.csl, sd.from_csl_json, "CSL JSON")): + if path: + s = reader(Path(path).read_text()) + print(f" {label} {path}: {len(s)} papers with a DOI or arXiv ID") + found += s + merged: dict[str, dict] = {} + for s in found: # the same paper from two sources: keep the first, fill its gaps + have = merged.setdefault(s["ref"], s) + for k in ("title", "abstract", "authors", "year"): + have[k] = have[k] or s[k] + if len(merged) < 3: + print("need at least 3 seed papers with a DOI or arXiv ID; see `bingham init --help`") + return + + cache_path = env.USER_DIR / "seed" / "seeds.json" + cache = {} if args.refresh or not cache_path.exists() else \ + {s["ref"]: s for s in json.loads(cache_path.read_text())} + for ref, s in merged.items(): + for k in ("title", "abstract", "authors", "year"): + s[k] = s[k] or cache.get(ref, {}).get(k) + todo = [s for s in merged.values() if not (s["abstract"] and s["authors"] and s["title"])] + print(f" resolving {len(todo)} papers from Crossref and arXiv ...") + _, missing = sd.resolve(todo) + seeds = list(merged.values()) + cache_path.parent.mkdir(parents=True, exist_ok=True) + cache_path.write_text(json.dumps(seeds, indent=1)) + n_abs = sum(bool(s["abstract"]) for s in seeds) + print(f" {len(seeds)} seed papers, {n_abs} with abstracts" + + (f"; not found: {', '.join(missing[:5])}{' ...' if len(missing) > 5 else ''}" if missing else "")) + + run_id = store.start_run(conn, "init", vars(args), env.device()) + d = draft.draft(conn, seeds, args.topics, owner, use_llm=not args.no_llm, run_id=run_id) + paths = draft.write(d, env.USER_DIR) + store.finish_run(conn, run_id, {}, {"seeds": len(seeds), "topics": len(d["profiles"])}) + print(f"\n{len(d['profiles'])} draft profiles ({', '.join(p['name'] for p in d['profiles'])})") + print(f"{len(d['tracked'])} tracked-author suggestions, {len(d['new_terms'])} vocabulary candidates") + for p in paths: + print(f" wrote {p}") + print("\nNext: edit user/profiles.draft.yaml, check it with\n" + " uv run bingham profile-test --file user/profiles.draft.yaml\n" + "then save it as user/profiles.yaml (and interest_profile.draft.md as interest_profile.md).") + + +def cmd_profile_test(conn, args) -> None: + """Show what each profile would pick up from the stored papers, to check it and set thresholds.""" + from pathlib import Path + from art.profile import profiles as prof + from art.screen import bm25, embed, screen + data = prof.load(Path(args.file) if args.file else None) + items = screen.load_items(conn) + if not items: + print("no papers in the store; run `bingham fetch` first") + return + cfg = env.config()["screen"] + vecs = embed.embed(conn, {it["id"]: embed.paper_text(it) for it in items}) + index = bm25.Index([it["id"] for it in items], [bm25.paper_text(it) for it in items]) + print(f"{len(items)} stored papers; re-ranker scores are 0-1\n") + for p in data.profiles: + if args.profile and p.id != args.profile: + continue + rows = [r for r in screen.score_batch(conn, p, items, vecs, index, data.tracked_authors, cfg) + if r["rerank"] is not None] + rows.sort(key=lambda r: -r["rerank"]) + th = cfg["thresholds"].get(p.id, cfg["thresholds"]["default"]) + counts = " ".join(f">={t}: {sum(r['rerank'] >= t for r in rows)}" for t in (0.1, 0.2, 0.3, 0.5)) + print(f"== {p.id}: {counts} (threshold {th})") + for r in rows[:args.n]: + it = r["item"] + print(f" {r['rerank']:.2f} {it['date']} {it['source']:9s} {it['title'][:88]}") + near = [r for r in rows if th * 0.5 <= r["rerank"] < th][:3] + if near: + print(" just below threshold:") + for r in near: + print(f" {r['rerank']:.2f} {r['item']['title'][:96]}") + print() + + +def cmd_download_models(conn, args) -> None: + from huggingface_hub import snapshot_download + cfg = env.config()["screen"] + for name in (cfg["embed_model"], cfg["rerank_model"]): + path = snapshot_download(name, allow_patterns=["*.json", "*.safetensors", "*.txt", + "*.model", "1_Pooling/*"]) + print(f"{name}: {path}") + + +def main() -> None: + ap = argparse.ArgumentParser(prog="bingham", description="Art literature screener") + sub = ap.add_subparsers(dest="command", required=True) + f = sub.add_parser("fetch", help="fetch new papers into the store") + f.add_argument("--source", choices=["arxiv", "jor", "all"], default="all") + f.add_argument("--since", metavar="DATE", help="backfill from DATE (YYYY-MM-DD)") + f.add_argument("--refresh", action="store_true", help="ignore the raw-fetch cache TTL") + sub.add_parser("screen", help="embed, score, and re-rank unscreened papers") + d = sub.add_parser("digest", help="write the Markdown digest") + d.add_argument("--since", metavar="DATE", help="include all papers dated DATE or later") + d.add_argument("--no-llm", action="store_true", help="skip the LLM passes") + t = sub.add_parser("topic", help="rank stored papers against a free-text query") + t.add_argument("query") + t.add_argument("-n", type=int, default=15) + dl = sub.add_parser("daily", help="fetch + screen + digest") + dl.add_argument("--no-llm", action="store_true", help="skip the LLM passes") + sub.add_parser("download-models", help="download HuggingFace models into ./models (needs network)") + i = sub.add_parser("init", help="draft profiles from your papers (ORCID, identifier list, BibTeX, CSL JSON)") + i.add_argument("--orcid", help="ORCID iD, e.g. 0000-0002-1825-0097") + i.add_argument("--ids", metavar="FILE", help="text file, one DOI or arXiv ID per line") + i.add_argument("--bib", metavar="FILE", help="BibTeX export (e.g. from Zotero)") + i.add_argument("--csl", metavar="FILE", help="CSL JSON export (Zotero: Export Collection > CSL JSON)") + i.add_argument("--topics", type=int, default=5, help="number of draft profiles (default 5)") + i.add_argument("--name", help="your name as it appears on papers, to leave yourself out of tracked authors") + i.add_argument("--no-llm", action="store_true", help="skip LLM-drafted descriptions") + i.add_argument("--refresh", action="store_true", help="re-fetch seed abstracts instead of using user/seed/") + pt = sub.add_parser("profile-test", help="show what each profile would pick up from stored papers") + pt.add_argument("--file", help="profiles file to test (default user/profiles.yaml)") + pt.add_argument("--profile", help="test one profile id") + pt.add_argument("-n", type=int, default=8) + args = ap.parse_args() + + conn = store.connect() + {"fetch": cmd_fetch, "screen": cmd_screen, "digest": cmd_digest, "topic": cmd_topic, + "daily": cmd_daily, "download-models": cmd_download_models, "init": cmd_init, + "profile-test": cmd_profile_test}[args.command](conn, args) + + +if __name__ == "__main__": + main() diff --git a/art/cluster.py b/art/cluster.py new file mode 100644 index 0000000..ac6c762 --- /dev/null +++ b/art/cluster.py @@ -0,0 +1,38 @@ +"""Average-linkage agglomerative clustering of a cosine-similarity matrix. + +Used to group similar topic labels in the digest's field pulse and to group an +adopter's seed papers into draft profiles in the toolbox. Deterministic, numpy +only, and small enough to read: repeatedly merge the two clusters with the +highest mean pairwise similarity, updating similarities with the +Lance-Williams rule for average linkage. +""" + +import numpy as np + + +def cluster(sims: np.ndarray, threshold: float = -np.inf, n_clusters: int = 1) -> list[list[int]]: + """Merge until the best pair is below `threshold` or `n_clusters` remain. + + Returns clusters as sorted index lists, in order of their first member. + """ + n = len(sims) + members = [[i] for i in range(n)] + S = sims.astype(float).copy() + np.fill_diagonal(S, -np.inf) + alive = np.ones(n, dtype=bool) + while alive.sum() > max(n_clusters, 1): + masked = np.where(np.outer(alive, alive), S, -np.inf) + a, b = divmod(int(np.argmax(masked)), n) + if masked[a, b] < threshold: + break + na, nb = len(members[a]), len(members[b]) + S[a, :] = S[:, a] = (na * S[a, :] + nb * S[b, :]) / (na + nb) + S[a, a] = -np.inf + members[a] += members[b] + alive[b] = False + return [sorted(members[i]) for i in range(n) if alive[i]] + + +def medoids(sims: np.ndarray, idx: list[int], k: int = 1) -> list[int]: + """The k members of a cluster with the highest mean similarity to the rest of it.""" + return sorted(idx, key=lambda i: -sims[i, idx].mean())[:k] diff --git a/art/digest.py b/art/digest.py new file mode 100644 index 0000000..5a4dea6 --- /dev/null +++ b/art/digest.py @@ -0,0 +1,338 @@ +"""`bingham digest`: select the day's papers, run the LLM passes, write Markdown. + +Window: by default, items with origin 'daily' fetched since the previous +digest (or in the last 24 hours if there is none). With --since DATE, every +screenable item dated on or after DATE, backfills included. + +Entries are grouped by profile and ordered by re-ranker score. A paper that +clears several profiles is listed under its best one, with the others noted. +Highlighted papers get a summary, a label, and a match reason from the LLM; +papers by tracked authors below threshold get a summary and a label; every +J. Rheol. paper in the window gets a label for the field-pulse section. If +Ollama is not running, or with --no-llm, the opening sentences of the abstract +stand in for the summary and the screen scores for the match reason. +""" + +import json +import re +import time +from collections import defaultdict +from dataclasses import dataclass, field +from datetime import date, datetime, timedelta, timezone + +import numpy as np + +from art import env, llm, store +from art.cluster import cluster +from art.profile import profiles as prof +from art.screen import embed, rerank +from art.summarize import summarize + + +def opening_sentences(text: str, n: int = 2, max_chars: int = 450) -> str: + sentences = re.split(r"(?<=[.!?])\s+(?=[A-Z\\$(])", text.strip()) + out = " ".join(sentences[:n]) + return out if len(out) <= max_chars else out[:max_chars].rsplit(" ", 1)[0] + " ..." + + +def first_author(authors: list[str]) -> str: + if not authors: + return "(authors not listed)" + return authors[0] + (" et al." if len(authors) > 1 else "") + + +# -- selection ---------------------------------------------------------------- + +@dataclass +class Entry: + item: dict + screen: dict # the screens row for the profile it is listed under + also: list[str] = field(default_factory=list) # other profiles it clears + tracked: list[str] = field(default_factory=list) # tracked authors on it + + +@dataclass +class Selection: + window: str + items: dict[str, dict] + by_profile: dict[str, list[Entry]] + tracked_only: list[Entry] + n_screened: int + catchups: list[str] + + @property + def highlighted(self) -> list[Entry]: + return [e for entries in self.by_profile.values() for e in entries] + + @property + def jor(self) -> list[dict]: + return [it for it in self.items.values() if it["source"] == "J. Rheol."] + + +def _window(conn, since: str | None, run_id: int | None) -> tuple[str, str, tuple]: + """(description, SQL condition, params) selecting the digest's items.""" + if since: + return f"items dated {since} or later", f"{store.SCREENABLE} AND date >= ?", (since,) + row = conn.execute("SELECT MAX(started_at) FROM runs WHERE command IN ('digest', 'daily') " + "AND finished_at IS NOT NULL AND id != ?", (run_id or -1,)).fetchone() + start = row[0] or (datetime.now(timezone.utc) - timedelta(days=1)).isoformat(timespec="seconds") + return f"items fetched since {start[:16].replace('T', ' ')} UTC", \ + "origin = 'daily' AND fetched_at >= ?", (start,) + + +def select(conn, since: str | None = None, run_id: int | None = None) -> Selection: + cfg = env.config() + data = prof.load() + names = {p.id: p.name for p in data.profiles} + label, where, params = _window(conn, since, run_id) + if not cfg["arxiv"]["include_replacements"]: + where += " AND COALESCE(announce_type, '') NOT IN ('replace', 'replace-cross')" + items = {r["id"]: store.item_dict(r) for r in + conn.execute(f"SELECT * FROM items WHERE {where}", params)} + + em, rm = embed.model_name(), rerank.model_name() + current = {(p.id, p.version) for p in data.profiles} + screens: dict[str, list[dict]] = {} + for r in conn.execute("SELECT * FROM screens WHERE embed_model = ? AND rerank_model = ?", (em, rm)): + if r["item_id"] in items and (r["profile_id"], r["profile_version"]) in current: + screens.setdefault(r["item_id"], []).append(dict(r)) + + # Highlight with the current thresholds, so a threshold change needs no re-screen. + th = cfg["screen"]["thresholds"] + for ss in screens.values(): + for s in ss: + s["highlighted"] = s["rerank"] is not None and s["rerank"] >= th.get(s["profile_id"], th["default"]) + + # Assign each highlighted paper to its best profile. + by_profile: dict[str, list[Entry]] = {p.id: [] for p in data.profiles} + tracked_only = [] + for iid, ss in screens.items(): + hits = sorted((s for s in ss if s["highlighted"]), key=lambda s: -s["rerank"]) + tracked = prof.tracked_matches(items[iid]["authors"], data.tracked_authors) + if hits: + by_profile[hits[0]["profile_id"]].append( + Entry(items[iid], hits[0], [names[s["profile_id"]] for s in hits[1:]], tracked)) + elif tracked: + best = max((s for s in ss if s["rerank"] is not None), key=lambda s: s["rerank"]) + tracked_only.append(Entry(items[iid], best, [], tracked)) + for entries in by_profile.values(): + entries.sort(key=lambda e: -e.screen["rerank"]) + tracked_only.sort(key=lambda e: -e.screen["rerank"]) + + catchups = [] if since else [r[0] for r in conn.execute( + "SELECT detail FROM fetches WHERE source = 'arxiv:catchup' AND ok = 1 AND fetched_at >= ?", + params)] + return Selection(label, items, by_profile, tracked_only, len(screens), catchups) + + +# -- LLM passes ----------------------------------------------------------------- + +def run_llm(conn, sel: Selection, run_id: int | None = None) -> dict: + """Summaries, labels, and match reasons for the selection. Returns stats.""" + stats = {"available": llm.available(), "calls": 0, "failed": 0, "seconds": 0.0} + if not stats["available"]: + print(" Ollama not reachable; writing the digest from screen results only") + return stats + profiles = {p.id: p for p in prof.load().profiles} + jobs = [] + for e in sel.highlighted: + jobs += [("summarize", e.item, None), ("label", e.item, None), + ("explain", e.item, profiles[e.screen["profile_id"]])] + for e in sel.tracked_only: + jobs += [("summarize", e.item, None), ("label", e.item, None)] + labeled = {e.item["id"] for e in sel.highlighted + sel.tracked_only} + jobs += [("label", it, None) for it in sel.jor if it["id"] not in labeled] + + t0 = time.perf_counter() + for task, item, profile in jobs: + if task == "summarize": + out, called = summarize.summarize_paper(conn, item, run_id) + elif task == "label": + out, called = summarize.assign_topic(conn, item, run_id) + else: + out, called = summarize.explain_match(conn, item, profile, run_id) + stats["calls"] += called + stats["failed"] += called and out is None + stats["seconds"] = round(time.perf_counter() - t0, 1) + print(f" LLM: {stats['calls']} new generations ({stats['failed']} failed) " + f"in {stats['seconds']} s; {len(jobs) - stats['calls']} already stored") + return stats + + +# -- rendering ----------------------------------------------------------------- + +def _generated(conn, task: str, item_id: str, profile=None) -> dict | None: + return llm.lookup(conn, task, item_id, profile) + + +def _entry(conn, e: Entry, profiles: dict) -> list[str]: + it, s = e.item, e.screen + summary = _generated(conn, "summarize", it["id"]) + label = _generated(conn, "label", it["id"]) + explain = _generated(conn, "explain", it["id"], profiles[s["profile_id"]]) \ + if s["highlighted"] else None + lines = [f"**[{it['title']}]({it['link']})** ", + f"{first_author(it['authors'])} · {it['date']} · {it['source']}" + + (f" ({it['categories'][0]}{', cross-list' if it['announce_type'] == 'cross' else ''})" + if it["categories"] else ""), + ""] + if summary and summary["text"]: + lines.append(f"> {summary['text']}") + elif it["abstract"]: + lines.append(f"> {opening_sentences(it['abstract'])} *(abstract opening)*") + else: + lines.append("> (no abstract)") + lines.append("") + if explain: + lines.append(f"*Why it matches: {explain['parsed']['reason']}* ") + meta = [] + if label and label["text"]: + meta.append(f"Topic: {label['text']}") + best = s["cosine_best"] if s["cosine_best"] == "description" else f"exemplar {s['cosine_best']}" + meta.append(f"re-rank {s['rerank']:.2f}") + if explain: + meta.append(f"LLM relevance {explain['parsed']['relevance']}/3") + meta.append(f"cosine {s['cosine']:.2f} ({best}), BM25 {s['bm25']:.1f}, via {s['nominated_by']}") + lines.append("*" + "; ".join(meta) + ".* ") + if e.also: + lines.append(f"*Also matches: {', '.join(e.also)}.* ") + if e.tracked: + lines.append(f"*Tracked author: {', '.join(e.tracked)}.* ") + lines.append("") + return lines + + +def _field_pulse(conn, sel: Selection) -> list[str]: + """J. Rheol. papers and highlighted papers, grouped by similar LLM topic labels. + + Free-text labels rarely repeat exactly (22 papers gave 19 distinct labels + in the first week), so labels are embedded with the screening model and + clustered; each group is headed by its most central label. + """ + papers = {it["id"]: it for it in sel.jor} + papers.update({e.item["id"]: e.item for e in sel.highlighted}) + by_label: dict[str, list[dict]] = defaultdict(list) + shown: dict[str, str] = {} # lower-cased label -> label as the model first wrote it + for it in papers.values(): + label = _generated(conn, "label", it["id"]) + if label and label["text"]: + key = label["text"].lower() + by_label[key].append(it) + shown.setdefault(key, label["text"]) + if not by_label: + return [] + keys = sorted(by_label) + vecs = embed.embed(conn, {f"label:{k}": k for k in keys}) + V = np.stack([vecs[f"label:{k}"] for k in keys]) + sims = V @ V.T + threshold = env.config()["digest"]["label_cluster_threshold"] + groups = [] + for idx in cluster(sims, threshold): + head = max(idx, key=lambda i: sims[i, idx].mean()) + items = [it for i in idx for it in by_label[keys[i]]] + groups.append((shown[keys[head]], items, [shown[keys[i]] for i in idx if i != head])) + groups.sort(key=lambda g: (-len(g[1]), g[0].lower())) + + lines = ["## Field pulse: by topic", "", + f"*J. Rheol. papers in the window and the highlighted papers above " + f"({len(papers)} papers), grouped by similar LLM topic labels.*", ""] + for head, items, others in groups: + lines.append(f"**{head[0].upper() + head[1:]}** ({len(items)})" + + (f" \n*also labeled: {'; '.join(others)}*" if others else "") + " ") + for it in sorted(items, key=lambda x: x["date"] or "", reverse=True): + lines.append(f"- [{it['title']}]({it['link']}), {first_author(it['authors'])}, {it['source']}") + lines.append("") + return lines + + +def render(conn, sel: Selection, llm_stats: dict | None = None, + stages: dict | None = None) -> tuple[str, dict]: + cfg = env.config() + data = prof.load() + profiles = {p.id: p for p in data.profiles} + n_hi = len(sel.highlighted) + lines = [f"# bingham digest, {date.today().isoformat()}", "", + f"{len(sel.items)} papers in window ({sel.window}); {n_hi} highlighted.", ""] + for p in data.profiles: + entries = sel.by_profile[p.id] + if entries: + lines += [f"## {p.name} ({len(entries)})", ""] + for e in entries: + lines += _entry(conn, e, profiles) + if sel.tracked_only: + lines += ["## Tracked authors, below threshold", ""] + for e in sel.tracked_only: + lines += [f"*Best profile: {profiles[e.screen['profile_id']].name}.*"] + _entry(conn, e, profiles) + if n_hi == 0 and not sel.tracked_only: + lines += ["No papers cleared a profile threshold in this window.", ""] + lines += _field_pulse(conn, sel) + + stats = {"in_window": len(sel.items), "screened": sel.n_screened, "highlighted": n_hi, + "tracked_below_threshold": len(sel.tracked_only), "arxiv_catchup": sel.catchups, + "llm": llm_stats or {}} + lines += _footer(conn, stats, cfg, stages) + return "\n".join(lines), stats + + +def _footer(conn, stats: dict, cfg: dict, stages: dict | None = None) -> list[str]: + """Counts, models, host, and timing. `stages` are this daily run's times so far; + without them, the latest finished fetch, screen, and daily runs are reported.""" + if stages is not None: + timing = [f"this daily run on {env.host()}, {env.device()}: " + + ", ".join(f"{k} {v:.1f} s" for k, v in stages.items())] + return _footer_lines(conn, stats, cfg, timing) + times = [] + for cmd in ("fetch", "screen", "daily"): + r = conn.execute("SELECT * FROM runs WHERE command = ? AND finished_at IS NOT NULL " + "ORDER BY id DESC LIMIT 1", (cmd,)).fetchone() + if r: + run_stages = json.loads(r["stages"] or "{}") + times.append(f"{cmd} run {r['id']} on {r['host']}, {r['device'] or 'no device'}: " + + ", ".join(f"{k} {v:.1f} s" for k, v in run_stages.items())) + return _footer_lines(conn, stats, cfg, times) + + +def _llm_line(conn, stats: dict) -> str: + s = stats["llm"] + c = llm.cfg() + if not s: + return "LLM: not run (--no-llm); texts shown are any already stored. " + if not s["available"]: + return "LLM: Ollama not reachable; summaries are abstract openings. " + row = conn.execute("SELECT model_digest FROM llm_calls WHERE model = ? AND model_digest != '' " + "ORDER BY id DESC LIMIT 1", (llm.model_tag(),)).fetchone() + versions = ", ".join(f"{t} {llm.prompt(t).version}" for t in ("summarize", "label", "explain")) + return (f"LLM: {llm.model_tag()} (weights {row[0][:12] if row else 'unknown'}), num_ctx {c['num_ctx']}, " + f"think {'on' if c['think'] else 'off'}; prompts {versions}; " + f"{s['calls']} new generations, {s['failed']} failed, {s['seconds']} s. ") + + +def _footer_lines(conn, stats: dict, cfg: dict, times: list[str]) -> list[str]: + th = cfg["screen"]["thresholds"] + return ["---", "", + f"Papers: {stats['in_window']} in window, {stats['screened']} screened, " + f"{stats['highlighted']} highlighted, {stats['tracked_below_threshold']} tracked-author papers below threshold. ", + *[f"arXiv catch-up from OAI-PMH for missed announcements, {span}. " + for span in stats["arxiv_catchup"]], + f"Screen: {cfg['screen']['embed_model']}, {cfg['screen']['rerank_model']}; " + f"thresholds {th} ({'calibrated ' + str(cfg['screen']['calibrated']) if cfg['screen']['calibrated'] else 'provisional'}). ", + _llm_line(conn, stats), + f"Host: {env.host()}. ", + *[f"Timing: {t}. " for t in times], ""] + + +def write(conn, since: str | None = None, run_id: int | None = None, + stages: dict | None = None, use_llm: bool = True) -> tuple[str, dict]: + """Select, run the LLM passes (unless use_llm is False), render, and write the file.""" + sel = select(conn, since, run_id) + t0 = time.perf_counter() + llm_stats = run_llm(conn, sel, run_id) if use_llm else None + if stages is not None: + stages["llm"] = round(time.perf_counter() - t0, 2) + text, stats = render(conn, sel, llm_stats, stages) + out_dir = env.path("digests") + out_dir.mkdir(parents=True, exist_ok=True) + name = date.today().isoformat() + (f"_since-{since}" if since else "") + path = out_dir / f"{name}.md" + path.write_text(text) + return str(path), stats diff --git a/art/env.py b/art/env.py new file mode 100644 index 0000000..6adb47f --- /dev/null +++ b/art/env.py @@ -0,0 +1,95 @@ +"""Runtime environment: offline HuggingFace settings, config, device, host. + +Import this module before anything that imports huggingface_hub, transformers, +or sentence_transformers: huggingface_hub reads HF_HUB_OFFLINE at import time. +(Convention copied from ssearch/query_hybrid.py.) + +Models are downloaded once with network access (`uv run bingham download-models`, +which sets ART_ONLINE=1); every other command runs offline. + +Personal files live in the user directory (art/user/, or ART_USER_DIR), kept +out of the code repository and usually a small private git repository of its +own. user/config.yaml is merged over config.yaml, and data files are looked up +in user/ before data/ (see data_file). +""" + +import os +import platform +import socket +from functools import cache +from pathlib import Path + +import yaml + +from art._paths import ROOT + + +USER_DIR = Path(os.environ.get("ART_USER_DIR", ROOT / "user")) + + +def _merge(base: dict, over: dict) -> dict: + """Recursive dict merge; values in `over` win, lists are replaced, not appended.""" + out = dict(base) + for key, value in over.items(): + out[key] = _merge(out[key], value) if isinstance(value, dict) and isinstance(out.get(key), dict) else value + return out + + +@cache +def config() -> dict: + """config.yaml, with user/config.yaml merged over it if present.""" + with open(ROOT / "config.yaml") as f: + cfg = yaml.safe_load(f) + user_cfg = USER_DIR / "config.yaml" + if user_cfg.exists(): + cfg = _merge(cfg, yaml.safe_load(user_cfg.read_text()) or {}) + return cfg + + +_warned: set[str] = set() + + +def data_file(name: str) -> Path: + """user/, else data/, else data/examples/ (with a one-time notice).""" + for candidate in (USER_DIR / name, ROOT / "data" / name): + if candidate.exists(): + return candidate + example = ROOT / "data" / "examples" / name + if example.exists(): + if name not in _warned: + _warned.add(name) + print(f" note: no {USER_DIR.name}/{name}; using the example in data/examples/ " + f"(see `bingham init`)") + return example + raise FileNotFoundError(f"{name} not found in {USER_DIR}, data/, or data/examples/") + + +def contact_email() -> str | None: + """Contact address sent to arXiv and Crossref (Crossref's "polite pool"); set in user/config.yaml.""" + return (config().get("contact") or {}).get("email") or None + + +def path(key: str): + """A path from config.yaml `paths`, resolved against ROOT.""" + return ROOT / config()["paths"][key] + + +os.environ["TOKENIZERS_PARALLELISM"] = "false" +os.environ["TRANSFORMERS_VERBOSITY"] = "error" # token counts over 512 are reported by embed.py +os.environ["HF_HUB_CACHE"] = str(path("models")) +os.environ["HF_HUB_OFFLINE"] = "0" if os.environ.get("ART_ONLINE") == "1" else "1" + + +@cache +def device() -> str: + """cuda, then mps, then cpu.""" + import torch + if torch.cuda.is_available(): + return "cuda" + if torch.backends.mps.is_available(): + return "mps" + return "cpu" + + +def host() -> str: + return f"{socket.gethostname()} ({platform.system()} {platform.machine()})" diff --git a/art/feeds/__init__.py b/art/feeds/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/art/feeds/cache.py b/art/feeds/cache.py new file mode 100644 index 0000000..7b9f923 --- /dev/null +++ b/art/feeds/cache.py @@ -0,0 +1,51 @@ +"""Raw-fetch cache for Art feed data. + +Stores the full list of items returned by the last network fetch for each feed +(data/cache/feeds/.json), with a timestamp. `bingham fetch` checks this +before hitting the network; if the data is fresher than feed_cache_ttl_hours +(config.yaml), the fetch is skipped. If a fetch fails, the stale copy is used. + +The v0 item cache (feed_cache.json) is replaced by the SQLite store; see +art/store.py for its one-time import. +""" + +import json +import re +from datetime import datetime, timezone +from pathlib import Path + +from art._paths import ROOT + +RAW_CACHE_DIR = ROOT / "data" / "cache" / "feeds" + + +def _raw_cache_path(feed_name: str) -> Path: + safe = re.sub(r"[^A-Za-z0-9_-]", "_", feed_name) + return RAW_CACHE_DIR / f"{safe}.json" + + +def load_raw(feed_name: str) -> tuple[list[dict], datetime | None]: + """Return (items, fetched_at) from the raw-fetch cache, or ([], None) if missing.""" + path = _raw_cache_path(feed_name) + if not path.exists(): + return [], None + with open(path) as f: + data = json.load(f) + fetched_at = datetime.fromisoformat(data["fetched_at"]) + return data["items"], fetched_at + + +def save_raw(feed_name: str, items: list[dict]) -> None: + RAW_CACHE_DIR.mkdir(parents=True, exist_ok=True) + path = _raw_cache_path(feed_name) + with open(path, "w") as f: + json.dump({"fetched_at": datetime.now(timezone.utc).isoformat(), + "items": items}, f, indent=2) + + +def is_fresh(fetched_at: datetime | None, ttl_hours: float) -> bool: + """True if fetched_at is within ttl_hours of now.""" + if fetched_at is None: + return False + age = datetime.now(timezone.utc) - fetched_at.astimezone(timezone.utc) + return age.total_seconds() < ttl_hours * 3600 diff --git a/art/feeds/collect.py b/art/feeds/collect.py new file mode 100644 index 0000000..9666970 --- /dev/null +++ b/art/feeds/collect.py @@ -0,0 +1,196 @@ +"""Run the fetchers for `bingham fetch` and write the results to the store. + +Daily mode (no --since): arXiv RSS per category, Crossref for J. Rheol. since +the last successful Crossref fetch (less an overlap), then the enabled journal +RSS feeds. Items found this way have origin 'daily'. + +arXiv RSS holds only the latest announcement, so a skipped weekday would be +lost. Each daily fetch records the announcement date it fully covered +(fetches source 'arxiv:covered'); when weekdays are missing since then, they +are filled from OAI-PMH ('arxiv:catchup'). A gap longer than +arxiv.max_catchup_days is still filled, but as origin 'backfill', so it does +not flood the next digest. + +Backfill mode (--since DATE): arXiv OAI-PMH and Crossref from DATE. Items have +origin 'backfill' and stay out of the default digest. The first J. Rheol. fetch +is a backfill from jor.backfill_from. +""" + +from datetime import date, timedelta + +import yaml + +from art import env, store +from art.feeds import cache, fetch + + +def _oai_set(category: str) -> str: + """arXiv category -> OAI-PMH set: cs.CL -> cs, cond-mat.soft -> physics:cond-mat.""" + archive = category.split(".")[0] + if archive in ("cs", "math", "stat", "q-bio", "q-fin", "eess", "econ"): + return archive + return f"physics:{archive}" + + +def _cached_fetch(name: str, fn, ttl_hours: float) -> tuple[list[dict], str]: + """Run fn() unless the raw cache for `name` is fresh; fall back to stale cache on failure.""" + items, fetched_at = cache.load_raw(name) + if cache.is_fresh(fetched_at, ttl_hours): + return items, "cached" + try: + items = fn() + except Exception as exc: + if items: + return items, f"FAILED ({exc}); using stale cache" + raise + cache.save_raw(name, items) + return items, "fetched" + + +def _record(conn, name: str, items: list[dict], origin: str, how: str, stats: dict) -> None: + n_new = store.upsert_items(conn, items, origin) + store.log_fetch(conn, name, len(items), n_new, True, how) + stats["fetched"] += len(items) + stats["new"] += n_new + print(f" {name}: {len(items)} items, {n_new} new ({how})") + + +def _oai_items(oai_set: str, since: str, cfg: dict) -> list[dict]: + """OAI-PMH papers for the configured categories, with cross-lists marked as RSS would.""" + cats = cfg["categories"] + items = fetch.fetch_arxiv_oai(oai_set, since, categories=cats, delay_s=cfg["request_delay_s"]) + for it in items: + if it["categories"] and it["categories"][0] not in cats: + it["announce_type"] = "cross" + if not cfg["include_cross_lists"]: + items = [it for it in items if it["announce_type"] != "cross"] + return items + + +def _weekdays_between(a: date, b: date) -> list[date]: + """Weekdays strictly between a and b (arXiv announces Monday to Friday).""" + days = (a + timedelta(days=i) for i in range(1, (b - a).days)) + return [d for d in days if d.weekday() < 5] + + +def _catch_up(conn, rss_day: date | None, rss_ok: bool, cfg: dict, stats: dict) -> None: + """Fill announcement days missed since the last covered one from OAI-PMH. + + rss_day is the announcement date of today's RSS items (None when the feeds + were empty, as on weekends). The covered date advances only when every RSS + feed and the catch-up succeeded, so a failure is retried on the next run. + """ + last = conn.execute("SELECT MAX(detail) FROM fetches WHERE source = 'arxiv:covered' " + "AND ok = 1").fetchone()[0] + if last is None: # first daily fetch: nothing to catch up from + if rss_day and rss_ok: + store.log_fetch(conn, "arxiv:covered", 0, 0, True, rss_day.isoformat()) + return + last = date.fromisoformat(last) + end = rss_day or date.today() + timedelta(days=1) + missing = _weekdays_between(last, end) + covered = rss_day or (missing[-1] if missing else None) + if missing: + since = (last + timedelta(days=1)).isoformat() + span = f"{missing[0]} to {missing[-1]}" + origin = "daily" + if (missing[-1] - last).days > cfg["max_catchup_days"]: + origin = "backfill" + print(f" arXiv catch-up: {span} exceeds {cfg['max_catchup_days']} days; " + "stored as backfill, not in the default digest") + n_items = n_new = 0 + try: + for oai_set in sorted({_oai_set(c) for c in cfg["categories"]}): + items = _oai_items(oai_set, since, cfg) + before = stats["new"] + _record(conn, f"oai-catchup:{oai_set}", items, origin, f"missed {span}", stats) + n_items, n_new = n_items + len(items), n_new + stats["new"] - before + except Exception as exc: + store.log_fetch(conn, "arxiv:catchup", n_items, n_new, False, f"{span}: {exc}") + print(f" arXiv catch-up FAILED ({exc}); will retry on the next run") + return + store.log_fetch(conn, "arxiv:catchup", n_items, n_new, True, span) + if covered and rss_ok: + store.log_fetch(conn, "arxiv:covered", 0, 0, True, covered.isoformat()) + + +def _fetch_arxiv(conn, since: str | None, ttl: float, stats: dict) -> None: + cfg = env.config()["arxiv"] + cats = cfg["categories"] + if since: + for oai_set in sorted({_oai_set(c) for c in cats}): + _record(conn, f"oai:{oai_set}", _oai_items(oai_set, since, cfg), "backfill", + f"since {since}", stats) + return + rss_days, rss_ok = set(), True + for cat in cats: + name = f"arxiv-rss:{cat}" + try: + items, how = _cached_fetch(name, lambda: fetch.fetch_arxiv_rss(cat), ttl) + except Exception as exc: + store.log_fetch(conn, name, 0, 0, False, str(exc)) + print(f" {name}: FAILED ({exc})") + rss_ok = False + continue + if how.startswith("FAILED"): + rss_ok = False + rss_days |= {it["date"] for it in items} + if not cfg["include_cross_lists"]: + items = [it for it in items if it["announce_type"] != "cross"] + if not items: + how += "; empty (arXiv does not announce on weekends)" if date.today().weekday() >= 5 else "; empty" + _record(conn, name, items, "daily", how, stats) + rss_day = date.fromisoformat(max(rss_days)) if rss_days else None + _catch_up(conn, rss_day, rss_ok, cfg, stats) + + +def _fetch_jor(conn, since: str | None, ttl: float, stats: dict) -> None: + cfg = env.config()["jor"] + name = "crossref:jor" + last = store.last_fetch(conn, name) + if since: + start, origin = since, "backfill" + elif last is None: + start, origin = str(cfg["backfill_from"]), "backfill" + else: + start = (date.fromisoformat(last[:10]) - timedelta(days=cfg["crossref_overlap_days"])).isoformat() + origin = "daily" + try: + items = fetch.fetch_crossref(cfg["issn"][0], start, source="J. Rheol.") + _record(conn, name, items, origin, f"created since {start}", stats) + except Exception as exc: + store.log_fetch(conn, name, 0, 0, False, str(exc)) + print(f" {name}: FAILED ({exc})") + for feed in _feeds(lambda f: f.get("source") == "J. Rheol."): + _fetch_feed(conn, feed, ttl, stats) + + +def _feeds(pred) -> list[dict]: + with open(env.data_file("feeds.yaml")) as f: + feeds = yaml.safe_load(f)["feeds"] + return [f for f in feeds if f.get("enabled") and pred(f)] + + +def _fetch_feed(conn, feed: dict, ttl: float, stats: dict) -> None: + name = f"rss:{feed['name']}" + try: + items, how = _cached_fetch( + name, lambda: fetch.fetch_rss(feed["name"], feed["url"], feed.get("source")), ttl) + except Exception as exc: + store.log_fetch(conn, name, 0, 0, False, str(exc)) + print(f" {name}: FAILED ({exc})") + return + _record(conn, name, items, "daily", how, stats) + + +def run(conn, source: str = "all", since: str | None = None, refresh: bool = False) -> dict: + ttl = 0.0 if refresh else env.config().get("feed_cache_ttl_hours", 6) + stats = {"fetched": 0, "new": 0} + if source in ("arxiv", "all"): + _fetch_arxiv(conn, since, ttl, stats) + if source in ("jor", "all"): + _fetch_jor(conn, since, ttl, stats) + if source == "all" and not since: + for feed in _feeds(lambda f: f.get("source") != "J. Rheol."): + _fetch_feed(conn, feed, ttl, stats) + return stats diff --git a/art/feeds/fetch.py b/art/feeds/fetch.py new file mode 100644 index 0000000..e961b79 --- /dev/null +++ b/art/feeds/fetch.py @@ -0,0 +1,343 @@ +"""Fetch new items from arXiv, journal RSS feeds, and Crossref. + +Each item is a dict with keys: + id -- arXiv ID without version (preprints) or lower-cased DOI (journals) + doi -- journal DOI, or 10.48550/arXiv. for preprints without one + arxiv_id -- arXiv ID without version, or None + version -- arXiv version ("v2"), or None + title -- paper title + authors -- list of author name strings + date -- ISO date string YYYY-MM-DD (arXiv announcement or publication) + abstract -- abstract text (may be empty); LaTeX in arXiv abstracts kept as written + source -- "arXiv", "J. Rheol.", or a feed name + link -- URL to the paper + categories -- list of arXiv categories (empty for journals) + announce_type -- arXiv: new, cross, replace, replace-cross; journals: None + fetched_at -- UTC timestamp of the fetch + +Sources, as measured on 2026-09-27: + arXiv RSS rss.arxiv.org/rss/: one day's announcement, full abstracts, + announce type; empty on Saturday and Sunday (). + arXiv OAI oaipmh.arxiv.org: date-range backfill with full metadata. The + datestamp is the last metadata change, so filter on the v1 date. + arXiv API export.arxiv.org/api/query: returned HTTP 406 to every request. + fetch_arxiv is kept in case it comes back. + AIP RSS J. Rheol. current and open issues: full abstracts and DOIs, no + authors, and only the issue in progress. + Crossref every J. Rheol. article with authors; abstracts for nearly all. + +arXiv answers bursts of requests with HTTP 406 as well, so every request goes +through http_get, which spaces and retries them. +""" + +import html +import json +import re +import time +import urllib.parse +import xml.etree.ElementTree as ET +from datetime import date, datetime, timedelta, timezone +from email.utils import parsedate_to_datetime + +import httpx + +from art import env + +def _user_agent() -> str: + email = env.contact_email() + return f"art-fetcher/0.1 (rheology literature tool{'; ' + email if email else ''})" + + +def _mailto() -> str: + """Crossref "polite pool" parameter, empty if no contact email is configured.""" + email = env.contact_email() + return f"&mailto={urllib.parse.quote(email)}" if email else "" + +_last_request: dict[str, float] = {} + + +def http_get(url: str, delay_s: float = 3.0, tries: int = 4, timeout: float = 120, + headers: dict | None = None) -> bytes: + """GET with a minimum delay between requests to the same host, retrying 406/429/5xx.""" + host = urllib.parse.urlparse(url).netloc + for attempt in range(1, tries + 1): + wait = _last_request.get(host, 0) + delay_s - time.monotonic() + if wait > 0: + time.sleep(wait) + _last_request[host] = time.monotonic() + resp = httpx.get(url, headers={"User-Agent": _user_agent(), **(headers or {})}, timeout=timeout, + follow_redirects=True) + if resp.status_code == 200: + return resp.content + if resp.status_code in (406, 429, 500, 502, 503, 504) and attempt < tries: + time.sleep(10 * attempt) + continue + resp.raise_for_status() + raise httpx.HTTPStatusError(f"HTTP {resp.status_code}", request=resp.request, response=resp) + raise RuntimeError("unreachable") + + +def _now() -> str: + return datetime.now(timezone.utc).isoformat(timespec="seconds") + + +def _clean(text: str) -> str: + """Strip HTML/JATS tags, unescape entities, collapse whitespace.""" + text = re.sub(r"<[^>]+>", " ", text or "") + return re.sub(r"\s+", " ", html.unescape(text)).strip() + + +def _arxiv_item(arxiv_id: str, version: str | None, title: str, authors: list[str], + day: str, abstract: str, categories: list[str], announce_type: str | None, + doi: str | None = None) -> dict: + return { + "id": arxiv_id, + "doi": doi.lower() if doi else f"10.48550/arXiv.{arxiv_id}", + "arxiv_id": arxiv_id, + "version": version, + "title": re.sub(r"\s+", " ", title).strip(), + "authors": authors, + "date": day, + "abstract": re.sub(r"\s+", " ", abstract).strip(), + "source": "arXiv", + "link": f"https://arxiv.org/abs/{arxiv_id}", + "categories": categories, + "announce_type": announce_type, + "fetched_at": _now(), + } + + +# -- arXiv RSS (daily) --------------------------------------------------------- + +_RSS_DESC = re.compile(r"arXiv:(\S+?)(v\d+)?\s+Announce Type:\s*(\S+)\s*Abstract:\s*(.*)", re.S) + + +def fetch_arxiv_rss(category: str) -> list[dict]: + """Return the day's announcement for one arXiv category from rss.arxiv.org. + + The per-item description reads "arXiv:ID vN Announce Type: new Abstract: ...". + Items carry no date of their own; the channel pubDate is the announcement date. + """ + ns = {"arxiv": "http://arxiv.org/schemas/atom", + "dc": "http://purl.org/dc/elements/1.1/"} + root = ET.fromstring(http_get(f"https://rss.arxiv.org/rss/{category}")) + channel = root.find("channel") + pub = channel.findtext("pubDate") + day = parsedate_to_datetime(pub).date().isoformat() if pub else date.today().isoformat() + items = [] + for it in channel.findall("item"): + m = _RSS_DESC.match(it.findtext("description") or "") + if not m: + continue + arxiv_id, version, announce_type, abstract = m.groups() + creators = it.findtext("dc:creator", namespaces=ns) or "" + items.append(_arxiv_item( + arxiv_id, version, it.findtext("title") or "", + [a.strip() for a in creators.split(",") if a.strip()], + day, abstract, [c.text for c in it.findall("category") if c.text], + it.findtext("arxiv:announce_type", namespaces=ns) or announce_type, + doi=it.findtext("arxiv:DOI", namespaces=ns))) + return items + + +# -- arXiv OAI-PMH (backfill, exemplars) ------------------------------------------ + +# arXivRaw, not the arXiv format: the latter's is not always the v1 +# date (2510.01365 reports its v2 date, 2026-09-21), while arXivRaw lists +# every version with its date. +_OAI = {"o": "http://www.openarchives.org/OAI/2.0/", "a": "http://arxiv.org/OAI/arXivRaw/"} + + +def _split_authors(authors: str) -> list[str]: + """ "A. B, C. D and E. F (Univ)" -> ["A. B", "C. D", "E. F"] """ + authors = re.sub(r"\([^)]*\)", "", re.sub(r"\s+", " ", authors)) + return [a.strip() for a in re.split(r",\s*|\s+and\s+", authors) if a.strip()] + + +def _oai_record(rec: ET.Element, lag_days: int = 7) -> dict | None: + """One arXivRaw record as an item dated by its OAI datestamp (about the announcement). + + announce_type is "new" when v1 was submitted within `lag_days` of the + datestamp, and "replace" for an older paper with a new version or other + metadata change. + """ + meta = rec.find(".//a:arXivRaw", _OAI) + if meta is None: + return None + stamp = rec.findtext("o:header/o:datestamp", namespaces=_OAI) + versions = meta.findall("a:version", _OAI) + v1 = parsedate_to_datetime(versions[0].findtext("a:date", namespaces=_OAI)).date() + fresh = (date.fromisoformat(stamp) - v1).days <= lag_days + return _arxiv_item( + meta.findtext("a:id", namespaces=_OAI), versions[-1].get("version"), + meta.findtext("a:title", namespaces=_OAI) or "", + _split_authors(meta.findtext("a:authors", namespaces=_OAI) or ""), stamp, + meta.findtext("a:abstract", namespaces=_OAI) or "", + (meta.findtext("a:categories", namespaces=_OAI) or "").split(), + "new" if fresh else "replace", + doi=meta.findtext("a:doi", namespaces=_OAI)) + + +def fetch_arxiv_oai(oai_set: str, since: str, until: str | None = None, + categories: list[str] | None = None, delay_s: float = 3.0, + lag_days: int = 7) -> list[dict]: + """Return papers newly announced on or after `since` from an OAI-PMH set (e.g. "physics:cond-mat"). + + OAI selects by metadata datestamp, which also catches old papers with a + new version or other metadata change; those are dropped (see _oai_record). + With `categories`, keep only papers listed in at least one of them. + """ + params = {"verb": "ListRecords", "metadataPrefix": "arXivRaw", "set": oai_set, "from": since} + if until: + params["until"] = until + items = [] + while True: + url = "https://oaipmh.arxiv.org/oai?" + urllib.parse.urlencode(params, safe=":") + root = ET.fromstring(http_get(url, delay_s=delay_s, timeout=300)) + for rec in root.findall(".//o:record", _OAI): + it = _oai_record(rec, lag_days) + if it and it["announce_type"] == "new" and ( + not categories or set(it["categories"]) & set(categories)): + items.append(it) + token = root.findtext(".//o:resumptionToken", namespaces=_OAI) + if not token or not token.strip(): + return items + params = {"verb": "ListRecords", "resumptionToken": token.strip()} + + +def get_arxiv_record(arxiv_id: str) -> dict | None: + """One paper's metadata from OAI-PMH GetRecord (used for exemplars).""" + url = ("https://oaipmh.arxiv.org/oai?verb=GetRecord&metadataPrefix=arXivRaw" + f"&identifier=oai:arXiv.org:{arxiv_id}") + rec = ET.fromstring(http_get(url)).find(".//o:record", _OAI) + return _oai_record(rec) if rec is not None else None + + +# -- arXiv API (kept for backfills; 406 as of 2026-09-27) -------------------------- + +def fetch_arxiv(category: str, max_results: int = 30) -> list[dict]: + """Return recent abstracts from an arXiv category via the Atom API.""" + base_url = "https://export.arxiv.org/api/query" + ns = {"atom": "http://www.w3.org/2005/Atom"} + params = { + "search_query": f"cat:{category}", + "sortBy": "submittedDate", + "sortOrder": "descending", + "max_results": max_results, + } + root = ET.fromstring(http_get(f"{base_url}?{urllib.parse.urlencode(params)}")) + items = [] + for entry in root.findall("atom:entry", ns): + m = re.match(r"(.+?)(v\d+)?$", entry.find("atom:id", ns).text.strip().split("abs/")[-1]) + items.append(_arxiv_item( + m.group(1), m.group(2), + entry.find("atom:title", ns).text, + [a.find("atom:name", ns).text for a in entry.findall("atom:author", ns)], + entry.find("atom:published", ns).text.strip()[:10], + entry.find("atom:summary", ns).text, + [c.get("term") for c in entry.findall("atom:category", ns)], + None)) + return items + + +# -- Journal RSS ------------------------------------------------------------------- + +def fetch_rss(feed_name: str, feed_url: str, source: str | None = None) -> list[dict]: + """Return items from an RSS/Atom feed via feedparser.""" + import feedparser + + feed = feedparser.parse(http_get(feed_url, delay_s=1.0)) + items = [] + for entry in feed.entries: + doi = getattr(entry, "prism_doi", None) or getattr(entry, "dc_identifier", None) + link = getattr(entry, "link", "") + item_id = doi.lower() if doi else link + if not item_id: + continue + + raw_abstract = "" + if hasattr(entry, "summary"): + raw_abstract = entry.summary + elif hasattr(entry, "content") and entry.content: + raw_abstract = entry.content[0].get("value", "") + + day = "" + if hasattr(entry, "published_parsed") and entry.published_parsed: + day = time.strftime("%Y-%m-%d", entry.published_parsed) + + authors = [] + if hasattr(entry, "authors"): + authors = [a.get("name", "") for a in entry.authors if a.get("name")] + elif hasattr(entry, "author"): + authors = [entry.author] + + items.append({ + "id": item_id, + "doi": doi.lower() if doi else None, + "arxiv_id": None, + "version": None, + "title": _clean(getattr(entry, "title", "")), + "authors": authors, + "date": day, + "abstract": _clean(raw_abstract), + "source": source or feed_name, + "link": link, + "categories": [], + "announce_type": None, + "fetched_at": _now(), + }) + return items + + +# -- Crossref ---------------------------------------------------------------------- + +def _crossref_item(w: dict, source: str) -> dict: + parts = (w.get("published-online") or w.get("published") or w.get("issued") or {}).get("date-parts", [[]])[0] + day = "-".join(f"{p:02d}" for p in parts) if len(parts) == 3 else "" + authors = [" ".join(filter(None, [a.get("given"), a.get("family")])) or a.get("name", "") + for a in w.get("author", [])] + abstract = _clean(w.get("abstract", "")) + abstract = re.sub(r"^Abstract\s+", "", abstract) + return { + "id": w["DOI"].lower(), + "doi": w["DOI"].lower(), + "arxiv_id": None, + "version": None, + "title": _clean((w.get("title") or [""])[0]), + "authors": authors, + "date": day, + "abstract": abstract, + "source": source, + "link": f"https://doi.org/{w['DOI']}", + "categories": [], + "announce_type": None, + "fetched_at": _now(), + } + + +def fetch_crossref(issn: str, since: str, source: str) -> list[dict]: + """Return journal articles created in Crossref on or after `since` (YYYY-MM-DD). + + Filters on created date: index date also changes whenever Crossref + re-indexes an old record (1066 J. Rheol. hits for one September). + """ + items, cursor = [], "*" + while True: + params = {"filter": f"from-created-date:{since},type:journal-article", + "rows": 200, "cursor": cursor} + url = f"https://api.crossref.org/journals/{issn}/works?" + urllib.parse.urlencode(params) + _mailto() + msg = json.loads(http_get(url, delay_s=1.0))["message"] + items.extend(_crossref_item(w, source) for w in msg["items"]) + cursor = msg.get("next-cursor") + if not msg["items"] or not cursor: + return items + + +def get_crossref_work(doi: str) -> dict | None: + """One DOI's metadata from Crossref (used for exemplars).""" + try: + data = http_get(f"https://api.crossref.org/works/{urllib.parse.quote(doi)}?{_mailto().lstrip('&')}", + delay_s=1.0, tries=2) + except httpx.HTTPStatusError: + return None + return _crossref_item(json.loads(data)["message"], source="Crossref") diff --git a/art/llm.py b/art/llm.py new file mode 100644 index 0000000..be64dbc --- /dev/null +++ b/art/llm.py @@ -0,0 +1,255 @@ +"""The single Ollama wrapper. Every LLM call in Art goes through generate(). + +Each call sets the model, options, and num_ctx explicitly (Ollama's default +context silently truncates long prompts, the first failure mode in Stan's +taxonomy) and writes one row to llm_calls: host, model tag and digest, +Modelfile hash, prompt name and version, num_ctx, token counts, options, +latency, the raw response, and the outcome. Raw responses are kept even when +they fail validation. + +The model is built from modelfiles/researcher.Modelfile and tagged with the +file's hash (art-researcher:), so editing the Modelfile creates a new +model and the tag alone identifies the Modelfile behind any text. + +Prompts live in prompts/_v.txt; the version recorded is +"v+". +""" + +import hashlib +import json +import os +import re +import subprocess +import time +from dataclasses import dataclass +from functools import cache +from typing import Callable + +import ollama +from pydantic import BaseModel, ValidationError + +from art import env, store +from art._paths import ROOT + +PROMPTS = ROOT / "prompts" + + +def cfg() -> dict: + return env.config()["llm"] + + +def _sha(text: str, n: int = 12) -> str: + return hashlib.sha256(text.encode()).hexdigest()[:n] + + +# -- prompts ----------------------------------------------------------------- + +@dataclass(frozen=True) +class Prompt: + name: str + version: str + template: str + + def render(self, **values: str) -> str: + text = self.template + for key, value in values.items(): + text = text.replace("{" + key + "}", value) + missing = re.findall(r"\{[a-z_]+\}", text) + if missing: + raise KeyError(f"prompt {self.name}: unfilled placeholders {missing}") + return text + + +@cache +def prompt(task: str) -> Prompt: + """The highest-numbered prompts/_v.txt.""" + files = sorted(PROMPTS.glob(f"{task}_v*.txt"), + key=lambda p: int(re.search(r"_v(\d+)\.txt$", p.name).group(1))) + if not files: + raise FileNotFoundError(f"no prompt file for task {task!r} in {PROMPTS}") + path = files[-1] + template = path.read_text() + number = re.search(r"_v(\d+)\.txt$", path.name).group(1) + return Prompt(task, f"v{number}+{_sha(template, 8)}", template) + + +# -- model ------------------------------------------------------------------- + +def modelfile_hash() -> str: + return _sha((ROOT / cfg()["modelfile"]).read_text()) + + +def model_tag() -> str: + return f"{cfg()['model']}:{modelfile_hash()}" + + +def client() -> ollama.Client: + return ollama.Client(host=os.environ.get("OLLAMA_HOST"), timeout=cfg()["timeout_s"]) + + +def available() -> bool: + """True if the Ollama server answers.""" + try: + ollama.Client(host=os.environ.get("OLLAMA_HOST"), timeout=5).list() + return True + except Exception: + return False + + +@cache +def ensure_model() -> tuple[str, str]: + """(tag, digest) of the researcher model, creating it from the Modelfile if missing.""" + tag = model_tag() + + def digest(): + for m in client().list().models: + if m.model == tag: + return m.digest + return None + + if digest() is None: + print(f" creating {tag} from {cfg()['modelfile']}") + subprocess.run(["ollama", "create", tag, "-f", str(ROOT / cfg()["modelfile"])], + check=True, capture_output=True, text=True) + return tag, digest() or "" + + +# -- calls ------------------------------------------------------------------- + +@dataclass +class Result: + text: str | None # raw response, or None on a connection error + parsed: object | None # validated value for structured or parsed calls + outcome: str # ok, invalid_json, schema_fail, parse_fail, error + call_id: int + prompt_version: str + + +def _validate(raw: str, schema: type[BaseModel]) -> tuple[str, dict | None, dict]: + """Classify a structured response. Fenced JSON is accepted but recorded.""" + details: dict = {} + text = raw.strip() + fence = re.fullmatch(r"```(?:json)?\s*(.*?)\s*```", text, re.S) + if fence: + text, details["fenced"] = fence.group(1), True + try: + obj = json.loads(text) + except json.JSONDecodeError as exc: + return "invalid_json", None, {**details, "error": str(exc)} + if not isinstance(obj, dict): + return "schema_fail", None, {**details, "error": f"top level is {type(obj).__name__}"} + fields = set(schema.model_fields) + details.update(missing=sorted(fields - set(obj)), extra=sorted(set(obj) - fields)) + try: + return "ok", schema.model_validate(obj).model_dump(), details + except ValidationError as exc: + details["errors"] = [{"loc": list(e["loc"]), "type": e["type"], "msg": e["msg"]} + for e in exc.errors()] + return "schema_fail", None, details + + +def generate(conn, task: str, values: dict[str, str], *, + schema: type[BaseModel] | None = None, + parse: Callable[[str], tuple[str, object, dict]] | None = None, + item_id: str | None = None, profile=None, run_id: int | None = None, + options: dict | None = None) -> Result: + """Run prompt `task` with `values`, log every attempt, and return the last. + + With `schema` (a pydantic model), the response must be a JSON object that + validates; failures are retried cfg()["retries"] times. With `parse`, a + function raw -> (outcome, value, details) classifies free-text responses. + """ + c = cfg() + tag, digest = ensure_model() + p = prompt(task) + text = p.render(**values) + opts = {"num_ctx": c["num_ctx"], **c["tasks"].get(task, {}), **(options or {})} + fmt = schema.model_json_schema() if schema and c["enforce_schema"] else None + attempts = 1 + (c["retries"] if schema else 0) + result = None + for attempt in range(1, attempts + 1): + t0 = time.perf_counter() + try: + r = client().generate(model=tag, prompt=text, options=opts, format=fmt, + think=c["think"]) + except Exception as exc: + call_id = _log(conn, tag, digest, p, item_id, profile, run_id, opts, fmt, c, + time.perf_counter() - t0, None, None, None, None, "error", + {"error": str(exc)}, attempt, None) + return Result(None, None, "error", call_id, p.version) + latency = time.perf_counter() - t0 + raw = r.response or "" + outcome, value, details = "ok", None, {} + if schema: + outcome, value, details = _validate(raw, schema) + elif parse: + outcome, value, details = parse(raw) + if r.prompt_eval_count and r.prompt_eval_count >= 0.9 * opts["num_ctx"]: + details["near_num_ctx"] = True + call_id = _log(conn, tag, digest, p, item_id, profile, run_id, opts, fmt, c, latency, + r.prompt_eval_count, r.eval_count, raw, r.done_reason, outcome, + details, attempt, getattr(r, "thinking", None)) + result = Result(raw, value, outcome, call_id, p.version) + if outcome == "ok": + break + return result + + +def _log(conn, tag, digest, p, item_id, profile, run_id, opts, fmt, c, latency, + prompt_tokens, eval_tokens, raw, done_reason, outcome, details, attempt, thinking) -> int: + cur = conn.execute( + "INSERT INTO llm_calls (ts, run_id, host, model, modelfile_hash, prompt_name, prompt_version, " + "item_id, profile_id, profile_version, num_ctx, prompt_tokens, eval_tokens, options, format, " + "think, latency_s, raw_response, outcome, validation, attempt, model_digest, done_reason, thinking) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (store.now(), run_id, env.host(), tag, modelfile_hash(), p.name, p.version, item_id, + profile.id if profile else None, profile.version if profile else None, opts["num_ctx"], + prompt_tokens, eval_tokens, json.dumps(opts), json.dumps(fmt) if fmt else None, + int(bool(c["think"])), round(latency, 3), raw, outcome, + json.dumps(details) if details else None, attempt, digest, done_reason, thinking or None)) + conn.commit() + return cur.lastrowid + + +# -- stored generations -------------------------------------------------------- + +def _key(task: str, item_id: str, profile) -> tuple: + return (item_id, task, profile.id if profile else "", profile.version if profile else "", + model_tag(), modelfile_hash(), prompt(task).version) + + +def lookup(conn, task: str, item_id: str, profile=None) -> dict | None: + """The stored generation for the current model, Modelfile, prompt, and profile versions. + + Needs no Ollama: the key is computed from the files on disk. + """ + row = conn.execute( + "SELECT text, parsed FROM generations WHERE item_id = ? AND task = ? AND profile_id = ? " + "AND profile_version = ? AND model = ? AND modelfile_hash = ? AND prompt_version = ?", + _key(task, item_id, profile)).fetchone() + if row is None: + return None + return {"text": row["text"], "parsed": json.loads(row["parsed"]) if row["parsed"] else None} + + +def run_task(conn, task: str, item_id: str, values: dict[str, str], *, profile=None, + run_id: int | None = None, schema: type[BaseModel] | None = None, + clean: Callable[[str], str] = str.strip) -> tuple[dict | None, bool]: + """Stored generation if current, else generate and store. Returns (generation, called). + + Failed generations are not stored, so they are retried on the next run; + their raw responses are in llm_calls. + """ + have = lookup(conn, task, item_id, profile) + if have is not None: + return have, False + r = generate(conn, task, values, schema=schema, item_id=item_id, profile=profile, run_id=run_id) + if r.outcome != "ok": + return None, True + gen = {"text": clean(r.text) if not schema else None, "parsed": r.parsed} + conn.execute("INSERT OR REPLACE INTO generations VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (*_key(task, item_id, profile)[:7], cfg()["num_ctx"], gen["text"], + json.dumps(gen["parsed"]) if gen["parsed"] is not None else None, + r.call_id, store.now())) + conn.commit() + return gen, True diff --git a/art/profile/__init__.py b/art/profile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/art/profile/profiles.py b/art/profile/profiles.py new file mode 100644 index 0000000..e783183 --- /dev/null +++ b/art/profile/profiles.py @@ -0,0 +1,84 @@ +"""Load and validate data/profiles.yaml. + +A profile's version is a short hash of its description, keywords, and +exemplars, so editing a profile changes its version and triggers a re-screen +for that profile only. A free-text query becomes an ad hoc profile with no +LLM call (`adhoc`). +""" + +import hashlib +import json +import re + +import yaml +from pydantic import BaseModel, model_validator + +from art import env + + + +class Exemplar(BaseModel): + doi: str | None = None + arxiv: str | None = None + abstract: str | None = None # pasted when no source has one + + @model_validator(mode="after") + def _one_ref(self): + if bool(self.doi) == bool(self.arxiv): + raise ValueError("an exemplar needs exactly one of doi or arxiv") + return self + + @property + def ref(self) -> str: + return f"doi:{self.doi.lower()}" if self.doi else f"arxiv:{self.arxiv}" + + +class Profile(BaseModel): + id: str + name: str + description: str + keywords: list[str] = [] + exemplars: list[Exemplar] = [] + + @property + def version(self) -> str: + payload = json.dumps(self.model_dump(include={"description", "keywords", "exemplars"}), + sort_keys=True) + return hashlib.sha256(payload.encode()).hexdigest()[:12] + + +class TrackedAuthor(BaseModel): + name: str + match: list[str] + + +class Profiles(BaseModel): + tracked_authors: list[TrackedAuthor] = [] + profiles: list[Profile] + + +def load(path=None) -> Profiles: + """Profiles from `path`, by default user/profiles.yaml (or the example; see env.data_file).""" + path = path or env.data_file("profiles.yaml") + with open(path) as f: + data = Profiles.model_validate(yaml.safe_load(f)) + ids = [p.id for p in data.profiles] + if len(ids) != len(set(ids)): + raise ValueError(f"duplicate profile ids in {path}") + return data + + +def adhoc(query: str) -> Profile: + """A profile from a free-text query: the query is both description and keywords.""" + return Profile(id="adhoc", name=query, description=query, keywords=query.split()) + + +def _norm(name: str) -> str: + """ "Samuel T. Okafor" -> "samuel t okafor" """ + return " ".join(re.sub(r"[\W_]", " ", name.lower()).split()) + + +def tracked_matches(authors: list[str], tracked: list[TrackedAuthor]) -> list[str]: + """Names of tracked authors among a paper's authors (full-name match, case and punctuation insensitive).""" + have = {_norm(a) for a in authors} + return [t.name for t in tracked if any(_norm(m) in have for m in t.match)] diff --git a/art/screen/__init__.py b/art/screen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/art/screen/bm25.py b/art/screen/bm25.py new file mode 100644 index 0000000..20dde31 --- /dev/null +++ b/art/screen/bm25.py @@ -0,0 +1,41 @@ +"""BM25 scores of papers against a profile's keywords and description. + +BM25 is classical lexical scoring with no model. It catches exact technique +abbreviations (SAOS, LAOS, DST), author names, and rare terms that embeddings +blur together (ssearch README). Papers are indexed as title, abstract, and +author names, English-stemmed, over every item in the store, so term +statistics come from the whole collection rather than one day's batch. +""" + +import bm25s +import numpy as np +import Stemmer + +_stemmer = Stemmer.Stemmer("english") + + +def paper_text(item: dict) -> str: + return " ".join([item["title"], item["abstract"], " ".join(item["authors"])]) + + +class Index: + def __init__(self, keys: list[str], texts: list[str]): + self.keys = keys + self.pos = {k: i for i, k in enumerate(keys)} + self.bm25 = bm25s.BM25() + self.bm25.index(bm25s.tokenize(texts, stopwords="en", stemmer=_stemmer, + show_progress=False), show_progress=False) + + def scores(self, query: str) -> np.ndarray: + """BM25 score of every indexed paper for `query`, in index order.""" + tokens = bm25s.tokenize([query], stopwords="en", stemmer=_stemmer, + return_ids=False, show_progress=False)[0] + vocab = self.bm25.vocab_dict + tokens = [t for t in tokens if t in vocab] + if not tokens: + return np.zeros(len(self.keys), dtype=np.float32) + return self.bm25.get_scores(tokens) + + +def profile_query(profile) -> str: + return " ".join(profile.keywords) + " " + profile.description diff --git a/art/screen/embed.py b/art/screen/embed.py new file mode 100644 index 0000000..8adf9ce --- /dev/null +++ b/art/screen/embed.py @@ -0,0 +1,69 @@ +"""Embed texts with bge-large (cached in the store, one vector per text). + +Each paper is embedded once, whole, as "title\\n\\nabstract". bge-large reads +at most 512 tokens; longer texts are truncated by the model and reported here. +Tokens are counted with bge's own tokenizer (ssearch: tiktoken undercounts by +up to a factor of three on unusual text). + +No query instruction is prepended: profile descriptions and exemplar abstracts +are passages like the papers they are compared with, not short queries. +""" + +from art import env # noqa: F401 (sets HF offline variables first) + +import hashlib +from functools import cache + +import numpy as np + +from art import store + + +@cache +def model(): + from sentence_transformers import SentenceTransformer + cfg = env.config()["screen"] + m = SentenceTransformer(cfg["embed_model"], device=env.device()) + m.max_seq_length = cfg["max_embed_tokens"] + return m + + +def model_name() -> str: + return env.config()["screen"]["embed_model"] + + +def paper_text(item: dict) -> str: + return f"{item['title']}\n\n{item['abstract']}".strip() + + +def count_tokens(texts: list[str]) -> list[int]: + tok = model().tokenizer + return [len(ids) for ids in tok(texts, add_special_tokens=True, truncation=False)["input_ids"]] + + +def _hash(text: str) -> str: + return hashlib.sha256(text.encode()).hexdigest()[:16] + + +def embed(conn, texts: dict[str, str], batch_size: int = 32) -> dict[str, np.ndarray]: + """{key: unit vector} for every key, computing only texts not already stored.""" + name = model_name() + have = store.get_vectors(conn, list(texts), name) + out = {k: v for k, (h, v) in have.items() if h == _hash(texts[k])} + todo = [k for k in texts if k not in out] + if todo: + batch = [texts[k] for k in todo] + n_tokens = count_tokens(batch) + vecs = model().encode(batch, batch_size=batch_size, normalize_embeddings=True, + convert_to_numpy=True, show_progress_bar=False) + limit = env.config()["screen"]["max_embed_tokens"] + long = [(k, n) for k, n in zip(todo, n_tokens) if n > limit] + for k, t, n, v in zip(todo, batch, n_tokens, vecs): + store.put_vector(conn, k, name, _hash(t), n, v) + out[k] = v + conn.commit() + print(f" embedded {len(todo)} texts with {name}" + + (f"; {len(long)} over {limit} tokens (truncated): " + + ", ".join(f"{k} ({n})" for k, n in long[:5]) + + (" ..." if len(long) > 5 else "") if long else "")) + return out diff --git a/art/screen/rerank.py b/art/screen/rerank.py new file mode 100644 index 0000000..fe0e252 --- /dev/null +++ b/art/screen/rerank.py @@ -0,0 +1,30 @@ +"""Cross-encoder re-ranking with bge-reranker-v2-m3. + +The re-ranker reads each (profile description, paper) pair jointly and returns +a score in 0-1. It replaced ms-marco-MiniLM in ssearch because that model +scored journal-style text far outside its calibrated range. Highlighting +thresholds apply to this score. +""" + +from art import env # noqa: F401 (sets HF offline variables first) + +from functools import cache + + +@cache +def model(): + from sentence_transformers import CrossEncoder + cfg = env.config()["screen"] + return CrossEncoder(cfg["rerank_model"], device=env.device(), + max_length=cfg["rerank_max_length"]) + + +def model_name() -> str: + return env.config()["screen"]["rerank_model"] + + +def score(query: str, passages: list[str], batch_size: int = 16) -> list[float]: + if not passages: + return [] + return [float(s) for s in model().predict([(query, p) for p in passages], + batch_size=batch_size, show_progress_bar=False)] diff --git a/art/screen/screen.py b/art/screen/screen.py new file mode 100644 index 0000000..c5f022d --- /dev/null +++ b/art/screen/screen.py @@ -0,0 +1,148 @@ +"""`bingham screen`: embed, score against each profile, re-rank, highlight. + +For each profile, the batch is every stored item not yet screened with the +current profile version, embedder, and re-ranker. Each paper in the batch gets + + cosine max similarity to the profile description and its exemplars (bge-large) + bm25 lexical score against the profile keywords and description + +and the union of the top candidates by each (the larger of screen.top_k and +screen.top_frac of the batch), plus any paper by a tracked author, is scored +by the cross-encoder. A paper is highlighted when its re-rank score exceeds +the profile's threshold. Scores are stored for every paper, nominated or not, +so the ablations can be run from the database. +""" + +import math +import time + +import numpy as np + +from art import env, store +from art.feeds import fetch +from art.profile import profiles as prof +from art.screen import bm25, embed, rerank + + +def load_items(conn, where: str = "1", params: tuple = ()) -> list[dict]: + """Stored items with text to screen (see store.SCREENABLE).""" + rows = conn.execute(f"SELECT * FROM items WHERE {store.SCREENABLE} AND {where}", params) + return [store.item_dict(r) for r in rows] + + +def exemplar_texts(conn, profile: prof.Profile) -> dict[str, str]: + """{ref: "title\\n\\nabstract"} for the profile's exemplars, fetching unknown ones once.""" + out = {} + for ex in profile.exemplars: + row = conn.execute("SELECT * FROM exemplars WHERE ref = ?", (ex.ref,)).fetchone() + if ex.abstract: # pasted in profiles.yaml; always wins + title = row["title"] if row else "" + conn.execute("INSERT OR REPLACE INTO exemplars VALUES (?, ?, ?, ?, ?)", + (ex.ref, title, ex.abstract.strip(), "profiles.yaml", store.now())) + elif row is None: + try: + it = fetch.get_crossref_work(ex.doi) if ex.doi else fetch.get_arxiv_record(ex.arxiv) + except Exception as exc: + print(f" exemplar {ex.ref}: fetch failed ({exc}); skipped this run") + continue + if it is None: + print(f" exemplar {ex.ref}: not found; skipped") + continue + conn.execute("INSERT INTO exemplars VALUES (?, ?, ?, ?, ?)", + (ex.ref, it["title"], it["abstract"], + "crossref" if ex.doi else "arxiv-oai", store.now())) + if not it["abstract"]: + print(f" exemplar {ex.ref}: no abstract available; using title only") + conn.commit() + row = conn.execute("SELECT * FROM exemplars WHERE ref = ?", (ex.ref,)).fetchone() + if row: + out[ex.ref] = f"{row['title']}\n\n{row['abstract'] or ''}".strip() + return out + + +def profile_vectors(conn, profile: prof.Profile) -> tuple[list[str], np.ndarray]: + """Labels ("description" or exemplar refs) and unit vectors for one profile.""" + texts = {f"profile:{profile.id}:{profile.version}": profile.description.strip()} + for ref, text in exemplar_texts(conn, profile).items(): + texts[f"exemplar:{ref}"] = text + vecs = embed.embed(conn, texts) + labels = ["description" if k.startswith("profile:") else k.removeprefix("exemplar:") + for k in texts] + return labels, np.stack([vecs[k] for k in texts]) + + +def score_batch(conn, profile, items, vecs, index, tracked, cfg) -> list[dict]: + """Cosine, BM25, nomination, and re-rank for one profile over a batch of items.""" + labels, pvecs = profile_vectors(conn, profile) + sims = np.stack([vecs[it["id"]] for it in items]) @ pvecs.T + cos, best = sims.max(axis=1), sims.argmax(axis=1) + all_bm25 = index.scores(bm25.profile_query(profile)) + bm = np.array([all_bm25[index.pos[it["id"]]] for it in items]) + + k = max(cfg["top_k"], math.ceil(cfg["top_frac"] * len(items))) + nominated: dict[int, list[str]] = {} + for how, s in (("cosine", cos), ("bm25", bm)): + for i in np.argsort(-s)[:k]: + if how == "bm25" and s[i] <= 0: + continue + nominated.setdefault(int(i), []).append(how) + for i, it in enumerate(items): + if prof.tracked_matches(it["authors"], tracked): + nominated.setdefault(i, []).append("author") + + idx = sorted(nominated) + scores = dict(zip(idx, rerank.score(profile.description.strip(), + [embed.paper_text(items[i]) for i in idx]))) + return [{"item": it, "cosine": float(cos[i]), "cosine_best": labels[best[i]], + "bm25": float(bm[i]), "nominated_by": ",".join(nominated[i]) if i in nominated else None, + "rerank": scores.get(i)} for i, it in enumerate(items)] + + +def run(conn, run_id: int) -> tuple[dict, dict]: + """Screen everything not yet screened. Returns (stats, stage seconds).""" + cfg = env.config()["screen"] + thresholds = cfg["thresholds"] + data = prof.load() + em, rm = embed.model_name(), rerank.model_name() + stages = {"embed": 0.0, "screen": 0.0, "rerank": 0.0} + stats = {"items": 0, "embedded": 0, "screened_pairs": 0, "reranked_pairs": 0, "highlighted": 0} + + items = load_items(conn) + if not items: + return stats, stages + t = time.perf_counter() + vecs = embed.embed(conn, {it["id"]: embed.paper_text(it) for it in items}) + stages["embed"] = time.perf_counter() - t + stats["items"] = len(items) + + t = time.perf_counter() + index = bm25.Index([it["id"] for it in items], [bm25.paper_text(it) for it in items]) + stages["screen"] += time.perf_counter() - t + + for p in data.profiles: + done = {r[0] for r in conn.execute( + "SELECT item_id FROM screens WHERE profile_id = ? AND profile_version = ? " + "AND embed_model = ? AND rerank_model = ?", (p.id, p.version, em, rm))} + batch = [it for it in items if it["id"] not in done] + if not batch: + continue + threshold = thresholds.get(p.id, thresholds["default"]) + t = time.perf_counter() + rows = score_batch(conn, p, batch, vecs, index, data.tracked_authors, cfg) + elapsed = time.perf_counter() - t + n_rr = sum(r["rerank"] is not None for r in rows) + n_hi = 0 + for r in rows: + hi = r["rerank"] is not None and r["rerank"] >= threshold + n_hi += hi + conn.execute("INSERT OR REPLACE INTO screens VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (r["item"]["id"], p.id, p.version, em, rm, r["cosine"], r["cosine_best"], + r["bm25"], r["nominated_by"], r["rerank"], threshold, int(hi), run_id)) + conn.commit() + stages["rerank"] += elapsed + stats["screened_pairs"] += len(rows) + stats["reranked_pairs"] += n_rr + stats["highlighted"] += n_hi + print(f" {p.id:24s} batch {len(batch):5d} re-ranked {n_rr:4d} highlighted {n_hi:3d}" + f" (threshold {threshold}, {elapsed:.1f} s)") + return stats, {k: round(v, 2) for k, v in stages.items()} diff --git a/art/store.py b/art/store.py new file mode 100644 index 0000000..6bd3f63 --- /dev/null +++ b/art/store.py @@ -0,0 +1,284 @@ +"""SQLite store for bingham: items, embeddings, screen scores, runs, LLM calls. + +One file (config.yaml paths.db). Embeddings are float32 blobs; a few thousand +abstracts do not need a vector database. Every stage checks here before +recomputing, keyed on the model tag and profile version it depends on. + +Item ids are the arXiv ID without version for preprints and the lower-cased +DOI for journal articles. `origin` is 'daily' for items found by the regular +fetch, 'backfill' for items pulled in by a date-range fetch, and 'legacy' for +entries imported from the v0 feed_cache.json; only 'daily' items reach the +default digest. A legacy entry has a title only until another source fills in +its abstract; from then on it is screened like any other item (SCREENABLE). +""" + +import json +import sqlite3 +from datetime import datetime, timezone + +import numpy as np + +from art import env +from art._paths import ROOT + +SCHEMA = """ +CREATE TABLE IF NOT EXISTS items ( + id TEXT PRIMARY KEY, + doi TEXT, + arxiv_id TEXT, + version TEXT, + title TEXT NOT NULL, + authors TEXT NOT NULL, -- JSON list of "Given Family" + date TEXT, -- announcement or publication date, YYYY-MM-DD + abstract TEXT NOT NULL, + source TEXT NOT NULL, -- e.g. "arXiv", "J. Rheol." + link TEXT, + categories TEXT NOT NULL, -- JSON list + announce_type TEXT, -- arXiv: new, cross, replace, replace-cross + origin TEXT NOT NULL, -- daily, backfill, legacy + fetched_at TEXT NOT NULL, -- first fetch + updated_at TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS items_doi ON items(doi); + +CREATE TABLE IF NOT EXISTS fetches ( + source TEXT NOT NULL, -- feed name, "crossref:jor", "oai:" + fetched_at TEXT NOT NULL, + n_items INTEGER, + n_new INTEGER, + ok INTEGER NOT NULL, + detail TEXT +); + +CREATE TABLE IF NOT EXISTS embeddings ( + key TEXT NOT NULL, -- item id, or "exemplar:", "profile::" + model TEXT NOT NULL, + text_hash TEXT NOT NULL, + n_tokens INTEGER NOT NULL, -- bge tokens before truncation + vec BLOB NOT NULL, -- float32, L2-normalized + PRIMARY KEY (key, model) +); + +CREATE TABLE IF NOT EXISTS exemplars ( + ref TEXT PRIMARY KEY, -- "doi:..." or "arxiv:..." + title TEXT, + abstract TEXT, + origin TEXT, -- crossref, arxiv-oai, profiles.yaml + fetched_at TEXT +); + +CREATE TABLE IF NOT EXISTS screens ( + item_id TEXT NOT NULL, + profile_id TEXT NOT NULL, + profile_version TEXT NOT NULL, + embed_model TEXT NOT NULL, + rerank_model TEXT NOT NULL, + cosine REAL NOT NULL, -- max over description and exemplars + cosine_best TEXT NOT NULL, -- "description" or the exemplar ref + bm25 REAL NOT NULL, + nominated_by TEXT, -- comma list of cosine, bm25, author; NULL if not nominated + rerank REAL, -- NULL if not nominated + threshold REAL, + highlighted INTEGER NOT NULL, + run_id INTEGER NOT NULL, + PRIMARY KEY (item_id, profile_id, profile_version, embed_model, rerank_model) +); + +CREATE TABLE IF NOT EXISTS runs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + command TEXT NOT NULL, + args TEXT, + started_at TEXT NOT NULL, + finished_at TEXT, + host TEXT NOT NULL, + device TEXT, + stages TEXT, -- JSON {stage: seconds} + stats TEXT -- JSON counts +); + +CREATE TABLE IF NOT EXISTS llm_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts TEXT NOT NULL, + run_id INTEGER, + host TEXT NOT NULL, + model TEXT NOT NULL, + modelfile_hash TEXT, + prompt_name TEXT NOT NULL, + prompt_version TEXT NOT NULL, + item_id TEXT, + profile_id TEXT, + profile_version TEXT, + num_ctx INTEGER NOT NULL, + prompt_tokens INTEGER, + eval_tokens INTEGER, + options TEXT, -- JSON + format TEXT, -- JSON schema if structured, else NULL + think INTEGER, + latency_s REAL, + raw_response TEXT, + outcome TEXT NOT NULL, -- ok, invalid_json, schema_fail, error, ... + validation TEXT, -- JSON details + attempt INTEGER NOT NULL DEFAULT 1, + model_digest TEXT, -- Ollama digest of the weights behind the tag + done_reason TEXT, -- stop, length, ... + thinking TEXT -- thinking text when think is on +); + +CREATE TABLE IF NOT EXISTS generations ( + item_id TEXT NOT NULL, + task TEXT NOT NULL, -- summarize, label, explain + profile_id TEXT NOT NULL, -- '' for tasks that do not use a profile + profile_version TEXT NOT NULL, -- '' likewise + model TEXT NOT NULL, + modelfile_hash TEXT NOT NULL, + prompt_version TEXT NOT NULL, + num_ctx INTEGER NOT NULL, + text TEXT, -- NULL if every attempt failed + parsed TEXT, -- JSON for structured tasks + llm_call_id INTEGER NOT NULL, -- the final attempt + created_at TEXT NOT NULL, + PRIMARY KEY (item_id, task, profile_id, profile_version, model, modelfile_hash, prompt_version) +); +""" + +# Columns added after a table was first created: (table, column, type). +MIGRATIONS = [ + ("llm_calls", "model_digest", "TEXT"), + ("llm_calls", "done_reason", "TEXT"), + ("llm_calls", "thinking", "TEXT"), +] + + +# Items with text to screen: everything except legacy entries still lacking an abstract. +SCREENABLE = "NOT (origin = 'legacy' AND abstract = '')" + + +def now() -> str: + return datetime.now(timezone.utc).isoformat(timespec="seconds") + + +def connect() -> sqlite3.Connection: + db_path = env.path("db") + db_path.parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + conn.executescript(SCHEMA) + for table, column, kind in MIGRATIONS: + if column not in {r["name"] for r in conn.execute(f"PRAGMA table_info({table})")}: + conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {kind}") + _import_legacy_cache(conn) + return conn + + +# -- items ------------------------------------------------------------------- + +ITEM_FIELDS = ["doi", "arxiv_id", "version", "title", "authors", "date", + "abstract", "source", "link", "categories", "announce_type"] + + +def upsert_items(conn, items: list[dict], origin: str) -> int: + """Insert new items; fill empty fields of existing ones. Returns the count of new ids. + + Filling only empty fields lets sources complement each other: the AIP feed + has abstracts but no authors, Crossref has both. An arXiv paper announced + as a cross-list in one feed and as new in another is stored once, as new. + """ + n_new = 0 + ts = now() + for it in items: + row = {k: it.get(k) for k in ITEM_FIELDS} + row["authors"] = json.dumps(it.get("authors") or []) + row["categories"] = json.dumps(it.get("categories") or []) + old = conn.execute("SELECT * FROM items WHERE id = ?", (it["id"],)).fetchone() + if old is None: + conn.execute( + f"INSERT INTO items (id, {', '.join(ITEM_FIELDS)}, origin, fetched_at, updated_at) " + f"VALUES (?, {', '.join('?' * len(ITEM_FIELDS))}, ?, ?, ?)", + (it["id"], *row.values(), origin, it.get("fetched_at") or ts, ts)) + n_new += 1 + continue + updates = {k: v for k, v in row.items() + if v not in (None, "", "[]") and old[k] in (None, "", "[]")} + if old["announce_type"] == "cross" and row["announce_type"] == "new": + updates["announce_type"] = "new" + if updates: + sets = ", ".join(f"{k} = ?" for k in updates) + conn.execute(f"UPDATE items SET {sets}, updated_at = ? WHERE id = ?", + (*updates.values(), ts, it["id"])) + conn.commit() + return n_new + + +def item_dict(row: sqlite3.Row) -> dict: + d = dict(row) + d["authors"] = json.loads(d["authors"]) + d["categories"] = json.loads(d["categories"]) + return d + + +def log_fetch(conn, source: str, n_items: int, n_new: int, ok: bool, detail: str = "") -> None: + conn.execute("INSERT INTO fetches VALUES (?, ?, ?, ?, ?, ?)", + (source, now(), n_items, n_new, int(ok), detail)) + conn.commit() + + +def last_fetch(conn, source: str) -> str | None: + row = conn.execute("SELECT MAX(fetched_at) FROM fetches WHERE source = ? AND ok = 1", + (source,)).fetchone() + return row[0] + + +# -- embeddings -------------------------------------------------------------- + +def get_vectors(conn, keys: list[str], model: str) -> dict[str, tuple[str, np.ndarray]]: + """{key: (text_hash, vector)} for the keys already embedded with this model.""" + out = {} + for i in range(0, len(keys), 500): + chunk = keys[i:i + 500] + q = f"SELECT key, text_hash, vec FROM embeddings WHERE model = ? AND key IN ({','.join('?' * len(chunk))})" + for r in conn.execute(q, (model, *chunk)): + out[r["key"]] = (r["text_hash"], np.frombuffer(r["vec"], dtype=np.float32)) + return out + + +def put_vector(conn, key: str, model: str, text_hash: str, n_tokens: int, vec: np.ndarray) -> None: + conn.execute("INSERT OR REPLACE INTO embeddings VALUES (?, ?, ?, ?, ?)", + (key, model, text_hash, n_tokens, vec.astype(np.float32).tobytes())) + + +# -- runs -------------------------------------------------------------------- + +def start_run(conn, command: str, args: dict, device: str | None = None) -> int: + cur = conn.execute("INSERT INTO runs (command, args, started_at, host, device) VALUES (?, ?, ?, ?, ?)", + (command, json.dumps(args, default=str), now(), env.host(), device)) + conn.commit() + return cur.lastrowid + + +def finish_run(conn, run_id: int, stages: dict, stats: dict, device: str | None = None) -> None: + conn.execute("UPDATE runs SET finished_at = ?, stages = ?, stats = ?, device = COALESCE(?, device) WHERE id = ?", + (now(), json.dumps(stages), json.dumps(stats), device, run_id)) + conn.commit() + + +# -- one-time import of the v0 item cache -------------------------------------- + +LEGACY_CACHE = ROOT / "data" / "cache" / "feed_cache.json" + + +def _import_legacy_cache(conn) -> None: + """Import data/cache/feed_cache.json (v0 apps/feed_summary.py) once. + + It holds only id, title, date, and source. The entries are stored with + origin 'legacy' so they are known but never appear in a default digest. + """ + if not LEGACY_CACHE.exists(): + return + marker = "legacy:feed_cache.json" + if conn.execute("SELECT 1 FROM fetches WHERE source = ?", (marker,)).fetchone(): + return + entries = json.loads(LEGACY_CACHE.read_text()) + items = [{"id": k, "title": v.get("title", ""), "date": v.get("date"), + "source": v.get("source", ""), "abstract": ""} for k, v in entries.items()] + n_new = upsert_items(conn, items, origin="legacy") + log_fetch(conn, marker, len(items), n_new, True) diff --git a/art/summarize/__init__.py b/art/summarize/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/art/summarize/summarize.py b/art/summarize/summarize.py new file mode 100644 index 0000000..309d162 --- /dev/null +++ b/art/summarize/summarize.py @@ -0,0 +1,65 @@ +"""Per-paper LLM passes: summary, topic label, and match reason. + +Three single-purpose calls per highlighted paper (Stan pattern: split the +composite task so a small model does not conflate summarizing, classifying, +and judging relevance): + + summarize_paper 2-4 sentence summary of the abstract + assign_topic short topic label (3-6 words), for the field-pulse section + explain_match one sentence on why the paper matches a profile, plus a + 0-3 relevance rating (structured JSON), to compare with + the re-ranker + +All three go through art/llm.py and are stored in the generations table, so a +paper is not sent to the LLM again unless the model, Modelfile, prompt, or +profile has changed. Each works from the title and abstract only; none depends +on another's output. +""" + +import re + +from pydantic import BaseModel, ConfigDict, Field + +from art import llm + + +class Explanation(BaseModel): + """Schema for explain_match. Unknown keys are schema drift, so they fail.""" + model_config = ConfigDict(extra="forbid") + reason: str + relevance: int = Field(ge=0, le=3) + + +def _values(item: dict) -> dict[str, str]: + return {"title": item["title"], "abstract": item["abstract"] or "(no abstract)"} + + +def summarize_paper(conn, item: dict, run_id: int | None = None) -> tuple[str | None, bool]: + """(2-4 sentence summary or None, whether the LLM was called).""" + if not item["abstract"]: + return None, False + gen, called = llm.run_task(conn, "summarize", item["id"], _values(item), run_id=run_id) + return (gen["text"] if gen else None), called + + +def _clean_label(raw: str) -> str: + """First line, without the markup and trailing punctuation small models add.""" + line = next((ln for ln in raw.strip().splitlines() if ln.strip()), "") + line = re.sub(r"^(topic|label)\s*:\s*", "", line.strip(), flags=re.I) + return line.strip(" *\"'`").rstrip(".,;:").strip() + + +def assign_topic(conn, item: dict, run_id: int | None = None) -> tuple[str | None, bool]: + """(short topic label or None, whether the LLM was called).""" + gen, called = llm.run_task(conn, "label", item["id"], _values(item), run_id=run_id, + clean=_clean_label) + return (gen["text"] if gen else None), called + + +def explain_match(conn, item: dict, profile, run_id: int | None = None) -> tuple[dict | None, bool]: + """({"reason": ..., "relevance": 0-3} or None, whether the LLM was called).""" + values = {**_values(item), "profile_name": profile.name, + "profile_description": profile.description.strip()} + gen, called = llm.run_task(conn, "explain", item["id"], values, profile=profile, + run_id=run_id, schema=Explanation) + return (gen["parsed"] if gen else None), called diff --git a/art/toolbox/__init__.py b/art/toolbox/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/art/toolbox/draft.py b/art/toolbox/draft.py new file mode 100644 index 0000000..6588c3d --- /dev/null +++ b/art/toolbox/draft.py @@ -0,0 +1,307 @@ +"""Draft profiles from seed papers: `bingham init`. + +Deterministic code does the structure; the LLM only writes prose: + + 1. embed each seed paper (title and abstract) with the screening embedder + 2. group them into topics by average-linkage clustering (art/cluster.py) + 3. per topic: the most central papers become exemplars; distinctive terms + and defined abbreviations become BM25 keywords + 4. coauthors on several seed papers become tracked-author suggestions + 5. the LLM drafts each topic's name and description from its exemplars + (prompts/profile_draft_v1.txt, through art/llm.py) + +Output goes to user/ as *.draft.* files for the adopter to edit and rename; +existing profiles are never overwritten. +""" + +import json +import math +import re +from collections import Counter +from datetime import date +from pathlib import Path + +import numpy as np +import yaml +from bm25s.stopwords import STOPWORDS_EN_PLUS +from pydantic import BaseModel, ConfigDict + +from art import env, llm +from art.cluster import cluster, medoids +from art.profile import profiles as prof +from art.screen import embed + +# Words common in abstracts of any field; kept out of keyword suggestions. +GENERIC = set(STOPWORDS_EN_PLUS) | set(""" + study studies show shows shown result results paper work approach method methods + using used use based new novel present presents propose proposed investigate + investigated demonstrate demonstrates find found analysis effect effects different + significant significantly however also well high low large small two three one + first second provide provides observed obtained within various including model + models data case cases number role further thus via across here respectively + findings suggest suggests expressed outperforms reveal reveals revealed overall + """.split()) + + +class ProfileDraft(BaseModel): + """Schema for the LLM's draft of one profile.""" + model_config = ConfigDict(extra="forbid") + name: str + description: str + + +def _text(s: dict) -> str: + return f"{s['title']}\n\n{s['abstract']}".strip() + + +def _tokens(text: str) -> list[str]: + return re.findall(r"[a-z][a-z0-9-]*[a-z0-9]", text.lower()) + + +def _terms(text: str) -> set[str]: + """Unigrams and bigrams without a generic word at either end.""" + toks = _tokens(text) + out = {t for t in toks if t not in GENERIC and len(t) > 2} + out |= {f"{a} {b}" for a, b in zip(toks, toks[1:]) if a not in GENERIC and b not in GENERIC} + return out + + +def abbreviations(text: str) -> dict[str, str]: + """{abbreviation: long form} for patterns like "large-amplitude oscillatory shear (LAOS)".""" + out = {} + for m in re.finditer(r"\(([A-Z][A-Za-z0-9]*[A-Z][A-Za-z0-9]*s?)\)", text): + abbr = m.group(1) + need = sum(c.isupper() for c in abbr) # one capital per word part: LAOS = large-amplitude oscillatory shear + words, parts = [], 0 + for w in reversed(re.findall(r"[\w-]+", text[:m.start()])): + words.insert(0, w) + parts += len([p for p in w.split("-") if p]) + if parts >= need: + break + while words and words[0].lower() in STOPWORDS_EN_PLUS: # "of fractal aggregates" -> "fractal aggregates" + words.pop(0) + if words and parts == need: + out.setdefault(abbr, " ".join(words)) + return out + + +def vocabulary_terms(path: Path) -> list[str]: + """Terms listed in vocabulary.md ("- term, term (ABBR), ..."), parentheses removed.""" + terms = [] + for line in path.read_text().splitlines(): + if line.startswith("- "): + for t in line[2:].split(","): + t = re.sub(r"\([^)]*\)", "", t).strip() + if t: + terms.append(t) + return terms + + +def keywords(members: list[str], background: list[str], vocab: list[str], n: int = 15) -> list[str]: + """Terms frequent in a topic's papers and rare in the background, plus vocabulary and abbreviation hits.""" + df_topic = Counter(t for doc in members for t in _terms(doc) if not re.search(r"\d", t)) + df_bg = Counter(t for doc in background for t in _terms(doc)) + # A keyword must recur: at least 2 papers, or a fifth of a large topic. + min_df = max(2, math.ceil(0.2 * len(members))) if len(members) >= 2 else 1 + idf = lambda t: math.log((len(background) + 1) / (df_bg[t] + 1)) + # Distinctive two-word phrases ("concentrated suspensions", "shear thickening"). + # Single words are taken only from the vocabulary and abbreviations below: + # rare-but-generic words ("loads", "detail") score too well on their own. + ranked = sorted((t for t, c in df_topic.items() if c >= min_df and " " in t), + key=lambda t: (-df_topic[t] * idf(t), t)) + chosen = ranked[:n] + # Vocabulary terms in several of the topic's papers, most frequent first (capped, + # since words like "viscosity" appear in nearly every rheology abstract). + lowered = [doc.lower() for doc in members] + vocab_df = Counter({v: sum(bool(re.search(rf"\b{re.escape(v.lower())}\b", d)) for d in lowered) + for v in vocab}) + hits = [v for v, c in vocab_df.most_common(8) if c >= min_df] + abbr_df = Counter(a for doc in members for a in set(abbreviations(doc)) | set(re.findall(r"\b[A-Z]{2,}[a-z]?\b", doc))) + abbrs = [a for a, c in abbr_df.most_common(6) if c >= min_df] + seen, out = set(), [] + for t in abbrs + hits + chosen: + key = t.lower().rstrip("s") # LLM and LLMs, suspension and suspensions + if key not in seen: + seen.add(key) + out.append(t) + return out[:n + 10] + + +def coauthor_suggestions(seeds: list[dict], owner: str | None, min_papers: int = 2, + n: int = 12) -> list[tuple[str, int]]: + """(name, number of seed papers) for frequent coauthors, excluding the adopter.""" + norm = prof._norm + counts, shown = Counter(), {} + for s in seeds: + for a in {norm(a): a for a in s["authors"]}.items(): + counts[a[0]] += 1 + shown.setdefault(a[0], a[1]) + if owner: + counts.pop(norm(owner), None) + elif counts: # no name given: drop whoever is on most of the papers + top, c = counts.most_common(1)[0] + if c >= 0.5 * len(seeds): + counts.pop(top) + return [(shown[k], c) for k, c in counts.most_common(n) if c >= min_papers] + + +def _slug(name: str, limit: int = 40) -> str: + words = re.sub(r"[^a-z0-9]+", " ", name.lower()).split() + out = "" + for w in words: + if out and len(out) + 1 + len(w) > limit: + break + out = f"{out}-{w}" if out else w + return out or "topic" + + +def _draft_prose(conn, papers: list[dict], run_id: int | None) -> ProfileDraft | None: + block = "\n\n".join(f"Title: {p['title']}\nAbstract: {p['abstract'] or '(no abstract)'}" for p in papers) + r = llm.generate(conn, "profile_draft", {"papers": block}, schema=ProfileDraft, run_id=run_id) + return ProfileDraft(**r.parsed) if r.outcome == "ok" else None + + +def draft(conn, seeds: list[dict], topics: int, owner: str | None = None, + use_llm: bool = True, run_id: int | None = None) -> dict: + """Cluster seeds into `topics` draft profiles. Returns the draft as a dict (see write).""" + usable = [s for s in seeds if s["title"]] + vecs = embed.embed(conn, {f"seed:{s['ref']}": _text(s) for s in usable}) + V = np.stack([vecs[f"seed:{s['ref']}"] for s in usable]) + S = V @ V.T + # Over-split, then keep the largest groups of at least two papers: a lone + # outlier (an old paper, a side interest) should not become a profile. + groups = sorted(cluster(S, n_clusters=min(2 * topics, len(usable))), key=len, reverse=True) + keep = [g for g in groups if len(g) >= 2][:topics] or groups[:1] + unassigned = [usable[i] for g in groups if g not in keep for i in g] + groups = keep + + background = [f"{r['title']}\n\n{r['abstract']}" for r in + conn.execute("SELECT title, abstract FROM items WHERE abstract != ''")] + vocab = vocabulary_terms(env.data_file("vocabulary.md")) + llm_ok = use_llm and llm.available() + out_profiles, used_ids = [], set() + for idx in groups: + members = [usable[i] for i in idx] + central = [usable[i] for i in medoids(S, idx, k=min(5, len(idx)))] + bg = background if len(background) >= 200 else \ + [_text(s) for j, s in enumerate(usable) if j not in idx] + prose = _draft_prose(conn, central, run_id) if llm_ok else None + name = prose.name.strip() if prose else f"Topic {len(out_profiles) + 1}" + pid = _slug(name) + while pid in used_ids: + pid += "-2" + used_ids.add(pid) + out_profiles.append({ + "id": pid, + "name": name, + "description": prose.description.strip() if prose else + "TODO: two or three sentences describing this topic. Its central papers: " + + "; ".join(p["title"] for p in central[:3]) + ".", + "keywords": keywords([_text(m) for m in members], bg, vocab), + "exemplars": [(p["ref"], p["title"], p["year"]) for p in central[:3]], + "n_papers": len(members), + "drafted_by": "llm" if prose else "placeholder", + }) + + all_abbrs = Counter(a for s in usable for a in abbreviations(_text(s))) + long_forms = {} + for s in usable: + for a, lf in abbreviations(_text(s)).items(): + long_forms.setdefault(a, lf) + known = {v.lower() for v in vocab} + return { + "profiles": out_profiles, + "tracked": coauthor_suggestions(usable, owner), + "new_terms": [(a, long_forms[a], c) for a, c in all_abbrs.most_common() + if a.lower() not in known and long_forms[a].lower() not in known][:40], + "unassigned": [(s["ref"], s["title"]) for s in unassigned], + "n_seeds": len(seeds), + "n_used": len(usable), + "sources": sorted({s["source"] for s in seeds}), + "llm": llm.model_tag() if llm_ok else None, + } + + +# -- writing --------------------------------------------------------------------- + +def _wrap(text: str, width: int = 76, indent: str = " ") -> str: + words, lines, line = text.split(), [], "" + for w in words: + if line and len(line) + 1 + len(w) > width: + lines.append(line) + line = w + else: + line = f"{line} {w}".strip() + lines.append(line) + return "\n".join(indent + ln for ln in lines) + + +def profiles_yaml(d: dict) -> str: + today = date.today().isoformat() + lines = [f"# Draft profiles from `bingham init`, {today}: {d['n_used']} seed papers " + f"({', '.join(d['sources'])}), {len(d['profiles'])} topics.", + f"# Descriptions drafted by {d['llm']}; everything else is deterministic." if d["llm"] + else "# No LLM was used; replace each TODO description.", + "# Review every field, then save as user/profiles.yaml. Run", + "# `bingham profile-test` to see what each profile would pick up.", + "", + "# Tracked authors: papers by these people always reach the digest.", + "# Suggested from coauthors on several seed papers; uncomment to track.", + "tracked_authors: []"] + for name, c in d["tracked"]: + lines.append(f" # - {{name: {json.dumps(name)}, match: [{json.dumps(name)}]}} # {c} seed papers") + if d.get("unassigned"): + lines += ["", "# Seed papers not in any profile (too few similar papers); add them as", + "# exemplars by hand, or add more papers like them and rerun `bingham init`:"] + lines += [f"# {ref} {title[:70]}" for ref, title in d["unassigned"]] + lines += ["", "profiles:"] + for p in d["profiles"]: + lines += ["", f" # {p['n_papers']} seed papers; description drafted by {p['drafted_by']}", + f" - id: {p['id']}", + f" name: {json.dumps(p['name'])}", + " description: >", + _wrap(p["description"]), + f" keywords: {yaml.safe_dump(p['keywords'], default_flow_style=True, width=10**6, allow_unicode=True).strip()}", + " exemplars:"] + for ref, title, year in p["exemplars"]: + kind, value = ref.split(":", 1) + key = "doi" if kind == "doi" else "arxiv" + value = json.dumps(value) if key == "arxiv" else value + lines.append(f" - {key}: {value} # {title[:70]}{' (' + str(year) + ')' if year else ''}") + text = "\n".join(lines) + "\n" + prof.Profiles.model_validate(yaml.safe_load(text)) # the draft must load as written + return text + + +def interest_profile_md(d: dict) -> str: + lines = ["# Research Interest Profile (draft)", "", + f"Drafted by `bingham init` on {date.today().isoformat()} from {d['n_used']} of your papers.", + "Edit freely and save as user/interest_profile.md. It is read only by the", + "LLM-as-classifier baseline; the screen uses user/profiles.yaml.", "", + "## Priority topics", ""] + lines += [f"- **{p['name']}**: {p['description']}" for p in d["profiles"]] + return "\n".join(lines) + "\n" + + +def vocabulary_md(d: dict) -> str: + lines = ["# Vocabulary candidates (draft)", "", + "Abbreviations defined in your seed papers that are not in data/vocabulary.md,", + "with the long form as written and the number of papers. Add the useful ones", + "to user/vocabulary.md (copy data/vocabulary.md first) and to profile keywords.", ""] + lines += [f"- {lf} ({a}) " for a, lf, c in d["new_terms"]] + if not d["new_terms"]: + lines.append("(none found)") + return "\n".join(lines) + "\n" + + +def write(d: dict, out_dir: Path) -> list[Path]: + out_dir.mkdir(parents=True, exist_ok=True) + files = {"profiles.draft.yaml": profiles_yaml(d), + "interest_profile.draft.md": interest_profile_md(d), + "vocabulary.draft.md": vocabulary_md(d)} + paths = [] + for name, text in files.items(): + path = out_dir / name + path.write_text(text) + paths.append(path) + return paths diff --git a/art/toolbox/seeds.py b/art/toolbox/seeds.py new file mode 100644 index 0000000..8aa7f6f --- /dev/null +++ b/art/toolbox/seeds.py @@ -0,0 +1,187 @@ +"""Seed papers for drafting a profile: from ORCID, an identifier list, or a Zotero export. + +Every reader returns seed dicts with the same keys: + + ref "doi:" or "arxiv:" (the form used for exemplars in profiles.yaml) + title paper title + abstract abstract text, "" until resolved + authors list of "Given Family" + year publication year or None + source where the seed came from (orcid, ids, bibtex, csl) + +resolve() fills in missing abstracts, titles, and authors from Crossref (DOIs) +or arXiv OAI-PMH (arXiv IDs). The LLM never produces identifiers; they come +from the adopter's own records. +""" + +import json +import re + +from art.feeds import fetch + +_ARXIV = re.compile(r"(?:arxiv[:/]|arxiv\.org/(?:abs|pdf)/|10\.48550/arxiv\.)?(\d{4}\.\d{4,5})(?:v\d+)?", re.I) +_DOI = re.compile(r"(10\.\d{4,9}/[^\s\"'<>{}]+)", re.I) + + +def ref_from_text(text: str) -> str | None: + """A doi: or arxiv: ref from a DOI, arXiv ID, or URL (arXiv DOIs become arxiv: refs).""" + text = text.strip().rstrip(".,;") + m = _ARXIV.search(text) + if m and ("arxiv" in text.lower() or re.fullmatch(r"\d{4}\.\d{4,5}(v\d+)?", text)): + return f"arxiv:{m.group(1)}" + m = _DOI.search(text) + return f"doi:{m.group(1).lower()}" if m else None + + +def _seed(ref: str, source: str, title: str = "", abstract: str = "", + authors: list[str] | None = None, year: int | None = None) -> dict: + return {"ref": ref, "title": title, "abstract": abstract, "authors": authors or [], + "year": year, "source": source} + + +# -- readers ------------------------------------------------------------------- + +def from_orcid(orcid: str) -> tuple[list[dict], str | None]: + """(seeds, the researcher's name) from the public ORCID API. + + Only identifiers marked "self" are used; "part-of" identifiers name the + journal (ISSN) or book, not the work. + """ + orcid = orcid.strip().removeprefix("https://orcid.org/") + hdr = {"Accept": "application/json"} + works = json.loads(fetch.http_get(f"https://pub.orcid.org/v3.0/{orcid}/works", delay_s=1, headers=hdr)) + name = None + try: + person = json.loads(fetch.http_get(f"https://pub.orcid.org/v3.0/{orcid}/person", delay_s=1, headers=hdr)) + n = person.get("name") or {} + name = " ".join(filter(None, [(n.get("given-names") or {}).get("value"), + (n.get("family-name") or {}).get("value")])) or None + except Exception: + pass + return parse_orcid_works(works), name + + +def parse_orcid_works(works: dict) -> list[dict]: + seeds = {} + for group in works.get("group", []): + for summary in group.get("work-summary", [])[:1]: + ref = None + for e in (summary.get("external-ids") or {}).get("external-id", []): + if e.get("external-id-relationship") != "self": + continue + kind, value = e.get("external-id-type", "").lower(), e.get("external-id-value", "") + if kind == "arxiv": + ref = ref_from_text(f"arxiv:{value}") + elif kind == "doi": + ref = ref_from_text(value) + if ref: + break + if ref: + title = ((summary.get("title") or {}).get("title") or {}).get("value", "") + year = ((summary.get("publication-date") or {}).get("year") or {}).get("value") + seeds[ref] = _seed(ref, "orcid", title, year=int(year) if year else None) + return list(seeds.values()) + + +def from_id_list(text: str) -> list[dict]: + """One DOI, arXiv ID, or URL per line; blank lines and # comments ignored.""" + seeds = {} + for line in text.splitlines(): + line = line.split("#", 1)[0].strip() + ref = ref_from_text(line) if line else None + if ref: + seeds[ref] = _seed(ref, "ids") + return list(seeds.values()) + + +def _bib_fields(body: str) -> dict[str, str]: + """name = {value} / "value" / bare fields of one BibTeX entry, with nested braces.""" + fields, i = {}, 0 + while True: + m = re.compile(r"\s*,?\s*([A-Za-z_-]+)\s*=\s*").match(body, i) + if not m: + return fields + key, i = m.group(1).lower(), m.end() + if i < len(body) and body[i] in "{\"": + close = "}" if body[i] == "{" else "\"" + depth, j = 0, i + while j < len(body): + if body[j] == "{": + depth += 1 + elif body[j] == "}": + depth -= 1 + if (close == "}" and depth == 0) or (close == "\"" and j > i and body[j] == "\"" and depth == 0): + break + j += 1 + value, i = body[i + 1:j], j + 1 + else: + m2 = re.compile(r"[^,\s}]+").match(body, i) + value, i = (m2.group(0), m2.end()) if m2 else ("", i + 1) + fields[key] = re.sub(r"\s+", " ", value.replace("{", "").replace("}", "")).strip() + + +def from_bibtex(text: str) -> list[dict]: + """Entries with a DOI or an arXiv eprint from a BibTeX file (Zotero's Better BibTeX or plain).""" + seeds = {} + for m in re.finditer(r"@\w+\s*\{\s*[^,]*,(.*?)(?=\n@|\Z)", text, re.S): + f = _bib_fields(m.group(1)) + ref = None + if f.get("eprint") and "arxiv" in (f.get("archiveprefix", "") + f.get("eprinttype", "")).lower(): + ref = ref_from_text(f"arxiv:{f['eprint']}") + ref = ref or (ref_from_text(f["doi"]) if f.get("doi") else None) \ + or (ref_from_text(f["url"]) if f.get("url") else None) + if not ref: + continue + authors = [" ".join(reversed([p.strip() for p in a.split(",", 1)])) if "," in a else a.strip() + for a in f.get("author", "").split(" and ") if a.strip()] + year = re.search(r"\d{4}", f.get("year", "") or f.get("date", "")) + seeds[ref] = _seed(ref, "bibtex", f.get("title", ""), f.get("abstract", ""), authors, + int(year.group()) if year else None) + return list(seeds.values()) + + +def from_csl_json(text: str) -> list[dict]: + """Items with a DOI or arXiv ID from a CSL-JSON export (Zotero: Export Collection > CSL JSON).""" + seeds = {} + for it in json.loads(text): + ref = None + for candidate in (it.get("DOI"), it.get("URL"), it.get("number")): + if candidate: + ref = ref_from_text(str(candidate)) + if ref: + break + if not ref: + continue + authors = [" ".join(filter(None, [a.get("given"), a.get("family")])) or a.get("literal", "") + for a in it.get("author", [])] + parts = (it.get("issued") or {}).get("date-parts") or [[None]] + seeds[ref] = _seed(ref, "csl", it.get("title", ""), it.get("abstract", ""), authors, + parts[0][0] if parts and parts[0] else None) + return list(seeds.values()) + + +# -- resolving ----------------------------------------------------------------- + +def resolve(seeds: list[dict]) -> tuple[list[dict], list[str]]: + """Fill missing abstracts, titles, and authors from Crossref or arXiv. + + Returns (seeds, refs that could not be resolved). Seeds are kept even + without an abstract; the title still carries some signal. + """ + missing = [] + for s in seeds: + if s["abstract"] and s["authors"] and s["title"]: + continue + kind, value = s["ref"].split(":", 1) + try: + it = fetch.get_crossref_work(value) if kind == "doi" else fetch.get_arxiv_record(value) + except Exception: + it = None + if it is None: + missing.append(s["ref"]) + continue + s["title"] = s["title"] or it["title"] + s["abstract"] = s["abstract"] or it["abstract"] + s["authors"] = s["authors"] or it["authors"] + s["year"] = s["year"] or (int(it["date"][:4]) if it.get("date") else None) + return seeds, missing diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..7d2cb00 --- /dev/null +++ b/config.yaml @@ -0,0 +1,68 @@ +# Personal settings go in user/config.yaml, which is merged over this file +# (nested keys override, lists replace). See data/examples/config.yaml. +contact: + email: null # sent to arXiv and Crossref (polite pool); set in user/config.yaml + +llm: + model: art-researcher # built from the Modelfile by art/llm.py + modelfile: modelfiles/researcher.Modelfile + num_ctx: 8192 # set on every call; Ollama's default silently truncates longer prompts + think: false # gemma4 supports thinking; recorded on every call + enforce_schema: true # structured calls pass the JSON schema as Ollama's `format` + retries: 1 # extra attempts for a structured call that fails validation + timeout_s: 300 + # Per-task options, sent explicitly on every call. Low temperature makes a + # small model's summaries formulaic; labels stay low so they group. + tasks: + summarize: {temperature: 0.7} + label: {temperature: 0.2} + explain: {temperature: 0.3} + llm_score: {temperature: 0.0} + profile_draft: {temperature: 0.3} # bingham init; drafts for a person to edit +max_results_per_feed: 30 # arXiv API backfill only +feed_cache_ttl_hours: 6 # skip network fetch if raw feed data is fresher than this + +digest: + # Field-pulse labels whose bge embeddings average at least this cosine + # similarity are grouped. Set 2026-09-28 from the first week: unrelated + # labels median 0.52; same-topic pairs 0.8 and above. + label_cluster_threshold: 0.8 + +paths: # relative to the repository root + db: data/art.sqlite + digests: digests + models: models # HuggingFace cache (HF_HUB_CACHE) + +arxiv: + # Daily run reads rss.arxiv.org/rss/. Every paper is screened + # against every profile in data/profiles.yaml, whatever its category. + # Override the list in user/config.yaml to follow other categories. + categories: + - cond-mat.soft + - physics.flu-dyn + include_cross_lists: true # papers whose primary category is elsewhere + include_replacements: false # new versions of already-announced papers + request_delay_s: 3 # between requests to any arXiv endpoint + max_catchup_days: 14 # longer gaps in daily runs are filled as backfill + +jor: + issn: [0148-6055, 1520-8516] # print, online; Crossref filter + backfill_from: 2025-01-01 # first-run reach for the J. Rheol. stream + crossref_overlap_days: 7 # re-query this far before the last fetch + +screen: + embed_model: BAAI/bge-large-en-v1.5 + rerank_model: BAAI/bge-reranker-v2-m3 + max_embed_tokens: 512 # bge-large limit; longer abstracts are truncated and logged + rerank_max_length: 1024 # tokens for a (profile description, paper) pair + # Candidates per profile from each retriever (cosine, BM25): the larger of + # top_k and top_frac of the batch. The union goes to the re-ranker. + top_k: 30 + top_frac: 0.2 + # Re-ranker score (0-1) above which a paper is highlighted. Broad profile + # descriptions score relevant papers lower, so set per-profile values in + # user/config.yaml (keyed by profile id) once you have looked at the first + # screen. The digest applies the current values; no re-screen is needed. + thresholds: + default: 0.2 + calibrated: null # date of the last calibration diff --git a/data/examples/config.yaml b/data/examples/config.yaml new file mode 100644 index 0000000..5d36790 --- /dev/null +++ b/data/examples/config.yaml @@ -0,0 +1,17 @@ +# Example user/config.yaml. Copy to user/config.yaml and edit; anything here +# overrides the code repository's config.yaml (nested keys override, lists +# replace). + +contact: + email: you@example.edu # sent to arXiv and Crossref (polite pool) + +arxiv: + categories: # rss.arxiv.org category names + - cond-mat.soft + - physics.flu-dyn + +screen: + thresholds: # re-ranker score for a highlight, per profile id + default: 0.2 + # suspension-rheology: 0.3 + calibrated: null diff --git a/data/examples/interest_profile.md b/data/examples/interest_profile.md new file mode 100644 index 0000000..a144912 --- /dev/null +++ b/data/examples/interest_profile.md @@ -0,0 +1,23 @@ +# Research Interest Profile (example) + +Replace this file with your own: copy it to user/interest_profile.md. It is a +prose version of your interests, read only by the LLM-as-classifier baseline +(art/baselines/llm_score.py); the screen itself uses user/profiles.yaml. + +## Research focus + +The structure, dynamics, and rheology of soft matter, with emphasis on +concentrated suspensions and the microscopic origins of bulk rheological +behavior. + +## Priority topics + +- **Suspension rheology**: viscosity, shear thickening and thinning, normal + stresses, and viscoelasticity of concentrated suspensions +- **Microrheology**: passive and active probe techniques and their connection + to bulk rheology + +## Not of primary interest + +- Polymer processing (extrusion, injection molding) +- Computational fluid dynamics without rheological content diff --git a/data/examples/profiles.yaml b/data/examples/profiles.yaml new file mode 100644 index 0000000..0b64adb --- /dev/null +++ b/data/examples/profiles.yaml @@ -0,0 +1,47 @@ +# Example interest profiles for bingham. Copy to user/profiles.yaml and edit, +# or draft one from your own papers with `bingham init` (see README). +# +# Each profile: +# description 2-3 sentences in the field's vocabulary; the query for +# embedding and re-ranking +# keywords technical terms and abbreviations for BM25 (see data/vocabulary.md) +# exemplars papers that typify the topic, as `doi:` or `arxiv:`. A paper's +# embedding score is its maximum similarity over the description +# and the exemplars. Abstracts are fetched once; paste one under +# `abstract:` if no source has it. +# +# A profile's version is a hash of its content, so editing a profile re-screens +# against that profile only. + +# Papers with a tracked author always reach the re-ranker and the digest. +# Match on full names; initials are ambiguous across arXiv. +tracked_authors: [] + # - name: Jane Q. Researcher + # match: [Jane Q. Researcher, Jane Researcher] + +profiles: + - id: suspension-rheology + name: Suspension rheology + description: > + Rheology of suspensions of solid particles in Newtonian and viscoelastic + liquids: viscosity as a function of volume fraction, shear thinning and + shear thickening, normal stress differences, yield stress, and + viscoelasticity of concentrated hard-sphere, charged, attractive, and + non-Brownian suspensions. + keywords: [suspension, non-Brownian, volume fraction, jamming, Peclet number, + hydrodynamic interactions, lubrication force, Stokesian dynamics, + normal stress difference, shear thickening, DST, yield stress, SAOS, LAOS] + exemplars: + - doi: 10.1146/annurev-chembioeng-060713-040221 # Denn & Morris 2014, review + + - id: microrheology + name: Microrheology + description: > + Passive and active microrheology of soft materials: tracer particle + tracking and the mean squared displacement, the generalized + Stokes-Einstein relation, one- and two-point microrheology, optical and + magnetic tweezers, and diffusing wave spectroscopy. + keywords: [microrheology, mean squared displacement, MSD, GSER, + two-point microrheology, particle tracking, optical tweezers, + magnetic tweezers, DWS] + exemplars: [] diff --git a/data/feeds.yaml b/data/feeds.yaml new file mode 100644 index 0000000..f35023d --- /dev/null +++ b/data/feeds.yaml @@ -0,0 +1,40 @@ +# Journal RSS feeds for rheology literature (read with fetch_rss). +# +# arXiv categories are in config.yaml (arxiv.categories); the daily run reads +# rss.arxiv.org for each. J. Rheol. is also queried through Crossref by ISSN +# (config.yaml jor), because these AIP feeds carry no authors and only the +# issue in progress. Items from both are merged by DOI. +# +# source: the name shown in the digest; feeds with the same source are merged. +# Journal RSS URLs are known to change. Verify each before enabling. + +feeds: + - name: Journal of Rheology - current issue + source: J. Rheol. + url: https://pubs.aip.org/rss/site_1000105/1000055.xml + enabled: true # verified 2026-09-27: full abstracts, DOIs, no authors + + - name: Journal of Rheology - open issues + source: J. Rheol. + url: https://pubs.aip.org/rss/site_1000105/LatestOpenIssueArticles_1000055.xml + enabled: true # verified 2026-09-27: subset of current issue + + - name: Rheologica Acta + source: Rheol. Acta + url: https://link.springer.com/search.rss?query=&search-within=Journal&facet-journal-id=397 + enabled: false + + - name: Soft Matter + source: Soft Matter + url: https://feeds.rsc.org/rss/sm + enabled: false + + - name: Journal of Non-Newtonian Fluid Mechanics + source: JNNFM + url: https://rss.sciencedirect.com/publication/science/03770257 + enabled: false + + - name: Macromolecules + source: Macromolecules + url: https://pubs.acs.org/action/showFeed?type=axatoc&feed=rss&jc=mamobx + enabled: false diff --git a/data/vocabulary.md b/data/vocabulary.md new file mode 100644 index 0000000..5edd91c --- /dev/null +++ b/data/vocabulary.md @@ -0,0 +1,111 @@ +# Rheology Vocabulary + +Maintained term list. Feeds BM25 keyword expansion (v0.5) and prompt context. + +## Core rheological quantities + +- viscosity, dynamic viscosity, kinematic viscosity +- shear rate, shear stress, normal stress, normal stress difference +- storage modulus G', loss modulus G'', complex modulus G* +- loss tangent tan(delta), phase angle +- viscoelasticity, linear viscoelastic regime (LVR) +- creep, creep compliance, stress relaxation +- relaxation time, relaxation spectrum, retardation time +- yield stress, static yield stress, dynamic yield stress +- flow curve, Newtonian plateau, non-Newtonian + +## Flow behavior + +- shear thinning, shear thickening +- discontinuous shear thickening (DST), continuous shear thickening (CST) +- thixotropy, rheopexy, anti-thixotropy +- jamming, jamming transition +- plug flow, slip, wall slip +- steady-state flow, transient flow, startup flow, cessation of flow + +## Constitutive models + +- Maxwell model, Kelvin-Voigt model, Jeffreys model +- Oldroyd-B, Giesekus, FENE-P +- Bingham model, Herschel-Bulkley, Casson model +- Cross model, Carreau model, Bird-Carreau +- Doi-Edwards, reptation + +## Suspension and colloidal systems + +- hard-sphere suspension, charged colloid +- volume fraction phi, packing fraction +- Peclet number (Pe), Stokes number +- hydrodynamic interactions, lubrication force +- frictional contact, frictionless contact +- microstructure, pair distribution function g(r) +- colloidal gel, arrested state, percolation +- depletion interaction, depletion flocculation +- DLVO theory, electrostatic double layer +- Hamaker constant, van der Waals force + +## Microrheology + +- passive microrheology, active microrheology +- mean squared displacement (MSD) +- generalized Stokes-Einstein relation (GSER) +- one-point microrheology, two-point microrheology +- optical tweezers, magnetic tweezers +- diffusing wave spectroscopy (DWS), dynamic light scattering (DLS) +- tracer particle, probe particle + +## Experimental techniques + +- rotational rheometer, cone-and-plate, parallel plate +- Couette geometry, Taylor-Couette flow +- capillary rheometer, slit rheometer +- small-amplitude oscillatory shear (SAOS), large-amplitude oscillatory shear (LAOS) +- strain sweep, frequency sweep, temperature sweep +- rheo-NMR, rheo-SANS, rheo-SAXS, rheo-microscopy +- particle image velocimetry (PIV), confocal microscopy + +## Polymer rheology + +- entanglement, entanglement molecular weight Me +- Rouse model, Zimm model, reptation, tube model +- plateau modulus GN0, terminal relaxation +- polydispersity, molecular weight distribution +- star polymer, ring polymer, branched polymer + +## Interfacial and emulsion rheology + +- interfacial rheology, surface viscosity, surface elasticity +- Marangoni effect, surfactant +- emulsion, droplet size distribution, coalescence +- Pickering emulsion, particle-stabilized emulsion +- foam, foam drainage, bubble coarsening + +## Active and biological matter + +- active matter, self-propulsion, motility +- active gel, cytoskeletal network +- biopolymer, actin, microtubule, collagen +- cell mechanics, extracellular matrix (ECM) + +## Friction and contact mechanics + +- lubrication-to-friction transition, shear jamming, Wyart-Cates model +- sliding friction, rolling friction, sticky friction, dynamic bonds +- surface roughness, asperity, contact mechanics +- tribology, nanotribology, lateral force microscopy, colloidal probe + +## Data-driven rheology + +- machine learning, neural network, physics-informed neural network (PINN) +- rheology-informed neural network (RhINN), symbolic regression +- constitutive model discovery, surrogate model, Gaussian process +- graph neural network, Bayesian inference + +## LLMs and retrieval + +- large language model (LLM), small language model, open-weight model, local model +- Ollama, quantization, context window, num_ctx +- retrieval-augmented generation (RAG), dense retrieval, BM25, hybrid retrieval +- cross-encoder, re-ranking, embedding, semantic search +- structured output, JSON schema, schema drift, hallucination, grounding, citation +- AI tutor, chatbot, generative AI, physics education, engineering education diff --git a/docs/failure-modes.md b/docs/failure-modes.md new file mode 100644 index 0000000..71cdd00 --- /dev/null +++ b/docs/failure-modes.md @@ -0,0 +1,10 @@ +# Art failure-mode log + +Each entry: date · prompt/task · model · observed output · mitigation applied. +This is the source material for the SoR talk's "what works / what doesn't" section. + +See also: the six-mode taxonomy in the Stan paper ([arXiv:2603.04657](https://arxiv.org/abs/2603.04657)). + +--- + +*No entries yet. Append here as failures are observed during development.* diff --git a/modelfiles/researcher.Modelfile b/modelfiles/researcher.Modelfile new file mode 100644 index 0000000..aac9055 --- /dev/null +++ b/modelfiles/researcher.Modelfile @@ -0,0 +1,14 @@ +FROM gemma4:e4b + +# Defaults only: art/llm.py sets temperature, num_ctx, and the other options +# explicitly on every call. top_k and top_p are Gemma's recommended values. +PARAMETER temperature 0.7 +PARAMETER top_k 64 +PARAMETER top_p 0.95 + +# Researcher-tone system prompt for Art. +# Terse, precise, grounded in the source, refuses to fabricate. +SYSTEM """You are an expert researcher in rheology and soft matter, and in the use of language models for scientific research and science and engineering education. You are writing for a colleague who is a domain expert. +Work only from the text you are given. Be specific about methods, materials, systems, and key findings, and use the field's own terminology. +Never invent data, results, citations, names, or identifiers that are not in the source text. +When the source does not say, say so rather than guess. Be concise.""" diff --git a/prompts/README.md b/prompts/README.md new file mode 100644 index 0000000..49e97e8 --- /dev/null +++ b/prompts/README.md @@ -0,0 +1,15 @@ +# Prompts + +One file per prompt, named `_v.txt`. Placeholders are `{name}`; +anything else in the file is sent as written. `art/llm.py` records the +prompt name and a version string (`v+`) with every call, +so an edit is tracked even if the file is not renamed. Make a new version +file for a deliberate change you want to compare against the old one. + +| File | Task | Used by | +|---|---|---| +| summarize_v1.txt | 2-4 sentence summary from the abstract | digest | +| label_v1.txt | 3-6 word topic label, for the field-pulse section | digest | +| explain_v1.txt | one-sentence match reason plus a 0-3 relevance rating (JSON) | digest | +| llm_score_v1.txt | LLM-as-classifier baseline: 0-3 rating against the whole profile | evaluation only | +| profile_draft_v1.txt | draft a profile name and description from seed papers | bingham init | diff --git a/prompts/explain_v1.txt b/prompts/explain_v1.txt new file mode 100644 index 0000000..aae85aa --- /dev/null +++ b/prompts/explain_v1.txt @@ -0,0 +1,12 @@ +A researcher follows this topic: + +{profile_name}: {profile_description} + +Here is a paper: + +Title: {title} +Abstract: {abstract} + +In one sentence, state what in this paper connects it to the topic, naming the specific system, method, or finding. Then rate how relevant the paper is to the topic: 0 = not relevant, 1 = tangentially relevant, 2 = relevant, 3 = highly relevant. + +Respond in JSON with two keys: "reason" (a string) and "relevance" (an integer from 0 to 3). diff --git a/prompts/label_v1.txt b/prompts/label_v1.txt new file mode 100644 index 0000000..4ba85dc --- /dev/null +++ b/prompts/label_v1.txt @@ -0,0 +1,12 @@ +Assign a short topic label (3-6 words) to this paper. +Examples: colloidal gel rheology, polymer viscoelasticity, emulsion stability, +yield stress fluids, microrheology techniques, discontinuous shear thickening, +suspension microstructure, interfacial rheology, active matter rheology, +AI tutoring in physics, retrieval-augmented generation, LLM evaluation benchmarks, +scientific information extraction. +Return only the label, with no punctuation and no explanation. + +Title: {title} +Abstract: {abstract} + +Topic: diff --git a/prompts/llm_score_v1.txt b/prompts/llm_score_v1.txt new file mode 100644 index 0000000..4ae0e8f --- /dev/null +++ b/prompts/llm_score_v1.txt @@ -0,0 +1,15 @@ +You are evaluating whether a paper is relevant to a researcher's interests. + +Researcher interests: +{profile_text} + +Paper title: {title} +Paper abstract: {abstract} + +Rate relevance on this scale: +0 = not relevant +1 = tangentially relevant +2 = relevant +3 = highly relevant + +Reply with a single digit (0, 1, 2, or 3). Nothing else. diff --git a/prompts/profile_draft_v1.txt b/prompts/profile_draft_v1.txt new file mode 100644 index 0000000..8612591 --- /dev/null +++ b/prompts/profile_draft_v1.txt @@ -0,0 +1,10 @@ +Below are papers that a researcher chose as typical of one of their research interests. + +{papers} + +Write a profile of this research interest for a literature screener that will look for new papers on the same topic. + +- "name": a short topic name, at most five words. +- "description": two or three sentences in the field's own technical vocabulary, describing the systems, methods, and questions these papers have in common. Begin with the subject itself (for example "Rheology of dense suspensions ..." or "Language models as ..."), not with "This research" or "This topic". Describe the topic, not the individual papers. Do not mention authors, titles, or years. + +Respond in JSON with two keys: "name" (a string) and "description" (a string). diff --git a/prompts/summarize_v1.txt b/prompts/summarize_v1.txt new file mode 100644 index 0000000..b8a9be3 --- /dev/null +++ b/prompts/summarize_v1.txt @@ -0,0 +1,6 @@ +Summarize the following paper abstract in 2-4 sentences. Be specific about the methods, materials or systems, and key findings. Use precise scientific terminology. Do not add context not present in the abstract. + +Title: {title} +Abstract: {abstract} + +Summary: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9e48f98 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools>=77"] +build-backend = "setuptools.build_meta" + +[project] +name = "art" +version = "0.1.0" +description = "Art: an LLM toolchain for rheology research" +license = "MIT" +license-files = ["LICENSE"] +authors = [{ name = "Eric M. Furst" }] +requires-python = ">=3.12" +dependencies = [ + "ollama>=0.4", + "feedparser>=6.0", + "pyyaml>=6.0", + "sentence-transformers>=6.1.0", + "torch>=2.14.0", + "numpy>=2.4.6", + "bm25s>=0.3.11", + "pydantic>=2.13.4", + "pystemmer>=3.1.0", + "httpx>=0.28.1", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["art*"] + + +[dependency-groups] +dev = [ + "pytest>=9.1.1", +] + +[project.scripts] +bingham = "art.cli:main" diff --git a/tests/fixtures/arxiv_rss_cond-mat.soft_2024-05-02.xml b/tests/fixtures/arxiv_rss_cond-mat.soft_2024-05-02.xml new file mode 100644 index 0000000..6e4a7a0 --- /dev/null +++ b/tests/fixtures/arxiv_rss_cond-mat.soft_2024-05-02.xml @@ -0,0 +1,167 @@ + + + + cond-mat.soft updates on arXiv.org + http://rss.arxiv.org/rss/cond-mat.soft + cond-mat.soft updates on the arXiv.org e-print archive. + + http://www.rssboard.org/rss-specification + en-us + Thu, 02 May 2024 05:59:06 +0000 + rss-help@arxiv.org + Thu, 02 May 2024 00:00:00 -0400 + + Saturday + Sunday + + + Recoverable strain from reverse plasticity + https://arxiv.org/abs/2405.00487 + arXiv:2405.00487v1 Announce Type: new +Abstract: Recoverable strain is the strain recovered once a stress is removed from a body, in the direction opposite to that in which the stress had acted. To date, the phenomenon has been understood as being elastic in origin: polymer chains stretched in the direction of an imposed stress will recoil after the stress is removed, for example. Any unrecoverable strain is instead attributed to irreversible plastic deformations. Here we study theoretically recoverable and unrecoverable strain within the soft glassy rheology model, aimed at describing the rheology of elastoplastic yield stress fluids and amorphous soft solids. We consider a material subject to the switch-on of a shear stress that is held constant before later being set back to zero, after which the strain recovery is observed. After an initially fast recoil that is indeed elastic in nature, consistent with the usual intuition, we find that significant subsequent strain recovery then arises not via further recoil of elasticity but instead by `reverse plasticity', in a sense that we discuss carefully. Also unexpectedly, although in rare parameter regimes, this plastic part of the strain post switch-off does not always in fact recover in the negative direction, counter to that of the previously imposed stress, but can sometimes continue to accumulate in the forward direction. The recovery is then non-monotonic overall, reminiscent of recent observations of non-monotonic stress relaxation after straining, which have been attributed to complex material memory effects. We elucidate the mechanisms underlying these behaviours in terms of the evolution of the SGR model's population of elastoplastic elements, and suggest connections with the notion of `reversible plasticity', as discussed in the recent physics literature. + oai:arXiv.org:2405.00487v1 + cond-mat.soft + cond-mat.mtrl-sci + cond-mat.stat-mech + new + http://arxiv.org/licenses/nonexclusive-distrib/1.0/ + Henry A. Lockwood, Suzanne M. Fielding + + + Hyperuniformity in phase ordering: the roles of activity, noise, and non-constant mobility + https://arxiv.org/abs/2405.00508 + arXiv:2405.00508v1 Announce Type: new +Abstract: Hyperuniformity emerges generically in the coarsening regime of phase-separating fluids. Numerical studies of active and passive systems have shown that the structure factor $S(q)$ behaves as $q^\varsigma$ for $q\to 0$, with hyperuniformity exponent $\varsigma = 4$. For passive systems, this result was explained in 1991 by a qualitative scaling analysis of Tomita, exploiting isotropy at scales much larger than the coarsening length $\ell$. Here we reconsider and extend Tomita's argument to address cases of active phase separation and of non-constant mobility, again finding $\varsigma=4$. We further show that dynamical noise of variance $D$ creates a transient $\varsigma = 2$ regime for $\hat q\ll \hat{q}_\ast \sim \sqrt{D} t^{[1-(d+2)\nu]/2}$, crossing over to $\varsigma = 4$ at larger $\hat{q}$. Here, $\nu$ is the coarsening exponent, with $\ell\sim t^\nu$, and $\hat{q} \propto q \ell$ is the rescaled wavenumber. In diffusive coarsening, $\nu=1/3$, so the rescaled crossover wavevector $\hat{q}_\ast$ vanishes at large times when $d\geq 2$. The slowness of this decay suggests a natural explanation for experiments that observe a long-lived $\varsigma = 2$ scaling in phase-separating active fluids (where noise is typically large). Conversely, in $d=1$, we demonstrate that with noise the $\varsigma = 2$ regime survives as $t\to\infty$, with $\hat{q}_\ast\sim D^{5/6}$. (The structure factor is not then determined by the zero-temperature fixed point.) We confirm our analytical predictions by numerical simulations of active and passive continuum theories in the deterministic case and of Model B for the stochastic case. We also compare them with related findings for a system near an absorbing-state transition rather than undergoing phase separation. A central role is played throughout by the presence or absence of a conservation law for the centre of mass position of the order parameter field. + oai:arXiv.org:2405.00508v1 + cond-mat.soft + cond-mat.stat-mech + new + http://arxiv.org/licenses/nonexclusive-distrib/1.0/ + Filippo De Luca, Xiao Ma, Cesare Nardini, Michael E. Cates + + + Nonlinear Poisson effect in affine semiflexible polymer networks + https://arxiv.org/abs/2405.00590 + arXiv:2405.00590v1 Announce Type: new +Abstract: Stretching an elastic material along one axis typically induces contraction along the transverse axes, a phenomenon known as the Poisson effect. From these strains, one can compute the specific volume, which generally either increases or, in the incompressible limit, remains constant as the material is stretched. However, in networks of semiflexible or stiff polymers, which are typically highly compressible yet stiffen significantly when stretched, one instead sees a significant reduction in specific volume under finite strains. This volume reduction is accompanied by increasing alignment of filaments along the strain axis and a nonlinear elastic response, with stiffening of the apparent Young's modulus. For semiflexible networks, in which entropic bending elasticity governs the linear elastic regime, the nonlinear Poisson effect is caused by the nonlinear force-extension relationship of the constituent filaments, which produces a highly asymmetric response of the constituent polymers to stretching and compression. The details of this relationship depend on the geometric and elastic properties of the underlying filaments, which can vary greatly in experimental systems. Here, we provide a comprehensive characterization of the nonlinear Poisson effect in an affine network model and explore the influence of filament properties on essential features of the macroscopic response, including strain-driven alignment and volume reduction. + oai:arXiv.org:2405.00590v1 + cond-mat.soft + cond-mat.stat-mech + physics.bio-ph + new + http://creativecommons.org/licenses/by/4.0/ + Jordan L. Shivers, Fred C. MacKintosh + + + Capillary-Assisted Printing of Droplets at a Solid-Like Liquid-Liquid Interface + https://arxiv.org/abs/2405.00609 + arXiv:2405.00609v1 Announce Type: new +Abstract: Capillary forces guide the motion of biomolecular condensates, water-borne insects, and breakfast cereal. These surface-mediated interactions can be harnessed to build units into materials with exotic properties deriving from mesoscale structure. Droplets are promising building blocks for these materials, finding applications in tissue engineering, adaptive optics, and structural colour. However, the instability of water droplets at many liquid-liquid interfaces hampers the use of capillarity for the assembly of droplet-based materials. Here, we use nanoparticle surfactants to form solid-like oil-water interfaces at which aqueous droplets sit for extended periods. We find that microlitre-sized droplets at these interfaces attract each other over millimetric scales. We rationalize this interaction with a modified theory of capillarity. Applying printing methods allows us to finely control initial droplet positions, from which they self-assemble into cellular materials. Finally, by functionalising the interface with gold nanoparticles, we use plasmon-assisted optofluidics to manipulate these droplet-based materials with temperature gradients. + oai:arXiv.org:2405.00609v1 + cond-mat.soft + new + http://creativecommons.org/licenses/by/4.0/ + Anshu Thapa, Robert Malinowski, Matthew O. Blunt, Giorgio Volpe, Joe Forth + + + Particle scale anisotropy controls bulk properties in sheared granular materials + https://arxiv.org/abs/2405.00653 + arXiv:2405.00653v1 Announce Type: new +Abstract: The bulk dynamics of dense granular materials arise through a combination of particle-scale and mesoscale effects. Theoretical and numerical studies have shown that collective effects are created by particle-scale anisotropic structures such as grain connectivity (fabric), force transmission, and frictional mobilization, all of which influence bulk properties like bulk friction and the stress tensor through the Stress-Force-Fabric (SFF) relationship. To date, establishing the relevance of these effects to laboratory systems has remained elusive due to the challenge of measuring both normal and frictional contact forces at the particle scale. In this study, we perform experiments on a sheared photoelastic granular system in an quasi-2D annular (Couette) cell. During these experiments, we measure particle locations, contacts, and normal and frictional forces vectors during loading. We reconstruct the angular distributions of the contact and force vectors, and extract the corresponding emergent anisotropies for each of these metrics. Finally, we show that the SFF relation quantitatively predicts the relationship between particle scale anisotropies, the stress tensor components, and the bulk friction coefficient, capturing even transient behaviors. As such, this method shows promise for application to other dense particulate systems where fabric anisotropy can provide a useful measure of bulk friction. + oai:arXiv.org:2405.00653v1 + cond-mat.soft + new + http://arxiv.org/licenses/nonexclusive-distrib/1.0/ + Carmen L. Lee, Ephraim Bililign, Emilien Az\'ema, Karen E. Daniels + + + Transport of topological defects in a biphasic mixture of active and passive nematic fluids + https://arxiv.org/abs/2405.00547 + arXiv:2405.00547v1 Announce Type: cross +Abstract: Collectively moving cellular systems often contain a proportion of dead cells or non-motile genotypes. When mixed, nematically aligning motile and non-motile agents are known to segregate spontaneously. However, the role that topological defects and active stresses play in shaping the distribution of the two phases remains unresolved. In this study, we investigate the behaviour of a two-dimensional binary mixture of active and passive nematic fluids to understand how topological defects are transported between the two phases and, ultimately, how this leads to the segregation of topological charges. When the activity of the motile phase is large, and the tension at the interface of motile and non-motile phases is weak, we find that the active phase tends to accumulate $+1/2$ defects and expel $-1/2$ defects so that the motile phase develops a net positive charge. Conversely, when the activity of the motile phase is comparatively small and interfacial tension is strong, the opposite occurs so that the active phase develops a net negative charge. We then use these simulations to develop a physical intuition of the underlying processes that drive the charge segregation. Lastly, we quantify the sensitivity of this process on the other model parameters, by exploring the effect that anchoring strength, orientational elasticity, friction, and volume fraction of the motile phase have on topological charge segregation. As $+1/2$ and $-1/2$ defects have very different effects on interface morphology and fluid transport, this study offers new insights into the spontaneous pattern formation that occurs when motile and non-motile cells interact. + oai:arXiv.org:2405.00547v1 + physics.bio-ph + cond-mat.soft + cross + http://arxiv.org/licenses/nonexclusive-distrib/1.0/ + KVS Chaithanya, Aleksandra Arda\v{s}eva, Oliver J. Meacock, William M. Durham, Sumesh P. Thampi, Amin Doostmohammadi + + + Theoretical Limits of Energy Extraction in Active Fluids + https://arxiv.org/abs/2307.00489 + arXiv:2307.00489v2 Announce Type: replace +Abstract: Active materials form a class of far-from-equilibrium systems that are driven internally and exhibit self-organization which can be harnessed to perform mechanical work. Inspired by experiments on synthetic active networks we examine limits of work extraction from an active viscoelastic medium by analyzing the transport of a particle. The active viscoelastic material possesses an equilibrium density where the active and passive forces are balanced out. In one dimension, a gliding activation front (AF) that converts a passive to an active medium, provides active energy at a constant rate, which is injected into the system at one end and propagates to the other. We demonstrate that there exists a maximum velocity of the AF, above which the activated region fails to deliver the transport power. We hypothesize, and intuitively argue based on the limit cases, that the feasibility and the velocity of transport can be interpreted in terms of the velocity of an equilibration Domain Wall of the field, which is set by two parameters: a measure of activity, and the viscoelastic timescale. The phase diagram comprises Transport and No-Transport sectors, namely for any pair of the two parameters, there exists a threshold velocity of the AF above which the particle transport becomes impossible. Constructing the phase diagram we find that there are regions of the phase diagram for which the threshold velocity of the AF diverges. Larger viscoelastic timescale makes the transport region more accessible, and increases the transport velocity therein. Also, we find that increasing the velocity of AF results in larger extracted power but smaller transport coefficient; the ratio of the transport velocity and that of the AF. Our model provides a framework for understanding the energetics of transport phenomena in biology, and designing efficient mechanisms of transport in synthetic active materials. + oai:arXiv.org:2307.00489v2 + cond-mat.soft + cond-mat.other + cond-mat.stat-mech + physics.flu-dyn + replace + http://arxiv.org/licenses/nonexclusive-distrib/1.0/ + Shahriar Shadkhoo, Matt Thomson + + + Active Sparse Bayesian Committee Machine Potential for Isothermal-Isobaric Molecular Dynamics Simulations + https://arxiv.org/abs/2402.06256 + arXiv:2402.06256v2 Announce Type: replace +Abstract: Recent advancements in machine learning potentials (MLPs) have significantly impacted the fields of chemistry, physics, and biology by enabling large-scale first-principles simulations. Among different machine learning approaches, kernel-based MLPs distinguish themselves through their ability to handle small datasets, quantify uncertainties, and minimize over-fitting. Nevertheless, their extensive computational requirements present considerable challenges. To alleviate these, sparsification methods have been developed, aiming to reduce computational scaling without compromising accuracy. In the context of isothermal and isobaric ML molecular dynamics (MD) simulations, achieving precise pressure estimation is crucial for reproducing reliable system behavior under constant pressure. Despite progress, sparse kernel MLPs struggle with precise pressure prediction. Here, we introduce a virial kernel function that significantly enhances pressure estimation accuracy of MLPs. Additionally, we propose the active sparse Bayesian committee machine (BCM) potential, an on-the-fly MLP architecture that aggregates local sparse Gaussian process regression (SGPR) MLPs. The sparse BCM potential overcomes the steep computational scaling with the kernel size, and a predefined restriction on the size of kernel allows for a fast and efficient on-the-fly training. Our advancements facilitate accurate and computationally efficient machine learning-enhanced MD (MLMD) simulations across diverse systems, including ice-liquid coexisting phases, \ce{Li10Ge(PS6)2} lithium solid electrolyte, and high-pressure liquid boron nitride. + oai:arXiv.org:2402.06256v2 + cond-mat.soft + cond-mat.mtrl-sci + replace + http://creativecommons.org/licenses/by/4.0/ + Soohaeng Yoo Willow, Dong Geon Kim, R. Sundheep, Amir Hajibabaej, Kwang S. Kim, Chang Woo Myung + + + Stochastic thermodynamics of a probe in a fluctuating correlated field + https://arxiv.org/abs/2305.16235 + arXiv:2305.16235v3 Announce Type: replace-cross +Abstract: We develop a framework for the stochastic thermodynamics of a probe coupled to a fluctuating medium with spatio-temporal correlations, described by a scalar field. For a Brownian particle dragged by a harmonic trap through a fluctuating Gaussian field, we show that near criticality (where the field displays long-range spatial correlations) the spatially-resolved average heat flux develops a dipolar structure, where heat is absorbed in front and dissipated behind the dragged particle. Moreover, a perturbative calculation reveals that the dissipated power displays three distinct dynamical regimes depending on the drag velocity. + oai:arXiv.org:2305.16235v3 + cond-mat.stat-mech + cond-mat.soft + replace-cross + http://arxiv.org/licenses/nonexclusive-distrib/1.0/ + 10.1209/0295-5075/ad3469 + EPL, 146 (2024) 27001 + Davide Venturelli, Sarah A. M. Loos, Benjamin Walter, \'Edgar Rold\'an, Andrea Gambassi + + + Reduced stress propagation leads to increased mechanical failure resistance in auxetic materials + https://arxiv.org/abs/2307.14914 + arXiv:2307.14914v2 Announce Type: replace-cross +Abstract: Materials with negative Poisson ratio have the counter-intuitive property of expanding laterally when they are stretched longitudinally. They are accordingly termed auxetic, from the Greek auxesis meaning to increase. Experimental studies have demonstrated auxetic materials to have superior material properties, compared with conventional ones. These include synclastic curvature, increased acoustic absorption, increased resilience to material fatigue, and increased resistance to mechanical failure. Until now, the latter observations have remained poorly understood theoretically. With this motivation, the contributions of this work are twofold. First, we elucidate analytically the way in which stress propagates spatially across a material following a localised plastic failure event, finding a significantly reduced stress propagation in auxetic materials compared with conventional ones. In this way, a plastic failure event occurring in one part of a material has a reduced tendency to trigger knock-on plastic events in neighbouring regions. Second, via the numerical simulation of a lattice elastoplastic model, we demonstrate a key consequence of this reduced stress propagation to be an increased resistance to mechanical failure. This is seen not only via an increase in the externally measured yield strain, but also via a decreased tendency for plastic damage to percolate internally across a sample in catastrophic system-spanning clusters. + oai:arXiv.org:2307.14914v2 + cond-mat.stat-mech + cond-mat.mtrl-sci + cond-mat.soft + replace-cross + http://arxiv.org/licenses/nonexclusive-distrib/1.0/ + Suzanne M. Fielding + + + Theory of Rayleigh-Brillouin optical activity light scattering applicable to chiral liquids + https://arxiv.org/abs/2310.18352 + arXiv:2310.18352v3 Announce Type: replace-cross +Abstract: It has long been understood that dilute samples of chiral molecules such as rarefied gases should exhibit Rayleigh optical activity. We extend the existing theory by accounting for molecular dynamics and correlations, thus obtaining a more general theory of Rayleigh-Brillouin optical activity applicable to dense samples such as neat liquids. + oai:arXiv.org:2310.18352v3 + physics.optics + cond-mat.soft + replace-cross + http://creativecommons.org/licenses/by/4.0/ + Robert P. Cameron, Emmanouil I. Alexakis, Aidan S. Arnold, Duncan McArthur + + + The role of viscosity on drop impact forces + https://arxiv.org/abs/2311.03012 + arXiv:2311.03012v3 Announce Type: replace-cross +Abstract: A liquid drop impacting a rigid substrate undergoes deformation and spreading due to normal reaction forces, which are counteracted by surface tension. On a non-wetting substrate, the drop subsequently retracts and takes off. Our recent work (Zhang et al., \textit{Phys. Rev. Lett.}, vol. 129, 2022, 104501) revealed two peaks in the temporal evolution of the normal force $F(t)$--one at impact and another at jump-off. The second peak coincides with a Worthington jet formation, which vanishes at high viscosities due to increased viscous dissipation affecting flow focusing. In this article, using experiments, direct numerical simulations, and scaling arguments, we characterize both the peak amplitude $F_1$ at impact and the one at take off ($F_2$) and elucidate their dependency on the control parameters: the Weber number $We$ (dimensionless impact velocity) and the Ohnesorge number $Oh$ (dimensionless viscosity). \VS{The first peak amplitude $F_1$ and the time $t_1$ to reach it depend on inertial timescales for low viscosity liquids, remaining nearly constant for viscosities up to 100 times that of water. For high viscosity liquids, we balance the rate of change in kinetic energy with viscous dissipation to obtain new scaling laws: $F_1/F_\rho \sim \sqrt{Oh}$ and $t_1/\tau_\rho \sim 1/\sqrt{Oh}$, where $F_\rho$ and $\tau_\rho$ are the inertial force and time scales, respectively, which are consistent with our data.} The time $t_2$ at which the amplitude $F_2$ appears is set by the inertio-capillary timescale $\tau_\gamma$, independent of both the viscosity and the impact velocity of the drop. However, these properties dictate the magnitude of this amplitude. + oai:arXiv.org:2311.03012v3 + physics.flu-dyn + cond-mat.soft + replace-cross + http://creativecommons.org/licenses/by-nc-nd/4.0/ + Vatsal Sanjay, Bin Zhang, Cunjing Lv, Detlef Lohse + + + diff --git a/tests/fixtures/orcid_works_0000-0002-1825-0097.json b/tests/fixtures/orcid_works_0000-0002-1825-0097.json new file mode 100644 index 0000000..9b47e40 --- /dev/null +++ b/tests/fixtures/orcid_works_0000-0002-1825-0097.json @@ -0,0 +1 @@ +{"last-modified-date":{"value":1653413908337},"group":[{"last-modified-date":{"value":1494016313835},"external-ids":{"external-id":[{"external-id-type":"doi","external-id-value":"10.5555/12345680","external-id-normalized":{"value":"10.5555/12345680","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"work-summary":[{"put-code":9543020,"created-date":{"value":1375526492206},"last-modified-date":{"value":1494016313835},"source":{"source-orcid":null,"source-client-id":{"uri":"https://orcid.org/client/0000-0002-3054-1567","path":"0000-0002-3054-1567","host":"orcid.org"},"source-name":{"value":"Crossref Metadata Search"},"assertion-origin-orcid":{"uri":"https://orcid.org/0000-0002-1825-0097","path":"0000-0002-1825-0097","host":"orcid.org"},"assertion-origin-client-id":null,"assertion-origin-name":{"value":"Josiah Carberry"}},"title":{"title":{"value":"A Methodology for the Emulation of Architecture"},"subtitle":{"value":"Journal of Psychoceramics"},"translated-title":null},"external-ids":{"external-id":[{"external-id-type":"issn","external-id-value":"0264-3561","external-id-normalized":{"value":"0264-3561","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"part-of"},{"external-id-type":"doi","external-id-value":"10.5555/12345680","external-id-normalized":{"value":"10.5555/12345680","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"url":null,"type":"journal-article","publication-date":{"year":{"value":"2012"},"month":null,"day":null},"journal-title":null,"visibility":"public","path":"/0000-0002-1825-0097/work/9543020","display-index":"2"},{"put-code":18777963,"created-date":{"value":1375497097656},"last-modified-date":{"value":1494016313835},"source":{"source-orcid":{"uri":"https://orcid.org/0000-0002-1825-0097","path":"0000-0002-1825-0097","host":"orcid.org"},"source-client-id":null,"source-name":{"value":"Josiah Carberry"},"assertion-origin-orcid":null,"assertion-origin-client-id":null,"assertion-origin-name":null},"title":{"title":{"value":"A Methodology for the Emulation of Architecture"},"subtitle":{"value":"Journal of Psychoceramics"},"translated-title":null},"external-ids":{"external-id":[{"external-id-type":"doi","external-id-value":"10.5555/12345680","external-id-normalized":{"value":"10.5555/12345680","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"url":null,"type":"journal-article","publication-date":{"year":{"value":"2012"},"month":null,"day":null},"journal-title":null,"visibility":"public","path":"/0000-0002-1825-0097/work/18777963","display-index":"1"}]},{"last-modified-date":{"value":1494016313836},"external-ids":{"external-id":[{"external-id-type":"doi","external-id-value":"10.5555/666655554444","external-id-normalized":{"value":"10.5555/666655554444","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"work-summary":[{"put-code":4562455,"created-date":{"value":1361131256556},"last-modified-date":{"value":1494016313836},"source":{"source-orcid":{"uri":"https://orcid.org/0000-0002-1825-0097","path":"0000-0002-1825-0097","host":"orcid.org"},"source-client-id":null,"source-name":{"value":"Josiah Carberry"},"assertion-origin-orcid":null,"assertion-origin-client-id":null,"assertion-origin-name":null},"title":{"title":{"value":"The Memory Bus Considered Harmful"},"subtitle":{"value":"Journal of Psychoceramics"},"translated-title":null},"external-ids":{"external-id":[{"external-id-type":"issn","external-id-value":"0264-3561","external-id-normalized":{"value":"0264-3561","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"part-of"},{"external-id-type":"doi","external-id-value":"10.5555/666655554444","external-id-normalized":{"value":"10.5555/666655554444","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"url":null,"type":"journal-article","publication-date":{"year":{"value":"2012"},"month":null,"day":null},"journal-title":null,"visibility":"public","path":"/0000-0002-1825-0097/work/4562455","display-index":"0"}]},{"last-modified-date":{"value":1494016313836},"external-ids":{"external-id":[{"external-id-type":"doi","external-id-value":"10.5555/987654321","external-id-normalized":{"value":"10.5555/987654321","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"work-summary":[{"put-code":4562454,"created-date":{"value":1361131256545},"last-modified-date":{"value":1494016313836},"source":{"source-orcid":{"uri":"https://orcid.org/0000-0002-1825-0097","path":"0000-0002-1825-0097","host":"orcid.org"},"source-client-id":null,"source-name":{"value":"Josiah Carberry"},"assertion-origin-orcid":null,"assertion-origin-client-id":null,"assertion-origin-name":null},"title":{"title":{"value":"The Impact of Interactive Epistemologies on Cryptography"},"subtitle":{"value":"Journal of Psychoceramics"},"translated-title":null},"external-ids":{"external-id":[{"external-id-type":"issn","external-id-value":"0264-3561","external-id-normalized":{"value":"0264-3561","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"part-of"},{"external-id-type":"doi","external-id-value":"10.5555/987654321","external-id-normalized":{"value":"10.5555/987654321","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"url":null,"type":"journal-article","publication-date":{"year":{"value":"2011"},"month":null,"day":null},"journal-title":null,"visibility":"public","path":"/0000-0002-1825-0097/work/4562454","display-index":"0"}]},{"last-modified-date":{"value":1494016313836},"external-ids":{"external-id":[{"external-id-type":"doi","external-id-value":"10.5555/12345679","external-id-normalized":{"value":"10.5555/12345679","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"work-summary":[{"put-code":4562453,"created-date":{"value":1361131256538},"last-modified-date":{"value":1494016313836},"source":{"source-orcid":{"uri":"https://orcid.org/0000-0002-1825-0097","path":"0000-0002-1825-0097","host":"orcid.org"},"source-client-id":null,"source-name":{"value":"Josiah Carberry"},"assertion-origin-orcid":null,"assertion-origin-client-id":null,"assertion-origin-name":null},"title":{"title":{"value":"Developing Thin Clients Using Amphibious Epistemologies"},"subtitle":{"value":"Journal of Psychoceramics"},"translated-title":null},"external-ids":{"external-id":[{"external-id-type":"issn","external-id-value":"0264-3561","external-id-normalized":{"value":"0264-3561","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"part-of"},{"external-id-type":"doi","external-id-value":"10.5555/12345679","external-id-normalized":{"value":"10.5555/12345679","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"url":null,"type":"journal-article","publication-date":{"year":{"value":"2008"},"month":null,"day":null},"journal-title":null,"visibility":"public","path":"/0000-0002-1825-0097/work/4562453","display-index":"0"}]},{"last-modified-date":{"value":1494016313835},"external-ids":{"external-id":[{"external-id-type":"doi","external-id-value":"10.5555/12345678","external-id-normalized":{"value":"10.5555/12345678","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"work-summary":[{"put-code":4562456,"created-date":{"value":1361131256560},"last-modified-date":{"value":1494016313835},"source":{"source-orcid":{"uri":"https://orcid.org/0000-0002-1825-0097","path":"0000-0002-1825-0097","host":"orcid.org"},"source-client-id":null,"source-name":{"value":"Josiah Carberry"},"assertion-origin-orcid":null,"assertion-origin-client-id":null,"assertion-origin-name":null},"title":{"title":{"value":"Toward a Unified Theory of High-Energy Metaphysics: Silly String Theory"},"subtitle":{"value":"Journal of Psychoceramics"},"translated-title":null},"external-ids":{"external-id":[{"external-id-type":"issn","external-id-value":"0264-3561","external-id-normalized":{"value":"0264-3561","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"part-of"},{"external-id-type":"doi","external-id-value":"10.5555/12345678","external-id-normalized":{"value":"10.5555/12345678","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"url":null,"type":"journal-article","publication-date":{"year":{"value":"2008"},"month":null,"day":null},"journal-title":null,"visibility":"public","path":"/0000-0002-1825-0097/work/4562456","display-index":"0"}]},{"last-modified-date":{"value":1653413908337},"external-ids":{"external-id":[{"external-id-type":"doi","external-id-value":"10.1109/TPS.1987.4316723","external-id-normalized":{"value":"10.1109/tps.1987.4316723","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"},{"external-id-type":"eid","external-id-value":"2-s2.0-0023398608","external-id-normalized":{"value":"2-s2.0-0023398608","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"work-summary":[{"put-code":19980729,"created-date":{"value":1446663146999},"last-modified-date":{"value":1653413908337},"source":{"source-orcid":null,"source-client-id":{"uri":"https://orcid.org/client/0000-0002-5982-8983","path":"0000-0002-5982-8983","host":"orcid.org"},"source-name":{"value":"Scopus - Elsevier"},"assertion-origin-orcid":{"uri":"https://orcid.org/0000-0002-1825-0097","path":"0000-0002-1825-0097","host":"orcid.org"},"assertion-origin-client-id":null,"assertion-origin-name":{"value":"Josiah Carberry"}},"title":{"title":{"value":"Bulk and surface plasmons in artificially structured materials"},"subtitle":null,"translated-title":null},"external-ids":{"external-id":[{"external-id-type":"doi","external-id-value":"10.1109/TPS.1987.4316723","external-id-normalized":{"value":"10.1109/tps.1987.4316723","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"},{"external-id-type":"eid","external-id-value":"2-s2.0-0023398608","external-id-normalized":{"value":"2-s2.0-0023398608","transient":true},"external-id-normalized-error":null,"external-id-url":null,"external-id-relationship":"self"}]},"url":{"value":"http://www.scopus.com/inward/record.url?eid=2-s2.0-0023398608&partnerID=MN8TOARS"},"type":"journal-article","publication-date":{"year":{"value":"1987"},"month":null,"day":null},"journal-title":{"value":"IEEE Transactions on Plasma Science"},"visibility":"public","path":"/0000-0002-1825-0097/work/19980729","display-index":"0"}]}],"path":"/0000-0002-1825-0097/works"} \ No newline at end of file diff --git a/tests/test_bingham.py b/tests/test_bingham.py new file mode 100644 index 0000000..627e178 --- /dev/null +++ b/tests/test_bingham.py @@ -0,0 +1,263 @@ +"""Tests for the deterministic parts of bingham (no network, no models).""" + +from pathlib import Path +from unittest import mock + +import pytest + +from art import store +from art.feeds import fetch +from art.profile import profiles as prof + +FIXTURES = Path(__file__).parent / "fixtures" + + +def test_arxiv_rss_parse(): + data = (FIXTURES / "arxiv_rss_cond-mat.soft_2024-05-02.xml").read_bytes() + with mock.patch.object(fetch, "http_get", return_value=data): + items = fetch.fetch_arxiv_rss("cond-mat.soft") + assert len(items) == 12 + first = items[0] + assert first["id"] == "2405.00487" and first["version"] == "v1" + assert first["doi"] == "10.48550/arXiv.2405.00487" + assert first["authors"] == ["Henry A. Lockwood", "Suzanne M. Fielding"] + assert first["date"] == "2024-05-02" + assert first["abstract"].startswith("Recoverable strain") + assert "Announce Type" not in first["abstract"] + assert {it["announce_type"] for it in items} == {"new", "cross", "replace", "replace-cross"} + # LaTeX is kept as written + assert any("$S(q)$" in it["abstract"] for it in items) + + +@pytest.fixture +def conn(tmp_path, monkeypatch): + monkeypatch.setattr(store.env, "path", lambda key: tmp_path / "art.sqlite") + return store.connect() + + +def _item(**kw): + base = {"id": "10.1122/x", "doi": "10.1122/x", "title": "T", "authors": [], + "date": "2026-09-22", "abstract": "", "source": "J. Rheol.", "categories": []} + return {**base, **kw} + + +def test_upsert_fills_only_empty_fields(conn): + assert store.upsert_items(conn, [_item(abstract="From the feed.")], "daily") == 1 + assert store.upsert_items(conn, [_item(abstract="From Crossref.", authors=["A. B"])], "daily") == 0 + row = store.item_dict(conn.execute("SELECT * FROM items").fetchone()) + assert row["abstract"] == "From the feed." + assert row["authors"] == ["A. B"] + + +def test_cross_list_then_new_is_stored_once_as_new(conn): + store.upsert_items(conn, [_item(id="2609.1", announce_type="cross")], "daily") + store.upsert_items(conn, [_item(id="2609.1", announce_type="new")], "daily") + rows = conn.execute("SELECT announce_type FROM items").fetchall() + assert [r[0] for r in rows] == ["new"] + + +def test_tracked_author_match_is_full_name(): + tracked = [prof.TrackedAuthor(name="Priya Raman", match=["Priya Raman"]), + prof.TrackedAuthor(name="Samuel T. Okafor", match=["Samuel T. Okafor"])] + assert prof.tracked_matches(["samuel t. okafor"], tracked) == ["Samuel T. Okafor"] + assert prof.tracked_matches(["P. Raman", "Anil Raman"], tracked) == [] + + +def test_profiles_file_loads_and_versions_are_stable(): + data = prof.load() + assert len(data.profiles) >= 1 + assert data.profiles[0].version == prof.load().profiles[0].version + + +def test_legacy_entry_is_screened_once_filled(conn): + from art.screen import screen + store.upsert_items(conn, [_item(abstract="")], "legacy") + assert screen.load_items(conn) == [] + store.upsert_items(conn, [_item(abstract="From Crossref.", authors=["A. B"])], "backfill") + assert [it["id"] for it in screen.load_items(conn)] == ["10.1122/x"] + + +def _arxiv(id_, day, cats=("cond-mat.soft",)): + return fetch._arxiv_item(id_, "v1", f"Paper {id_}", ["A. B"], day, "Abstract.", + list(cats), "new") + + +def test_weekdays_between(): + from datetime import date + from art.feeds.collect import _weekdays_between + fri, mon, thu = date(2026, 9, 25), date(2026, 9, 28), date(2026, 10, 1) + assert _weekdays_between(fri, mon) == [] + assert _weekdays_between(mon, thu) == [date(2026, 9, 29), date(2026, 9, 30)] + + +def test_daily_fetch_catches_up_skipped_days(conn, monkeypatch): + from art.feeds import collect + monkeypatch.setattr(collect.cache, "load_raw", lambda name: ([], None)) + monkeypatch.setattr(collect.cache, "save_raw", lambda name, items: None) + cfg = {"categories": ["cond-mat.soft"], "include_cross_lists": True, + "request_delay_s": 0, "max_catchup_days": 14} + monkeypatch.setattr(collect.env, "config", lambda: {"arxiv": cfg}) + oai_calls = [] + + def fake_oai(oai_set, since, **kw): + oai_calls.append((oai_set, since)) + return [_arxiv("2609.2", "2026-09-29"), _arxiv("2609.3", "2026-09-30", ("physics.flu-dyn", "cond-mat.soft"))] + + monkeypatch.setattr(collect.fetch, "fetch_arxiv_oai", fake_oai) + + # Monday: first daily fetch records coverage and does not catch up. + monkeypatch.setattr(collect.fetch, "fetch_arxiv_rss", lambda cat: [_arxiv("2609.1", "2026-09-28")]) + collect._fetch_arxiv(conn, None, 0, {"fetched": 0, "new": 0}) + assert oai_calls == [] + + # Thursday: Tuesday and Wednesday were skipped. + monkeypatch.setattr(collect.fetch, "fetch_arxiv_rss", lambda cat: [_arxiv("2610.1", "2026-10-01")]) + collect._fetch_arxiv(conn, None, 0, {"fetched": 0, "new": 0}) + assert oai_calls == [("physics:cond-mat", "2026-09-29")] + rows = {r["id"]: (r["origin"], r["announce_type"]) for r in conn.execute("SELECT * FROM items")} + assert rows["2609.2"] == ("daily", "new") + assert rows["2609.3"] == ("daily", "cross") + assert store.last_fetch(conn, "arxiv:catchup") is not None + covered = conn.execute("SELECT MAX(detail) FROM fetches WHERE source = 'arxiv:covered'").fetchone()[0] + assert covered == "2026-10-01" + + # Thursday again: nothing missing. + collect._fetch_arxiv(conn, None, 0, {"fetched": 0, "new": 0}) + assert len(oai_calls) == 1 + + +def test_failed_rss_feed_does_not_advance_coverage(conn, monkeypatch): + from art.feeds import collect + monkeypatch.setattr(collect.cache, "load_raw", lambda name: ([], None)) + monkeypatch.setattr(collect.cache, "save_raw", lambda name, items: None) + cfg = {"categories": ["cond-mat.soft", "cs.CL"], "include_cross_lists": True, + "request_delay_s": 0, "max_catchup_days": 14} + monkeypatch.setattr(collect.env, "config", lambda: {"arxiv": cfg}) + store.log_fetch(conn, "arxiv:covered", 0, 0, True, "2026-09-28") + + def rss(cat): + if cat == "cs.CL": + raise RuntimeError("HTTP 503") + return [_arxiv("2609.9", "2026-09-29")] + + monkeypatch.setattr(collect.fetch, "fetch_arxiv_rss", rss) + collect._fetch_arxiv(conn, None, 0, {"fetched": 0, "new": 0}) + covered = conn.execute("SELECT MAX(detail) FROM fetches WHERE source = 'arxiv:covered'").fetchone()[0] + assert covered == "2026-09-28" + + +def test_cluster_average_linkage(): + import numpy as np + from art.cluster import cluster + S = np.array([[1.0, 0.9, 0.85, 0.1], + [0.9, 1.0, 0.82, 0.2], + [0.85, 0.82, 1.0, 0.1], + [0.1, 0.2, 0.1, 1.0]]) + assert sorted(cluster(S, 0.8)) == [[0, 1, 2], [3]] + assert sorted(cluster(S, 0.95)) == [[0], [1], [2], [3]] + + +def test_user_config_is_merged_over_base(tmp_path, monkeypatch): + from art import env + (tmp_path / "config.yaml").write_text( + "contact: {email: a@b.org}\narxiv: {categories: [cs.CL]}\nscreen: {thresholds: {x: 0.5}}\n") + monkeypatch.setattr(env, "USER_DIR", tmp_path) + env.config.cache_clear() + try: + cfg = env.config() + assert env.contact_email() == "a@b.org" + assert cfg["arxiv"]["categories"] == ["cs.CL"] # lists replace + assert cfg["arxiv"]["request_delay_s"] == 3 # other keys kept + assert cfg["screen"]["thresholds"] == {"default": 0.2, "x": 0.5} # dicts merge + finally: + env.config.cache_clear() + + +def test_data_file_prefers_user_then_data_then_example(tmp_path, monkeypatch): + from art import env + monkeypatch.setattr(env, "USER_DIR", tmp_path) + assert env.data_file("profiles.yaml").parent.name == "examples" + assert env.data_file("feeds.yaml").parent.name == "data" + (tmp_path / "profiles.yaml").write_text("profiles: []\n") + assert env.data_file("profiles.yaml") == tmp_path / "profiles.yaml" + + +def test_cluster_stops_at_n_clusters(): + import numpy as np + from art.cluster import cluster, medoids + S = np.array([[1.0, 0.9, 0.3, 0.2], + [0.9, 1.0, 0.25, 0.2], + [0.3, 0.25, 1.0, 0.7], + [0.2, 0.2, 0.7, 1.0]]) + assert cluster(S, n_clusters=2) == [[0, 1], [2, 3]] + assert medoids(S, [0, 1, 2], k=1) == [0] + + +# -- profile toolbox ---------------------------------------------------------- + +def test_ref_from_text(): + from art.toolbox.seeds import ref_from_text + assert ref_from_text("https://doi.org/10.1122/8.0001213") == "doi:10.1122/8.0001213" + assert ref_from_text("10.1039/D3SM00479A.") == "doi:10.1039/d3sm00479a" + assert ref_from_text("arXiv:2603.04657v2") == "arxiv:2603.04657" + assert ref_from_text("https://arxiv.org/abs/2609.21066") == "arxiv:2609.21066" + assert ref_from_text("10.48550/arXiv.2609.21066") == "arxiv:2609.21066" + assert ref_from_text("2609.21066") == "arxiv:2609.21066" + assert ref_from_text("no identifier here") is None + + +def test_orcid_works_use_self_identifiers_only(): + import json + from art.toolbox.seeds import parse_orcid_works + works = json.loads((FIXTURES / "orcid_works_0000-0002-1825-0097.json").read_text()) + seeds = parse_orcid_works(works) + assert len(seeds) == 6 and all(s["ref"].startswith("doi:10.") for s in seeds) + assert not any("0264-3561" in s["ref"] for s in seeds) # ISSNs are part-of, not self + assert any(s["title"] == "The Memory Bus Considered Harmful" and s["year"] == 2012 for s in seeds) + + +def test_id_list_bibtex_and_csl_readers(): + from art.toolbox.seeds import from_bibtex, from_csl_json, from_id_list + ids = from_id_list("# my papers\n10.1122/8.0001213\n\narXiv:2609.21066 # rolling friction\n10.1122/8.0001213\n") + assert [s["ref"] for s in ids] == ["doi:10.1122/8.0001213", "arxiv:2609.21066"] + bib = from_bibtex("""@article{a, + title = {Rheology of {Non-Brownian} Suspensions}, + author = {Denn, Morton M. and Morris, J. F.}, + year = {2014}, doi = {10.1146/annurev-chembioeng-060713-040221}, + abstract = {A review.} +} +@misc{b, title = "A preprint", eprint = {2609.21066}, archivePrefix = {arXiv}, author = {Q. Doe}} +@book{c, title = {No identifier}} +""") + assert [s["ref"] for s in bib] == ["doi:10.1146/annurev-chembioeng-060713-040221", "arxiv:2609.21066"] + assert bib[0]["title"] == "Rheology of Non-Brownian Suspensions" + assert bib[0]["authors"] == ["Morton M. Denn", "J. F. Morris"] and bib[0]["year"] == 2014 + csl = from_csl_json('[{"DOI": "10.1039/D3SM00479A", "title": "Sticky friction", "abstract": "We show.",' + ' "author": [{"given": "Hojin", "family": "Kim"}], "issued": {"date-parts": [[2023]]}},' + ' {"URL": "http://arxiv.org/abs/2603.04657", "title": "Stan"}, {"title": "none"}]') + assert [s["ref"] for s in csl] == ["doi:10.1039/d3sm00479a", "arxiv:2603.04657"] + assert csl[0]["authors"] == ["Hojin Kim"] and csl[0]["year"] == 2023 + + +def test_abbreviations_and_coauthors(): + from art.toolbox.draft import abbreviations, coauthor_suggestions + text = "We use large-amplitude oscillatory shear (LAOS) and small amplitude oscillatory shear (SAOS)." + assert abbreviations(text) == {"LAOS": "large-amplitude oscillatory shear", + "SAOS": "small amplitude oscillatory shear"} + seeds = [{"authors": ["Ada Owner", "Bo Co"]}, {"authors": ["Ada Owner", "Bo Co", "Cy Once"]}, + {"authors": ["Ada Owner"]}] + assert coauthor_suggestions(seeds, None) == [("Bo Co", 2)] # owner dropped automatically + assert coauthor_suggestions(seeds, "Ada Owner") == [("Bo Co", 2)] + + +def test_draft_yaml_loads_as_profiles(): + from art.toolbox.draft import profiles_yaml + d = {"n_used": 4, "sources": ["ids"], "llm": None, "tracked": [("Bo Co", 2)], + "profiles": [{"id": "dense-suspensions", "name": "Dense suspensions", "n_papers": 4, + "description": "Friction and lubrication in dense suspensions: " * 5, + "keywords": ["DST", "shear thickening", "friction"], "drafted_by": "placeholder", + "exemplars": [("doi:10.1039/d3sm00479a", "Sticky friction", 2023), + ("arxiv:2609.21066", "Rolling friction", None)]}]} + data = prof.Profiles.model_validate(__import__("yaml").safe_load(profiles_yaml(d))) + assert data.profiles[0].exemplars[1].arxiv == "2609.21066" + assert data.tracked_authors == [] # suggestions stay commented out diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..cf4f1e5 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1402 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "sys_platform == 'linux'", + "sys_platform != 'linux'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[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.13.0" +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/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "art" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "bm25s" }, + { name = "feedparser" }, + { name = "httpx" }, + { name = "numpy" }, + { name = "ollama" }, + { name = "pydantic" }, + { name = "pystemmer" }, + { name = "pyyaml" }, + { name = "sentence-transformers" }, + { name = "torch" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "bm25s", specifier = ">=0.3.11" }, + { name = "feedparser", specifier = ">=6.0" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "numpy", specifier = ">=2.4.6" }, + { name = "ollama", specifier = ">=0.4" }, + { name = "pydantic", specifier = ">=2.13.4" }, + { name = "pystemmer", specifier = ">=3.1.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "sentence-transformers", specifier = ">=6.1.0" }, + { name = "torch", specifier = ">=2.14.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.1.1" }] + +[[package]] +name = "bm25s" +version = "0.3.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/f6/38a4eb22be0c715aa9bc290e44a48bbdf7a27cffa7f85914fd10242442e4/bm25s-0.3.11.tar.gz", hash = "sha256:8ae0f0707d279969829383c7aaeef00fb09ca343101160222571d6bfc969f889", size = 80978, upload-time = "2026-08-25T03:14:57.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/d9/bb770dabb7e0edeb2c949460a05c38df7f4f564a1693a778352fb0d213b0/bm25s-0.3.11-py3-none-any.whl", hash = "sha256:3d1d28badb299d6fc9324111e5c8276927e990b908607b39ff9bd65b102e9a24", size = 74985, upload-time = "2026-08-25T03:14:56.051Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[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.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/11/1293429c1c3a3e19b551275e65efddd122a905bbe7e368816a59f3ef2a41/cuda_bindings-13.4.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfbd3f7d4ac04dd41dc49121b9e408c8283992f47124c2290ecb79bbbadcca8e", size = 6488226, upload-time = "2026-09-23T02:22:00.578Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c3/efb6bbb7307bf5c83dc4acca650280b210c67ed1a1a60f898a90e7c82e38/cuda_bindings-13.4.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f72bcfcdf3be23e1da3c792f68f508586f48d037bca8b10f552c4cca5971f2", size = 7172185, upload-time = "2026-09-23T02:22:03.114Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a9/c83eb5aa055a4b0c3776d83f6f88b9e778a6fe0415210977c889c6a0bb8a/cuda_bindings-13.4.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7c6c9f46fca7f3fc61959ef9a2398ac656172145b43f408e0a6492360cf1c0c", size = 6316533, upload-time = "2026-09-23T02:22:09.694Z" }, + { url = "https://files.pythonhosted.org/packages/8a/24/9c01edfd2210737ee9471b47db857a079e5a23f2677e5d9778c0ff23d099/cuda_bindings-13.4.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fd7d8459b364aedc11f3e59703453ced823135f78a9111ca70feef8d56d4d21", size = 6929124, upload-time = "2026-09-23T02:22:11.765Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e6/3c094ef0eb00a7b0ff69a3915327e2c2d14e712ebe471a2217bf7f020f33/cuda_bindings-13.4.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4796864ce829bd95ef2ef0d23c6ba21bb64e08f7fab0a377302ed1affb6605c7", size = 6407948, upload-time = "2026-09-23T02:22:18.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/7a3769c43e432b0434dd46424058b47af4347167f0dfca1ecb27e2de92a1/cuda_bindings-13.4.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbacde6f75665b197016b986164cfdaa33b17515e5e635a63ddb75926aaa71c3", size = 6978955, upload-time = "2026-09-23T02:22:20.535Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ca/2c4419ca787278f65faf0f0155791a80fa141f39a628e97e4663e2ba09fa/cuda_bindings-13.4.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6eb969920e28f66f8fc3b0b3afcb6e09381cc96bf8e8158d774e9488ae89980", size = 6314272, upload-time = "2026-09-23T02:22:26.785Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f878de5de8e6d1a64d55096539b7b72821e6dc62682d5968e842b95d97df/cuda_bindings-13.4.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e11cfe8fec4c85ce79feda18124971c52596f0cbd642a94f5dafc257124a4b3", size = 6865978, upload-time = "2026-09-23T02:22:29.041Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/59/239c7259e669c46ddbcac0aa60e3a0ef00bfeaaa687f905b24dd6a7a10fe/cuda_pathfinder-1.8.2-py3-none-any.whl", hash = "sha256:4e65059febdb4d19d5cbc4798677e19db2b582f2f702f457b609e571690d357e", size = 62551, upload-time = "2026-09-17T22:19:09.396Z" }, +] + +[[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')" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +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')" }, +] +curand = [ + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cusolver = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[[package]] +name = "feedparser" +version = "6.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sgmllib3k" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/79/db7edb5e77d6dfbc54d7d9df72828be4318275b2e580549ff45a962f6461/feedparser-6.0.12.tar.gz", hash = "sha256:64f76ce90ae3e8ef5d1ede0f8d3b50ce26bcce71dd8ae5e82b1cd2d4a5f94228", size = 286579, upload-time = "2025-09-10T13:33:59.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/eb/c96d64137e29ae17d83ad2552470bafe3a7a915e85434d9942077d7fd011/feedparser-6.0.12-py3-none-any.whl", hash = "sha256:6bbff10f5a52662c00a2e3f86a38928c37c48f77b3c511aedcd51de933549324", size = 81480, upload-time = "2025-09-10T13:33:58.022Z" }, +] + +[[package]] +name = "filelock" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/d7/37691dc5063438a448b646f6f2442b4beebf16cc0e18d8cdfa7aeec60b8c/filelock-4.0.4.tar.gz", hash = "sha256:90999ed63a26ccf86b93b959ab10cf1017f422d816be454ed54cbed263e71ab5", size = 244639, upload-time = "2026-09-26T15:19:12.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/ac/8b9c6dc2aa7e9cf5582e20a337006c3428c3d752c2255dbaca2c78a8be45/filelock-4.0.4-py3-none-any.whl", hash = "sha256:0df72be195ca7892216d16f2edce8d9b93a571f02402972020a8cff84c594c7b", size = 108629, upload-time = "2026-09-26T15:19:10.603Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/cd/9be253869fc42e764de7f3dedd6969af7d44ff9c3375214a3442a6f3fc08/fsspec-2026.9.0.tar.gz", hash = "sha256:0f08147951c8cb31d844c3547d631053b127863b60be04cf06e121333ee0e2fe", size = 333545, upload-time = "2026-09-18T17:50:42.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/c0/a98505f18594f1bce828bb159cec0fcf9860562f1a2c85913409fc8f3d9e/fsspec-2026.9.0-py3-none-any.whl", hash = "sha256:8dd6e646e99ea382bd85f97a45e6b526a442d79423a7dc673f1e2756d05fcb5f", size = 221738, upload-time = "2026-09-18T17:50:41.341Z" }, +] + +[[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 = "hf-xet" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/62/3c062f593bd92ef4e77a0ef39541e3d82a0a1d3947c8a777a02a13a27828/hf_xet-1.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:70cbb9c896901600128cb9b6f06e132954fbede1db30f31f7c6c63f84cb7c31d", size = 4074584, upload-time = "2026-08-03T22:32:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1e/c0ad437dd267a8e435bef594acf781bbc3874ff0b6435b4962d03ecf7cc4/hf_xet-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23379c2f9ec8696d952b16414a2bae72cad86a52df869b050698ba60f538c675", size = 3867381, upload-time = "2026-08-03T22:32:49.049Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ee/7c0d7b6ab336167531b1c30af2af003f054af4c749becbd7209ae33a77c3/hf_xet-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f7278c05c22fd60cb436cda1269649b3e81db65ecdc8496e5e164aa4143e7b", size = 4453982, upload-time = "2026-08-03T22:32:50.568Z" }, + { url = "https://files.pythonhosted.org/packages/63/06/ad8eab1c9525246650cbaa821caa3cdbaca734ab1a5b8c91bea09cbd8d69/hf_xet-1.6.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:948f15d3a9545cfe5932f6bd8b440f6ae630aee108f14b7bd6c561f7c2dcc522", size = 4249445, upload-time = "2026-08-03T22:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/d8/26/1eee8aedb0dafc1ab9717dc9ac602cde33361b232dc06803f1f6ed18b58c/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5153e6bb103ad49d6ea9f1b2e230db5a2ea32551ad09a706d2f61d7c7c80d80e", size = 4451099, upload-time = "2026-08-03T22:32:54.114Z" }, + { url = "https://files.pythonhosted.org/packages/67/57/0b88af1f194ab6c9c650547d9cc06bfeaab836ae4dcdb331676bfb8be95a/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:35cec30d75c6f9eb9c16a77cef68e85a103b72e24d4b473714ec9ff06428bab9", size = 4664712, upload-time = "2026-08-03T22:32:55.547Z" }, + { url = "https://files.pythonhosted.org/packages/53/a0/26b717a9d1840e8abf48dcec64b5ed8fbe472671d38ad28d30e147132b33/hf_xet-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5789835d7c6bc9436962853192082374297fb72d7eff7e7762ec25ceb7e25338", size = 4025906, upload-time = "2026-08-03T22:32:57.391Z" }, + { url = "https://files.pythonhosted.org/packages/49/f6/4a9966633c6fef83af997e2cff68ec1963676d412bdfd096df2a93b8e185/hf_xet-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:75765820ce4700db3750c94acc8fe27c5fae4c9ec000a0dbac3ca082acf97765", size = 3849221, upload-time = "2026-08-03T22:32:59.123Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" }, + { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" }, + { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" }, +] + +[[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 = "huggingface-hub" +version = "1.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/2a/484d112c0d8fc5f665d7b65137ac9cdb2953c982391598c3597968a12ee7/huggingface_hub-1.33.0.tar.gz", hash = "sha256:367be21a201db9523eddf8aeac7048f2602c1b308691c97640d5e72ed188007e", size = 1043548, upload-time = "2026-09-24T09:49:29.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/16/963096d224b80909432dc16561a615fd33d2d13beef3ce4c63fa25e40867/huggingface_hub-1.33.0-py3-none-any.whl", hash = "sha256:04e434b06e100eddbce9a6e817d72693a7884b10a79bd67ab48080d5c07eb899", size = 846435, upload-time = "2026-09-24T09:49:28.059Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[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.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/1d/537ab090f302b838943a1b56497dd53059b9a9b46a074936470173a2e207/joblib-1.6.0.tar.gz", hash = "sha256:2ccc96785b12046c08fd6d55839c12857831b54a3c1673ffadd2f04bfc4eda03", size = 327903, upload-time = "2026-08-31T09:39:04.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/53/84099323c2ec4be98d935f63c033ac4151ee83836ca1050ede3b3aadf155/joblib-1.6.0-py3-none-any.whl", hash = "sha256:3dbbf9f6e4b592a2357b854608e980fe6390d131d7a82f011a377ef2ebef7aba", size = 306115, upload-time = "2026-08-31T09:39:02.298Z" }, +] + +[[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/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.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/68/5351e34623d253423240ea7de3f8fc74fa8ab14b1ab3c0ec4ac8997413c9/narwhals-2.26.0.tar.gz", hash = "sha256:6b9cadca82f375c7e4cf584fdc86ca25da54827307a9c58f94547ee6104b82dd", size = 686970, upload-time = "2026-09-08T13:32:08.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b5/1b84b2c784db76d69442334bc8b8748c840f13ca53be086f4f250ad4a0bc/narwhals-2.26.0-py3-none-any.whl", hash = "sha256:29326d74f107c347fd1009bd58e38d9f7c7c5b51e6de97bc93dbc325d9038b54", size = 474034, upload-time = "2026-09-08T13:32:07.159Z" }, +] + +[[package]] +name = "networkx" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/76/3af777226b63a5e64a6b36b1ec5855c14e2b94a37096d4760e595fc43511/networkx-3.7.tar.gz", hash = "sha256:fd77a511bd90f39f3d016351345b52cf5319b813bdca01de3f755d3cca62e96a", size = 1866482, upload-time = "2026-09-21T16:45:16.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/cd/fe58041e9011f307c490e3e17dd48cc516448f7c698a3f2d9d9d65d7e6a8/networkx-3.7-py3-none-any.whl", hash = "sha256:e3fd2c13a7814cee3746340d8d7f8598a67f16a58bf47fb7f8793fab6efca1b0", size = 2142205, upload-time = "2026-09-21T16:45:14.609Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" }, + { url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" }, + { url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" }, + { url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" }, + { url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" }, + { url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" }, + { url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" }, + { url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" }, + { url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" }, + { url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c7/29285be1e5232a6e7ee3268a33c85843f5a8ee93350c6465cddd66ebbf76/numpy-2.5.3-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f3ed25271581281f2fccb1adcedfcde4c07362eec69189b50baf6f90e3ae159", size = 15697322, upload-time = "2026-09-06T16:26:49.415Z" }, + { url = "https://files.pythonhosted.org/packages/55/49/bbad5335fb4996a16881f853ff3e0ba582f01720e55c89b1c06b8fc42a90/numpy-2.5.3-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdc76bfcae6b255dff75202c5e7feaf95b40246bc0a17944facc1fecf9f79ab", size = 16708995, upload-time = "2026-09-06T16:26:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e9/1df35483760b04a65ea44669f89dc64f30e5aca098b48ceb8b1310b0e0fe/numpy-2.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:116f96cadd935c6122e9228d676fe7ede19e741f5c8bb1c3cddbe0c51ccebea2", size = 17052508, upload-time = "2026-09-06T16:26:56.464Z" }, + { url = "https://files.pythonhosted.org/packages/b8/99/66e54da8265cc8be8a7382bf96edce17aaa2837d6f484432025932a3caa5/numpy-2.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:09ffa5d903faeaa5c4dd05009cf81c8bab9f2cb37c548b8d39b65b4cfa7c97f7", size = 18468224, upload-time = "2026-09-06T16:26:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" }, + { url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/e3813329498596cb842703dcacac1741612ed9fb9c4e6a3e0c7e2ebbc597/numpy-2.5.3-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:595d020938c84e320bcf40ad71089e108eac0d377cd018e14a8c094f39e98d85", size = 15745777, upload-time = "2026-09-06T16:27:25.181Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/4e7a07fd0776dc2210cdacf2010be8665194d094defc10c419d7dea794cc/numpy-2.5.3-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f24021b9f22bc6301c37b196974a92c1c18dccedb6fef3dd252e95f2d6adbe4", size = 16746949, upload-time = "2026-09-06T16:27:28.576Z" }, + { url = "https://files.pythonhosted.org/packages/91/db/01674c0e20335057813a00c2ebd546ed25bff9ed7914f9bced00f8c55d94/numpy-2.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:71b39d9f935b6ec0f8753e3e2afb51e3efba6f2e05b68b32a40754d24bcd4a3c", size = 17108994, upload-time = "2026-09-06T16:27:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/584c5e71f8d378e57cac0b033891ed65c683ef90573ba4854e8c28203db0/numpy-2.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b05c171afb3aa07adbd20abc00aea86fe375beb0fdb9ef780ec5b7f63bab1c0", size = 18512266, upload-time = "2026-09-06T16:27:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, +] +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.24.0.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/30/7c257e3d5cb4fecb147b93895c66e29c93f8e76d74b45bb418ff0587c4ec/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:a6812a554a1ff0413e9c52b84c26c050380649ab9615f9c16bded368ce9f421f", size = 650976863, upload-time = "2026-07-02T16:23:39.248Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ba/791cffd048fe5b044e620df55267e3e95c0e6e07d50b41e377c03dfc910f/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:71f181cd810e90f9b6023b01186fe82d13d65f0ec098581ee201d39fad769e4b", size = 553099438, upload-time = "2026-07-02T16:27:42.58Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +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", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +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", marker = "sys_platform == 'linux'" }, +] +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.30.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/21/a73174c6157101bdf1ffc22b517f76ff0082613989dd9bc8f43e8034caac/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:ca786ffa5a647c75d4d1f5cc72a6c4f537947e2ba8823d7c8aaf768e7a7b9f77", size = 215983881, upload-time = "2026-06-09T03:23:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/c500f90c7ae641b8e0f98965b36b8a7ac79cc8b296e8d251fe3eb592ee54/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:cefa7fdb9710efd0f39c5f1be1d61ff6fc9a996c451265bd7fbdcf9455ed4b50", size = 215965170, upload-time = "2026-06-09T03:23:39.73Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.4.92" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6b/eef7a9e32872b8f41e145bf10cddc9af26e153c338852811fe9a9baddf9e/nvidia_nvjitlink-13.4.92-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:e0391f24ed94ec879b84e3da4d4ec320c879aff681f2c7a638462f7199284323", size = 42452378, upload-time = "2026-09-16T20:45:29.042Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a8/1cbd4014898af8b419e69b0d7dbc63da2121ee92d92b47d59f4fe9075349/nvidia_nvjitlink-13.4.92-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:25f74fad0d654271c921ac4dca614bd6258bc21791242fc7b2289dad7ae9c099", size = 40420120, upload-time = "2026-09-16T20:45:19.163Z" }, +] + +[[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 = "ollama" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/72/5f12423b6b39ca8430fbe56f77fcf4ef60f63067c7c4a2e30e200ed9ec16/ollama-0.6.2.tar.gz", hash = "sha256:936d55daa684f474364c098611c933626f8d6c7d67065c5b7ae0c477b508b07f", size = 53145, upload-time = "2026-04-29T21:21:15.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/d6722beeb2d10f7a3b9ff49375708904fde18f82b5609a0bc4aeb5996a4d/ollama-0.6.2-py3-none-any.whl", hash = "sha256:3ad7daab28e5a973445c36a73882a3ef698c2ebb00e21e308652741577509f7d", size = 15115, upload-time = "2026-04-29T21:21:13.794Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[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/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/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" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pystemmer" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/95/bb893462b08db211b248f6b1aaa0dc07d068dc86f180178bc9072fef86bb/pystemmer-3.1.0.tar.gz", hash = "sha256:083cc3ed90f4c3b0668f8e31c2925cbb4db3bf0fd6d710e0ad0914f33685f7df", size = 302551, upload-time = "2026-05-22T11:23:44.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/0a/4a6a42cf93c26a8449573249f2ddbaf027b8d84beb3cb6da536f0b4a033f/pystemmer-3.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a85fd3afc08a0aba9142d92c85df87a127faab787619941bb1c433dadfbbbb53", size = 242185, upload-time = "2026-05-22T11:14:08.951Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5a/cace8a3b00dfeb8497d240ed4ca34d38d22c47bb90de302bcb8e5fb6bd35/pystemmer-3.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b40b669f121949bbbd97fe77a2606d46b14df9fcadabc9539982398772592ac4", size = 247353, upload-time = "2026-05-22T11:14:10.609Z" }, + { url = "https://files.pythonhosted.org/packages/06/55/78699a2621a472be98bae7fb36bdda610b7d1c8bcd8ff448301230260cf9/pystemmer-3.1.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:550bd609aa8dc324033eb1c8d34bfd68649183bbdab871ff534332dcafdc5e5a", size = 752410, upload-time = "2026-05-22T11:14:12.075Z" }, + { url = "https://files.pythonhosted.org/packages/67/63/e3937ac1df5243e94bde11fc1e57584480de419587f46a4badf5088e7ca6/pystemmer-3.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6db50dd5e92a2a0acf2b2cec55fddcff3928bf490e26d0c45bc0483272a46c", size = 761062, upload-time = "2026-05-22T11:14:13.78Z" }, + { url = "https://files.pythonhosted.org/packages/e7/63/1f7ef438e19029025fc73fe95480d0dbb2ff323f1aec9c087ad9eb952afb/pystemmer-3.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b86b0b14634788060cab98ad0cae023620aa7b29f9155e8b474632fc80793405", size = 756304, upload-time = "2026-05-22T11:14:15.419Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5e/816d59b780aa552c87a7e25148005f41be18193482b122d67c6235741b31/pystemmer-3.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6a1289aa31f5f613a31916e7d4e6eaf6b0e6e70a3d494bf08b1d999a0aaf458e", size = 754520, upload-time = "2026-05-22T11:14:16.754Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bc/58abda8bf0f87c18804a2ebfe05091417d121b3dde08a57e87aab8509fa4/pystemmer-3.1.0-cp312-cp312-win32.whl", hash = "sha256:2bab76be269302075cb892be2b884ae3c2ae22dd3d8fbcf20e01d969243719d0", size = 157476, upload-time = "2026-05-22T11:14:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b9/785aff6e2ca5947bc7139ec320df0510f02ae2bbb625d534245a8fcda549/pystemmer-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:805ac25b73d54200026ef4733a516d3b5a7598644454d5eb161f4ba73e43c9be", size = 225133, upload-time = "2026-05-22T11:14:19.219Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/0c5cabf4a6f92c1c42d248366b6d967be1da6ee75a993e2a1834620c5226/pystemmer-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:ab2dfe428ef626eec34e5360d20a04c41cc14fd70a96e9866902d19dc27cc5b9", size = 226532, upload-time = "2026-05-22T11:14:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d2/07/d3c7d6ea2e47fec881e54a588a5f126d525f517b0c3df8f811e8c2af8f45/pystemmer-3.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6b9bbe98fb698f2d19f6d58a0f6f6858bea93457e5c3aca0c5fd889658fba61a", size = 241824, upload-time = "2026-05-22T11:14:22.003Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/55a89144ea63c6247fa87aafc0124bb043a95f428a1862fd37248c318eb0/pystemmer-3.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ec88f8a24d6fd49a0301efe4e4f0277063158f46f9cc227f2cd49afec78bb169", size = 247100, upload-time = "2026-05-22T11:14:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/78/b0/907b842baea0a5667ae06630afea3727949a3f25b7615b1dd8adf8e849bb/pystemmer-3.1.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec37dead495a6da4349c66f5984ef4148059f97bb44c95077f1aa5e50059ebc7", size = 747692, upload-time = "2026-05-22T11:14:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/b0/30/08cec4c31ec281e690027a9c4dfa5da914915c959069560b5d2d3622c5b8/pystemmer-3.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58c5bb84ebbd380226d90cc0df81cc84e325ed0f30f8f2439f84da50e681b316", size = 755850, upload-time = "2026-05-22T11:14:26.789Z" }, + { url = "https://files.pythonhosted.org/packages/49/07/9fb7825dbba2280ba59bac5cced2d05cd09047c8354b03c4e55589c6c8c6/pystemmer-3.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cc7fefd98903b8fd42f3c650f7d8c229172e435d5e122d1821f1a744b13c428", size = 752512, upload-time = "2026-05-22T11:14:28.282Z" }, + { url = "https://files.pythonhosted.org/packages/74/da/06c449c1332e0afae4af59e9749ce2b1a079ef96724240b745c17276b090/pystemmer-3.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e17dab9e6a44066c3a57e8133353f984026c0cc73340e136a60f946200db9881", size = 749671, upload-time = "2026-05-22T11:14:30.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bb/c47f3b748080733ac3e7636eb27f9161ac1897ea0c2c0a1e1951ce01dc68/pystemmer-3.1.0-cp313-cp313-win32.whl", hash = "sha256:2a3a2ee3429b877a2ce3e7c8e82c5e7f76416281b9b839bd8de44bfbe2b14881", size = 157354, upload-time = "2026-05-22T11:14:32.26Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9e/99c8071e5c7c23fd855e11941d780792262f76f19864f761b40f73b12f61/pystemmer-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8527e1718b80a628303378d439057c90ef242dfc99efcad60ba2e8fb14ded8c6", size = 225078, upload-time = "2026-05-22T11:14:33.423Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2b/fc2c23f4961f530ba646e9c7913e491c610936bfc40689d53103ad6b16c4/pystemmer-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:b94cec510cc2da557e048cbe300532c4435dca9093cb49839c8cbb78709eaba0", size = 226424, upload-time = "2026-05-22T11:14:35.006Z" }, + { url = "https://files.pythonhosted.org/packages/44/1d/0a609707682d53049048efa661e792ca0b7a5e0a8cb3d65884bf0b08d80d/pystemmer-3.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:359d3d0d8ce96fc3e978631a19df4f794518cc1a09232e1f8f7a017853e534e1", size = 241790, upload-time = "2026-05-22T11:14:36.509Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c3/f5359565107c2fc861e9b242ae96e29a2f171a3768816a0c9c34640b1928/pystemmer-3.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a9192e71ed4fe04b7f378b79fee0cae87c7a2ed5d8a153d749a15f8b42e02628", size = 247345, upload-time = "2026-05-22T11:14:38.014Z" }, + { url = "https://files.pythonhosted.org/packages/55/ca/f649a8bc2c1e748b92cd56dd5c6bb240cd15027fae587d858d6f8da867e5/pystemmer-3.1.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5cfffb0c455b656c301c8c2ffebda9eb0ad966211c1c1309d4839967dac63b73", size = 747126, upload-time = "2026-05-22T11:14:39.672Z" }, + { url = "https://files.pythonhosted.org/packages/5b/47/bb91c7daa193afb2861a940912078d1eff90de57fa59828fe9bfd9f9315e/pystemmer-3.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25c6df202e11cfee660879423f4025f10603e0aa4fc630c41eda4eaa53ebd809", size = 756594, upload-time = "2026-05-22T11:14:41.191Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/dd706b4d611952a8885932d3e6706f3b7dd34ba531142f308bd35cf51570/pystemmer-3.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a27d775dc5ce542b6a8886810b800f507cf28409c755e3612a3d94ec0ccfcec7", size = 753503, upload-time = "2026-05-22T11:14:42.553Z" }, + { url = "https://files.pythonhosted.org/packages/29/46/5508fd451016d2bc98f1cbe3b4914c665e54f01751a614b5d7d52960c85b/pystemmer-3.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4bc6fa139ad8c0a326de8b0580029852b1490646a59024b6f7b563f45832de77", size = 748517, upload-time = "2026-05-22T11:14:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/aa/08/826177ca4a52ee903ca2d805ba993b815cfeaf1f552c21d4c6a72b74de3c/pystemmer-3.1.0-cp314-cp314-win32.whl", hash = "sha256:f0e9e8c19dded337ea7ed744f6c5b7aa629fedbc1bf326feeb50aa72b5226b65", size = 161159, upload-time = "2026-05-22T11:14:58.371Z" }, + { url = "https://files.pythonhosted.org/packages/91/ae/bf92ca59a720100517cc6027802fd9b7f3e74b34d32736a26b2225755e24/pystemmer-3.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:da6910e6933729224136041528b98dfb6d2639de88c943ed0da2fec116dd779f", size = 228561, upload-time = "2026-05-22T11:14:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/10/dd/f013f95ee5a52ee78bd59025b125be4ac13770086afdab3a48b939265ba7/pystemmer-3.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:2563f8a39b8e3e9686e633c2a6333beeb93f398ea3a4d21da1867a15475812ae", size = 230822, upload-time = "2026-05-22T11:15:00.873Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1d/969be7cbf655e0f1e59a5c1949614ba5720adf0d7b877906a62534904eb7/pystemmer-3.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3a5e38092677b47e1bcb19ecc9ede5fbce115ebdcde6cb868e25da8c79ed4d87", size = 243079, upload-time = "2026-05-22T11:14:45.304Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/529a38a90d9c1fda9684b37287806e011e5b5fac6fed063b373eb1f4372d/pystemmer-3.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0449bd466c36629620ad7fc2f1df3fcd52567f19948766aabdbbdeac4a347f24", size = 251206, upload-time = "2026-05-22T11:14:46.778Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b5/0a60082cfac11e3220e73eb99bd9d1aae95d1d9c48e502827ba6bff3ab0c/pystemmer-3.1.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5657e19c33303b9e478cb845fdf75949c52f9a745c310bd50bf0e48a9062611b", size = 805514, upload-time = "2026-05-22T11:14:48.095Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ae/308c922066d2a1f2bcf2206487cb1f4dbc429b0303167badc6a09c4a6fb6/pystemmer-3.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b63bf8ac1943838fa194fe00d0cf6205a59fd9b402735c56254f6a6f561bbd58", size = 820433, upload-time = "2026-05-22T11:14:49.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/28/5350570742808cdc5ab6aab04bba16f9e983bbc0f32c4063cf766734b34f/pystemmer-3.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:87fb4da0b4bc3c13e0d37ff9f50f661a5d75c499bf5dbd46a8a4de2ea8050fa4", size = 810894, upload-time = "2026-05-22T11:14:51.019Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1b/c0f260c0993835fb340c184c78369b0c0a49425c9a06657563e15ead16a8/pystemmer-3.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9ac13dbcc75d5f351eaf55e5d84615b2e8bdac18bc84ddb8b346b98e74cbe8db", size = 806881, upload-time = "2026-05-22T11:14:52.892Z" }, + { url = "https://files.pythonhosted.org/packages/17/b0/7411aaeaca247526d584865a65cf70c9ab2a20f5c975e36964e09723d433/pystemmer-3.1.0-cp314-cp314t-win32.whl", hash = "sha256:3efa66285316e7ce356b26d0c22619fb03f5c98518572c4b20cd111f665323e0", size = 165978, upload-time = "2026-05-22T11:14:54.378Z" }, + { url = "https://files.pythonhosted.org/packages/6a/39/beced63cca50ec55433fec9f9feccb5c7bcc00bdc1b491a65dca248cb0c5/pystemmer-3.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f6b619268ba66e54d4f7befed283bc7fe2354417ad8c399a166ffd3468632e7e", size = 235182, upload-time = "2026-05-22T11:14:55.692Z" }, + { url = "https://files.pythonhosted.org/packages/18/60/b3280c297bbcfcf08dcccb1aef087a1bd78876ec02d4d529a180ce885334/pystemmer-3.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:abf809c951f7ed83f3556880895d26db626e12cd67796e12783532a3cd3331a1", size = 233356, upload-time = "2026-05-22T11:14:57.203Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "regex" +version = "2026.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/5c/f403115361de25809e8f785686ec7096e30fef73be9ae35aa51da4e80abb/regex-2026.9.10.tar.gz", hash = "sha256:1e321e2c84f0e52c457f5ea5944f796d6e8e09cb99738ea98dcc1bfe402a128d", size = 417072, upload-time = "2026-09-09T21:00:21.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/c8/bfbe893e90ee0148bd2860dd086f09b5d2080ca2b125f740c2e118c16982/regex-2026.9.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880ac684c27176464c00c3fdc456116364f5ebc70da07aad0c2d4a7ba45e98db", size = 496609, upload-time = "2026-09-09T20:57:09.987Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ac/56d5ae6efb759255c3b3db650a4be25f96a844ea3613b91e1e189a3b7294/regex-2026.9.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9d36b03dc362aa40ffaaec9d9bd75e87763529563ec008c43b0e07782f5be7a", size = 297024, upload-time = "2026-09-09T20:57:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/60/4b/0f2d5f6bbb791cc10f22f0ed16c487e630dde8fa8fa0bd92a2bfe21a4b20/regex-2026.9.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866de9f98df0611d7b62b3a8729d3284a64c0cc6edd90bb95a533e443a4939cb", size = 291905, upload-time = "2026-09-09T20:57:13.127Z" }, + { url = "https://files.pythonhosted.org/packages/89/51/3fb5fe0d32f4cf0bc982286722c729a8d6f522d2fa2d5d14a702d9fc87f8/regex-2026.9.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c66d54042a14a503907d81861b8a5235e6d1f03d4fbc1d8767f652eaf957ac1", size = 800055, upload-time = "2026-09-09T20:57:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/89/46/ee507bd2f9d4420f26a594b35c551d7194b66f5d7897f63730fae6ec05c1/regex-2026.9.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:032da15431c890d376f53547f0a6219f4f4cd19f3e4f11bdc321453b5bd207e4", size = 871133, upload-time = "2026-09-09T20:57:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/cbaa90689684f91b1bc017e7f8c6d9425c6bd299108db02482dc51376d8a/regex-2026.9.10-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:23ac9a28180f274d7dd7651fa131ad5b02d343b75df4b040737f0356223895dd", size = 919627, upload-time = "2026-09-09T20:57:18.402Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/dcdf5e0d898024005cfcce631e3e934d111dfbe177ca0b7f253ae8a735a2/regex-2026.9.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e67f8843f0e4b931f1fa860bf3bbe4134b714c0155cc5c7c0d7ea450230aae0", size = 804587, upload-time = "2026-09-09T20:57:19.859Z" }, + { url = "https://files.pythonhosted.org/packages/73/70/eedfe81c29bae266a06ab4250978361a9bccd474704d88d4f4ef4506dff8/regex-2026.9.10-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bb7774924f8cd69f49cba0b3c2d679a6326f777e0e67d130ad5203e4df53f0d3", size = 777320, upload-time = "2026-09-09T20:57:21.62Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e5/86b207077efbcd91305700488f170b7eb1e1c54721cea74175273aa3b9a4/regex-2026.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c32480f3371b75068decaf9e5da72c224e953830dd71e36e06cf80e30ea39d8", size = 790572, upload-time = "2026-09-09T20:57:23.048Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/f622c3b2323f4c035b98e80221740a442127ad7993135b814f52057430db/regex-2026.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d2d377fd1cad611b806cdd732d86b65f536c768209890cb442556548daa65a23", size = 865485, upload-time = "2026-09-09T20:57:24.658Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/34bd621d3d6ac906ad67e57ed56c40cd45f7d51b9c0328335e97a7cb8ecb/regex-2026.9.10-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c014641157e9049b0603b8daa5343bd408d9b757b709aaa0f373cd3fab2d7944", size = 767925, upload-time = "2026-09-09T20:57:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/0d/28/ddbf7cba86f2adf5038c6c16aa829636ffc6e437f81bb0cbf302899cea5e/regex-2026.9.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1562aabd9d4eb09bd88a62ad97ed06800094b529ac43419e43020b9cefec79b0", size = 858800, upload-time = "2026-09-09T20:57:27.901Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f1/e8d7656ff3d3bd32e881d32f540b4981c79dee61908d6b790a45966e6895/regex-2026.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2479171edccced52ef02b899558f88ab2c235fe05b93180fdcae1670aacd89e1", size = 791648, upload-time = "2026-09-09T20:57:29.786Z" }, + { url = "https://files.pythonhosted.org/packages/fc/65/eac1a79115c8475d8ff539602eb54031564d719fdea5a599c4b0a1a26d1b/regex-2026.9.10-cp312-cp312-win32.whl", hash = "sha256:239620b0e0681669367c0e218c8eb2551d9f8fe3b9fccfc8d0003377804e8348", size = 267326, upload-time = "2026-09-09T20:57:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d4/4dcd0a05d3e97ca829165df1c39717f899d1d484dac8a6032439f2cb8d6d/regex-2026.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4db7d00c4afbfbb55b8e17b1e371da11418ea9389b030acec63c1fa4c7ad4b86", size = 277933, upload-time = "2026-09-09T20:57:33.648Z" }, + { url = "https://files.pythonhosted.org/packages/55/f8/22617a80dee28f2451011eae36bc26b3d78c4994ba87b5281d60acf9b6c0/regex-2026.9.10-cp312-cp312-win_arm64.whl", hash = "sha256:c25a754bb81a2edcfc3b65eda50f017d736f818112ed43e8aafd595cb00678ae", size = 277447, upload-time = "2026-09-09T20:57:35.156Z" }, + { url = "https://files.pythonhosted.org/packages/20/90/d4452bf1ef7dbe406980e8b921a257024482203c1dafac535eae207611bc/regex-2026.9.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ef5a059ea1c6ee5d1c7e99a2484e628608d010921efe876c6f0e2029d2f35eca", size = 496408, upload-time = "2026-09-09T20:57:36.757Z" }, + { url = "https://files.pythonhosted.org/packages/6a/35/c763c6424a0f99d021d46dc1f9065147bb5a40c2b2cdf28d2ebdbcd96508/regex-2026.9.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dce932f8e3ba936475ea3d0d8b59f7b050a9e206e994f53f8fd80299871e87da", size = 296931, upload-time = "2026-09-09T20:57:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/fa/68/241f88458b17c46ed2f80147a60a03b2ada7fb815c23b6bc76c298abb0a5/regex-2026.9.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8c668af8f7bdb1d18739c27d30cd9f4b371495a883f75a002fb7a39d740fecd", size = 291741, upload-time = "2026-09-09T20:57:40.482Z" }, + { url = "https://files.pythonhosted.org/packages/90/9e/974d6de404c63e2d09525f4ddb99874c7ab8e1f781ccbe0dd3e26fa6f6e5/regex-2026.9.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aebdd9a946de328b3f6f61dbf48dd064a36eb6dddf96e34ae6651d37f6e9383", size = 800088, upload-time = "2026-09-09T20:57:42.098Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fd/3875b73f9e7ba3321dcaa02c19f650c05c61345328acf84599ac6f45ceed/regex-2026.9.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f2374c27deb189b282ec7e16106752c22ad39b056bbd8018960b1e4cc95d67a1", size = 871212, upload-time = "2026-09-09T20:57:44.03Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/2358e791c0e171194dd6a8b97b520579098a21397fb79dbe6b7edc9e3fa7/regex-2026.9.10-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e0dc78251154b66dc60211563fc115345da332eaa881e4e2523fb1edae3772f4", size = 919752, upload-time = "2026-09-09T20:57:45.691Z" }, + { url = "https://files.pythonhosted.org/packages/20/3b/000c79c3f9c06b7542225a5d3a7f9a85405da7224b3b9af94a491d07abea/regex-2026.9.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bafa41b0dd63669e5c0f8adf3d24819efeb73c847f492eb011212eb352e69041", size = 804578, upload-time = "2026-09-09T20:57:47.548Z" }, + { url = "https://files.pythonhosted.org/packages/30/6d/195eedb1de87f26639191e7487e41eb81e2ce255bc7563a64f3f5a95eb08/regex-2026.9.10-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ebb2ba68e4641a994061f70bf44ed448fba0b9b1d18c94ffb9efc1cca805b39b", size = 777345, upload-time = "2026-09-09T20:57:49.63Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/11fe2b313fcd92cb75c583648f2746031b9f4da9e9ed4241204a5e8b3721/regex-2026.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:048a89ee797db10160bd2bd519286577a6b43a100279bd4b7d8456a3d69c80a0", size = 790556, upload-time = "2026-09-09T20:57:51.27Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c0/07ec9b4c43b0e16d62454971a5ab3886eccb0bfa161300a02d801ab28620/regex-2026.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:79e9432995e14c749d34209413de5e621ec8e67789bf4f46dbfabea9d06a2406", size = 865572, upload-time = "2026-09-09T20:57:53.163Z" }, + { url = "https://files.pythonhosted.org/packages/19/07/43bc9a9cf9fc8e37d2ba47980dfe4a6e151d2cf3ab969e0031e2a9b21484/regex-2026.9.10-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5847e22bbf959764d776937d791d034cc2d19b787e361c88d97e859e8dc68502", size = 767971, upload-time = "2026-09-09T20:57:54.805Z" }, + { url = "https://files.pythonhosted.org/packages/9c/49/3b9286a3a94f3c89ed4ddbe74e72bdde21c1a5eadd520d5f4ed4a61936cb/regex-2026.9.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c103b3b14e011774af4fb7e4617ad4d72b9171905cd3b231a70a4efd76e477d7", size = 858835, upload-time = "2026-09-09T20:57:56.627Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/e5d27ce9fee8e3ef95f886c7b6ecec211efa4cfc18bd73bd5cf26cca4741/regex-2026.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6b34a778c695d24e77c140e3b4c95da69282e34f2f6b02b55656aa4a0379f643", size = 791793, upload-time = "2026-09-09T20:57:58.313Z" }, + { url = "https://files.pythonhosted.org/packages/63/03/c28a6bebedc3e2d86ee27ec2de16f7ec0419dcd10e771d43dcc9c58a2e99/regex-2026.9.10-cp313-cp313-win32.whl", hash = "sha256:7abb38b8c40f3a235235a44da452c64b7b5c1d650ec6351027db0e090804f2e5", size = 267298, upload-time = "2026-09-09T20:58:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/cd/fd/5c85fa6cfb8e034080bda5a72fa0a4df2b7777a35eb7e73c2799c2adda7a/regex-2026.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:20e8bfb07ad79a282f8b95b56fe67f9750b1b7f775724e4ba1f23cb296115ce4", size = 277894, upload-time = "2026-09-09T20:58:01.731Z" }, + { url = "https://files.pythonhosted.org/packages/c1/28/f5a25f6f65501675977fda35d9f61abb1468c4b87c0f73e536d8b21a60b8/regex-2026.9.10-cp313-cp313-win_arm64.whl", hash = "sha256:3bdeed3318a8eb2bbadc9c56347e0ff651639e934a47e168d05a3b12929fd0e7", size = 277436, upload-time = "2026-09-09T20:58:03.422Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ed/98e9b07d8bb9c765d07774f0b2c19b301b96d51f44630fea48951051c94e/regex-2026.9.10-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd6bd89b9fc06018d35851cab0240adb7dd84d51941b19f6574ac90cd54e3ae5", size = 496662, upload-time = "2026-09-09T20:58:05.118Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a8/9dfe9be48378b47c5a8f04b0f200ea225f9ee0f8e93f010433f661a37878/regex-2026.9.10-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ef4c0a9dfdc90581b90b1b95a8c3d1557f8ff8f5a2a53536d26314de699d1468", size = 297115, upload-time = "2026-09-09T20:58:06.822Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e4/5d1f005a3825ec49842ad349061c1c26e6d42f47ccf105e6e5aa6aeed392/regex-2026.9.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:14caa05ce39ec70437af5aac8814c50ee6628f4a90353871c059692f448a164f", size = 291896, upload-time = "2026-09-09T20:58:08.675Z" }, + { url = "https://files.pythonhosted.org/packages/be/15/44ce83fca50c6058f42b62fa8300a8030eea7e4e5a973a2dd33db0f557fb/regex-2026.9.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3264132d576847ab5f88bb83e7debe67854bf165b3ea613bd467312b6099536a", size = 800534, upload-time = "2026-09-09T20:58:10.339Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/a62e070a5a033643b287489576f02ae6a9c584c337d62349e340a2b4d001/regex-2026.9.10-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cef9f3d14796500ea834c41dbe688f1f6b23c7024dc23e8a794d7ebaf5d71d0", size = 872038, upload-time = "2026-09-09T20:58:12.186Z" }, + { url = "https://files.pythonhosted.org/packages/f3/06/8b8e2483949b1329df10c5b615e85d332066deb426b11332b799629b9201/regex-2026.9.10-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d278ad30ec83b6b9202685b0f80b741a51ea3ca7f0595ebda96e7628b6398876", size = 918927, upload-time = "2026-09-09T20:58:13.903Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/9b56f69100d3afdbc9c4fa6e302764f9cb717fbc06a9d50558d98ca89cd2/regex-2026.9.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb018cfe7144585fc83882405906ff84994a2d154afc2509ecc7752c51f864", size = 803694, upload-time = "2026-09-09T20:58:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/07/43/d00d59a7c8fd0e070ae8457a8743597f45ad9682b100f57b9c9c405fbdfd/regex-2026.9.10-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6fd555fc9abef50c530869690b2daca054c8811a7aff632d11f9a7b2590b2742", size = 777770, upload-time = "2026-09-09T20:58:17.628Z" }, + { url = "https://files.pythonhosted.org/packages/46/6b/a11d0446484efbc9eb67abec133f254c6d66a1568b8f3fb36d39a73a1129/regex-2026.9.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8d5c4518235a2ec1611e57af85fa488d529c1106aacff12adadcedf8687012cd", size = 791234, upload-time = "2026-09-09T20:58:19.476Z" }, + { url = "https://files.pythonhosted.org/packages/12/09/bcd24e78b373fd4f98090caa43eade439223f6703b909be79b9efd9ab0ab/regex-2026.9.10-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:175cf49ce7a994c88b8f15e3cb17cdb66a48ebb2d36de736b8205033db950f89", size = 866259, upload-time = "2026-09-09T20:58:21.683Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/c57ba5e94222a813260af4c17ced92d40cdb44737eb6b50979688310b6a3/regex-2026.9.10-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b71649169a9fcf30b395ee01047fa7ad6654a4c900ca75b23c04dedcce6a1f8c", size = 768219, upload-time = "2026-09-09T20:58:23.842Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a2/3820037587d00901ace96c5864335c2cd1b899d5263ea0dd2261359e0bee/regex-2026.9.10-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8ba1f78bd4fef2d8f84b894ec28ac3481afe6cc07aaa253ad4717ef7b3fe6bcb", size = 858582, upload-time = "2026-09-09T20:58:25.607Z" }, + { url = "https://files.pythonhosted.org/packages/47/f0/f9a838ca6219ae4821de0175e4548db73ef56de5ec08d032fb427732fa07/regex-2026.9.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:217e98ba5fc8908ed8ffd4ebac04753a0c831067cbfb495b9821b94cc61eaa76", size = 791405, upload-time = "2026-09-09T20:58:27.406Z" }, + { url = "https://files.pythonhosted.org/packages/f7/bf/67d71cc4e13ae2e0022d21243ca069e0868701a99eb29cdecd13d356e694/regex-2026.9.10-cp314-cp314-win32.whl", hash = "sha256:b298cdc33c5cc6969ff07f0fba19cc73e0fd8576373c50935feadaca2f6b4405", size = 272702, upload-time = "2026-09-09T20:58:29.116Z" }, + { url = "https://files.pythonhosted.org/packages/c1/38/40a93e72703a741235115ed1b1e5f6b869917677b7643005034ce1611d70/regex-2026.9.10-cp314-cp314-win_amd64.whl", hash = "sha256:c32818b28bcd153b25b63038348a9fe9b9fbcddb60df43f204c3ab55eeb57f77", size = 281170, upload-time = "2026-09-09T20:58:30.899Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ac/e95387f00617c16bb41b786d900bacd17eab88afa81b4f263df532b3a731/regex-2026.9.10-cp314-cp314-win_arm64.whl", hash = "sha256:75242f44a3e283106077be4ab717bc535e4701c9d54ad69e195945c22f137a1d", size = 281511, upload-time = "2026-09-09T20:58:32.594Z" }, + { url = "https://files.pythonhosted.org/packages/39/e5/a4b12262edc488a8a7a95b672db317dd8aa9bf2fab98297f9c91bb11ad4d/regex-2026.9.10-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2dd9286093c71afc8f55ef035c5b9d2776641fd72c6535f1febc92d0b0be9666", size = 501128, upload-time = "2026-09-09T20:58:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/55/c8/9ca31c0fa5197ded8614c8ae0e105bcff2d979025ffa3c75580baa334e2f/regex-2026.9.10-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:71879292c9c7ac67b1680345b16daba1be937cb027362cfa04e68f65db2dcfdd", size = 299428, upload-time = "2026-09-09T20:58:36.296Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d1/ee7662561735f90475443c3ca1977e5cecaeaa8f29620dec75580aebc839/regex-2026.9.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ccd139b2061132e7b265cfb4b4721baeb9f8928b81415304abf1ec7e3181c26", size = 294494, upload-time = "2026-09-09T20:58:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b1/333168e45ed6cfe71f6d17e21e5f54725f44d171f84edd12469a2f739227/regex-2026.9.10-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7327795089ddb44912dce1434e1d7244be2e9fb48fcc2d6782936af7a3062db", size = 814925, upload-time = "2026-09-09T20:58:40.438Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a5/df1b38536d0a3b24a030eb4130ce98b403cc925220ff530f5313a7c436eb/regex-2026.9.10-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff4d7b14ea19e50c8d9d6d83f45bd9b45cbb624c07ac1fa54db0a019049abed7", size = 873323, upload-time = "2026-09-09T20:58:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/93/ea/aa71fe62dd63a8336bbdec1ff002a6c53b40ccfca95961c18ee4f09bf03c/regex-2026.9.10-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f0407474ffac8e5e89d93ca41d60891e29f0ab8423eb66ff292d850a86a0843", size = 923080, upload-time = "2026-09-09T20:58:44.488Z" }, + { url = "https://files.pythonhosted.org/packages/86/38/49f8d6fd34fc1a9c75b5b96364ebdde702a8144e5ed63d2213c58d454c27/regex-2026.9.10-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ad10a135fa0b4e4a462a61d07c6654d7518cfdb5cb8da08f9ff7d61384af1fe", size = 821284, upload-time = "2026-09-09T20:58:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/32/45/91a977c96d4be13d1ade8208c260c26841c836d4c91745e1828a30589070/regex-2026.9.10-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9fbd2e5d8002dc49a6129fb321ec51c57a025e752ed525ddce0ba9223c4350a7", size = 789256, upload-time = "2026-09-09T20:58:48.473Z" }, + { url = "https://files.pythonhosted.org/packages/2d/79/4d110bf01bf9651bf9b3f88a6d9fa7e643e0586921a18432b25a478edbfa/regex-2026.9.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4a761ea45f2ad74c575ef5850ea514cef97302a552d3c7c9d1a1a870d4661d6c", size = 803722, upload-time = "2026-09-09T20:58:50.286Z" }, + { url = "https://files.pythonhosted.org/packages/e7/67/b587a0d3bbac2635309ed9c40c197120c39e1ec0afb8813cbed1338fdd75/regex-2026.9.10-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:75aa39d3f4f1650eea84e46b0d8cefe77dd5478c10e3d0aaf0b0f00493475a7a", size = 870085, upload-time = "2026-09-09T20:58:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/07/fc/0827bca20ddba1d70fa5111a2a64e6b6b38bfdab43fc435022b54172a111/regex-2026.9.10-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f5c629df03adec31ee505dda3c8988f106c9390e4cbd343600036eb8b3d6724f", size = 776970, upload-time = "2026-09-09T20:58:54.478Z" }, + { url = "https://files.pythonhosted.org/packages/9b/03/ab9d08d30568ca868791bfb99551db60b947f05e2e65dcd1261e87083c21/regex-2026.9.10-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3a66e40a1a20de96a2fee00ed67e11012b62d85b277688258677fd19997addb7", size = 863611, upload-time = "2026-09-09T20:58:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6d/8063ae86b543ae878a7d6e7ba21ebf0af4af06161230e0012e2d652320c6/regex-2026.9.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:968c1e33edd9a104d1bf24c8d476c72de7e3839ae7f894b37e9e4f4739fdeeca", size = 804064, upload-time = "2026-09-09T20:58:59.066Z" }, + { url = "https://files.pythonhosted.org/packages/c5/63/b19305c4b8d3d7867699f7d83c43550273d91a2f31793452d87edb1d259f/regex-2026.9.10-cp314-cp314t-win32.whl", hash = "sha256:fbc4e2f3cb7ce8436154e6483079e7d35eeb321a952fa936e180300630d8b873", size = 274607, upload-time = "2026-09-09T20:59:00.942Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/1695072a512a49060294024e23b945eeb87675b02c06e53a92a1b42b0bfd/regex-2026.9.10-cp314-cp314t-win_amd64.whl", hash = "sha256:c37fa93bf18bf4f90b01c0fa9f11ea567ee4b7dd8bf96e63663e5edc37aa38cf", size = 283944, upload-time = "2026-09-09T20:59:02.876Z" }, + { url = "https://files.pythonhosted.org/packages/a9/9f/65bac17f39991a67e22f8b3c849fdc02a56147c97029b5351494341959b4/regex-2026.9.10-cp314-cp314t-win_arm64.whl", hash = "sha256:ffc2da104e43db716ce30cef9f28049a1faa6aca385dd8771b033268d0730b07", size = 283780, upload-time = "2026-09-09T20:59:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/67/ca/1d1f83bc2f8fff4f186266ac82d73254e686565530cec9ab5228fb5c63dc/regex-2026.9.10-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:6afcad14310f1311d077553ed374b42a5e538f85a8c884b4e38e52de091c8077", size = 496869, upload-time = "2026-09-09T20:59:07.051Z" }, + { url = "https://files.pythonhosted.org/packages/33/42/4217510286501a2ebcd372b781b4754ac961e043fa13ef8dce803c44d89c/regex-2026.9.10-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:3fb4ae8cf83ef4e9addd43b2da31a9f45be816a8036fae8af59c8998b72718e2", size = 297121, upload-time = "2026-09-09T20:59:09.007Z" }, + { url = "https://files.pythonhosted.org/packages/55/a7/595468ed0bbccd94be92c6b5d67736ba128b204d942429a8485c2693914d/regex-2026.9.10-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f7d4656e17ab736e9415a6442a345bfc97bb8b7dcce47884bb74a37f70f08d0c", size = 292139, upload-time = "2026-09-09T20:59:10.859Z" }, + { url = "https://files.pythonhosted.org/packages/29/1c/ac92c123e0ab9bea75a904272171b356940bd6139e4a35de44f2254dca8f/regex-2026.9.10-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ba3bab0c45079735f55ac61526774de1d84bc4a0333cc554e1a4ab74913924", size = 802375, upload-time = "2026-09-09T20:59:12.823Z" }, + { url = "https://files.pythonhosted.org/packages/8d/16/d349f6fa9f908162359004e4f067353a0146e8074d24989490778244be44/regex-2026.9.10-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff6b3267318661dfddf6b3628663e00e5946bd0a5c8fa678537a1401f0388f91", size = 872328, upload-time = "2026-09-09T20:59:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/df/81/251b5aef23147057e926346fdb7c8c352d0568f65a492e4e9eb6120f6446/regex-2026.9.10-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030fa9e23624e39b3b94e46b90a5abd1a1678eb2f58fcdd3fd6c27526bf91c7e", size = 919594, upload-time = "2026-09-09T20:59:17.31Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6e/1f25319dc1b9cf4b7ffa303f3d16ca53260fe92aff45e81aa1b3c6c7cba2/regex-2026.9.10-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fbc8314436353e097c050e11b01a6c11433579437ed0579730157676ef59e2f", size = 807394, upload-time = "2026-09-09T20:59:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/3da6b3dffddf12d5e96cbbe6f5e65ebcd64f92e3388a6690a53e0878232d/regex-2026.9.10-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e6c0b5ec6ddee4032247585dc491b0fa58627745b66a705728703a3f0331231", size = 786018, upload-time = "2026-09-09T20:59:21.592Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6f/1cc86dafddc912ef44c5ccd4f729060be8c6735600932664eb6d0e25469b/regex-2026.9.10-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bf29611e5376fec8f795879bb5c6153a76c3a292573d173c26784042b01eb840", size = 793504, upload-time = "2026-09-09T20:59:23.734Z" }, + { url = "https://files.pythonhosted.org/packages/b1/cb/11692e29388d006211163627fcefa0c79917ab9f94d46a1b2254fe5efc81/regex-2026.9.10-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:ec8855f08c17895a26fbf5f19ed829722e19b34a96629e49a43c92974924026b", size = 866766, upload-time = "2026-09-09T20:59:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/3631df969830f94d83fcfc5fc71a7b39caad905e18f7b17350a94135d625/regex-2026.9.10-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94c5ce3bc41d226b4eb89ca3f842b2e28c031487fb1f34eb2153d98235831325", size = 775825, upload-time = "2026-09-09T20:59:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/762892a8e3e21d119eacaffde848aa247e6cf4e1d90c46011671e86b1b9e/regex-2026.9.10-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:9ce239acb15843ab03976626af810a4424b0409689ec2bbc52088ab5479ab487", size = 858901, upload-time = "2026-09-09T20:59:30.656Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/78048fada5c2f61d795efb26ea24f820a5564c4aa26574920284d45cd656/regex-2026.9.10-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c22df8dd6373bbe3898e77429ffc85594300e39d752fd0e68a31e59d37899376", size = 796208, upload-time = "2026-09-09T20:59:32.852Z" }, + { url = "https://files.pythonhosted.org/packages/aa/58/632681f7b9aaa3d83b40e5862ba46364a453c8eb2bc7d43fece3dc31f972/regex-2026.9.10-cp315-cp315-win32.whl", hash = "sha256:1aa309ab7ba89a62d6cf70dbd38d4176440bce3c7001ab86256704cf4c18c6eb", size = 272704, upload-time = "2026-09-09T20:59:34.927Z" }, + { url = "https://files.pythonhosted.org/packages/5d/64/81cce28754c37037b1fe740b6d7a556d51d97cf935ea4547bfab104f43f7/regex-2026.9.10-cp315-cp315-win_amd64.whl", hash = "sha256:58da726d3e766c0b3f5a3997dfaf0275898a1107b8191cdd6b0437fe45fd817d", size = 281182, upload-time = "2026-09-09T20:59:36.86Z" }, + { url = "https://files.pythonhosted.org/packages/33/28/5a13a340c9c759e863a0e7f765d323601d03d6538c8a627ed628662e083b/regex-2026.9.10-cp315-cp315-win_arm64.whl", hash = "sha256:75f9297b16fcb588a1f8d8a55dabef3c0c20b0c7bac43c87ceaaaf1a825c12f4", size = 281512, upload-time = "2026-09-09T20:59:38.875Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/a3caebcd5105be90708071db20bd261b0961b8ea4fe5e8be45c2632519b5/regex-2026.9.10-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1270cdec69248592bbe38a0b263ed58d907b891bd2b93703e225c317e421bda1", size = 501336, upload-time = "2026-09-09T20:59:40.987Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b5/ec8887b2658bf0a5df143c7c1fcd562b0abd2fa208c04ebf15c6607c9bb2/regex-2026.9.10-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:681ed38664b64c6617d3c3c332018d1948c77e139c5ea667c1886efa671e426f", size = 299318, upload-time = "2026-09-09T20:59:43.039Z" }, + { url = "https://files.pythonhosted.org/packages/d5/58/84724a9eccf6e8cd46f7e4534576093e2476a5eeb55e2453aa6606e86059/regex-2026.9.10-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:8e127d9a80cbf1c3276bb465c6d047e8705e97b58c2b8f2f0c0a69c336b44b37", size = 294847, upload-time = "2026-09-09T20:59:44.954Z" }, + { url = "https://files.pythonhosted.org/packages/50/03/70ccc5e53905984abf8eab63eebd3ce740522ef8c8d392622b64d79ef290/regex-2026.9.10-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:990797e765d89a423880052c68b61c31afe701de94a8c060f61c40605ca6c727", size = 814359, upload-time = "2026-09-09T20:59:47.194Z" }, + { url = "https://files.pythonhosted.org/packages/29/53/40f7a11ec547e4a947883c9d5e8a075f6d4f59af2b8dbcaa8bf5b504aca0/regex-2026.9.10-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e5e4a6e0734a685d13b9685622bb503bdbb2927f8b0df025a5085f0ea067475b", size = 875586, upload-time = "2026-09-09T20:59:49.537Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9d/83f3e022d99ce601727c4ef5f7b527753901ce4509ed89a1bb6a2263380a/regex-2026.9.10-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:63bb62cf62217dc38c8a6b2b61b165b0e4eb8fa93b0aba12139251c0986a8fa3", size = 920990, upload-time = "2026-09-09T20:59:51.776Z" }, + { url = "https://files.pythonhosted.org/packages/b2/64/dfdb367d8f4f5c9b8ccb4b59789c2ce996f2c69c3b8192aa986fe7d92ec4/regex-2026.9.10-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb76a9c4e07a6a47849726af0ed14c41741a182f097f134a8cf29c1bc0f4dde8", size = 818660, upload-time = "2026-09-09T20:59:54.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/a6/fb7d0b64487913845834f319aa84f8377d55305958a5db7e19c128798366/regex-2026.9.10-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31e4df2b11d48f61d511019bc1ee9b477055f17c352b68fe72db7a98b14d603c", size = 794976, upload-time = "2026-09-09T20:59:56.799Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1c/23484edaae387ea0d31f4d414463211e3516c8aa210ce8d0640f5aff3502/regex-2026.9.10-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:cf377960d2ac37d987394a9dbaa75e91338c41a46d41e1d25e90125e7b3ee2dc", size = 804081, upload-time = "2026-09-09T20:59:59.39Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/e30d138f13aecec99ea9aecef7e31563de6ec6a2f5ce1d65fc506aee33fe/regex-2026.9.10-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:c8fbd9cb30c68c1686b94029b9ef845d5870d3d65baf66cb126b676849b9d72b", size = 870430, upload-time = "2026-09-09T21:00:03.304Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/81f9ce86f7ae4f57901543597c95751fa01c41a672ec1636dd913f8a000a/regex-2026.9.10-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:53e182b6b04d0011909b47d51a2d72d908de07c7b1c7f16b3adda2204d723bc1", size = 783327, upload-time = "2026-09-09T21:00:05.68Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1b/d7bf8f91534740f6a8ca17e5ac9c5337903baf527c8de240fbac1bbedfd0/regex-2026.9.10-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:0aa7589394230e0f0a422ab6b90841ff12c87e855e7aaf75d192a54a5f124548", size = 860874, upload-time = "2026-09-09T21:00:08.41Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1d/52cc88364aca7c9013dfe9abe0fea67f8d394efe384d07798300a6e2f27d/regex-2026.9.10-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1b891f77554bff991804cee24b78b40789f7d5993a24c7907bc7025fd2a70c8d", size = 806705, upload-time = "2026-09-09T21:00:10.768Z" }, + { url = "https://files.pythonhosted.org/packages/cb/51/37a194df7707f92f33173260ba7b221d4ec376419a53babaec50019a2804/regex-2026.9.10-cp315-cp315t-win32.whl", hash = "sha256:5bef622850cf760154719d4e0d74b0a855962432995168e250069899ae12fe8f", size = 274780, upload-time = "2026-09-09T21:00:14.003Z" }, + { url = "https://files.pythonhosted.org/packages/77/12/3227a52970d90908b230f15b2c86df903f49ac72c9eedf4f6e8b5bb5e1a7/regex-2026.9.10-cp315-cp315t-win_amd64.whl", hash = "sha256:07b45ba5c94b8fcb30cb6c56a11f715c57533a3017964504322ea52690a27b72", size = 283936, upload-time = "2026-09-09T21:00:16.275Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bc/c567c5a61671f04d30e83f20b496b465432879196574d051007285576205/regex-2026.9.10-cp315-cp315t-win_arm64.whl", hash = "sha256:f70b9f0e39c2dba1d9da6bf7ef7c377cad7277f8440e9a69be05ede529ff024c", size = 283747, upload-time = "2026-09-09T21:00:19.113Z" }, +] + +[[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 = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "narwhals" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/eb/eaf5e07fcc0da7149b0e084f24e54edd7441b9a89ce7e034032ae97fe3a0/scikit_learn-1.9.1.tar.gz", hash = "sha256:629cada3e33e2b9bf376cdc7614a47a4140b8aedc1d836579e359736fbd82977", size = 7786908, upload-time = "2026-09-10T18:34:04.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/a7/25f0a43d2fde306e8ef45f45121192f687b79beaf4bae8c21607c46c5e63/scikit_learn-1.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c0f8b5d09b44101cea2767f300680bada1ea27f976fe4b48b83950a4f55a49a", size = 8775209, upload-time = "2026-09-10T18:32:42.804Z" }, + { url = "https://files.pythonhosted.org/packages/60/ea/57e57539ce175d774fc291ed091b0a6d756854b92cd92554c6bb4d0ae498/scikit_learn-1.9.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:8c14ce41d561f7749f990b41d6703fe02c4669fbc485e598e069e0a1967b488e", size = 8295541, upload-time = "2026-09-10T18:32:45.069Z" }, + { url = "https://files.pythonhosted.org/packages/78/2b/5721a174406bfba49bce20ae997b3b64cf355c3f623a2638284ab6a82156/scikit_learn-1.9.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4c20a6c017d820faa7ac8c783e3d0c6a9a2e297bf9f55332ca17cdf7fd4d04d", size = 8871592, upload-time = "2026-09-10T18:32:46.999Z" }, + { url = "https://files.pythonhosted.org/packages/8e/57/a50162f3d29feb979ab6347c6debda506dfb525bcff3c50dd17606651c7e/scikit_learn-1.9.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5d7b18a5b9dca241a74695f3275fa4c895a9dadc72b3d8df5fa9d1083c9b83e", size = 9166043, upload-time = "2026-09-10T18:32:49.343Z" }, + { url = "https://files.pythonhosted.org/packages/72/8d/27c054166bac671770d1ea0ef7716134fe8119c0df224a58c89fd735a61c/scikit_learn-1.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4b59abb30618121cc46b45972d6bf53a7128b4df4cd346c6ca6f4d5f9031e49c", size = 8262238, upload-time = "2026-09-10T18:32:51.679Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d6/493086006ea0c68ad62c40a8dece1961b61bf503f45400f133d47f56e5be/scikit_learn-1.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:d5945a2908be62350e2978344e62b56c1552c2ca4f844ebf6277c94944d647dd", size = 7902102, upload-time = "2026-09-10T18:32:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8d/b60d5e7354ff0ff5cc9400e60273696589d87a30b8b2235886a76d80d062/scikit_learn-1.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c2b312fd8c02951a364fa120ea08c1cec10d863466bf1701b013152d7537835", size = 8739300, upload-time = "2026-09-10T18:32:55.483Z" }, + { url = "https://files.pythonhosted.org/packages/2f/81/3c6392c03665d2899457a76e535a9a6f597dddddf3220fd2e1d790da88c5/scikit_learn-1.9.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:61cd968ab831a76d0ecbaf0347ab2270268716da28f94fd022497e3d6f205f13", size = 8262364, upload-time = "2026-09-10T18:32:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/0f/35/a15b8653499692879821301d48059376d6e68e8b65cd0f22d19b6ee83cd9/scikit_learn-1.9.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5990f9c69e431bfaddcde1a6d7c5355243e026bc9b9e560c13893b90dab53fb4", size = 8823698, upload-time = "2026-09-10T18:33:00.632Z" }, + { url = "https://files.pythonhosted.org/packages/23/e5/688703d357e5393f708d98eb189fd415ae69e39f6de03c6bd4005aef6118/scikit_learn-1.9.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e79d6e9b0923f1a978179822bd43d7f5543f45e970a00fe861f43486380aba", size = 9121732, upload-time = "2026-09-10T18:33:02.825Z" }, + { url = "https://files.pythonhosted.org/packages/96/45/a10add34c08184d373be9384660c75758128ca881ed27b503b6f6a742478/scikit_learn-1.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:2070f271e5375dc42c6bb93b461ab1c0aa5841d4009267e0cfd95a39dca94a43", size = 8237244, upload-time = "2026-09-10T18:33:05.26Z" }, + { url = "https://files.pythonhosted.org/packages/9e/08/7a89bcdadd1fff0d464d01056417b646c9abcbc54f7297a0a1203bba5ebb/scikit_learn-1.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:613f0a783ca05aa844a4e1ac42d48425058f2c52be73f40f8cd98b7cd111acd6", size = 7875398, upload-time = "2026-09-10T18:33:07.506Z" }, + { url = "https://files.pythonhosted.org/packages/64/e3/b58e45082dcf3dcf0eb1192ee03545ec43d8c98441dfe20e88afca8442ce/scikit_learn-1.9.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5d117952769b563067656784e03c75a2d8235a7a05cf7fffa78a311e75aac08", size = 8747414, upload-time = "2026-09-10T18:33:10.698Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ed/d68115577c8b42b0442ebd8180945d4008880a33176094640e00b585128e/scikit_learn-1.9.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:8893bc6331f60f18d4ac75e12ed356e2dcf6a564bf767918b5b7ca54c8c8be49", size = 8279175, upload-time = "2026-09-10T18:33:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6c/06c7eb61a438e389cbf3f7210897069bec5a883dbe03c189ae792781e11a/scikit_learn-1.9.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5492cf2df5226691c32611de8734bcf42148c6547ae53c7f4e6b847793addc0", size = 8881706, upload-time = "2026-09-10T18:33:14.741Z" }, + { url = "https://files.pythonhosted.org/packages/86/4e/0bab75490ca4b85fad8388739c7ebc71d9db553f8c69e39943ee8db0aaae/scikit_learn-1.9.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993d332ff80e62efae9e39603b7e872297c418d780f01a01855269a3489c950f", size = 9152030, upload-time = "2026-09-10T18:33:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/9a/13/31c6f8ba1b7eecef9dd9558576c752d2ec5785fd0e456bb9fc59305be23f/scikit_learn-1.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:ca9051447455dae341d4d591eece7deb2d8e3d1020298fc87a81fc51e4da8f53", size = 8377024, upload-time = "2026-09-10T18:33:19.941Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/6d3cb8a45f5228f915acd66b819dd6b8232ccbe24532f51d278e3991df31/scikit_learn-1.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:90de6573f733a9fb79476ff1371af52a397d41c8b35f9146e20923db010d67b6", size = 8014196, upload-time = "2026-09-10T18:33:22.126Z" }, + { url = "https://files.pythonhosted.org/packages/66/6e/6befb2d5961490d18d9dbc16a5df37aa121d08bc9893316a6a363977a903/scikit_learn-1.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b5cad1624de8b75e5b9ccb7b0ce1ff1d01306340a3efc56d5529c5ba92392eb", size = 9066066, upload-time = "2026-09-10T18:33:24.396Z" }, + { url = "https://files.pythonhosted.org/packages/cb/18/11271f2f7db337db01f598e358721b1e83989407131272e5dd64214c28a8/scikit_learn-1.9.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:d137ce8a6142029fb5c35bd82f470c40cd9e760e5e2f7694b362c497c4ab3fa2", size = 8647654, upload-time = "2026-09-10T18:33:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/e7/04/9c15d201e1b6a2e81b8215865df7646c5a360f560831769c8dc92ac1ab9a/scikit_learn-1.9.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66f852f7325b5070bc28329005aca76055a2def78faac039548ae889aeaa45a6", size = 8889070, upload-time = "2026-09-10T18:33:28.95Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5a/4cb6c85160af4a639e87a3b7bf8b1c25cfc3b504c5af710ca416a6dcfc5f/scikit_learn-1.9.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:748bcb0a4cc04aec470652c9e5ec68450948e867387e7dfade647107ade68d25", size = 9131227, upload-time = "2026-09-10T18:33:31.008Z" }, + { url = "https://files.pythonhosted.org/packages/00/0e/361440972ae3d19b90ea88a84791138a51de0e8432a770ba741b2c8d9ced/scikit_learn-1.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:38cd925e893e5539be704d5edc64dbe081aacdab6b89d8c2977c1f6a7a453ce5", size = 8683001, upload-time = "2026-09-10T18:33:33.188Z" }, + { url = "https://files.pythonhosted.org/packages/e2/8f/a9f405c5c0e2df6f343a871b40c97fb32969e3ccc38e3033dd118f3c261e/scikit_learn-1.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:b01e5b01735d38474127ca3f49319b592506225a87793b27559816b5c75cea39", size = 8257996, upload-time = "2026-09-10T18:33:35.343Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c5/74a83ea39cef7cd07f53e06cc1cf51e79f35df74f81db956835d59ec34b1/scikit_learn-1.9.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8dec64f31a6e0ec826aca6c1b39a51e16d946e400d4c0904316f3ca72ccfb825", size = 8747991, upload-time = "2026-09-10T18:33:37.4Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/cc93e8a7fe204e43d70e96e5eb89871643be20025eea05eb4fdaf19afe39/scikit_learn-1.9.1-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:e1b468241f4a7a9a7a0d6479ad3cc47681cc151a4046c530f2777c3d68f08942", size = 8279340, upload-time = "2026-09-10T18:33:39.705Z" }, + { url = "https://files.pythonhosted.org/packages/a2/61/0c6080f0d356fb966053009e7f25e9bff6cf74b0b16195ccf0c3757d1ae8/scikit_learn-1.9.1-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ca869d0080a5723cde2d5a8b54a2da1ff7e68735a9e9adb3da1243183a0fa01", size = 8881917, upload-time = "2026-09-10T18:33:41.912Z" }, + { url = "https://files.pythonhosted.org/packages/47/bb/98a31f10fffbd39edcc2f8bf4119b29652248bb110b7d45c84e68aa293ab/scikit_learn-1.9.1-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6754b7cabfc3df0b1f7b38f7a344f559bbae9d82f0ac5e3d48cccbd19fdcefdf", size = 9152805, upload-time = "2026-09-10T18:33:44.23Z" }, + { url = "https://files.pythonhosted.org/packages/f6/48/858ceff52213cfd97c0a069362071756bcb70b9fac9771b38d87c4cf7f17/scikit_learn-1.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:52cfdb1fed3a34362dbc0bd96f2e761a66fd5724d6901629f5a558f1f3bd9849", size = 8376719, upload-time = "2026-09-10T18:33:46.492Z" }, + { url = "https://files.pythonhosted.org/packages/2a/1e/5337a871bdea53effbd154b61429df048f2665653251de74a3bd8a6dea9e/scikit_learn-1.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:ae6571a4828c6f5019bcd2b4125e5b18c0af3dbc9c99726c891f45f41335ec8e", size = 8014341, upload-time = "2026-09-10T18:33:48.762Z" }, + { url = "https://files.pythonhosted.org/packages/0e/35/150383a42d83ec4c7b39f9c50bd68408ecf04c19fc30ea5198fa42e67d9c/scikit_learn-1.9.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:48fefd8eb42bd4eec3e2d348149368ccd6d71987e20c30706a56a24eb86a6e73", size = 9059953, upload-time = "2026-09-10T18:33:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/9f/dd/aa0d738808540f7eaacfab93e01982db8ef1c1c7473ef0ad38193e6aebd1/scikit_learn-1.9.1-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:09f4d73049cd63575157f6b1060e06a8c83a4bd3488dbfaeedf35ccba7aad712", size = 8646602, upload-time = "2026-09-10T18:33:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/7e/cc/687ae4214c2f598906c3b9fa5f86fbaf834b35e20360625528ff1b713f06/scikit_learn-1.9.1-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3da53831534214322d9cb240fa6f390b36cf69eba727a6d4bd3238677630d70", size = 8880366, upload-time = "2026-09-10T18:33:55.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/03/82215cb78ad1c513a4498777571fb28444621ad26ef636287551767b7732/scikit_learn-1.9.1-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caae15634feceafa2612566b109a3082d3293167fac388eedaf77bff66b51983", size = 9129640, upload-time = "2026-09-10T18:33:58.186Z" }, + { url = "https://files.pythonhosted.org/packages/d4/90/4b7af4efd7909a4a0524a9f18457e4eb2eb60616eff2c7627eda8e3cdceb/scikit_learn-1.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ffbcbbbb44202fbe9bc64bced25a145759adb9ef010b3d37a8064958ac13df2a", size = 8678083, upload-time = "2026-09-10T18:34:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/31/27/068e484d4b83004302e0d9cfc1faca69bcc010d76fbb66a642446095af1b/scikit_learn-1.9.1-cp315-cp315t-win_arm64.whl", hash = "sha256:800dd22dd87fe97dcea484c24e85dd93cf1734d86bd74e668ad18f7967f4d1b5", size = 8259434, upload-time = "2026-09-10T18:34:02.678Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307", size = 30781235, upload-time = "2026-08-21T23:28:50.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/f7/240c110c08693826b4513a52f5717d62ec7c7af72f2920821247c03b17b3/scipy-1.18.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:457fd7a2a8edeb044ab6ffbc0aa03ff6cd18491356e5e0c834d76ce621b916d1", size = 31111061, upload-time = "2026-08-21T23:23:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/05/4a/78c6285577c375e7cf27277ea8ee6961224327f1e1a0c44af5f17f23635c/scipy-1.18.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:e708533e8b2ae2497d65346538a7dcc92814410b25b81432eac66de0f2af8265", size = 28733332, upload-time = "2026-08-21T23:23:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f6/a5b82f8abbe14d134691b8b903696f701d25a081353a29dc655c364d9e62/scipy-1.18.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7bbf207c4453ce1ad2e00b17313852b33310b83090c2311bdaf97f93c0380d12", size = 20475078, upload-time = "2026-08-21T23:23:54.138Z" }, + { url = "https://files.pythonhosted.org/packages/23/22/0858a0bbd6b3e825ceb8cd9baf9eaf3b2f2b1d77727eb6be40500bcdc92f/scipy-1.18.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:78c0665edead396b1abb4897c41a5c1d9bf090c8a637a4c20a61678e0a264e66", size = 23108904, upload-time = "2026-08-21T23:23:57.824Z" }, + { url = "https://files.pythonhosted.org/packages/75/9a/2e71719f31eaefe0e3a1706c4a1ded94e664bfd95ffca2b219a671faee01/scipy-1.18.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c085faa2cfa879c5141df483f836f4d691045a078224a670fa570fa01612d89", size = 34025113, upload-time = "2026-08-21T23:24:02.209Z" }, + { url = "https://files.pythonhosted.org/packages/df/64/ff35eb9e54894cf471ff4716abd3c81eb0a0626869217ce3e6ba4ccf17d7/scipy-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f55fa87b6c612ecd6b058f167c53231b1d14e412efe361d3d6e38b3631c73218", size = 35344199, upload-time = "2026-08-21T23:24:07.844Z" }, + { url = "https://files.pythonhosted.org/packages/d3/af/c5538be1792f7034c12c7db6ee67cace58253c7b87b122d68253eaf5de89/scipy-1.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c35d74ce0e193ff740c2f2be2ac913ddc232fe6c1ff40b26cfecb9c670c63314", size = 35639587, upload-time = "2026-08-21T23:24:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/91/4c/075e4f66471bac101141ac739e9e135549be1bae584571bd03a530c056e1/scipy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2924a03db38dc2e848bca2fe9f077dafb891480b91a00a0963a8cf86dfc31c1", size = 37480330, upload-time = "2026-08-21T23:24:19.608Z" }, + { url = "https://files.pythonhosted.org/packages/39/e7/979fd14e75008623df31ba70d6bb144700f68feadcea042021c06a05bf82/scipy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:5e4d44984abc0020154ea81b247adeddcc3ac5527b975ff798bd1ba0adc513c2", size = 36658278, upload-time = "2026-08-21T23:24:25.463Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/e1525354ff9d7d5feb6d1b31af6d14072e5c91e9607b421fa1ec889660b3/scipy-1.18.1-cp312-cp312-win_arm64.whl", hash = "sha256:d65d448389b8436493abcf629cc94ad0cf32aecaf06e1acca1de53cc795f2f12", size = 24400588, upload-time = "2026-08-21T23:24:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/4540ee0f9c42a9ad7109d0d1a8cc70de54c3572b01c6693a2b1c70e90ceb/scipy-1.18.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:3ab3523da44749156e1f68b464dc56af11ae4cbc5c739a49d05f32b982eca9f3", size = 31089958, upload-time = "2026-08-21T23:24:35.8Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f5/769f36d14922b8071a43e95d24d18b6bdafad10d7f5cf647867e1ac052bc/scipy-1.18.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6fb6a55cc0ba97b59a1f288fb86dc6fce8bdfc0fffcbfd015e3a954bf2a2d93", size = 28715106, upload-time = "2026-08-21T23:24:40.775Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d7/21d890274f75ea37a8209d5519e72da3da90302e3b9fb8397a0918386a62/scipy-1.18.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea324d9dd34c38bfb9bec8ca4d1b407db97dbb74029f566b8e322b1b6fe56fe6", size = 20456846, upload-time = "2026-08-21T23:24:45.066Z" }, + { url = "https://files.pythonhosted.org/packages/ec/01/798430ecea2e78ec7c02663d5f71c007bb6abeca931080debd40d7fa55ea/scipy-1.18.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b00eb8fb802090aa903f4ea1c7f5a584779f967361e68b7e98e531cc2d7174", size = 23087986, upload-time = "2026-08-21T23:24:49.539Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5f/4634e9d35c68496e4e34cb6946eafab044458e6cedab42b40b6588e475b6/scipy-1.18.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d416b16cccfd70fbf62400e84d0bb2f4e6af519a45557f1692c749b37f14b315", size = 33998146, upload-time = "2026-08-21T23:24:54.714Z" }, + { url = "https://files.pythonhosted.org/packages/41/48/6450ed9243315322bbc19ac57b9b70d66a20bf1d38d124c96bc4bf6af9ea/scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fdaf5ea890a6183d0565f51a61799d67081bd5b1cf03c5f4b3fd3732108625c9", size = 35312578, upload-time = "2026-08-21T23:25:00.44Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/bf5a4be6a3525676499f6dff307991739ff6fdcad1481b1aeb6745339f58/scipy-1.18.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c825cef2f49e46753726a7181a8e199804a912b29519ada542c6ebc654951899", size = 35612621, upload-time = "2026-08-21T23:25:06.144Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4e/3c45c33e00a77996c4b1cb707929f833ba7b1d522ee29f882512c330676d/scipy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07", size = 37457323, upload-time = "2026-08-21T23:25:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/e0348fbc0dbab65c114cf78957e7dfeb49f8e8b556b4d930cc12ff195e18/scipy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28", size = 36622841, upload-time = "2026-08-21T23:25:18.722Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/6a77f5f267c555108f0a864b6db714363dab567a8266422a79a385f9232b/scipy-1.18.1-cp313-cp313-win_arm64.whl", hash = "sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf", size = 24399315, upload-time = "2026-08-21T23:25:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/d8eb4e280ddb56a4ab2c6f02ee49b56b23f6e977cf0802fd6d68dbef14f5/scipy-1.18.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83de5453a7799afc9048b4616bd085cef126e36412f0ea2f6370c36a2a3a51e7", size = 31090936, upload-time = "2026-08-21T23:25:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/59ea385dc3a62ff498ddf3cfff7c2b41b0f9f9d3c4122b3f1dcb6d6327fe/scipy-1.18.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9554bcc6d715ee87a633a3cc8e7703c6628b100dd29cb8a2efc4c0533c7ff729", size = 28725221, upload-time = "2026-08-21T23:25:33.244Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/6b0c288c50942d78193696c9f15f9a0874f5178aa0ddf40f83d9924b3e8d/scipy-1.18.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:011413b7426b75012840e35649e00fe0a2c3bae89fed433876e3a99251572efc", size = 20466839, upload-time = "2026-08-21T23:25:37.516Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/54fd3793c729e3b936782f181b59cbb1205bf250ab605a16cb1ba61cdd5e/scipy-1.18.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:88f0e784020649f88ea48c9f5ddfa403bf9205820667c0914740b392035afb82", size = 23089121, upload-time = "2026-08-21T23:25:42.019Z" }, + { url = "https://files.pythonhosted.org/packages/0b/56/030af62bea3cf878e0028515dff78c123b01633606a879b63f42d2db99cc/scipy-1.18.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d3ab0e8c69a17dd3559eab8cbb88f258e285c94d572c2719033f90f83290c89", size = 34053851, upload-time = "2026-08-21T23:25:47.998Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/2a844506d49651e9aa1af6ef95b6bd8031cb1d5a4375edec6155037e04cf/scipy-1.18.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac0333bdf38309aa3dcbe7e3fa7ea29e7a2c37c6ea306a757b700ded8e4596ad", size = 35329183, upload-time = "2026-08-21T23:25:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/c7370c3640e92ac9613cbf26cb3f729f9b12ddf1727b55b94b53b24d6f48/scipy-1.18.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:911de823097db8b63f034299d12662db93344e6ffa0b881cbb57748974b70168", size = 35672551, upload-time = "2026-08-21T23:25:59.387Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/ec8536f351421f8bf60a1120930638f83790f4710b8230446aca3d6159d4/scipy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:95298364e251be3e60249facbeeca03631d3bb7584f85879516ec55ac717b81f", size = 37469416, upload-time = "2026-08-21T23:26:05.432Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/d73da0d28f16c45bb9b0a5691b91610b0275c5ef0eb5e43c87cf2dc1bf31/scipy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:78a0d7c918e74a232394117160e7e3db503377572a45bcef8826e4ab8a35feba", size = 37362755, upload-time = "2026-08-21T23:26:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/e996e4dc74e10e227b1e14db5eaf6608bb6dd33884a64851c38f18dd4249/scipy-1.18.1-cp314-cp314-win_arm64.whl", hash = "sha256:cbf38d043c1aa4ab306e1ada6ab6eddacc3322a20b7af1b30bc93254b366fe09", size = 25036090, upload-time = "2026-08-21T23:26:15.887Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c9/c00213f92309d753b48903e6a451b87eb52ff5b7a16e789d1568bbf221c4/scipy-1.18.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0fcb3c93519f27bb4f0c4b0f7802cdcaca7fcf93267b75edda2e9f4e8a55cbd7", size = 31485550, upload-time = "2026-08-21T23:26:20.776Z" }, + { url = "https://files.pythonhosted.org/packages/74/b2/e3067c487982d4eeab2938928529410370c06fea84a4d3f4925e7d96647d/scipy-1.18.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ddef79fb382df40104a19bb7151b3b23e57c1778fcf857c71ceecd9bd264513f", size = 29174642, upload-time = "2026-08-21T23:26:25.395Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ab/374c9fe2d1ec014e576c781a4b5d8e1ba340e8f6b4638c16f711d2b194f0/scipy-1.18.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0e82073ecc7acc6436fac4b31674109c7e1d3e596789767eda01258a8c9e8123", size = 20916357, upload-time = "2026-08-21T23:26:30.112Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/223915c88a17317cafbf8ca2a42b11c265a9fb1e804aa665544132b5fe8a/scipy-1.18.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8bcf3c1ba5d6456e2effd30fcbd3459b044d683fcdac79a2e6830f0bdf7de487", size = 23482611, upload-time = "2026-08-21T23:26:34.846Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d1/db0948da8ca57a80b36520ef0a768b967d99f3af65f4b6f1bf6362ad4dd4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfbf154f2ba187f2ed6cce2639efff7d105f1140573642c0161615b6d91d6a87", size = 34143202, upload-time = "2026-08-21T23:26:40.4Z" }, + { url = "https://files.pythonhosted.org/packages/87/53/39d046cc7574ed6acacb6bd5723e220107ece80bff12faaf3efc4ddeede4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d33a7836f7ddc1993427966a0823468ec41bcbdb1a9f9942d1d7e57f803ba3", size = 35380876, upload-time = "2026-08-21T23:26:46.1Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/32e0e799d875a85ca57d9bde6c78148afcc0e38276df683d95854eadc8c3/scipy-1.18.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4b8bc363b6d65ee2152bec57568e3c52639bb34c46057b09857a307ed5e21d", size = 35770885, upload-time = "2026-08-21T23:26:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/88/2e/f97a666d362fee68b18f41c9c30ed502ca5c98b549749bfcb52a8b74d1eb/scipy-1.18.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11c423f1049c5755ad4409af52a9ada1cff96fe9b50795d4af3619f292901239", size = 37525424, upload-time = "2026-08-21T23:26:56.751Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d5/a9e765a84654ebba8479a1fd1b059ced1af72b168a3b2a3a46540ea38d20/scipy-1.18.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c24acac1e18912761c4700239bbc1fd32f615af690f1584d49b35859be51324d", size = 37416961, upload-time = "2026-08-21T23:27:01.546Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/e79e0d1c63ef698879d85439d37e9fb434e3b804e506a6991038d086ebd9/scipy-1.18.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9f2897bf7737392ad0d5213ea7b6add72a4edf5679b3153106aeb88b6507b3b9", size = 25331848, upload-time = "2026-08-21T23:27:05.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/4f/1bd37c883b67163e2ca1f60977a399500e6879c15defecac62831c8d078d/scipy-1.18.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:eb0dfcf4e28a99c12c999744a2ff67c9b06200e20401c7c88186e33552a46331", size = 31091484, upload-time = "2026-08-21T23:27:11.051Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c5/ba929d7feb9b2332f96827c12e0e924b61973b59b4dea383b603372c65ce/scipy-1.18.1-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:30f464bee641fa8e282577c7dce027308403213c6ca8270bba73285c91024bc5", size = 28725057, upload-time = "2026-08-21T23:27:15.9Z" }, + { url = "https://files.pythonhosted.org/packages/a4/19/68f1c50f609d955d230e66d25d02bd3e1e167ec540232135354fb9a4b9e3/scipy-1.18.1-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:1bca3b943fc2567ea49cd02c99abde49da4d5178ec46f624bd8255cda8755beb", size = 20466734, upload-time = "2026-08-21T23:27:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/ef/6d/319fa29b73d1802fa80b32a6eaf3f5be456ef81526da2716a9493bcb5501/scipy-1.18.1-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:c9d18a33309122074ea483dd92dd444189166b8b2ec429fe9ed5ac73c7a0aa23", size = 23089664, upload-time = "2026-08-21T23:27:24.345Z" }, + { url = "https://files.pythonhosted.org/packages/b7/db/30992f9b51a63de671daf3888ffd18378b6cb9ec9f2c972264238ffa7fd6/scipy-1.18.1-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f201b4c878551d48558337aab270d3c6cca5507b8737c8d8a608d234cccde0", size = 34054035, upload-time = "2026-08-21T23:27:29.409Z" }, + { url = "https://files.pythonhosted.org/packages/91/d4/bf3e735dc0b9d5a8ff45079d2540e17d3aff7a2f0048dd8f552ffd031d2b/scipy-1.18.1-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ac49ea97594532dd44b7136094d35f5440fa06e6d9c6384a74c01764df388c5", size = 35333883, upload-time = "2026-08-21T23:27:34.293Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/12d78ce9f871fe945fca588d32644e6e63f553c2a35c564d73f3b22a3313/scipy-1.18.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ceb30a00ce7c92d459819443d29ca486d882b83fb6738bdcbb2a1cce94ac5daa", size = 35673124, upload-time = "2026-08-21T23:27:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/70/cd/886219313a1012a48e6ae0ec4f302c837151beb92e1ff0d709ef8fdfc488/scipy-1.18.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f29633129f9fa7e88a3f0fca835de2d030bfc9643f7799e1a0c46cee24d38fc7", size = 37470753, upload-time = "2026-08-21T23:27:44.435Z" }, + { url = "https://files.pythonhosted.org/packages/17/6c/a776888ce618bee54fbde26172f0f46ac1da70d27b63861797fe78e1904b/scipy-1.18.1-cp315-cp315-win_amd64.whl", hash = "sha256:92c14f5bdbfb6216315ce33e78080474082de8b3830122ba97809bfbe65f75c0", size = 37361483, upload-time = "2026-08-21T23:27:49.334Z" }, + { url = "https://files.pythonhosted.org/packages/ab/09/97b651691322ebee97999b017ffc18a15a0b815103844c97e8da9d469731/scipy-1.18.1-cp315-cp315-win_arm64.whl", hash = "sha256:e402cf31eb68f453dbb2d36fc6d722b33f24a55d68b2ae1d92fa6305ca71c298", size = 25035883, upload-time = "2026-08-21T23:27:53.596Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0f/9ec20467bbabd0d44e2a77d0fd3d124f884b4d67df92af82c91d2d6a486f/scipy-1.18.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2a0b02f9fc46f8520330c23d45e6560db7e3a0d927232139427637f98943e11d", size = 31474926, upload-time = "2026-08-21T23:27:57.993Z" }, + { url = "https://files.pythonhosted.org/packages/8a/58/dcb79161e56efbedc50079fcd2f5fe427a0ebb53022eb476aa73c015ad8f/scipy-1.18.1-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:1d73131e358976663dd969e1fb4ed1404b815cd977eaaedc3b3a133ba2d81c35", size = 29164940, upload-time = "2026-08-21T23:28:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/71/d3/1eeea80c817fcb8ef7bd4a05a58824977a0e57a375cfc3d7ea7c911c01ad/scipy-1.18.1-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:bff0b729edd992766136b34e39cc76bc2fad905aa58897ee72a9cd000a6d8443", size = 20906742, upload-time = "2026-08-21T23:28:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/54/46/e59350428b6099301a20128108c995e2eb175a43f383af9a346e38824f9b/scipy-1.18.1-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:10ac20c69d880f77f375db44c22e3e6a644f9fefa291d4cd2fb9790a89fc99fd", size = 23472183, upload-time = "2026-08-21T23:28:12.109Z" }, + { url = "https://files.pythonhosted.org/packages/89/31/cc91623fa98f0621766a0f0aaaadb2c66de74a7ea7e3837164f6e4354260/scipy-1.18.1-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33a834464fdabc0f26a45508df31b3cc5d028e04dbf6c5ed398541418e0a12fe", size = 34130796, upload-time = "2026-08-21T23:28:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3e/8572ef536957ddb8aa81bb4090d9e25f257e3b4e05d97deb54319deb8a3a/scipy-1.18.1-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49023963c193dacee096301452f223ee24d86ec5807f8df93c0f7221d119e305", size = 35374253, upload-time = "2026-08-21T23:28:23.732Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c6/59fdeffb4f1435299f93d9dc8140b43ad2916e6cfc944be6c3041fcec86d/scipy-1.18.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:d84a09d0dad90ba6525d8ac1c2334b33e64bf3ccfe9e841f02feb867a22681e4", size = 35758543, upload-time = "2026-08-21T23:28:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d9/135be205d9de8783193aff9cc3bf483a03a38e4b29432c954e8cb66ac14e/scipy-1.18.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:179ce34a8d0fe273d8883ba59e17e052247d08973dfcb743ca52bb1cce2d60b0", size = 37521946, upload-time = "2026-08-21T23:28:35.245Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a2/5b7d5270621ab7cfa3f7766067bf95dc360b5efb6394694e8143b4156e2b/scipy-1.18.1-cp315-cp315t-win_amd64.whl", hash = "sha256:5632e3ae3d09197c446310cd5187de63e28448ce22f0f67b2b93d97503c0c230", size = 37408295, upload-time = "2026-08-21T23:28:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/63/ad/741c19fcb66755ff953daf9243af8480e4bf3d7fbe57583c178c7d2b6b51/scipy-1.18.1-cp315-cp315t-win_arm64.whl", hash = "sha256:eda632a7981f69730d6281f451db9c1c370993a2c0d7ddb43e2a809a2862b83a", size = 25319710, upload-time = "2026-08-21T23:28:45.713Z" }, +] + +[[package]] +name = "sentence-transformers" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "tokenizers" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/a1/53ae87971817e2d8370f8e79b843a881be33ab339502d71c5f82ac31f7af/sentence_transformers-6.1.0.tar.gz", hash = "sha256:299025df51550dc1a38f05be27a9b0bf881c4e5e70542b3b7757d05e00aa3868", size = 576566, upload-time = "2026-09-18T10:44:24.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/6e/9115e19589c83172bd37ec793cfef7f7c719464053fa6a8882700c1f4488/sentence_transformers-6.1.0-py3-none-any.whl", hash = "sha256:eb8122f4d180f552eda26dc3d77e84e8c11dc2b1d456a406b9f24abb70ceeadd", size = 740560, upload-time = "2026-09-18T10:44:22.841Z" }, +] + +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + +[[package]] +name = "sgmllib3k" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750, upload-time = "2010-08-24T14:33:52.445Z" } + +[[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 = "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.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/dc/6c58154c1c65f758ea979e7139cb76993a9cfc662d14e9be3c4a667cfb77/threadpoolctl-3.7.0.tar.gz", hash = "sha256:61348cfb77d53b9242e0017029244b559b810c142ced65b4e21eeca1843959a7", size = 31961, upload-time = "2026-09-15T15:46:20.263Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/3f/f88a53f60a472b46f4023f56d204dd7de33d34c5d2acbfa0d70a674e639e/threadpoolctl-3.7.0-py3-none-any.whl", hash = "sha256:cd8b60b5641b45c67bbf73c64c843235fc2d8a480c87389f52f5dbee893b86be", size = 26362, upload-time = "2026-09-15T15:46:19.168Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/1e/bc6587c5ab643b2e17776cace9070a2ae73549c86bffac9934a600bf3c31/tokenizers-0.23.2.tar.gz", hash = "sha256:7f0f085686b9de0d0079e6f874ae053600db64c5d13049e0bbc0119926d25aac", size = 385745, upload-time = "2026-09-03T08:55:42.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ed/8a443528baa6fac8dfe8c3b75b038c63ac92bb539bcabe311e227c718173/tokenizers-0.23.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:85a9a357a3764aecc904ee76bdaf8cf1ad8e5a67a1b929a487c4a39b49ed0e90", size = 3148852, upload-time = "2026-09-03T08:55:30.874Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/22da045a91732384d3a3771816bf188dc5a1f702c32e635afa7c679c0bef/tokenizers-0.23.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:986670e43691469dcee610ea0f846f91a8f84e91fc6f7a48d4c064414c0ec2bf", size = 3101593, upload-time = "2026-09-03T08:55:28.587Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4d/8f569ed49372a3ed8e57099bd515055fd48d7c95912c4307cda6973c2168/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a37039b5dfc4af84eb3ef0a92f4307e28936c8f9adccba2629d36f652e9bf7a2", size = 3516830, upload-time = "2026-09-03T08:55:14.741Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/e2f14c8919d5bf51874051d00d6c7b7e0e8bde6c6a2dbeddda7f642896ff/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b7e37ba198f24150f523e1242e83c4970de4a525480586be5dcc24d9add32c5", size = 3407975, upload-time = "2026-09-03T08:55:16.842Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bd/93c69152d02ef06ce47aed8b2bf4952dcf733c935a62791873932b2934d9/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43e4f2071e3cc8d5d86421c874aebc82659bb51a68bcdef5a0da75ee89511ccb", size = 3748165, upload-time = "2026-09-03T08:55:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b7/56b84b80bc96942bba8eb23751a9e8a1fce4faaf4390425e7083f721c98c/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325fee2e0418a9dc6c9ecf736a5f5f0db7875183ace9549ae339da76f7a1fbb7", size = 4024165, upload-time = "2026-09-03T08:55:18.806Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/0175e216f005c2fe08238292663aa41e4c802b216e71047a69a0e9fc6fa3/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:950d7c9426fa72406a0ffeacdbc0bb9985f5db20eb8b263f29c79aaf83105703", size = 3591899, upload-time = "2026-09-03T08:55:22.752Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ca/ca6b93c7820df123b2662a9469e8facc826ccc94e98fdd0d615f6431e73a/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41c2f84d172449b4dadb9cdc508e3e364076613c35b16e76ecfe47a60d1e3305", size = 3386843, upload-time = "2026-09-03T08:55:26.584Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a4/4f9106d317b14a80aefea9f0e3a8d07ef25f856a7607eb7f5ab894281fcb/tokenizers-0.23.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:12f0835dc2ee694746a76adf7b1567d4346a4a502ebe93fb1f5f80ea49799b78", size = 3577314, upload-time = "2026-09-03T08:55:20.825Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6a/1552b70fb0d9ab074fd3fc961435d01364e79c9058481822c3af6e8d402c/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eb2f9c8a24da020ea8c11a01a19c1c2547912d92121ae4a01cfbca46125dee40", size = 9967367, upload-time = "2026-09-03T08:55:33.188Z" }, + { url = "https://files.pythonhosted.org/packages/06/01/3ccb3a956c7528b2507b8a9714155c4baf86af593039db6ea375dd0c96c3/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f486f402f6f9abee5bb032553736813af0c710a86b2e0ca592634c55cea1f835", size = 9811886, upload-time = "2026-09-03T08:55:35.642Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/7038e612d48bda1599457f712f6bd3854eae1a9dc9c13aa47f835349db48/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:bef235815a067b2648caf6dcc7a71091b0b0fff9ee8057f6451eb9335fae52ef", size = 10146224, upload-time = "2026-09-03T08:55:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d8/8e9e4e0b287a338d8f88976729628c9d22e8a54cfaf9777018a7f7cb58a0/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5c56bda1511921587789163e524d196ed8284174ac23abd7685d5ea8da6c4718", size = 10256304, upload-time = "2026-09-03T08:55:40.977Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1f/c79a01f671a49728ebb0b61f7ff9ea45663b66cab40bc0858e9859b25c16/tokenizers-0.23.2-cp310-abi3-win32.whl", hash = "sha256:debf978920d93ba9c219bd67cc4bbfaf912c9039e41e7a28b91ec15e3728c95a", size = 2592809, upload-time = "2026-09-03T08:55:48.02Z" }, + { url = "https://files.pythonhosted.org/packages/db/f7/0a69ac6b82dbccf3f71add938a161c497952749294b8dd6dfe03a819dc40/tokenizers-0.23.2-cp310-abi3-win_amd64.whl", hash = "sha256:2e96f5699d5249c9c64aa8412e044f727aae3a4098cf830f9901ec1afc361cde", size = 2863236, upload-time = "2026-09-03T08:55:46.193Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/dee84cb44175be1b4c35bd2f770727494e78f0bb38e571a623ade94dbebb/tokenizers-0.23.2-cp310-abi3-win_arm64.whl", hash = "sha256:e49c394456dd9985787fec76132438ba3fb8911f857b1bf3d40119f9292d41aa", size = 2729352, upload-time = "2026-09-03T08:55:44.345Z" }, +] + +[[package]] +name = "torch" +version = "2.14.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/17/76/bb4770f56cf6d8971671dbcbb7493e5a6a15ad2825f4e359b02c27c38297/torch-2.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c1f844f1c750e87df4b68bc3afbc0e2b0c7ef19d7b8f666e48bdcf6a0c4f0056", size = 127303200, upload-time = "2026-09-02T13:43:20.311Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/ec44bacf2c8886b85ba4ca2285e8b09f2dff5d9c99e6a031326954082cb5/torch-2.14.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ada340e62591d06a2bcc2d68170f20f45f0b0665d372dc510a8ed7eb3b1d609a", size = 454010251, upload-time = "2026-09-02T13:44:24.927Z" }, + { url = "https://files.pythonhosted.org/packages/15/71/49399acd41f750a906c686bd23c08a2001ccb8dd25f2971003c2ed89c1dd/torch-2.14.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fecffb58f51fd643d213acd68da21cc3fc19bea05a3bc64b4ee55128f47a4963", size = 554620488, upload-time = "2026-09-02T13:44:49.442Z" }, + { url = "https://files.pythonhosted.org/packages/be/16/9489b137112040f9911d7527e452854f21cc4e499ce0da79864e6a7451a7/torch-2.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:cad84f41bbdf3dcf333ce394aeeaf25237c4d94fd6b659ba5eb813c829978823", size = 124114011, upload-time = "2026-09-02T13:43:55.034Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/0db773452c2a62b37761d3f418acf933d381f9e87077036fb57c2a386c37/torch-2.14.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9d4b1022a5d9b71282ec67ad0d9e7235870096b8a246dc1c32d6ea1fc83dc998", size = 127311393, upload-time = "2026-09-02T13:43:59.607Z" }, + { url = "https://files.pythonhosted.org/packages/13/36/537fd9da2adad49e7b2bb20741398625bee548493158274e87369a8eed56/torch-2.14.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:731b9ebdea402b8b1996d4c2ae613b16660e559b19e47bdc45d970568bc91c53", size = 454010525, upload-time = "2026-09-02T13:45:03.719Z" }, + { url = "https://files.pythonhosted.org/packages/21/f1/39bd13b21f57d1982b7f3ddf663f01c7266e2957714880744eba9e8c8d11/torch-2.14.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:84bf384779a10c02fc3c6bdbab71a9cb66b0dd93c652d1ed5d6dfc0cb37e5962", size = 554619993, upload-time = "2026-09-02T13:45:28.209Z" }, + { url = "https://files.pythonhosted.org/packages/89/a8/683d9c44737554b67ca76dd2db4f42258a0f014246cb511293e51e0154bd/torch-2.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0e7cf18cb0d8bd666b6120932e29c7aef3502b61a08b44da4839580c539a7cdb", size = 124113865, upload-time = "2026-09-02T13:44:33.705Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/1241e7db5ccc2455f8735bd6b1becfad39916206ad18001c4c0014d139e2/torch-2.14.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:860423e970f2ce02c4476e8e2d1350131b1c5c5a5e4912180e78b50b53241efa", size = 127321431, upload-time = "2026-09-02T13:44:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/bbeba65e3fbb4b8f61ea19cf4ebea9f4268cc6fe64e6f7d38bfb6cc152eb/torch-2.14.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b985c7defeb8d28691b7513ebaecc64946c5f68ec770e9f4ddba39688864f46a", size = 454027631, upload-time = "2026-09-02T13:45:43.73Z" }, + { url = "https://files.pythonhosted.org/packages/50/75/8d2b9a7e724759470c209489b79260cac537f091cc9aac8001c8d2bc845c/torch-2.14.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b2cd92bce63d40bf6fc2e5d840fc2f0063bd180a242daa761cb6088cc2f46e27", size = 554623549, upload-time = "2026-09-02T13:46:04.252Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/a5475c00b0555e686333e6b4036f2213e7cbea021a772ce6f9ced4dcbd2f/torch-2.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:44b044b9f6f633d982839422a57433d6a1da520037fd88e0c8a47efde589b3b8", size = 124110863, upload-time = "2026-09-02T13:45:12.764Z" }, + { url = "https://files.pythonhosted.org/packages/a5/49/bbaee76337742a42d2c5b0296ea62252567050e1d821bc2283b2e72ba6fb/torch-2.14.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:553aec938d37d77b783bcf801e638cad068870e7643c47eadac21eb180f551ed", size = 127653463, upload-time = "2026-09-02T13:45:17.1Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/6cc7a511bab384fbe8f4b8ddeecdd22e724b2162d6f15076f07a0a7ef15b/torch-2.14.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cd8cd8f714d511ccdca907282d1da3d9be8322d4e1520b9c3bce39a5c1318a4b", size = 454009927, upload-time = "2026-09-02T13:46:19.637Z" }, + { url = "https://files.pythonhosted.org/packages/b6/da/0f04fe15fd05bf3f613a359b14acb44c35ae06fe69da0aefa8b5cdb4f9f9/torch-2.14.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d2526f71e6638133b97b3cd2881ece3df521460a4727030e8ae2a72a7d3ae31d", size = 554580530, upload-time = "2026-09-02T13:46:34.818Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c3/72ae1f02747b1f012e1975743e48cd608f83095d7f9ce58de78b79248b35/torch-2.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:731784e3914843c6bcc7aba3987ff7610ac57dbbc816a5d6b9b62e04c240a641", size = 124400194, upload-time = "2026-09-02T13:45:53.555Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/ea/b2a5bd54b28a324dae8211928b2d730b6547500342c7e6c6dea08bd0a485/tqdm-4.70.1.tar.gz", hash = "sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4", size = 171846, upload-time = "2026-09-11T07:25:16.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl", hash = "sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73", size = 80199, upload-time = "2026-09-11T07:25:14.599Z" }, +] + +[[package]] +name = "transformers" +version = "5.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/9e/750649904a065007a838981785b2bd8d9ff26154c6c341ac67d0b7f82c68/transformers-5.17.0.tar.gz", hash = "sha256:a153be279169b55b92d8000bf4af294aed684503d091cca7804da2dd8a9de000", size = 9817878, upload-time = "2026-09-09T15:39:56.886Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/d0/c502b60d684adbd98a8dc7d5bb866842772b816ac4354e4608be240041ae/transformers-5.17.0-py3-none-any.whl", hash = "sha256:78ec1ce21579b38dfb83950a0658cd119f87212a2fcfdff478096ce9d6c03801", size = 12295140, upload-time = "2026-09-09T15:39:53.746Z" }, +] + +[[package]] +name = "triton" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/07/0f8cd8e8db0472334253efdaaab3d0819fea27aa99bf0e7f1aeea4ceb5ae/triton-3.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9c404c69ed4a39e8ec632eaf6b9fe058a060bf98979c177f6ef666f06bb8d50", size = 226474486, upload-time = "2026-08-28T16:08:18.29Z" }, + { url = "https://files.pythonhosted.org/packages/c1/09/b7012e5bfae67640f268aa584caa80fe1674f6b0da949046b679972c33e3/triton-3.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e91ffa46d095b252248297292dd22bcbacd53a125a0c2eefbbbf74925a320bc3", size = 247972921, upload-time = "2026-08-28T15:55:53.157Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/4c564374bcdadb166fccbf3e45aee0d4a473f88d341761bd2fefe3b8e8c1/triton-3.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7004666652f500ed854a86988e4b3d69d247188b5d2092b5df1e44f4a954099", size = 226476793, upload-time = "2026-08-28T16:08:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/3394d5548404c1cabd1dadadd28d0b3f9478db1dff8180da53bb3f0a1e19/triton-3.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f0497218e26b7d79773ad9c2a3fa3b539ee69f587a13fac2e552b1d322a8015", size = 247975122, upload-time = "2026-08-28T15:56:04.112Z" }, + { url = "https://files.pythonhosted.org/packages/b8/59/bf0e9493118bb353ab59a5d6a65db3618d9b314417cc1459f0121e0ec5c9/triton-3.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f6b48d0591929a3867973acac3dccd4e058585f91bfb41022de496c9ffab304", size = 226488654, upload-time = "2026-08-28T16:08:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/93/d9/08c75f3459f19ad00425b564058e40efa4bcd79b816064cf27499303ea42/triton-3.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:387dae4cb0089a7b6ba1a428ae0782b65c4c58f57d94617cb22ca8593d8ccbca", size = 247972313, upload-time = "2026-08-28T15:56:14.007Z" }, + { url = "https://files.pythonhosted.org/packages/7c/34/429c5592181cfb7361a0a8e0bff218e7224b726709d75da2472b3e819f70/triton-3.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b84e7d512490ba529111260fa6f7cad8b254a6bb5fbdf41d5ef9a5e57f52d0a", size = 226591133, upload-time = "2026-08-28T16:09:02.271Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d1/aa8a3e935c37efee7945984fdb64d7e0851bf6d920afd97b2d21f9d23360/triton-3.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74217bb56ed8692759227758e4c4b3bd2d608a209c1a7a081bf361fb4c2c1bf9", size = 248077577, upload-time = "2026-08-28T15:56:24.94Z" }, +] + +[[package]] +name = "typer" +version = "0.27.2" +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/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[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" }, +]