diff --git a/.gitignore b/.gitignore index 83972fa..837580e 100644 --- a/.gitignore +++ b/.gitignore @@ -206,6 +206,10 @@ tempCodeRunnerFile.py # Ruff stuff: .ruff_cache/ +# step-up training artifacts and local data +outputs/ +data/processed/ + # PyPI configuration file .pypirc @@ -216,3 +220,7 @@ __marimo__/ # Streamlit .streamlit/secrets.toml + +# Slurm job logs (scripts/train.sh writes train-.out into the submit dir) +*-[0-9][0-9][0-9][0-9][0-9][0-9][0-9].out +slurm-*.out diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ede1c38 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "external/ReBIND"] + path = external/ReBIND + url = https://github.com/holymollyhao/ReBIND.git +[submodule "external/GTMGC"] + path = external/GTMGC + url = https://github.com/Rich-XGK/GTMGC.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee936fb..dc16807 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,14 +4,17 @@ Thanks for helping improve `step-up`. ## Development Setup -Clone the repository and install dependencies: +Clone the repository (with `--recursive`, so the vendored ReBind submodule under +`external/ReBIND` is checked out) and install dependencies: ```bash -git clone https://github.com/LeMaterial/step-up.git +git clone --recursive https://github.com/LeMaterial/step-up.git cd step-up uv sync --dev ``` +If you already cloned without `--recursive`, run `git submodule update --init --recursive`. + Install the pre-commit hook: ```bash diff --git a/README.md b/README.md index ba92bc8..6f88603 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,24 @@ A benchmark for 2D to 3D conformer generation models. ## Getting Started -This project uses [`uv`](https://docs.astral.sh/uv/) for Python, dependency, and -environment management. +This project uses [`uv`](https://docs.astral.sh/uv/) for Python, dependency, and environment management. + +The benchmark models are vendored as git submodules (`external/ReBIND`, +`external/GTMGC`), so clone with `--recursive`: ```bash -git clone https://github.com/LeMaterial/step-up.git +git clone --recursive https://github.com/LeMaterial/step-up.git cd step-up uv sync --dev ``` -Run the test suite: +If you already cloned without `--recursive`: + +```bash +git submodule update --init --recursive +``` + +Run the test suite (CPU-only, ~30s): ```bash uv run pytest @@ -26,15 +34,371 @@ uv run ruff format --check uv run ruff check ``` -## Usage +## CPU Smoke Runs + +Each config under `configs/*_smoke.yaml` trains a tiny model on a 100-molecule +subset for 3 epochs on CPU. The goal is to verify the pipeline end-to-end (data +loader --> model forward --> loss --> optimizer), not to produce meaningful +metrics. Loss should decrease monotonically across epochs. + +```bash +uv run step-up train -c configs/qm9_smoke.yaml # ~25s +uv run step-up train -c configs/tmqmg_smoke.yaml # ~5 min (larger complexes) +uv run step-up train -c configs/bostmc_smoke.yaml # ~6 min (larger complexes) +``` + +## Production Training + +Each benchmark model is a thin wrapper over a vendored upstream implementation, +selected by `model:` in the config (`rebind` or `gtmgc`, see +`step_up.models.MODEL_NAMES`). Both descend from the same code base, share the +dataset and metric code, and need the same two fixes: the Laplacian positional +encoding is added out of place so fp32 training works, and charge/spin +conditioning is bolted on for the organometallic sets. + +ReBind additionally hardcodes Lennard-Jones parameters only up to Kr, and +KeyErrors on anything heavier. [lj_params.py](src/step_up/models/lj_params.py) +carries the full UFF table through Lr, identical to theirs over Z=1..36, and the +wrapper patches it in. This is not cosmetic: 65% of tmQMg and 46% of BOSTMC +low-spin structures contain at least one element past Kr, usually the metal +centre itself. + +Three full-scale configs are staged. They target GPU (`device: cuda`) and follow +ReBind's published QM9 setup from +`external/ReBIND/experiments/conformer_prediction/rebind.sh`: 8 encoder + 8 +decoder layers, d_model=512, AdamW with betas (0.9, 0.99) and eps 1e-8, lr 9e-5, +linear warmup over 10% of steps followed by linear decay, gradient clipping at +1.0, batch 100 with the last partial batch dropped, and 20 epochs. + +The one setting not mirrored is precision: ReBind trains with fp16 mixed +precision, this loop in fp32. fp32 is the more precise of the two, and upstream's +in-place Laplacian write (see [rebind.py](src/step_up/models/rebind.py)) only +survives autograd under autocast, so their fp16 looks like a constraint of the +code rather than a modelling choice. + +| Config | Dataset | Rows | Split | Notes | +|---|---|---|---|---| +| `configs/qm9_rebind.yaml` | QM9 (gdb9.sdf) | 130,831 | published (ReBind/GTMGC) | Organic reproduction target | +| `configs/qm9_gtmgc.yaml` | QM9 (gdb9.sdf) | 130,831 | published (ReBind/GTMGC) | Same data, second model | +| `configs/tmqmg_complete.yaml` | tmQMg complete | 60,799 | published (TMCgen) | Matches the published baselines | +| `configs/tmqmg.yaml` | tmQMg, outliers removed | 58,409 | published (TMCgen) | Cleaner data, not comparable to them | +| `configs/bostmc.yaml` | BOSTMC low-spin | 121,496 | project random split | Singlets + doublets, full d-block | + +Molecules carry a formal charge and spin multiplicity, which neither upstream +model takes. Both are fed in as scalars (charge in electrons, and unpaired +electrons = multiplicity - 1) through a small MLP added to every atom embedding. +Scalars rather than one-hot categories because BOSTMC's charges run from -8 to ++8 with single-structure tails, and because a scalar lets you ask a trained model +for a charge/spin state that never appeared in training. Set `charge_column` / +`spin_column` to switch it on; QM9 needs neither. + +Splits come from one of three places, in order of precedence: `split_files` +(published ID lists, used for tmQMg and BOSTMC), `split_column` (a published +label per row, used for QM9), or a hash of each molecule's ID (`id_column`, or +the CSV row number) and `split_seed`. The hash keeps a molecule in the same +split regardless of `subset_size`, filtering, or which rows fail featurization, +so different runs and models are scored on the same test molecules. + +To dry-run a config (validates the YAML and dataset path without training): + +```bash +uv run step-up train -c configs/qm9.yaml --dry-run +``` + +```bash +sbatch scripts/train.sh configs/qm9_rebind.yaml +sbatch scripts/train.sh configs/tmqmg_complete.yaml +sbatch scripts/train.sh configs/bostmc.yaml +``` + +The Slurm script lives at [scripts/train.sh](scripts/train.sh). It (1) +initializes the submodule, (2) runs `uv sync --dev`, and (3) launches +`uv run step-up train -c `. Each run writes its config, TensorBoard +logs, per-epoch history, best checkpoint (by val D-MAE), and the test-set +metrics of that checkpoint (`test_metrics.json`) to the `output_dir` specified +in the config — default is `outputs/_full/`. + +### Scoring a run and exporting its structures + +```bash +sbatch scripts/eval_and_dump.sh configs/bostmc.yaml +``` + +That scores the test split and exports every predicted structure from the same +checkpoint, so the metrics and the geometry can never describe different states. +It writes `eval_test.json` (heavy-atom RMSD), `eval_test_keephs.json` (RMSD over +all atoms) and `structures_test.csv` into the run's `output_dir`. + +`structures_test.csv` carries one row per test molecule: + +| Column | | +|---|---| +| `id` | the row's `id_column` value — `mol_id`, tmQMg `id`, BOSTMC `refcode` | +| `charge`, `spin_multiplicity` | from the source CSV; **blank when the data does not say**, rather than defaulted | +| `n_atoms`, `elements` | atom count and symbols, in prediction order | +| `xyz_true`, `xyz_pred` | XYZ blocks, same atoms in the same order, overlayable as-is | +| `d_mae`, `d_rmse`, `rmsd` | that molecule's own errors, for sorting and filtering | + +`xyz_pred` is Kabsch-aligned onto `xyz_true` per molecule: rotation and +translation only, so bond lengths and angles are the model's own. The alignment +is done here because both vendored models align inside their prediction head over +the *padded* batch tensor, where padding zeros drag the centroid and rotation off. + +`scripts/compare_runs.py` reads those dumps plus the metrics JSON and prints the +comparison table below, recomputing D-MAE from the geometry as a cross-check that +the two agree. + +### Reproducing ReBind's published QM9 numbers + +`configs/qm9_rebind.yaml` trains on the QM9 copy ReBind and GTMGC used +(HuggingFace `RichXuOvO/HFQm9`), with their published split. Fetch the raw data +and build the CSV once: + +```bash +mkdir -p ~/step-up-data/qm9 && cd ~/step-up-data/qm9 +for f in gdb9.sdf train_indices.csv valid_indices.csv test_indices.csv; do + curl -sLO "https://huggingface.co/datasets/RichXuOvO/HFQm9/resolve/main/$f" +done +cd - +uv run python scripts/prepare_qm9_rebind.py \ + --raw-dir ~/step-up-data/qm9 --out ~/step-up-data/qm9/qm9-rebind.csv +``` + +Molblocks are copied out of `gdb9.sdf` verbatim, so bonds are the published ones +rather than re-perceived from coordinates (`dataset_source: sdf`), and each row +carries its published split label (110,000 / 10,000 / 10,831). About 1.4% of +records fail RDKit sanitization and are dropped at load time, leaving +108,478 / 9,873 / 10,661. ReBind's own evaluation skips such records too, though +their RDKit version dropped ~400 fewer. + +```bash +sbatch scripts/train.sh configs/qm9_rebind.yaml +uv run step-up evaluate -c configs/qm9_rebind.yaml --split test +``` + +`step-up evaluate` uses ReBind's metric definitions from their `evaluate.py`: +D-MAE and D-RMSE pooled over every pairwise distance in the split (larger +molecules therefore count more), and C-RMSD from RDKit's `GetBestRMS` with +hydrogens removed. The D-MAE the training loop prints is a per-batch mean, so it +is not directly comparable. + +All numbers below are the same protocol on the same 10,661-molecule test split. +Published values are as tabulated in the ReBind paper. + +| Metric | ReBind paper | ReBind ours | GTMGC paper | GTMGC ours | GTMGC released weights | +|---|---|---|---|---|---| +| D-MAE | 0.254 | 0.233 | 0.281 | 0.264 | 0.265 | +| D-RMSE | 0.446 | 0.439 | 0.471 | 0.462 | 0.459 | +| C-RMSD | 0.321 | 0.265 | 0.414 | 0.372 | 0.344 | + +Two things this table is good for, and one it isn't. + +**Our training is sound.** Training GTMGC ourselves lands within 0.3% of their +released checkpoint on D-MAE (0.264 vs 0.265) and 0.6% on D-RMSE, having never +seen their weights. + +**Model-to-model comparison holds up.** Under one protocol ReBind beats GTMGC by +12% on D-MAE, close to the 10% separating them in the paper, so the benchmark +reproduces the published ordering. + +**Absolute comparisons to published tables do not.** Scoring GTMGC's *own* +released checkpoint with our code gives numbers 6% better than their published +row, and training cannot explain a gap on someone else's weights. Our whole +column therefore carries a systematic offset of roughly that size, which is most +of what makes our ReBind run look better than its paper. The likely causes are +the slightly different test set (we drop 10,661 of their ~10,697 molecules, a +newer RDKit rejecting a few more) and the published row possibly coming from a +different run than the released checkpoint. Quote our numbers against each +other, not against the papers. + +### GTMGC on the same QM9 split + +`configs/qm9_gtmgc.yaml` trains GTMGC (Xu et al., ICLR 2024) on exactly the data +and split above, so the two models are directly comparable. Their conformer +models embed Mole-BERT tokenizer ids rather than atom types, so precompute the +per-atom ids once: + +```bash +uv run python scripts/tokenize_molebert.py -c configs/qm9_gtmgc.yaml \ + --out ~/step-up-data/qm9/qm9-molebert-tokens.csv +sbatch scripts/train.sh configs/qm9_gtmgc.yaml +``` + +`step-up evaluate` also scores released checkpoints, which is a useful check on +our pipeline: their weights under our protocol should reproduce their paper. -The benchmark is in early development. As functionality is added, reusable code -will live under the `step_up` Python package and can be run through `uv`: +```bash +uv run step-up evaluate -c configs/qm9_gtmgc.yaml --checkpoint RichXuOvO/GTMGC-Qm9 +``` + +Compute nodes on this cluster have no internet access, so anything pulled from +HuggingFace (the tokenizer, released checkpoints) has to be fetched from a login +node first. Either warm the cache by running the command there once, or download +the files and pass the directory: ```bash -uv run python +mkdir -p ~/step-up-data/checkpoints/GTMGC-Qm9 && cd $_ +for f in config.json pytorch_model.bin; do + curl -sLO "https://huggingface.co/RichXuOvO/GTMGC-Qm9/resolve/main/$f" +done ``` +### Benchmark results + +Every row is that dataset's published or project split, scored with +`step-up evaluate`, and reproduced independently by `scripts/compare_runs.py` +from the per-molecule structure dumps. D-MAE is an absolute distance error that +grows with molecule size, so "Rel." normalizes it by that test set's own mean +pairwise distance ("Scale"), pooled the same way. RMSD is heavy-atom in every +row — GetBestRMS on QM9, Kabsch without symmetry matching on the MOL2 sets, so +compare RMSD across the organometallic rows freely but to QM9 only loosely. + +| Run | Test mols | Atoms | D-MAE | D-RMSE | RMSD | Scale | Rel. D-MAE | RMSD/scale | +|---|---|---|---|---|---|---|---|---| +| QM9, with H | 10,661 | 18.1 | 0.233 | 0.439 | 0.265 | 3.100 | 7.5% | 8.5% | +| QM9, heavy atom | 10,661 | 8.8 | 0.042 | 0.132 | 0.187 | 2.441 | 1.7% | 7.7% | +| tmQMg, with H | 1,360 | 56.7 | 0.892 | 1.353 | 1.810 | 5.806 | 15.4% | 31.2% | +| tmQMg, heavy atom | 1,360 | 30.2 | 0.627 | 1.058 | 1.823 | 5.205 | 12.0% | 35.0% | +| BOSTMC, with H | 12,150 | 64.2 | 0.964 | 1.496 | 2.024 | 6.581 | 14.6% | 30.8% | +| BOSTMC, heavy atom | 12,150 | 35.1 | 0.696 | 1.192 | 2.081 | 5.921 | 11.8% | 35.1% | + +**Organometallic error is about twice QM9's relative to molecule size,** not the +four times raw D-MAE suggests. Global structure degrades much further than local +distances do: RMSD is 8.5% of the mean pairwise distance on QM9 against 31% on +the organometallic sets. Local geometry is largely right and the overall shape is +not. + +**Dropping hydrogen does not produce a better heavy-atom structure.** It makes +D-MAE look much better — QM9 7.5% to 1.7%, tmQMg 15.4% to 12.0% — but that is +mostly the hardest atoms leaving the average. On the identical heavy atoms under +the identical metric, the model trained *with* hydrogen wins: tmQMg RMSD 1.810 +against 1.823, BOSTMC 2.024 against 2.081. Hydrogens are useful supervision for +the heavy-atom frame, not just extra work. Only QM9 improves at all on RMSD +(0.265 to 0.187), and barely once normalized (8.5% to 7.7%). + +Hydrogens are predicted throughout unless a config sets `remove_hs: true` +(`configs/*_noh.yaml`), which drops them from the graph so the model neither sees +nor predicts them — a different model, not a different metric. Each heavy atom +keeps its hydrogen count in the `numH` feature either way. D-MAE and D-RMSE +always include hydrogen where it is present, as in ReBind's own `evaluate.py`. + +### ReBind's rewiring never fires in the released code + +ReBind's contribution over GTMGC is rewiring the decoder's attention with +Lennard-Jones forces: compute an LJ force between non-bonded atom pairs, keep the +top-k per node, and feed those as extra attraction and repulsion adjacency +channels. In the released implementation it is dead code, and the numbers above +were produced without it. + +`Collator.__call__` samples `keys = mol_sq[0].keys()` from the *input* graph dict, +then `_transform` computes `num_near_edges` onto the `Data` object afterwards. The +padding step guards on `if "num_near_edges" in keys`, which is therefore never +true, so `num_near_edges` stays the all-zero tensor it was initialized as. With +`k = 0` everywhere, `retain_top_k` keeps nothing, and both rewiring channels reach +the decoder as all-zero matrices. Upstream's own `data/utils.py:mol_to_graph_dict` +returns the same keys ours does and never includes it, so this is not an artifact +of our featurization. + +Measured rather than inferred, on a tmQMg batch containing 4d/5d metals: + +- `num_near_edges` is 0 for all 235 real atoms, against adjacency degrees of 1-6. +- 13,380 candidate non-bonded pairs, **0 retained**. +- Perturbing the LJ table changes the predicted coordinates by **exactly zero**: + not the pre-fix flat sigma/epsilon, not epsilon x1000, not sigma x2. + +This explains two things that looked wrong. The LJ retraining was a no-op — the +corrected tmQMg run scores 0.8918838762 against the pre-fix run's 0.8918838298, +identical to 7 significant figures because the two trainings differed in nothing +that reaches the loss. And our QM9 reproduction beats the published C-RMSD (0.265 +against 0.321) while running what is effectively the paper's own ablation. + +The full UFF table is still the right thing to carry: it is what the paper +describes, and it costs nothing. But no result here depends on it, and fixing the +propagation would produce a model meaningfully different from the released one, so +it is left as a deliberate choice rather than a silent patch. + +Training on tmQMg's 2,379 flagged-unphysical structures costs 0.7% D-MAE +(0.8902 vs 0.8962 on the identical outlier-free test set) and nothing on RMSD, so +the run that matches the published baselines is not meaningfully handicapped. + +### Charge and spin conditioning is used but barely pays + +`scripts/analyze_conditioning.py` re-scores a split with the molecule-level +features zeroed. On BOSTMC the conditioning is clearly active — perturbing charge +and spin moves predicted atoms by 1.3 A against a 3.3 A coordinate spread — but +zeroing it costs only 0.2% D-MAE overall, and at most ~2.5% on any well-populated +charge state. Open-shell doublets are not the hard case: they score 14.4% +relative against singlets' 16.5%. + +So scalars added to the atom embeddings are the wrong lever, or at least a weak +one. Before spending another multi-day run on it, try conditioning that a +LayerNorm cannot wash out: FiLM-style scale and shift per block, or a global +token the attention has to read. + +### Adjusting training duration + +To train longer, edit the config's `epochs` field. ReBind's paper used 20 +epochs; more epochs only help if val D-MAE is still trending down at the end. +Inspect TensorBoard during the run: + +```bash +uv run tensorboard --logdir outputs/qm9_full/tb +``` + +### Resuming after compute interruptions + +The current loop saves `best.pt` (model weights only) on the best val D-MAE +epoch but doesn't snapshot the optimizer / scheduler state. Resumption is a +TODO for the next iteration. For long runs, prefer over-provisioning the time +budget in the Slurm header. + +## Repository Layout + +``` +src/step_up/ +|-- data/ +| |-- csv_dataset.py # CSV --> graph-dict dataset +| |-- featurize.py # XYZ path (RDKit DetermineBonds for QM9) +| |-- mol2.py # direct MOL2 parser (no RDKit, used for organometallics) +| |-- splits.py # hash-based, stable train/val/test splits +|-- models/ +| |-- common.py # shared conditioning + out-of-place Laplacian helpers +| |-- lj_params.py # full UFF Lennard-Jones table (ReBind's stops at Kr) +| |-- rebind.py # thin wrapper over external/ReBIND + 3 runtime patches +| |-- gtmgc.py # thin wrapper over external/GTMGC + Mole-BERT tokenizer +|-- eval/ +| |-- metrics.py # D-MAE, D-RMSE, coord-RMSD, per-element D-MAE +| |-- conformer_eval.py # ReBind's published metric protocol +|-- train.py # config-driven training loop +|-- cli.py # `uv run step-up train|evaluate -c ` + +configs/ # per-dataset YAML configs (smoke + full) +external/ReBIND/ # git submodule, vendored upstream ReBind +external/GTMGC/ # git submodule, vendored upstream GTMGC +scripts/train.sh # Slurm job script (sbatch scripts/train.sh ) +scripts/eval_and_dump.sh # score a checkpoint + export its structures +scripts/dump_structures.py # per-molecule true/predicted geometry --> CSV +scripts/compare_runs.py # size-normalized comparison across finished runs +scripts/prepare_qm9_rebind.py # QM9 + ReBind's published split --> CSV +scripts/tokenize_molebert.py # per-atom Mole-BERT ids, needed by GTMGC +tests/ # imports, dataset loading, MOL2 parsing, forward pass, metrics +``` + +## Data Path Notes + +- **QM9 (sdf)**: molblocks copied out of `gdb9.sdf`, featurized with ReBind's + own `mol_to_graph_dict`. Bonds and coordinates come from the record, so + nothing is inferred. This is the path used to reproduce their numbers. +- **QM9 (smiles + xyz)**: built from the XYZ block via + `Chem.MolFromXYZBlock` + `rdDetermineBonds.DetermineBonds`. The SMILES + column is intentionally unused because its atom order doesn't match the + XYZ block in QM9-full.csv. +- **Organometallics (mol2 + xyz)**: built directly from the MOL2 block via + the in-house parser in `step_up/data/mol2.py`. No RDKit* is involved; + connectivity and bond types come straight from the MOL2 file, SYBYL atom + types provide hybridization and aromaticity (e.g. `C.ar`, `N.am`), and rings + are computed from the bond graph via Tarjan bridge-finding. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, pre-commit hooks, diff --git a/configs/bostmc.yaml b/configs/bostmc.yaml new file mode 100644 index 0000000..90b79a0 --- /dev/null +++ b/configs/bostmc.yaml @@ -0,0 +1,50 @@ +# Full BOSTMC low-spin training run: singlets and doublets together (121,496 +# complexes), with the model conditioned on charge and spin so the two manifolds +# are distinguishable. +# +# Data is the filtered release, which drops structures whose molecular graph +# changed during optimization and deduplicates by initial graph hash, keeping the +# best-R-factor representative. +# +# Split: the project's own random split (seed 0, 80/10/10 by refcode) from +# datasets/filtered/splits/random, which covers all 121,496 low-spin rows. The +# similarity ("spectral") split under splits/spectral is the planned follow-up. +dataset_path: /home/gridsan/jtoney/BOSTMC/datasets/filtered/BOSTMC-low-spin.csv +dataset_source: mol2 +subset_size: null +id_column: refcode +split_files: + train: /home/gridsan/jtoney/BOSTMC/datasets/filtered/splits/random/train-random.csv + val: /home/gridsan/jtoney/BOSTMC/datasets/filtered/splits/random/val-random.csv + test: /home/gridsan/jtoney/BOSTMC/datasets/filtered/splits/random/test-random.csv + +# Charges span -8..+8 and spin multiplicity is 1 or 2. Both enter as scalars, so +# rare charge states still inform the model and inference isn't restricted to the +# combinations seen in training. +charge_column: charge +spin_column: spinmult + +n_layers: 8 +d_model: 512 +d_ffn: 1024 +n_head: 8 +dropout: 0.0 + +# Optimization as in ReBind's rebind.sh (see README for the fp16 exception). +epochs: 20 +batch_size: 100 +eval_batch_size: 100 +lr: 9.0e-5 +weight_decay: 0.0 +warmup_ratio: 0.1 +adam_beta1: 0.9 +adam_beta2: 0.99 +adam_eps: 1.0e-8 +lr_schedule: linear +grad_clip: 1.0 +drop_last: true +num_workers: 4 + +device: cuda +output_dir: outputs/bostmc_full +seed: 42 diff --git a/configs/bostmc_noh.yaml b/configs/bostmc_noh.yaml new file mode 100644 index 0000000..f5b95f8 --- /dev/null +++ b/configs/bostmc_noh.yaml @@ -0,0 +1,58 @@ +# Heavy-atom BOSTMC: configs/bostmc.yaml with hydrogens dropped. +# complexes), with the model conditioned on charge and spin so the two manifolds +# are distinguishable. +# +# Data is the filtered release, which drops structures whose molecular graph +# changed during optimization and deduplicates by initial graph hash, keeping the +# best-R-factor representative. +# +# Split: the project's own random split (seed 0, 80/10/10 by refcode) from +# datasets/filtered/splits/random, which covers all 121,496 low-spin rows. The +# similarity ("spectral") split under splits/spectral is the planned follow-up. +dataset_path: /home/gridsan/jtoney/BOSTMC/datasets/filtered/BOSTMC-low-spin.csv +dataset_source: mol2 +subset_size: null +id_column: refcode +split_files: + train: /home/gridsan/jtoney/BOSTMC/datasets/filtered/splits/random/train-random.csv + val: /home/gridsan/jtoney/BOSTMC/datasets/filtered/splits/random/val-random.csv + test: /home/gridsan/jtoney/BOSTMC/datasets/filtered/splits/random/test-random.csv + +# Charges span -8..+8 and spin multiplicity is 1 or 2. Both enter as scalars, so +# rare charge states still inform the model and inference isn't restricted to the +# combinations seen in training. +charge_column: charge +spin_column: spinmult + +n_layers: 8 +d_model: 512 +d_ffn: 1024 +n_head: 8 +dropout: 0.0 + +# Optimization as in ReBind's rebind.sh (see README for the fp16 exception). +epochs: 20 +batch_size: 100 +eval_batch_size: 100 +lr: 9.0e-5 +weight_decay: 0.0 +warmup_ratio: 0.1 +adam_beta1: 0.9 +adam_beta2: 0.99 +adam_eps: 1.0e-8 +lr_schedule: linear +grad_clip: 1.0 +drop_last: true +num_workers: 4 + +device: cuda +output_dir: outputs/bostmc_full_noh +seed: 42 + +# Heavy-atom variant: hydrogens are dropped from the graph, so the model never +# sees or predicts them. Paired with configs/bostmc.yaml to separate "the model is +# bad at hydrogen" from "the model is bad at geometry". Each heavy atom keeps its +# hydrogen count in the numH feature, so the graph is not missing the chemistry, +# only the positions. Note this departs from the published setup, which keeps +# hydrogen explicit, so it is not a reproduction run. +remove_hs: true diff --git a/configs/bostmc_smoke.yaml b/configs/bostmc_smoke.yaml new file mode 100644 index 0000000..4aa42f4 --- /dev/null +++ b/configs/bostmc_smoke.yaml @@ -0,0 +1,31 @@ +# BOSTMC smoke run on the first 100 low-spin rows (like bostmc.yaml). Verifies the +# direct MOL2 parser end-to-end (no RDKit), the LJ patch on d-block elements, and +# the charge/spin conditioning path. +dataset_path: /home/gridsan/jtoney/BOSTMC/datasets/filtered/BOSTMC-low-spin.csv +dataset_source: mol2 +subset_size: 100 +# Split key: refcode. +id_column: refcode +charge_column: charge +spin_column: spinmult +cache_dataset: true +split_ratios: [0.8, 0.1, 0.1] +split_seed: 0 + +n_layers: 2 +d_model: 64 +d_ffn: 128 +n_head: 4 +dropout: 0.0 + +epochs: 3 +batch_size: 4 +eval_batch_size: 4 +lr: 3.0e-4 +weight_decay: 0.0 +warmup_ratio: 0.1 +num_workers: 0 + +device: cpu +output_dir: outputs/bostmc_smoke +seed: 0 diff --git a/configs/qm9.yaml b/configs/qm9.yaml new file mode 100644 index 0000000..d7d83ff --- /dev/null +++ b/configs/qm9.yaml @@ -0,0 +1,29 @@ +# Full QM9-full.csv training run (~134K molecules). +# GPU-only, staged for the Slurm job once compute is available. +# Follows ReBind's QM9 setup from external/ReBIND/experiments/conformer_prediction/rebind.sh +# (see README for the few deliberate differences). +dataset_path: /home/gridsan/jtoney/ElemeNet-benchmarking/benchmarking/datasets/QM9-full.csv +dataset_source: smiles +subset_size: null +# Split key: mol_id, so QM9's few duplicated rows always share a split. +id_column: mol_id +split_ratios: [0.9, 0.05, 0.05] +split_seed: 0 + +n_layers: 8 +d_model: 512 +d_ffn: 1024 +n_head: 8 +dropout: 0.0 + +epochs: 20 +batch_size: 100 +eval_batch_size: 100 +lr: 9.0e-5 +weight_decay: 0.0 +warmup_ratio: 0.1 +num_workers: 4 + +device: cuda +output_dir: outputs/qm9_full +seed: 0 diff --git a/configs/qm9_gtmgc.yaml b/configs/qm9_gtmgc.yaml new file mode 100644 index 0000000..de538ab --- /dev/null +++ b/configs/qm9_gtmgc.yaml @@ -0,0 +1,50 @@ +# GTMGC (Xu et al., ICLR 2024) on QM9, same data and split as configs/qm9_rebind.yaml +# so the two models are directly comparable. +# +# Reference numbers for QM9 test, as tabulated in the ReBind paper: +# GTMGC D-MAE 0.281 D-RMSE 0.471 C-RMSD 0.414 +# ReBind D-MAE 0.254 D-RMSE 0.446 C-RMSD 0.321 +# +# Architecture follows their released QM9 checkpoint (RichXuOvO/GTMGC-Qm9): +# 6 + 6 layers, d_model 256, d_ffn 1024, 8 heads, Mole-BERT token embeddings. +# Their conformer models embed tokenizer ids rather than atom types, so the +# per-atom ids must be precomputed first: +# +# uv run python scripts/tokenize_molebert.py -c configs/qm9_gtmgc.yaml \ +# --out /home/gridsan/jtoney/step-up-data/qm9/qm9-molebert-tokens.csv +model: gtmgc +dataset_path: /home/gridsan/jtoney/step-up-data/qm9/qm9-rebind.csv +dataset_source: sdf +subset_size: null +id_column: mol_id +split_column: split +token_file: /home/gridsan/jtoney/step-up-data/qm9/qm9-molebert-tokens.csv +max_drop_fraction: 0.05 + +n_layers: 6 +d_model: 256 +d_ffn: 1024 +n_head: 8 +dropout: 0.0 + +# Optimization from their experiments/conformer_prediction/gtmgc_for_conformer_prediction.sh. +# That script is the Molecule3D one (the repo ships no QM9 variant), so the learning +# rate is theirs for Molecule3D; everything else is shared across their runs. As with +# ReBind we train in fp32 rather than their fp16 (see README). +epochs: 20 +batch_size: 100 +eval_batch_size: 100 +lr: 5.0e-5 +weight_decay: 0.0 +warmup_ratio: 0.1 +adam_beta1: 0.9 +adam_beta2: 0.99 +adam_eps: 1.0e-8 +lr_schedule: linear +grad_clip: 1.0 +drop_last: true +num_workers: 4 + +device: cuda +output_dir: outputs/qm9_gtmgc +seed: 42 diff --git a/configs/qm9_rebind.yaml b/configs/qm9_rebind.yaml new file mode 100644 index 0000000..1478918 --- /dev/null +++ b/configs/qm9_rebind.yaml @@ -0,0 +1,44 @@ +# Reproduction run for ReBind's published QM9 conformer-prediction numbers +# (paper, test split: D-MAE 0.254, D-RMSE 0.446, C-RMSD 0.321). +# +# Data is the QM9 copy ReBind and GTMGC trained on (HuggingFace RichXuOvO/HFQm9), +# converted by scripts/prepare_qm9_rebind.py. Molblocks are copied out of gdb9.sdf +# verbatim, so bonds are the published ones rather than re-perceived from geometry, +# and the CSV carries their published split (110,000 / 10,000 / 10,831). +# +# Hyperparameters follow external/ReBIND/experiments/conformer_prediction/rebind.sh; +# see README for the few places the loop still differs from their script. +dataset_path: /home/gridsan/jtoney/step-up-data/qm9/qm9-rebind.csv +dataset_source: sdf +subset_size: null +id_column: mol_id +split_column: split +# About 1.4% of records fail RDKit sanitization and are dropped, as in ReBind's +# own evaluation; fail the run if that rate jumps. +max_drop_fraction: 0.05 + +n_layers: 8 +d_model: 512 +d_ffn: 1024 +n_head: 8 +dropout: 0.0 + +# Optimization, verbatim from rebind.sh (fp16 is the one setting we don't mirror; +# we train in fp32, which is the more precise choice — see README). +epochs: 20 +batch_size: 100 +eval_batch_size: 100 +lr: 9.0e-5 +weight_decay: 0.0 +warmup_ratio: 0.1 +adam_beta1: 0.9 +adam_beta2: 0.99 +adam_eps: 1.0e-8 +lr_schedule: linear +grad_clip: 1.0 +drop_last: true +num_workers: 4 + +device: cuda +output_dir: outputs/qm9_rebind +seed: 42 diff --git a/configs/qm9_rebind_noh.yaml b/configs/qm9_rebind_noh.yaml new file mode 100644 index 0000000..8c5e56f --- /dev/null +++ b/configs/qm9_rebind_noh.yaml @@ -0,0 +1,53 @@ +# Heavy-atom QM9: the ReBind QM9 setup with hydrogens dropped from the graph. +# Not a reproduction of their published numbers — see the remove_hs note below. +# (paper, test split: D-MAE 0.254, D-RMSE 0.446, C-RMSD 0.321). +# +# Data is the QM9 copy ReBind and GTMGC trained on (HuggingFace RichXuOvO/HFQm9), +# converted by scripts/prepare_qm9_rebind.py. Molblocks are copied out of gdb9.sdf +# verbatim, so bonds are the published ones rather than re-perceived from geometry, +# and the CSV carries their published split (110,000 / 10,000 / 10,831). +# +# Hyperparameters follow external/ReBIND/experiments/conformer_prediction/rebind.sh; +# see README for the few places the loop still differs from their script. +dataset_path: /home/gridsan/jtoney/step-up-data/qm9/qm9-rebind.csv +dataset_source: sdf +subset_size: null +id_column: mol_id +split_column: split +# About 1.4% of records fail RDKit sanitization and are dropped, as in ReBind's +# own evaluation; fail the run if that rate jumps. +max_drop_fraction: 0.05 + +n_layers: 8 +d_model: 512 +d_ffn: 1024 +n_head: 8 +dropout: 0.0 + +# Optimization, verbatim from rebind.sh (fp16 is the one setting we don't mirror; +# we train in fp32, which is the more precise choice — see README). +epochs: 20 +batch_size: 100 +eval_batch_size: 100 +lr: 9.0e-5 +weight_decay: 0.0 +warmup_ratio: 0.1 +adam_beta1: 0.9 +adam_beta2: 0.99 +adam_eps: 1.0e-8 +lr_schedule: linear +grad_clip: 1.0 +drop_last: true +num_workers: 4 + +device: cuda +output_dir: outputs/qm9_rebind_noh +seed: 42 + +# Heavy-atom variant: hydrogens are dropped from the graph, so the model never +# sees or predicts them. Paired with configs/qm9_rebind.yaml to separate "the model is +# bad at hydrogen" from "the model is bad at geometry". Each heavy atom keeps its +# hydrogen count in the numH feature, so the graph is not missing the chemistry, +# only the positions. Note this departs from the published setup, which keeps +# hydrogen explicit, so it is not a reproduction run. +remove_hs: true diff --git a/configs/qm9_smoke.yaml b/configs/qm9_smoke.yaml new file mode 100644 index 0000000..665fa2b --- /dev/null +++ b/configs/qm9_smoke.yaml @@ -0,0 +1,29 @@ +# Smallest possible run that exercises the full pipeline on CPU. +# Goal: prove the loss strictly decreases. Numbers are not meaningful. +dataset_path: /home/gridsan/jtoney/ElemeNet-benchmarking/benchmarking/datasets/QM9-full.csv +dataset_source: smiles +subset_size: 100 +# Split key: mol_id, so QM9's few duplicated rows always share a split. +id_column: mol_id +cache_dataset: true +split_ratios: [0.8, 0.1, 0.1] +split_seed: 0 + +# Tiny model — enough capacity to learn distance regression on 80 toy molecules. +n_layers: 2 +d_model: 64 +d_ffn: 128 +n_head: 4 +dropout: 0.0 + +epochs: 3 +batch_size: 8 +eval_batch_size: 8 +lr: 3.0e-4 +weight_decay: 0.0 +warmup_ratio: 0.1 +num_workers: 0 + +device: cpu +output_dir: outputs/qm9_smoke +seed: 0 diff --git a/configs/tmqmg.yaml b/configs/tmqmg.yaml new file mode 100644 index 0000000..7e51a55 --- /dev/null +++ b/configs/tmqmg.yaml @@ -0,0 +1,58 @@ +# Full tmQMg training run (~58K organometallic complexes, closed-shell singlets, +# full d-block + La). GPU-only; staged for Slurm. +# +# This copy of tmQMg is the original release minus the 2,390 IDs in the dataset's +# outliers.txt (unphysical geometries the community flagged), so it is the cleaner +# training set but NOT the data the published baselines used. See +# configs/tmqmg_complete.yaml for the run that matches TMCgen exactly. +# +# Split: tmQMg's authors publish no canonical ML split, so we adopt TMCgen's +# (Schaufelberger & Jorner, arXiv 2606.00666), whose paper also reports the GeoDiff +# and ConfGF tmQMg baselines we will compare against. Their refcode lists live in +# github.com/digital-chemistry-laboratory/TMCgen under data/raw/tmqmg. They cover +# 58,086 of the rows here; the 2,379 they list that are missing are exactly the +# removed outliers, 38 of which are in their test split. Rows in no list are dropped. +dataset_path: /home/gridsan/jtoney/ElemeNet-benchmarking/benchmarking/datasets/tmQMg-full.csv +dataset_source: mol2 +subset_size: null +id_column: id +split_files: + train: /home/gridsan/jtoney/step-up-data/tmqmg/complexes-train.txt + val: /home/gridsan/jtoney/step-up-data/tmqmg/complexes-val-split.txt + test: /home/gridsan/jtoney/step-up-data/tmqmg/complexes-test-split.txt + +# Charge runs -1 / 0 / +1 here and is read from the CSV. +# +# There is no spin column in tmQMg to read, so the conditioning collator defaults +# the unpaired-electron input to 0 for every row. That default is consistent with +# the data but not measured from it: every complex has an even electron count, so +# none is *forced* to be open-shell, and tmQM was assembled from closed-shell +# mononuclear complexes. Treat the spin slot as a constant, which a linear layer +# absorbs into its bias; it exists only so tmQMg and BOSTMC share an architecture. +charge_column: charge +spin_column: null + +n_layers: 8 +d_model: 512 +d_ffn: 1024 +n_head: 8 +dropout: 0.0 + +# Optimization as in ReBind's rebind.sh (see README for the fp16 exception). +epochs: 20 +batch_size: 100 +eval_batch_size: 100 +lr: 9.0e-5 +weight_decay: 0.0 +warmup_ratio: 0.1 +adam_beta1: 0.9 +adam_beta2: 0.99 +adam_eps: 1.0e-8 +lr_schedule: linear +grad_clip: 1.0 +drop_last: true +num_workers: 4 + +device: cuda +output_dir: outputs/tmqmg_full +seed: 42 diff --git a/configs/tmqmg_complete.yaml b/configs/tmqmg_complete.yaml new file mode 100644 index 0000000..b6bd2b8 --- /dev/null +++ b/configs/tmqmg_complete.yaml @@ -0,0 +1,60 @@ +# tmQMg trained on exactly the data TMCgen used, so our numbers sit alongside the +# GeoDiff and ConfGF tmQMg baselines from their paper without a caveat. +# +# Three snapshots of tmQMg exist, and they are nested, not recalculated (n_atoms, +# charge and electronic energies are bit-identical wherever they overlap): +# - complete, 60,799 rows: the original release, used here. +# - filtered, 58,409 rows: the same minus the 2,390 IDs in tmQMg's outliers.txt +# (unphysical geometries the community flagged). That's configs/tmqmg.yaml. +# - latest, 74,547 rows: the 2024 extension, which also dropped 87 of the +# originals, 87 of them in TMCgen's split. +# +# Only the complete snapshot covers all 60,465 complexes in TMCgen's split, +# including all 1,360 of their test complexes; 2,379 of the outlier structures are +# in their split and 38 are in their test set, so the baselines were trained and +# scored with them. The 334 rows here that TMCgen never used are dropped. +dataset_path: /home/gridsan/jtoney/step-up-data/tmQMg-complete.csv +dataset_source: mol2 +subset_size: null +id_column: id +split_files: + train: /home/gridsan/jtoney/step-up-data/tmqmg/complexes-train.txt + val: /home/gridsan/jtoney/step-up-data/tmqmg/complexes-val-split.txt + test: /home/gridsan/jtoney/step-up-data/tmqmg/complexes-test-split.txt + +# Charge runs -1 / 0 / +1 and is read from the CSV. +# +# There is no spin column in tmQMg to read, so `spin_column: null` and the +# conditioning collator defaults the unpaired-electron input to 0 for every row. +# That default is consistent with the data but is not measured from it: all +# 60,799 complexes have an even electron count, so none is *forced* to be +# open-shell, and tmQM was assembled from closed-shell mononuclear complexes. +# Treat the spin slot here as a constant, which a linear layer absorbs into its +# bias; it exists only so tmQMg and BOSTMC share one architecture. +charge_column: charge +spin_column: null + +n_layers: 8 +d_model: 512 +d_ffn: 1024 +n_head: 8 +dropout: 0.0 + +# Optimization as in ReBind's rebind.sh (see README for the fp16 exception). +epochs: 20 +batch_size: 100 +eval_batch_size: 100 +lr: 9.0e-5 +weight_decay: 0.0 +warmup_ratio: 0.1 +adam_beta1: 0.9 +adam_beta2: 0.99 +adam_eps: 1.0e-8 +lr_schedule: linear +grad_clip: 1.0 +drop_last: true +num_workers: 4 + +device: cuda +output_dir: outputs/tmqmg_complete_full +seed: 42 diff --git a/configs/tmqmg_complete_noh.yaml b/configs/tmqmg_complete_noh.yaml new file mode 100644 index 0000000..c9e3cf0 --- /dev/null +++ b/configs/tmqmg_complete_noh.yaml @@ -0,0 +1,68 @@ +# Heavy-atom tmQMg: configs/tmqmg_complete.yaml with hydrogens dropped. +# GeoDiff and ConfGF tmQMg baselines from their paper without a caveat. +# +# Three snapshots of tmQMg exist, and they are nested, not recalculated (n_atoms, +# charge and electronic energies are bit-identical wherever they overlap): +# - complete, 60,799 rows: the original release, used here. +# - filtered, 58,409 rows: the same minus the 2,390 IDs in tmQMg's outliers.txt +# (unphysical geometries the community flagged). That's configs/tmqmg.yaml. +# - latest, 74,547 rows: the 2024 extension, which also dropped 87 of the +# originals, 87 of them in TMCgen's split. +# +# Only the complete snapshot covers all 60,465 complexes in TMCgen's split, +# including all 1,360 of their test complexes; 2,379 of the outlier structures are +# in their split and 38 are in their test set, so the baselines were trained and +# scored with them. The 334 rows here that TMCgen never used are dropped. +dataset_path: /home/gridsan/jtoney/step-up-data/tmQMg-complete.csv +dataset_source: mol2 +subset_size: null +id_column: id +split_files: + train: /home/gridsan/jtoney/step-up-data/tmqmg/complexes-train.txt + val: /home/gridsan/jtoney/step-up-data/tmqmg/complexes-val-split.txt + test: /home/gridsan/jtoney/step-up-data/tmqmg/complexes-test-split.txt + +# Charge runs -1 / 0 / +1 and is read from the CSV. +# +# There is no spin column in tmQMg to read, so `spin_column: null` and the +# conditioning collator defaults the unpaired-electron input to 0 for every row. +# That default is consistent with the data but is not measured from it: all +# 60,799 complexes have an even electron count, so none is *forced* to be +# open-shell, and tmQM was assembled from closed-shell mononuclear complexes. +# Treat the spin slot here as a constant, which a linear layer absorbs into its +# bias; it exists only so tmQMg and BOSTMC share one architecture. +charge_column: charge +spin_column: null + +n_layers: 8 +d_model: 512 +d_ffn: 1024 +n_head: 8 +dropout: 0.0 + +# Optimization as in ReBind's rebind.sh (see README for the fp16 exception). +epochs: 20 +batch_size: 100 +eval_batch_size: 100 +lr: 9.0e-5 +weight_decay: 0.0 +warmup_ratio: 0.1 +adam_beta1: 0.9 +adam_beta2: 0.99 +adam_eps: 1.0e-8 +lr_schedule: linear +grad_clip: 1.0 +drop_last: true +num_workers: 4 + +device: cuda +output_dir: outputs/tmqmg_complete_full_noh +seed: 42 + +# Heavy-atom variant: hydrogens are dropped from the graph, so the model never +# sees or predicts them. Paired with configs/tmqmg_complete.yaml to separate "the model is +# bad at hydrogen" from "the model is bad at geometry". Each heavy atom keeps its +# hydrogen count in the numH feature, so the graph is not missing the chemistry, +# only the positions. Note this departs from the published setup, which keeps +# hydrogen explicit, so it is not a reproduction run. +remove_hs: true diff --git a/configs/tmqmg_smoke.yaml b/configs/tmqmg_smoke.yaml new file mode 100644 index 0000000..4e05a3f --- /dev/null +++ b/configs/tmqmg_smoke.yaml @@ -0,0 +1,30 @@ +# Same shape as qm9_smoke, but reading the MOL2 path on organometallic data. +# Verifies the LJ patch and the MOL2 → graph pipeline end-to-end on CPU. +dataset_path: /home/gridsan/jtoney/ElemeNet-benchmarking/benchmarking/datasets/tmQMg-full.csv +dataset_source: mol2 +subset_size: 100 +# Split key: the tmQMg id (CSD refcode). +id_column: id +charge_column: charge +spin_column: null +cache_dataset: true +split_ratios: [0.8, 0.1, 0.1] +split_seed: 0 + +n_layers: 2 +d_model: 64 +d_ffn: 128 +n_head: 4 +dropout: 0.0 + +epochs: 3 +batch_size: 4 # tmQMg complexes are larger (~50 atoms vs ~15 for QM9) +eval_batch_size: 4 +lr: 3.0e-4 +weight_decay: 0.0 +warmup_ratio: 0.1 +num_workers: 0 + +device: cpu +output_dir: outputs/tmqmg_smoke +seed: 0 diff --git a/external/GTMGC b/external/GTMGC new file mode 160000 index 0000000..b33855b --- /dev/null +++ b/external/GTMGC @@ -0,0 +1 @@ +Subproject commit b33855bc23b23a749aac750fc651b3c91f8c3b05 diff --git a/external/ReBIND b/external/ReBIND new file mode 160000 index 0000000..8d240c6 --- /dev/null +++ b/external/ReBIND @@ -0,0 +1 @@ +Subproject commit 8d240c6e04a6f23867df98755100a9fbd230b24d diff --git a/pyproject.toml b/pyproject.toml index 1d30d04..a90727c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,30 @@ [project] name = "step-up" version = "0.1.0" -description = "Add your description here" +description = "Benchmark for 2D to 3D molecular conformer generation models" readme = "README.md" authors = [ - { name = "Siddharth Betala", email = "betalas5@gmail.com" } + { name = "Siddharth Betala", email = "betalas5@gmail.com" }, + { name = "Jacob Toney", email = "jwt@mit.edu" } ] requires-python = ">=3.12" -dependencies = [] +dependencies = [ + "numpy>=1.26", + "pandas>=2.2", + "pyyaml>=6.0", + "rdkit>=2024.3.3", + "tensorboard>=2.16", + # Upper bound keeps every platform on the same torch as the cluster: the cu124 index + # (see [tool.uv.sources]) has no builds past 2.6, while PyPI would resolve the latest. + "torch>=2.6,<2.7", + "torch-geometric>=2.3", + "torchmetrics>=1.0", + "tqdm>=4.66", + "transformers>=4.43", +] + +[project.scripts] +step-up = "step_up.cli:main" [build-system] requires = ["uv_build>=0.10.8,<0.11.0"] @@ -38,3 +55,24 @@ max-complexity = 12 [tool.ruff.format] quote-style = "double" indent-style = "space" + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = [ + "ignore::DeprecationWarning", + "ignore::UserWarning", +] + +[tool.uv.sources] +# CUDA-12.4 torch wheels are only published for Linux x86_64. On macOS / other +# platforms we fall back to the default PyPI index, which provides CPU wheels. +# This keeps `uv sync --dev` working on contributor laptops and CI runners. +torch = [ + { index = "pytorch-cu124", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" }, +] + +[[tool.uv.index]] +name = "pytorch-cu124" +url = "https://download.pytorch.org/whl/cu124" +explicit = true + diff --git a/scripts/analyze_conditioning.py b/scripts/analyze_conditioning.py new file mode 100644 index 0000000..14a9eeb --- /dev/null +++ b/scripts/analyze_conditioning.py @@ -0,0 +1,113 @@ +"""Check whether a conditioned model actually uses its charge/spin input. + +Scores a split three ways: + +1. as trained, +2. with the molecule-level features zeroed at inference — if the metrics barely + move, the model learned to ignore the conditioning, and +3. optionally broken down by a column of the source CSV (e.g. ``spinmult``), to + see whether one subgroup is carrying the error. + +Usage:: + + uv run python scripts/analyze_conditioning.py -c configs/bostmc.yaml \ + --checkpoint outputs/bostmc_full/best.pt --breakdown-column spinmult +""" + +from __future__ import annotations + +import argparse +import json +from dataclasses import replace +from pathlib import Path + +import torch +from torch.utils.data import Subset + +from step_up.eval.conformer_eval import evaluate_split +from step_up.models import N_GLOBAL_FEATURES, build_model, build_model_collator +from step_up.train import TrainConfig, build_splits + + +class ZeroConditioning: + """Collator wrapper that blanks the molecule-level features.""" + + def __init__(self, base): + self.base = base + + def __call__(self, mols): + batch = self.base(mols) + batch["global_features"] = torch.zeros_like(batch["global_features"]) + return batch + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-c", "--config", required=True) + parser.add_argument("--checkpoint", help="Defaults to /best.pt") + parser.add_argument("--split", default="test", choices=["train", "val", "test"]) + parser.add_argument("--breakdown-column", help="CSV column to group the split by") + parser.add_argument("--out", help="Defaults to /conditioning_analysis.json") + args = parser.parse_args(argv) + + cfg = TrainConfig.from_yaml(args.config) + conditioned = cfg.charge_column is not None or cfg.spin_column is not None + if not conditioned: + raise ValueError(f"{args.config} trains without conditioning; nothing to ablate") + device = cfg.device if torch.cuda.is_available() or cfg.device == "cpu" else "cpu" + cfg = replace(cfg, device=device) + + dataset, train_set, val_set, test_set = build_splits(cfg) + subset = {"train": train_set, "val": val_set, "test": test_set}[args.split] + model = build_model( + cfg.model, + n_layers=cfg.n_layers, + d_model=cfg.d_model, + d_ffn=cfg.d_ffn, + n_head=cfg.n_head, + dropout=cfg.dropout, + n_global_features=N_GLOBAL_FEATURES, + ) + checkpoint = args.checkpoint or str(Path(cfg.output_dir) / "best.pt") + model.load_state_dict(torch.load(checkpoint, map_location="cpu")) + model = model.to(device) + collator = build_model_collator(cfg.model, conditioned=True) + + results: dict[str, dict] = {} + + def score(tag: str, rows: Subset, coll) -> None: + metrics = evaluate_split( + model, dataset, rows, coll, device=device, batch_size=cfg.eval_batch_size + ) + results[tag] = metrics + print( + f"{tag:30s} n={int(metrics['n_molecules']):6d} D-MAE={metrics['d_mae']:.4f} " + f"D-RMSE={metrics['d_rmse']:.4f} RMSD={metrics['c_rmsd']:.3f}", + flush=True, + ) + + score(f"{args.split} (as trained)", subset, collator) + score(f"{args.split} (conditioning zeroed)", subset, ZeroConditioning(collator)) + + if args.breakdown_column: + rows = dataset._df.iloc[[dataset._valid_indices[i] for i in subset.indices]] + values = rows[args.breakdown_column].tolist() + for value in sorted(set(values)): + group = Subset( + dataset, [i for i, v in zip(subset.indices, values, strict=True) if v == value] + ) + # Both ways per group: conditioning can be useless on average yet matter + # for the subgroups it describes, e.g. charged or open-shell species. + score(f"{args.breakdown_column}={value}", group, collator) + score(f"{args.breakdown_column}={value} (zeroed)", group, ZeroConditioning(collator)) + + out_path = Path(args.out or Path(cfg.output_dir) / "conditioning_analysis.json") + out_path.parent.mkdir(parents=True, exist_ok=True) + with open(out_path, "w") as handle: + json.dump(results, handle, indent=2) + print(f"wrote {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/compare_runs.py b/scripts/compare_runs.py new file mode 100644 index 0000000..ffa4646 --- /dev/null +++ b/scripts/compare_runs.py @@ -0,0 +1,150 @@ +"""Compare finished runs on one table, with D-MAE normalized by molecule size. + +D-MAE is an absolute distance error, so it grows with the molecule: a 60-atom +complex and a 9-atom skeleton cannot be read off the same column. Every row here +therefore carries a *relative* D-MAE as well — the pooled error divided by that +test set's own mean pairwise distance, pooled the same way (over every ordered +pair including the zero diagonal, as ReBind's ``evaluate.py`` does). That scale +is computed from the reference structures in each run's ``structures_test.csv``, +so the normalizer comes from the same molecules that were scored. + +The script also recomputes D-MAE from the dumped structures and checks it against +the metrics JSON. They are produced by separate passes over the same checkpoint, +so a disagreement means one of them is stale. + +Usage:: + + uv run python scripts/compare_runs.py + uv run python scripts/compare_runs.py --runs outputs/bostmc_full outputs/bostmc_full_noh +""" + +from __future__ import annotations + +import argparse +import json +import math +from pathlib import Path + +import numpy as np +import pandas as pd + +# (label, output_dir). The pairs sit next to each other so the hydrogen +# comparison reads down the table. +_DEFAULT_RUNS: tuple[tuple[str, str], ...] = ( + ("QM9, with H", "outputs/qm9_rebind"), + ("QM9, heavy atom", "outputs/qm9_rebind_noh"), + ("tmQMg, with H", "outputs/tmqmg_complete_full"), + ("tmQMg, heavy atom", "outputs/tmqmg_complete_full_noh"), + ("BOSTMC, with H", "outputs/bostmc_full"), + ("BOSTMC, heavy atom", "outputs/bostmc_full_noh"), +) + + +def _coords(xyz_block: str) -> np.ndarray: + lines = xyz_block.splitlines() + count = int(lines[0]) + return np.array( + [[float(v) for v in line.split()[1:4]] for line in lines[2 : 2 + count]], + dtype=float, + ) + + +def _pooled_scale_and_dmae(frame: pd.DataFrame) -> tuple[float, float, float]: + """Mean pairwise distance, pooled D-MAE, and mean atom count over a dump. + + Both pooled quantities use the same denominator as the published protocol: + the total number of ordered atom pairs in the split, diagonal included. + """ + total_distance, total_abs_error, total_pairs, total_atoms = 0.0, 0.0, 0, 0 + for true_block, pred_block in zip(frame["xyz_true"], frame["xyz_pred"], strict=True): + target, pred = _coords(true_block), _coords(pred_block) + d_true = np.linalg.norm(target[:, None, :] - target[None, :, :], axis=-1) + d_pred = np.linalg.norm(pred[:, None, :] - pred[None, :, :], axis=-1) + total_distance += float(d_true.sum()) + total_abs_error += float(np.abs(d_pred - d_true).sum()) + total_pairs += d_true.size + total_atoms += len(target) + return ( + total_distance / max(total_pairs, 1), + total_abs_error / max(total_pairs, 1), + total_atoms / max(len(frame), 1), + ) + + +def _load(label: str, run_dir: Path) -> dict | None: + dump = run_dir / "structures_test.csv" + metrics_path = run_dir / "eval_test.json" + if not dump.exists() or not metrics_path.exists(): + missing = [str(p.name) for p in (metrics_path, dump) if not p.exists()] + print(f"skipping {label}: missing {', '.join(missing)}") + return None + + metrics = json.loads(metrics_path.read_text()) + frame = pd.read_csv(dump) + scale, dmae_from_dump, mean_atoms = _pooled_scale_and_dmae(frame) + + keephs_path = run_dir / "eval_test_keephs.json" + keephs = json.loads(keephs_path.read_text()) if keephs_path.exists() else {} + + return { + "label": label, + "n": len(frame), + "mean_atoms": mean_atoms, + "d_mae": metrics["d_mae"], + "d_rmse": metrics["d_rmse"], + "rmsd": metrics["c_rmsd"], + "rmsd_method": metrics.get("c_rmsd_method", "?"), + "rmsd_keephs": keephs.get("c_rmsd", math.nan), + "scale": scale, + "relative": metrics["d_mae"] / scale if scale else math.nan, + "rmsd_over_scale": metrics["c_rmsd"] / scale if scale else math.nan, + "dmae_from_dump": dmae_from_dump, + } + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--runs", nargs="*", help="Output directories; defaults to all six") + parser.add_argument("--out", help="Also write the table as JSON here") + args = parser.parse_args(argv) + + if args.runs: + pairs = [(Path(r).name, r) for r in args.runs] + else: + pairs = list(_DEFAULT_RUNS) + + rows = [row for label, d in pairs if (row := _load(label, Path(d))) is not None] + if not rows: + print("no finished runs found") + return 1 + + header = ( + f"| {'Run':<20} | {'Test mols':>9} | {'Atoms':>6} | {'D-MAE':>6} | {'D-RMSE':>6} " + f"| {'RMSD':>6} | {'Scale':>6} | {'Rel. D-MAE':>10} | {'RMSD/scale':>10} |" + ) + print(header) + print("|" + "|".join("-" * (len(part)) for part in header.split("|")[1:-1]) + "|") + for r in rows: + print( + f"| {r['label']:<20} | {r['n']:>9,} | {r['mean_atoms']:>6.1f} | {r['d_mae']:>6.3f} " + f"| {r['d_rmse']:>6.3f} | {r['rmsd']:>6.3f} | {r['scale']:>6.3f} " + f"| {r['relative'] * 100:>9.1f}% | {r['rmsd_over_scale'] * 100:>9.1f}% |" + ) + + print() + for r in rows: + gap = abs(r["d_mae"] - r["dmae_from_dump"]) + flag = "OK" if gap < 5e-3 else "MISMATCH" + print( + f"{flag:8s} {r['label']:<20} json={r['d_mae']:.4f} " + f"recomputed_from_dump={r['dmae_from_dump']:.4f}" + ) + + if args.out: + Path(args.out).write_text(json.dumps(rows, indent=2)) + print(f"\nwrote {args.out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/dump_structures.py b/scripts/dump_structures.py new file mode 100644 index 0000000..17057c7 --- /dev/null +++ b/scripts/dump_structures.py @@ -0,0 +1,162 @@ +"""Write the true and predicted geometry of every test-set molecule to a CSV. + +One row per molecule, so the file can be read back with pandas and the two +structures overlaid directly: + + id, charge, spin_multiplicity, n_atoms, elements, xyz_true, xyz_pred, + d_mae, d_rmse, rmsd + +``xyz_true`` and ``xyz_pred`` are standard XYZ blocks (count line, comment line, +then ``element x y z``), the same format the source CSVs use, with atoms in the +same order in both. + +Two things worth knowing about ``xyz_pred``: + +- It is **Kabsch-aligned onto the true structure** per molecule. The models + align inside their own prediction head over the *padded* batch tensor, so the + raw output sits in a frame corrupted by padding zeros; aligning here is what + makes the two blocks comparable. Only a rotation and translation are applied, + so bond lengths and angles are the model's own. +- Hydrogens are absent for a model trained with ``remove_hs``, because that + model never predicted any. + +``charge`` and ``spin_multiplicity`` come from the source CSV. They are empty +when the dataset has no such column, which is not a claim of neutral or +closed-shell — it means the data does not say. + +Usage:: + + uv run python scripts/dump_structures.py -c configs/bostmc.yaml + uv run python scripts/dump_structures.py -c configs/qm9_rebind.yaml \ + --checkpoint outputs/qm9_rebind/best.pt --split test +""" + +from __future__ import annotations + +import argparse +import csv +from dataclasses import replace +from pathlib import Path + +import torch +from rdkit import Chem +from torch.utils.data import DataLoader +from tqdm import tqdm + +from step_up.eval.conformer_eval import kabsch_rmsd +from step_up.models import N_GLOBAL_FEATURES, build_model, build_model_collator +from step_up.train import TrainConfig, build_splits + +_PERIODIC_TABLE = Chem.GetPeriodicTable() + +_COLUMNS = ( + "id", + "charge", + "spin_multiplicity", + "n_atoms", + "elements", + "xyz_true", + "xyz_pred", + "d_mae", + "d_rmse", + "rmsd", +) + + +def _xyz_block(symbols: list[str], coords: torch.Tensor, comment: str) -> str: + lines = [str(len(symbols)), comment] + for symbol, (x, y, z) in zip(symbols, coords.tolist(), strict=True): + lines.append(f"{symbol} {x:.6f} {y:.6f} {z:.6f}") + return "\n".join(lines) + + +def _aligned(pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor: + """``pred`` rigidly moved onto ``target`` — rotation and translation only.""" + p = pred - pred.mean(dim=0, keepdim=True) + q = target - target.mean(dim=0, keepdim=True) + u, _, vt = torch.linalg.svd(p.T @ q) + sign = torch.sign(torch.det(vt.T @ u.T)) + correction = torch.diag(torch.tensor([1.0, 1.0, float(sign)], dtype=p.dtype)) + rotation = vt.T @ correction @ u.T + return (rotation @ p.T).T + target.mean(dim=0, keepdim=True) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-c", "--config", required=True) + parser.add_argument("--checkpoint", help="Defaults to /best.pt") + parser.add_argument("--split", default="test", choices=["train", "val", "test"]) + parser.add_argument("--out", help="Defaults to /structures_.csv") + args = parser.parse_args(argv) + + cfg = TrainConfig.from_yaml(args.config) + device = cfg.device if torch.cuda.is_available() or cfg.device == "cpu" else "cpu" + cfg = replace(cfg, device=device) + conditioned = cfg.charge_column is not None or cfg.spin_column is not None + + dataset, train_set, val_set, test_set = build_splits(cfg) + subset = {"train": train_set, "val": val_set, "test": test_set}[args.split] + + model = build_model( + cfg.model, + n_layers=cfg.n_layers, + d_model=cfg.d_model, + d_ffn=cfg.d_ffn, + n_head=cfg.n_head, + dropout=cfg.dropout, + n_global_features=N_GLOBAL_FEATURES if conditioned else 0, + ) + checkpoint = args.checkpoint or str(Path(cfg.output_dir) / "best.pt") + model.load_state_dict(torch.load(checkpoint, map_location="cpu")) + model = model.to(device).eval() + + collator = build_model_collator(cfg.model, conditioned=conditioned) + loader = DataLoader(subset, batch_size=cfg.eval_batch_size, shuffle=False, collate_fn=collator) + + out_path = Path(args.out or Path(cfg.output_dir) / f"structures_{args.split}.csv") + out_path.parent.mkdir(parents=True, exist_ok=True) + + position = 0 + with open(out_path, "w", newline="") as handle: + writer = csv.writer(handle) + writer.writerow(_COLUMNS) + for batch in tqdm(loader, desc=f"dumping {args.split}", dynamic_ncols=True): + device_batch = { + k: (v.to(device) if torch.is_tensor(v) else v) for k, v in batch.items() + } + with torch.no_grad(): + out = model(**device_batch) + node_mask = device_batch["node_mask"].bool() + for row in range(node_mask.shape[0]): + keep = node_mask[row] + pred = out.conformer_hat[row][keep].double().cpu() + target = out.conformer[row][keep].double().cpu() + # node_type is Z here: the collator shifts by 1 so 0 means padding. + symbols = [ + _PERIODIC_TABLE.GetElementSymbol(int(z)) + for z in device_batch["node_type"][row][keep].cpu() + ] + delta = (torch.cdist(pred, pred) - torch.cdist(target, target)).abs() + meta = dataset.metadata(subset.indices[position + row]) + writer.writerow( + [ + meta["id"], + "" if meta["charge"] is None else meta["charge"], + "" if meta["spin_multiplicity"] is None else meta["spin_multiplicity"], + len(symbols), + " ".join(symbols), + _xyz_block(symbols, target, f"{meta['id']} reference"), + _xyz_block(symbols, _aligned(pred, target), f"{meta['id']} predicted"), + f"{float(delta.mean()):.6f}", + f"{float((delta**2).mean().sqrt()):.6f}", + f"{kabsch_rmsd(pred, target):.6f}", + ] + ) + position += node_mask.shape[0] + + print(f"wrote {out_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/eval_and_dump.sh b/scripts/eval_and_dump.sh new file mode 100755 index 0000000..6c78207 --- /dev/null +++ b/scripts/eval_and_dump.sh @@ -0,0 +1,44 @@ +#!/bin/bash +#SBATCH --job-name=step-up-eval +#SBATCH --gres=gpu:volta:1 +#SBATCH --cpus-per-task=8 +#SBATCH --mem=64G +#SBATCH --output=eval-%j.out + +# Usage: sbatch scripts/eval_and_dump.sh configs/bostmc.yaml +# +# Scores the test split with ReBind's metric definitions and writes the +# per-molecule true/predicted structures beside it. Both read the same +# checkpoint and the same split, so the pooled metrics and the CSV always +# describe one state of one model. +# +# RMSD is reported twice on purpose: once hydrogen-free (comparable with the +# published C-RMSD column) and once keeping hydrogen, so a model trained with +# explicit hydrogen can be compared with its heavy-atom twin on equal terms. + +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " + exit 2 +fi +CONFIG="$(realpath "$1")" + +REPO_ROOT="$(git -C "${SLURM_SUBMIT_DIR:-$(dirname "$0")}" rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +SETUP_LOCK="$REPO_ROOT/.git/step-up-setup.lock" +if [[ ! -f external/ReBIND/models/rebind/modeling_rebind.py ]]; then + flock "$SETUP_LOCK" git submodule update --init --recursive +fi +flock "$SETUP_LOCK" uv sync --dev + +OUT_DIR="$(uv run python -c " +from step_up.train import TrainConfig +print(TrainConfig.from_yaml('$CONFIG').output_dir) +")" + +uv run step-up evaluate -c "$CONFIG" --split test --out "$OUT_DIR/eval_test.json" +uv run step-up evaluate -c "$CONFIG" --split test --keep-hs \ + --out "$OUT_DIR/eval_test_keephs.json" +uv run python scripts/dump_structures.py -c "$CONFIG" --split test diff --git a/scripts/prepare_qm9_rebind.py b/scripts/prepare_qm9_rebind.py new file mode 100644 index 0000000..f5c4df0 --- /dev/null +++ b/scripts/prepare_qm9_rebind.py @@ -0,0 +1,93 @@ +"""Build a QM9 CSV carrying ReBind's published train/val/test split. + +Source data is the HuggingFace dataset ``RichXuOvO/HFQm9``, the QM9 copy that +GTMGC and ReBind trained on: + + gdb9.sdf 133,885 molecules, one SDF record each + train_indices.csv 110,000 0-based positions into gdb9.sdf + valid_indices.csv 10,000 + test_indices.csv 10,831 + +The three index files are disjoint and together cover 130,831 molecules; the +3,054 QM9 entries that failed the original consistency check are left out. + +Download them with:: + + mkdir -p && cd + for f in gdb9.sdf train_indices.csv valid_indices.csv test_indices.csv; do + curl -sLO "https://huggingface.co/datasets/RichXuOvO/HFQm9/resolve/main/$f" + done + +Molblocks are copied out of the SDF verbatim (no RDKit round trip), so the +training pipeline featurizes exactly the published structures and bond orders. + +Usage:: + + uv run python scripts/prepare_qm9_rebind.py --raw-dir --out +""" + +from __future__ import annotations + +import argparse +import csv +import sys +from pathlib import Path + +SPLIT_FILES = {"train": "train_indices.csv", "val": "valid_indices.csv", "test": "test_indices.csv"} + + +def read_molblocks(sdf_path: Path) -> list[str]: + """Split an SDF into per-molecule blocks, keeping each record verbatim.""" + records = sdf_path.read_text().split("$$$$\n") + if records and not records[-1].strip(): + records.pop() + return records + + +def read_indices(raw_dir: Path) -> dict[str, list[int]]: + indices: dict[str, list[int]] = {} + for split, name in SPLIT_FILES.items(): + with open(raw_dir / name) as f: + rows = list(csv.DictReader(f)) + indices[split] = [int(row["index"]) for row in rows] + return indices + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--raw-dir", required=True, type=Path, help="Directory holding gdb9.sdf") + parser.add_argument("--out", required=True, type=Path, help="CSV to write") + args = parser.parse_args(argv) + + csv.field_size_limit(sys.maxsize) + blocks = read_molblocks(args.raw_dir / "gdb9.sdf") + print(f"gdb9.sdf: {len(blocks)} records") + indices = read_indices(args.raw_dir) + + assigned: dict[int, str] = {} + for split, idx_list in indices.items(): + for i in idx_list: + if i in assigned: + raise ValueError(f"index {i} appears in both {assigned[i]} and {split}") + if not 0 <= i < len(blocks): + raise ValueError(f"index {i} is out of range for {len(blocks)} records") + assigned[i] = split + print("split sizes: " + ", ".join(f"{s}={len(v)}" for s, v in indices.items())) + + args.out.parent.mkdir(parents=True, exist_ok=True) + with open(args.out, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["mol_id", "split", "sdf"]) + for i in sorted(assigned): + block = blocks[i] + # The first line of an SDF record is the molecule title, e.g. "gdb_73134". + mol_id = block.lstrip("\n").split("\n", 1)[0].strip() + if not mol_id: + raise ValueError(f"record {i} has no title line to use as mol_id") + writer.writerow([mol_id, assigned[i], block]) + print(f"wrote {len(assigned)} rows to {args.out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/tokenize_molebert.py b/scripts/tokenize_molebert.py new file mode 100644 index 0000000..df72290 --- /dev/null +++ b/scripts/tokenize_molebert.py @@ -0,0 +1,89 @@ +"""Precompute Mole-BERT tokenizer ids for a dataset. + +GTMGC's published conformer models embed atoms as Mole-BERT tokenizer ids +(``embed_style="atom_tokenized_ids"``), so every molecule needs per-atom +``input_ids``. Upstream tokenizes the dataset once and saves it to disk +(``tokenize_mole.py``); we do the same, writing a small ``id,input_ids`` CSV that +``CSVMoleculeDataset`` joins on via ``token_file``. + +The dataset is built from the same config the training run uses, so the rows and +the featurization match; rows that fail featurization are dropped here exactly as +they are in training. + +Usage:: + + uv run python scripts/tokenize_molebert.py -c configs/qm9_gtmgc.yaml \ + --out ~/step-up-data/qm9/qm9-molebert-tokens.csv +""" + +from __future__ import annotations + +import argparse +import csv +from pathlib import Path + +import torch +from tqdm import tqdm + +from step_up.data.csv_dataset import CSVMoleculeDataset +from step_up.models.gtmgc import load_molebert_tokenizer +from step_up.train import TrainConfig + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-c", "--config", required=True, help="Training config to take data from") + parser.add_argument("--out", required=True, type=Path, help="CSV to write") + parser.add_argument("--checkpoint", default="RichXuOvO/MoleBERT-Tokenizer") + parser.add_argument("--batch-size", type=int, default=256) + parser.add_argument("--device", default="cpu") + args = parser.parse_args(argv) + + cfg = TrainConfig.from_yaml(args.config) + if cfg.id_column is None: + raise ValueError("the config needs id_column so tokens can be keyed by molecule") + dataset = CSVMoleculeDataset( + path=cfg.dataset_path, + source=cfg.dataset_source, # type: ignore[arg-type] + subset_size=cfg.subset_size, + validate=cfg.validate_dataset, + max_drop_fraction=cfg.max_drop_fraction, + filter_column=cfg.filter_column, + filter_value=cfg.filter_value, + id_column=cfg.id_column, + # token_file deliberately omitted: this script is what creates it. + ) + keys = dataset.split_keys() + tokenizer, collator = load_molebert_tokenizer(args.checkpoint) + tokenizer = tokenizer.eval().to(args.device) + + args.out.parent.mkdir(parents=True, exist_ok=True) + written = 0 + with open(args.out, "w", newline="") as handle: + writer = csv.writer(handle) + writer.writerow(["id", "input_ids"]) + for start in tqdm(range(0, len(dataset), args.batch_size), desc="tokenizing", unit="batch"): + chunk = list(range(start, min(start + args.batch_size, len(dataset)))) + graphs = [dataset[i] for i in chunk] + batch = collator(graphs) + with torch.no_grad(): + out = tokenizer(**{k: v.to(args.device) for k, v in batch.items()}) + token_ids = out["quantized_indices"].cpu().tolist() + offset = 0 + for index, graph in zip(chunk, graphs, strict=True): + n = graph["num_nodes"] + writer.writerow( + [keys[index], " ".join(str(t) for t in token_ids[offset : offset + n])] + ) + offset += n + written += 1 + if offset != len(token_ids): + raise RuntimeError( + f"tokenizer returned {len(token_ids)} ids for {offset} atoms in this batch" + ) + print(f"wrote {written} rows to {args.out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/train.sh b/scripts/train.sh new file mode 100755 index 0000000..ed93be9 --- /dev/null +++ b/scripts/train.sh @@ -0,0 +1,38 @@ +#!/bin/bash +#SBATCH --job-name=step-up +#SBATCH --gres=gpu:volta:1 +#SBATCH --cpus-per-task=8 +#SBATCH --mem=64G +#SBATCH --output=train-%j.out + +# Usage: sbatch scripts/train.sh configs/qm9.yaml +# - Edit partition / gres / time to match your cluster. +# - The job runs from the repo root and uses uv for everything. + +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " + exit 2 +fi +# Resolve the config before changing directory so relative paths keep working. +CONFIG="$(realpath "$1")" + +# Under sbatch, $0 is Slurm's spooled copy of this script (in the slurmd spool +# directory), not the file in the repo, so locate the repo from the submission +# directory instead. Outside Slurm, fall back to the script's own location. +REPO_ROOT="$(git -C "${SLURM_SUBMIT_DIR:-$(dirname "$0")}" rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +# Make sure the submodule is initialized in case the job runs on a fresh checkout. +# Jobs launched together share this clone, and git's config lock is not safe against +# concurrent writers (a second job dies with "could not lock config file"), so skip +# the update once the submodule is populated and serialize the setup steps. +SETUP_LOCK="$REPO_ROOT/.git/step-up-setup.lock" +if [[ ! -f external/ReBIND/models/rebind/modeling_rebind.py ]]; then + flock "$SETUP_LOCK" git submodule update --init --recursive +fi + +flock "$SETUP_LOCK" uv sync --dev + +uv run step-up train -c "$CONFIG" diff --git a/src/step_up/__init__.py b/src/step_up/__init__.py index a88258d..7b57afa 100644 --- a/src/step_up/__init__.py +++ b/src/step_up/__init__.py @@ -1,2 +1,3 @@ -def hello() -> str: - return "Hello from step-up!" +"""step-up: a benchmark for 2D to 3D molecular conformer generation models.""" + +__version__ = "0.1.0" diff --git a/src/step_up/cli.py b/src/step_up/cli.py new file mode 100644 index 0000000..f3c566a --- /dev/null +++ b/src/step_up/cli.py @@ -0,0 +1,121 @@ +"""Command-line entry point: ``uv run step-up ``.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import torch + +from .eval.conformer_eval import evaluate_split, write_metrics +from .models import N_GLOBAL_FEATURES, build_model, build_model_collator +from .train import TrainConfig, build_splits, train + + +def _cmd_train(args: argparse.Namespace) -> int: + cfg = TrainConfig.from_yaml(args.config) + if args.dry_run: + print(f"[dry-run] Loaded config from {args.config}") + print(f" dataset: {cfg.dataset_path} ({cfg.dataset_source})") + if not Path(cfg.dataset_path).exists(): + print(f" ERROR: dataset path does not exist: {cfg.dataset_path}") + return 1 + print(f" subset_size={cfg.subset_size} epochs={cfg.epochs} batch={cfg.batch_size}") + print(f" model: layers={cfg.n_layers} d_model={cfg.d_model} d_ffn={cfg.d_ffn}") + print(f" device={cfg.device} output_dir={cfg.output_dir}") + return 0 + result = train(cfg) + print(f"\nBest val D-MAE: {result['best_val_dmae']:.4f}") + return 0 + + +def _cmd_evaluate(args: argparse.Namespace) -> int: + """Score a checkpoint with ReBind's published metric definitions.""" + cfg = TrainConfig.from_yaml(args.config) + checkpoint = str(args.checkpoint or Path(cfg.output_dir) / "best.pt") + # A `.pt` path is one of our state dicts; anything else is a released + # checkpoint (HuggingFace repo id or a directory), e.g. RichXuOvO/GTMGC-Qm9. + is_state_dict = Path(checkpoint).suffix in {".pt", ".pth"} + if is_state_dict and not Path(checkpoint).exists(): + print(f"ERROR: checkpoint not found: {checkpoint}") + return 1 + + dataset, train_set, val_set, test_set = build_splits(cfg) + subset = {"train": train_set, "val": val_set, "test": test_set}[args.split] + if len(subset) == 0: + print(f"ERROR: the {args.split} split is empty") + return 1 + + conditioned = cfg.charge_column is not None or cfg.spin_column is not None + if is_state_dict: + model = build_model( + cfg.model, + n_layers=cfg.n_layers, + d_model=cfg.d_model, + d_ffn=cfg.d_ffn, + n_head=cfg.n_head, + dropout=cfg.dropout, + n_global_features=N_GLOBAL_FEATURES if conditioned else 0, + ).to(cfg.device) + model.load_state_dict(torch.load(checkpoint, map_location=cfg.device)) + elif cfg.model == "gtmgc": + from .models.gtmgc import load_pretrained_gtmgc + + print(f"[evaluate] loading released checkpoint {checkpoint}", flush=True) + model = load_pretrained_gtmgc(checkpoint).to(cfg.device) + else: + print(f"ERROR: {cfg.model} cannot load a released checkpoint: {checkpoint}") + return 1 + metrics = evaluate_split( + model, + dataset, + subset, + build_model_collator(cfg.model, conditioned=conditioned), + device=cfg.device, + batch_size=cfg.eval_batch_size, + remove_hs=not args.keep_hs, + ) + out_path = Path(args.out) if args.out else Path(cfg.output_dir) / f"eval_{args.split}.json" + write_metrics(metrics, out_path) + hydrogens = "without hydrogens" if metrics["remove_hs"] else "with hydrogens" + print( + f"[{args.split}] D-MAE={metrics['d_mae']:.4f} D-RMSE={metrics['d_rmse']:.4f} " + f"C-RMSD={metrics['c_rmsd']:.4f} ({hydrogens}, {metrics['c_rmsd_method']}) " + f"over {int(metrics['n_molecules'])} molecules" + ) + print(f"wrote {out_path}") + return 0 + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="step-up") + sub = parser.add_subparsers(dest="command", required=True) + + p_train = sub.add_parser("train", help="Train a model from a YAML config") + p_train.add_argument("-c", "--config", required=True, help="Path to YAML config") + p_train.add_argument( + "--dry-run", action="store_true", help="Validate config and dataset path without training" + ) + p_train.set_defaults(func=_cmd_train) + + p_eval = sub.add_parser("evaluate", help="Score a checkpoint (ReBind's metric protocol)") + p_eval.add_argument("-c", "--config", required=True, help="Path to YAML config") + p_eval.add_argument( + "--checkpoint", + help="Our .pt state dict, or a released checkpoint such as RichXuOvO/GTMGC-Qm9. " + "Defaults to /best.pt", + ) + p_eval.add_argument("--split", default="test", choices=["train", "val", "test"]) + p_eval.add_argument("--out", help="Defaults to /eval_.json") + p_eval.add_argument( + "--keep-hs", action="store_true", help="Keep hydrogens in C-RMSD (ReBind removes them)" + ) + p_eval.set_defaults(func=_cmd_evaluate) + + args = parser.parse_args(argv) + return args.func(args) + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main()) diff --git a/src/step_up/data/__init__.py b/src/step_up/data/__init__.py new file mode 100644 index 0000000..084a543 --- /dev/null +++ b/src/step_up/data/__init__.py @@ -0,0 +1 @@ +"""Data loading and featurization for step-up benchmark datasets.""" diff --git a/src/step_up/data/csv_dataset.py b/src/step_up/data/csv_dataset.py new file mode 100644 index 0000000..b1812c8 --- /dev/null +++ b/src/step_up/data/csv_dataset.py @@ -0,0 +1,351 @@ +"""Unified CSV → graph-dict dataset. + +Backs all three current targets (QM9-full, tmQMg-full, BOSTMC) with one class. +Each row is featurized lazily into a ReBind-compatible dict that the Collator +can pad and batch. + +The dataset performs an upfront *validation pass* that attempts to featurize +every row and keeps only the indices that succeed. This is the explicit answer +to the silent-corruption failure mode where RDKit could not infer bond orders +on certain QM9 rows: rather than silently substituting a wrong feature value, +those rows are filtered out and reported. +""" + +from __future__ import annotations + +import csv +import sys +from collections.abc import Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Literal + +import pandas as pd +from rdkit import Chem +from torch.utils.data import Dataset +from tqdm import tqdm + +from .featurize import ( + featurize_mol2_xyz, + featurize_molblock, + featurize_xyz, + mol_from_xyz_block, + remove_hydrogens, +) + +# Embedded XYZ/MOL2 fields blow past the default csv.field_size_limit. Raise it +# once at import time so pandas (which uses Python's csv internally for the C +# fallback path) doesn't truncate. +csv.field_size_limit(sys.maxsize) + + +@dataclass(frozen=True) +class DatasetSpec: + """Where to find a CSV and how to read it.""" + + path: Path + source: Literal["smiles", "mol2"] + + +def _read_token_file(path: Path) -> dict[str, list[int]]: + """Read ``id,input_ids`` rows into a mapping of id to per-atom token ids.""" + frame = pd.read_csv(path) + missing = {"id", "input_ids"} - set(frame.columns) + if missing: + raise ValueError(f"token file {path} is missing columns: {sorted(missing)}") + return { + str(key): [int(v) for v in str(ids).split()] + for key, ids in zip(frame["id"], frame["input_ids"], strict=True) + } + + +def _read_csv_subset(path: Path, columns: Sequence[str], nrows: int | None) -> pd.DataFrame: + df = pd.read_csv(path, usecols=list(columns), nrows=nrows, low_memory=False) + df = df.reset_index(drop=True) + return df + + +class CSVMoleculeDataset(Dataset): + """Dataset over one of QM9-full / tmQMg-full / BOSTMC. + + The needed CSV columns are loaded into memory with pandas; rows are + featurized when accessed. + + Parameters + ---------- + path + Path to the CSV. + source + One of ``"smiles"`` (QM9-style: featurize from the ``xyz`` column via + RDKit ``DetermineBonds``), ``"mol2"`` (organometallic: featurize from + the ``mol2`` column directly via the in-house parser), or ``"sdf"`` + (featurize an SDF record held in the ``sdf`` column, keeping its own + bonds and coordinates). + subset_size + Optional cap on the number of CSV rows to load (the first rows of the + file). Useful for smoke runs. + validate + If ``True`` (default), every row is featurized once at construction + time and rows whose featurization fails are dropped. Set to ``False`` + only for datasets known to be clean: a bad row then raises during + training instead. The upfront pass takes about a minute on QM9-full. + max_drop_fraction + If validation drops more than this fraction of rows, raise. Defaults + to ``0.5`` — a sane upper bound that flags catastrophic dataset issues + (e.g. wrong column names, encoding problems) while tolerating the + ~6% drop rate seen on QM9-full.csv. + filter_column, filter_value + If both set, restrict the dataset to rows where + ``df[filter_column] == filter_value`` before validation. Used to e.g. + pre-filter BOSTMC to ``spinmult == 1`` so the model trains on a single + spin manifold. + cache + If ``True``, featurized graph dicts are kept in memory and reused by + ``__getitem__`` instead of re-featurizing rows every epoch. The cache is + filled during validation, or on first access when validation is off. + Off by default: worth it for small / smoke datasets, but with + ``num_workers > 0`` each DataLoader worker holds its own copy. + id_column + Optional column holding a stable per-molecule ID (e.g. ``refcode``), + used as the split key by :meth:`split_keys`. Rows sharing an ID always + land in the same split. Without it, a row's key is its row number in + the CSV. + token_file + Optional CSV of ``id,input_ids`` (space-separated per-atom token ids, + see ``scripts/tokenize_molebert.py``), joined on ``id_column``. GTMGC's + published conformer models embed Mole-BERT tokenizer ids rather than + atom types, so their batches need this. + split_column + Optional column holding a published split name per row (``train`` / + ``val`` / ``test``), read by :meth:`split_labels`. Use it to reproduce + someone else's split instead of hashing one. + charge_column, spin_column + Optional columns holding the molecule's formal charge (electrons) and + spin multiplicity. When given, each graph dict carries ``charge`` and + ``spin_multiplicity`` for the model's global conditioning. A missing + column means neutral / closed-shell. + remove_hs + Drop hydrogens from every graph, so the model neither sees nor predicts + them. This is a different model, not a different metric: it changes what + the network is trained on. Each heavy atom keeps its hydrogen count in + the ``numH`` feature either way. + """ + + def __init__( + self, + path: str | Path, + source: Literal["smiles", "mol2", "sdf"], + subset_size: int | None = None, + validate: bool = True, + max_drop_fraction: float = 0.5, + filter_column: str | None = None, + filter_value: Any = None, + cache: bool = False, + id_column: str | None = None, + split_column: str | None = None, + charge_column: str | None = None, + spin_column: str | None = None, + token_file: str | Path | None = None, + remove_hs: bool = False, + ) -> None: + self.path = Path(path) + if not self.path.exists(): + raise FileNotFoundError(self.path) + self.source = source + self.remove_hs = remove_hs + self.id_column = id_column + self.split_column = split_column + self.charge_column = charge_column + self.spin_column = spin_column + self.token_file = Path(token_file) if token_file is not None else None + self._tokens: dict[str, list[int]] | None = None + if self.token_file is not None: + if id_column is None: + raise ValueError("token_file needs id_column to join the tokens on") + self._tokens = _read_token_file(self.token_file) + self._cache_enabled = cache + self._cache: dict[int, dict[str, Any]] = {} + + if source == "smiles": + cols = ["smiles", "xyz"] + elif source == "mol2": + cols = ["mol2", "xyz"] + elif source == "sdf": + cols = ["sdf"] + else: + raise ValueError(f"Unknown source: {source!r}") + for extra in (filter_column, id_column, split_column, charge_column, spin_column): + if extra is not None and extra not in cols: + cols.append(extra) + + # The DataFrame index is each row's position in the CSV. Filtering keeps it + # (no reset_index), so split keys don't depend on subsetting or filtering. + self._df = _read_csv_subset(self.path, cols, nrows=subset_size) + + if filter_column is not None: + before = len(self._df) + self._df = self._df[self._df[filter_column] == filter_value] + print( + f"[{self.path.name}] filter {filter_column}=={filter_value}: " + f"kept {len(self._df)}/{before} rows", + flush=True, + ) + + if len(self._df) == 0: + raise ValueError( + f"Dataset {self.path} is empty after loading " + f"(subset_size={subset_size}, filter={filter_column}=={filter_value})" + ) + + if validate: + self._valid_indices = self._validate_rows(max_drop_fraction) + else: + self._valid_indices = list(range(len(self._df))) + + def __len__(self) -> int: + return len(self._valid_indices) + + def __getitem__(self, idx: int) -> dict[str, Any]: + real_idx = self._valid_indices[idx] + graph = self._cache.get(real_idx) + if graph is None: + graph = self._featurize(real_idx) + if self._cache_enabled: + self._cache[real_idx] = graph + return graph + + def metadata(self, idx: int) -> dict[str, Any]: + """Source-CSV facts about item ``idx``: its id, charge and spin multiplicity. + + ``charge`` and ``spin_multiplicity`` are ``None`` when the dataset was + built without those columns, which is not the same as neutral or + closed-shell — it means the CSV does not say. The conditioning collator + does default a missing value, so a ``None`` here marks a number the model + was handed rather than told. + """ + real_idx = self._valid_indices[idx] + row = self._df.iloc[real_idx] + key = ( + str(row[self.id_column]) + if self.id_column is not None + else str(self._df.index[real_idx]) + ) + return { + "id": key, + "charge": float(row[self.charge_column]) if self.charge_column else None, + "spin_multiplicity": float(row[self.spin_column]) if self.spin_column else None, + } + + def split_keys(self) -> list[str]: + """Stable per-row keys for :func:`step_up.data.splits.stable_split`. + + Aligned with dataset indices. Each key is the row's ``id_column`` value + if one was given, otherwise its row number in the CSV. Neither depends + on ``subset_size``, filtering, or which rows fail featurization. + """ + rows = self._df.iloc[self._valid_indices] + if self.id_column is None: + return [str(i) for i in rows.index] + ids = rows[self.id_column] + if ids.isna().any(): + raise ValueError(f"id_column {self.id_column!r} has missing values in {self.path}") + return ids.astype(str).tolist() + + def rdkit_mol(self, idx: int): + """RDKit molecule for item ``idx``, atoms in the same order as its graph dict. + + Available for the ``sdf`` and ``smiles`` sources. The MOL2 path never + builds an RDKit molecule, so it raises instead. + """ + row = self._df.iloc[self._valid_indices[idx]] + if self.source == "sdf": + mol = Chem.MolFromMolBlock(str(row["sdf"]), removeHs=False) + elif self.source == "smiles": + mol = mol_from_xyz_block(str(row["xyz"])) + else: + raise NotImplementedError(f"source {self.source!r} does not go through RDKit") + # Must have the same atoms in the same order as the graph dict, or the + # evaluator would write predicted coordinates onto the wrong atoms. + return remove_hydrogens(mol, self.remove_hs) if mol is not None else None + + def split_labels(self) -> list[str]: + """Published split name per row, aligned with dataset indices.""" + if self.split_column is None: + raise ValueError("split_labels() needs split_column to be set") + labels = self._df.iloc[self._valid_indices][self.split_column] + if labels.isna().any(): + raise ValueError( + f"split_column {self.split_column!r} has missing values in {self.path}" + ) + return labels.astype(str).tolist() + + def _featurize(self, real_idx: int) -> dict[str, Any]: + row = self._df.iloc[real_idx] + if self.source == "smiles": + graph = featurize_xyz(str(row["xyz"]), remove_hs=self.remove_hs) + elif self.source == "sdf": + graph = featurize_molblock(str(row["sdf"]), remove_hs=self.remove_hs) + else: + graph = featurize_mol2_xyz(str(row["mol2"]), str(row["xyz"]), remove_hs=self.remove_hs) + # Molecule-level state for conditioning; the collator turns these into + # model inputs. Absent columns mean a neutral closed-shell molecule. + if self.charge_column is not None: + graph["charge"] = float(row[self.charge_column]) + if self.spin_column is not None: + graph["spin_multiplicity"] = float(row[self.spin_column]) + if self._tokens is not None: + key = str(row[self.id_column]) + tokens = self._tokens.get(key) + if tokens is None: + raise KeyError(f"no Mole-BERT tokens for {key!r} in {self.token_file}") + if len(tokens) != graph["num_nodes"]: + raise ValueError( + f"{key!r} has {len(tokens)} tokens for {graph['num_nodes']} atoms; " + "the token file was built from a different featurization" + ) + graph["input_ids"] = tokens + return graph + + def _validate_rows(self, max_drop_fraction: float) -> list[int]: + n = len(self._df) + kept: list[int] = [] + failures: dict[str, int] = {} + pbar = tqdm(range(n), desc=f"validating {self.path.name}", unit="mol", dynamic_ncols=True) + for i in pbar: + try: + graph = self._featurize(i) + kept.append(i) + if self._cache_enabled: + self._cache[i] = graph + except (FileNotFoundError, ImportError): + # Environment problems (e.g. the ReBind submodule isn't checked out) + # are not bad rows. Re-raise so the actionable message surfaces instead + # of being counted as a per-row failure. + raise + except Exception as e: + # Bucket by error class so the summary line is interpretable. + key = type(e).__name__ + failures[key] = failures.get(key, 0) + 1 + dropped = n - len(kept) + if dropped: + summary = ", ".join(f"{k}={v}" for k, v in sorted(failures.items())) + print( + f"[{self.path.name}] validation: kept {len(kept)}/{n} rows, " + f"dropped {dropped} ({summary})", + flush=True, + ) + # Fail fast on catastrophic drop rates — an empty dataset reaching the + # training loop produces meaningless metrics and silently corrupt + # checkpoints. + if len(kept) == 0: + raise RuntimeError( + f"Validation dropped all {n} rows of {self.path}. " + "Check the column names, encoding, and source format." + ) + if dropped / n > max_drop_fraction: + raise RuntimeError( + f"Validation dropped {dropped}/{n} rows of {self.path} " + f"({dropped / n:.1%}), exceeding max_drop_fraction=" + f"{max_drop_fraction:.1%}. Investigate before training." + ) + return kept diff --git a/src/step_up/data/featurize.py b/src/step_up/data/featurize.py new file mode 100644 index 0000000..0c8cf9b --- /dev/null +++ b/src/step_up/data/featurize.py @@ -0,0 +1,168 @@ +"""Convert raw XYZ / MOL2 blocks into ReBind-format graph dicts. + +Two paths: + +- **QM9 (XYZ + SMILES column)**: uses RDKit's ``MolFromXYZBlock`` + + ``rdDetermineBonds.DetermineBonds`` to recover the bond graph from + coordinates. SMILES is intentionally ignored because the SMILES atom order + doesn't match the XYZ order in QM9-full.csv. + +- **Organometallic (MOL2 + XYZ)**: parses MOL2 directly via + :mod:`step_up.data.mol2` — no RDKit perception. Connectivity and bond types + come straight from the file, and SYBYL atom types (``C.3``, ``C.ar``, + ``N.am``, ...) provide hybridization / aromaticity that RDKit with + ``sanitize=False`` would discard. Rings are computed from the bond graph. +""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path +from typing import Any + +import numpy as np +from rdkit import Chem +from rdkit.Chem import rdDetermineBonds + +from .mol2 import mol2_to_graph_dict + +# Load ReBind's `data/utils.py` directly so we share the canonical featurization +# without putting the entire ReBIND tree on sys.path. The load is deferred until +# the first featurization call so importing this module never fails on a fresh +# checkout that hasn't initialized the submodule yet — `pytest` test collection +# stays clean, and the missing-submodule error gets raised with an actionable +# message only when someone actually tries to featurize a molecule. +_REBIND_ROOT = Path(__file__).resolve().parents[3] / "external" / "ReBIND" +_REBIND_UTILS_PATH = _REBIND_ROOT / "data" / "utils.py" + +_rebind_utils: Any = None + + +def _load_rebind_data_utils() -> Any: + global _rebind_utils + if _rebind_utils is not None: + return _rebind_utils + if not _REBIND_UTILS_PATH.exists(): + raise FileNotFoundError( + f"ReBind submodule files missing at {_REBIND_UTILS_PATH}. " + "Run: git submodule update --init --recursive" + ) + spec = importlib.util.spec_from_file_location( + "_step_up_rebind_data_utils", str(_REBIND_UTILS_PATH) + ) + assert spec is not None and spec.loader is not None + mod = importlib.util.module_from_spec(spec) + sys.modules["_step_up_rebind_data_utils"] = mod + spec.loader.exec_module(mod) + _rebind_utils = mod + return mod + + +def remove_hydrogens(mol: Chem.Mol, remove_hs: bool) -> Chem.Mol: + """Strip explicit hydrogens when asked, so the model never sees them. + + RDKit keeps the hydrogen count in each heavy atom's ``GetTotalNumHs``, which + is the ``numH`` atom feature, so removal costs the graph no information about + how many hydrogens were there — only where they were. + """ + if not remove_hs: + return mol + stripped = Chem.RemoveHs(mol) + if stripped.GetNumAtoms() == 0: + raise ValueError("molecule has no heavy atoms") + return stripped + + +def mol_to_graph_dict(mol: Chem.Mol) -> dict[str, Any]: + """Thin wrapper around ReBind's canonical ``data.utils.mol_to_graph_dict``.""" + return _load_rebind_data_utils().mol_to_graph_dict(mol) + + +def get_allowable_features() -> dict[str, Any]: + """Expose ReBind's ``ALLOWABLE_FEATURES`` table on demand.""" + return _load_rebind_data_utils().ALLOWABLE_FEATURES + + +def parse_xyz_block(block: str) -> tuple[list[str], np.ndarray]: + """Parse a standard XYZ block string into (symbols, coords). + + Standard XYZ format: + line 1: integer atom count + line 2: comment (ignored) + lines 3..: ``element x y z`` (whitespace-separated) + """ + lines = block.strip().splitlines() + if len(lines) < 2: + raise ValueError(f"XYZ block too short ({len(lines)} lines)") + n = int(lines[0].strip()) + atom_lines = lines[2 : 2 + n] + if len(atom_lines) != n: + raise ValueError(f"XYZ header says {n} atoms but body has {len(atom_lines)}") + symbols: list[str] = [] + coords = np.empty((n, 3), dtype=np.float64) + for i, line in enumerate(atom_lines): + parts = line.split() + symbols.append(parts[0]) + coords[i] = [float(parts[1]), float(parts[2]), float(parts[3])] + return symbols, coords + + +def mol_from_xyz_block(xyz_block: str, charge: int = 0) -> Chem.Mol: + """Build an RDKit molecule from a raw XYZ block, inferring bonds from coords. + + Atom order matches the XYZ block (no remapping). Bond *orders* are inferred + via ``rdDetermineBonds.DetermineBonds`` using the supplied formal charge. + Raises ``ValueError`` if bond inference fails — callers (typically the CSV + dataset's validation pass) are responsible for filtering bad rows out. + + Suitable for closed-shell organic systems (QM9). For organometallics we use + the MOL2 path instead, which carries explicit bonds. + + NOTE: An earlier version of this function fell back to + ``DetermineConnectivity`` (bond presence without bond orders). This was a + silent data-corruption bug: ReBind's ``safe_index`` maps unspecified bond + types to the AROMATIC bucket, so every bond on those rows was being + featurized as aromatic. We now raise instead, so the bad rows get + explicitly filtered upstream. + """ + mol = Chem.MolFromXYZBlock(xyz_block) + if mol is None: + raise ValueError("RDKit failed to parse XYZ block") + rdDetermineBonds.DetermineBonds(mol, charge=charge) + return mol + + +def featurize_xyz(xyz_block: str, charge: int = 0, remove_hs: bool = False) -> dict[str, Any]: + """One-shot helper: XYZ-only featurization (QM9 path). + + Coordinates come from the XYZ block; bonds are inferred. The original + SMILES is unused — RDKit's ``DetermineBonds`` recovers the same chemistry + and the resulting atom order matches the XYZ. + """ + mol = mol_from_xyz_block(xyz_block, charge=charge) + return mol_to_graph_dict(remove_hydrogens(mol, remove_hs)) + + +def featurize_molblock(molblock: str, remove_hs: bool = False) -> dict[str, Any]: + """Featurize one SDF/MOL record (the QM9 path for ReBind's published data). + + Bonds and coordinates are read from the record, so nothing is re-perceived + from geometry, and hydrogens stay explicit. Raises ``ValueError`` when RDKit + cannot parse or sanitize the record; the dataset's validation pass filters + those rows out (ReBind's own evaluation skips them too). + """ + mol = Chem.MolFromMolBlock(molblock, removeHs=False) + if mol is None: + raise ValueError("RDKit failed to parse the molblock") + return mol_to_graph_dict(remove_hydrogens(mol, remove_hs)) + + +def featurize_mol2_xyz(mol2_block: str, xyz_block: str, remove_hs: bool = False) -> dict[str, Any]: + """One-shot helper for MOL2-sourced rows (tmQMg, BOSTMC). + + No RDKit — the MOL2 file already contains all connectivity and bond types + we need, and we keep the XYZ-block coordinates as the canonical geometry. + """ + _, coords = parse_xyz_block(xyz_block) + return mol2_to_graph_dict(mol2_block, coords=coords, remove_hs=remove_hs) diff --git a/src/step_up/data/mol2.py b/src/step_up/data/mol2.py new file mode 100644 index 0000000..d5932b5 --- /dev/null +++ b/src/step_up/data/mol2.py @@ -0,0 +1,401 @@ +"""Direct Tripos MOL2 parser — no RDKit perception. + +For organometallic data we already have the connectivity and bond types in the +MOL2 file (from molSimplify), and we have SYBYL atom types that encode +hybridization and aromaticity. Going through ``Chem.MolFromMol2Block`` with +``sanitize=False`` would give us nothing beyond what's already in the file +while throwing the SYBYL information away — so we parse MOL2 ourselves and +emit a graph dict in the same shape ReBind's collator expects. + +Atom feature semantics match ``ALLOWABLE_FEATURES`` in +``external/ReBIND/data/utils.py``: + + [atomic_num_idx, chirality_idx, degree_idx, formal_charge_idx, + numH_idx, num_radical_idx, hybridization_idx, is_aromatic_idx, + is_in_ring_idx] + +Bond features (4 per bond, undirected expanded to two directed entries): + + [bond_type_idx, bond_dir_idx, bond_stereo_idx, is_conjugated_idx] +""" + +from __future__ import annotations + +from dataclasses import dataclass, replace +from typing import Any + +import numpy as np + +# --------------------------------------------------------------------------- +# Element table (Z=1..103). Kept inline to avoid a periodic-table dep. +# --------------------------------------------------------------------------- +_ELEMENTS: tuple[str, ...] = ( + "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", + "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", + "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", + "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", + "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", + "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", + "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", + "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", + "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", + "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", + "Md", "No", "Lr", +) # fmt: skip +_SYMBOL_TO_Z: dict[str, int] = {sym: i + 1 for i, sym in enumerate(_ELEMENTS)} + +# --------------------------------------------------------------------------- +# ReBind vocab indices (mirroring ALLOWABLE_FEATURES). Keeping these inline so +# the file is independent — if upstream changes, the smoke tests will catch +# the mismatch in feature dimensions. +# --------------------------------------------------------------------------- +_POSSIBLE_ATOMIC_NUM_LEN = 118 # list(range(1, 119)) +_POSSIBLE_DEGREE = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) # plus "misc" -> idx 11 +_POSSIBLE_FORMAL_CHARGE = (-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5) # +"misc"->11 +_POSSIBLE_NUMH = (0, 1, 2, 3, 4, 5, 6, 7, 8) # +"misc"->9 + +# Chirality, hybridization, bond_type, bond_dir, bond_stereo indices line up +# with ReBind's ALLOWABLE_FEATURES tuples. We only use specific entries: +_CHI_UNSPECIFIED = 0 +_HYB = {"SP": 0, "SP2": 1, "SP3": 2, "SP3D": 3, "SP3D2": 4, "misc": 5} +_BOND_TYPE = {"SINGLE": 0, "DOUBLE": 1, "TRIPLE": 2, "AROMATIC": 3} +_BOND_DIR_NONE = 0 +_BOND_STEREO_NONE = 0 + +# SYBYL atom type -> (hybridization name, is_aromatic) +_SYBYL_HYB: dict[str, tuple[str, bool]] = { + "1": ("SP", False), + "2": ("SP2", False), + "3": ("SP3", False), + "4": ("SP3", False), # quaternary N -> SP3 + "ar": ("SP2", True), # aromatic + "am": ("SP2", False), # amide N + "pl3": ("SP2", False), # planar N + "cat": ("SP2", False), # guanidinium C + "co2": ("SP2", False), # carboxylate O + "spc": ("SP3", False), # SPC water O + "t3p": ("SP3", False), # TIP3P water O + "o": ("SP3", False), # generic +} + +# MOL2 bond-type string -> (canonical bond_type_name, is_conjugated) +_MOL2_BOND_TYPE: dict[str, tuple[str, bool]] = { + "1": ("SINGLE", False), + "2": ("DOUBLE", False), + "3": ("TRIPLE", False), + "ar": ("AROMATIC", True), + "am": ("SINGLE", True), # amide C-N -> single but conjugated + "du": ("SINGLE", False), # dummy + "un": ("SINGLE", False), # unknown -> treat as single + "nc": ("SINGLE", False), # not connected (shouldn't appear, but) +} + + +@dataclass +class Mol2Atom: + """One row from the ``@ATOM`` block.""" + + idx: int # 0-indexed + element: str # parsed from SYBYL type prefix + sybyl: str # full SYBYL string (e.g., "C.3", "Ni") + coords: np.ndarray # (3,) + + +@dataclass +class Mol2Bond: + """One row from the ``@BOND`` block.""" + + a: int # 0-indexed + b: int # 0-indexed + mol2_type: str # raw MOL2 type string + + +@dataclass +class Mol2: + atoms: list[Mol2Atom] + bonds: list[Mol2Bond] + + +# --------------------------------------------------------------------------- +# Parsing +# --------------------------------------------------------------------------- + + +def _element_from_sybyl(sybyl: str) -> str: + """Element symbol = SYBYL prefix before the first dot (or full string).""" + return sybyl.split(".", 1)[0] + + +def _hyb_from_sybyl(sybyl: str) -> tuple[str, bool]: + """SYBYL → (hybridization, is_aromatic). Metals/unknowns default to misc.""" + if "." not in sybyl: + # No SYBYL hybridization given (e.g. metals: "Ni", "Fe"). misc bucket. + return ("misc", False) + suffix = sybyl.split(".", 1)[1] + return _SYBYL_HYB.get(suffix, ("misc", False)) + + +def parse_mol2(text: str) -> Mol2: + """Parse a single-molecule MOL2 block string.""" + atoms: list[Mol2Atom] = [] + bonds: list[Mol2Bond] = [] + section: str | None = None + for raw_line in text.splitlines(): + line = raw_line.strip() + if not line: + continue + if line.startswith("@"): + section = line[len("@") :].strip().upper() + continue + if section == "ATOM": + atoms.append(_parse_atom_line(line, len(atoms))) + elif section == "BOND": + bonds.append(_parse_bond_line(line)) + # Other sections (MOLECULE header, SUBSTRUCTURE, etc.) are ignored. + return Mol2(atoms=atoms, bonds=bonds) + + +def _parse_atom_line(line: str, idx0: int) -> Mol2Atom: + """ATOM line: id name x y z sybyl_type [subst_id [subst_name [charge]]].""" + parts = line.split() + if len(parts) < 6: + raise ValueError(f"MOL2 ATOM line too short: {line!r}") + x, y, z = float(parts[2]), float(parts[3]), float(parts[4]) + sybyl = parts[5] + element = _element_from_sybyl(sybyl) + return Mol2Atom( + idx=idx0, + element=element, + sybyl=sybyl, + coords=np.array([x, y, z], dtype=np.float64), + ) + + +def _parse_bond_line(line: str) -> Mol2Bond: + """BOND line: id atom_a atom_b type.""" + parts = line.split() + if len(parts) < 4: + raise ValueError(f"MOL2 BOND line too short: {line!r}") + a = int(parts[1]) - 1 # MOL2 is 1-indexed + b = int(parts[2]) - 1 + return Mol2Bond(a=a, b=b, mol2_type=parts[3]) + + +# --------------------------------------------------------------------------- +# Graph-dict construction +# --------------------------------------------------------------------------- + + +def _safe_index(seq: tuple, value: Any, miss_idx: int) -> int: + try: + return seq.index(value) + except ValueError: + return miss_idx + + +def _tarjan_bridges(adj: list[list[tuple[int, int]]]) -> set[int]: + """Return the set of bridge edge-ids in an undirected graph. + + A bridge is an edge whose removal disconnects the graph. The implementation + is iterative Tarjan with explicit ``disc``/``low`` arrays. + """ + n = len(adj) + disc = [-1] * n + low = [0] * n + bridges: set[int] = set() + timer = 0 + for start in range(n): + if disc[start] != -1: + continue + # Iterative DFS rooted at ``start``. Stack entries are + # (node, parent_edge_id, neighbor_iter_index). + stack: list[list[int]] = [[start, -1, 0]] + disc[start] = low[start] = timer + timer += 1 + while stack: + node, pedge, i = stack[-1] + if i >= len(adj[node]): + stack.pop() + if not stack: + break + parent = stack[-1][0] + low[parent] = min(low[parent], low[node]) + if low[node] > disc[parent]: + bridges.add(pedge) + continue + stack[-1][2] = i + 1 + nbr, eid = adj[node][i] + if eid == pedge: + continue + if disc[nbr] == -1: + disc[nbr] = low[nbr] = timer + timer += 1 + stack.append([nbr, eid, 0]) + else: + low[node] = min(low[node], disc[nbr]) + return bridges + + +def _find_ring_atoms_and_bonds( + n_atoms: int, bond_pairs: list[tuple[int, int]] +) -> tuple[set[int], set[frozenset[int]]]: + """Return (atoms_in_any_ring, bonds_in_any_ring). + + An edge is a *bridge* iff its removal disconnects the graph, equivalently + iff it is not part of any cycle. The complement of bridges is the set of + ring bonds. + """ + adj: list[list[tuple[int, int]]] = [[] for _ in range(n_atoms)] + for eid, (a, b) in enumerate(bond_pairs): + adj[a].append((b, eid)) + adj[b].append((a, eid)) + bridges = _tarjan_bridges(adj) + ring_edge_set: set[frozenset[int]] = set() + ring_atom_set: set[int] = set() + for eid, (a, b) in enumerate(bond_pairs): + if eid not in bridges: + ring_edge_set.add(frozenset((a, b))) + ring_atom_set.add(a) + ring_atom_set.add(b) + return ring_atom_set, ring_edge_set + + +def _count_h_neighbors(parsed: Mol2) -> list[int]: + """Number of hydrogens bonded to each atom.""" + counts = [0] * len(parsed.atoms) + for b in parsed.bonds: + if parsed.atoms[b.b].element == "H": + counts[b.a] += 1 + if parsed.atoms[b.a].element == "H": + counts[b.b] += 1 + return counts + + +def _drop_hydrogens( + parsed: Mol2, conformer: list[list[float]], num_h_neighbors: list[int] +) -> tuple[Mol2, list[list[float]], list[int]]: + """Return ``parsed`` without its hydrogens, reindexed, with coords and numH to match.""" + keep = [i for i, atom in enumerate(parsed.atoms) if atom.element != "H"] + if not keep: + raise ValueError("MOL2 block has no heavy atoms") + remap = {old: new for new, old in enumerate(keep)} + filtered = Mol2( + atoms=[replace(parsed.atoms[i], idx=remap[i]) for i in keep], + bonds=[ + replace(b, a=remap[b.a], b=remap[b.b]) + for b in parsed.bonds + if b.a in remap and b.b in remap + ], + ) + return filtered, [conformer[i] for i in keep], [num_h_neighbors[i] for i in keep] + + +def mol2_to_graph_dict( + mol2_text: str, coords: np.ndarray | None = None, remove_hs: bool = False +) -> dict[str, Any]: + """Build a ReBind-format graph dict directly from a MOL2 block. + + If ``coords`` is supplied (e.g. parsed from a separate XYZ column), + it overrides the MOL2 coordinates — useful when the canonical geometry + lives in a different column. Otherwise we use the MOL2 atom positions. + + ``remove_hs`` drops hydrogens from the graph, so the model neither sees nor + predicts them. It mirrors RDKit's ``Chem.RemoveHs``: the hydrogen count + survives in each heavy atom's ``numH`` feature, and ``degree`` becomes the + heavy-atom degree. + """ + parsed = parse_mol2(mol2_text) + n = len(parsed.atoms) + if n == 0: + raise ValueError("MOL2 block has zero atoms") + + # ---- Coordinates ------------------------------------------------------ + if coords is not None: + if coords.shape != (n, 3): + raise ValueError(f"coords shape {coords.shape} does not match MOL2 atom count {n}") + conformer = coords.tolist() + else: + conformer = np.stack([a.coords for a in parsed.atoms], axis=0).tolist() + + # Counted over the whole molecule, before any hydrogen is dropped, so the + # numH feature says the same thing either way. + num_h_neighbors = _count_h_neighbors(parsed) + + if remove_hs: + parsed, conformer, num_h_neighbors = _drop_hydrogens(parsed, conformer, num_h_neighbors) + n = len(parsed.atoms) + + # ---- Bonds first (need degree, numH, in-ring) ------------------------- + bond_pairs: list[tuple[int, int]] = [] + bond_types: list[str] = [] + bond_conjugated: list[bool] = [] + for b in parsed.bonds: + canonical_type, is_conj = _MOL2_BOND_TYPE.get(b.mol2_type, ("SINGLE", False)) + bond_pairs.append((b.a, b.b)) + bond_types.append(canonical_type) + bond_conjugated.append(is_conj) + + ring_atoms, _ring_bonds = _find_ring_atoms_and_bonds(n, bond_pairs) + degree = [0] * n + for a, b in bond_pairs: + degree[a] += 1 + degree[b] += 1 + + # ---- Atom features ---------------------------------------------------- + node_attr: list[list[int]] = [] + node_type: list[int] = [] + for atom in parsed.atoms: + z = _SYMBOL_TO_Z.get(atom.element) + if z is None: + raise ValueError(f"Unknown element symbol {atom.element!r} in MOL2") + atom_num_idx = z - 1 # index into list(range(1, 119)) + hyb_name, is_aromatic = _hyb_from_sybyl(atom.sybyl) + in_ring = atom.idx in ring_atoms + feat = [ + atom_num_idx, + _CHI_UNSPECIFIED, # MOL2 has no chirality field + _safe_index(_POSSIBLE_DEGREE, degree[atom.idx], len(_POSSIBLE_DEGREE)), + _safe_index(_POSSIBLE_FORMAL_CHARGE, 0, len(_POSSIBLE_FORMAL_CHARGE)), + _safe_index(_POSSIBLE_NUMH, num_h_neighbors[atom.idx], len(_POSSIBLE_NUMH)), + 0, # num radical electrons (MOL2 has no radical info) + _HYB.get(hyb_name, _HYB["misc"]), + int(is_aromatic), + int(in_ring), + ] + node_attr.append(feat) + node_type.append(atom_num_idx) + + # ---- Bond features (bidirectional) ------------------------------------ + edges_a: list[int] = [] + edges_b: list[int] = [] + edge_attr: list[list[int]] = [] + for (a, b), btype, conj in zip(bond_pairs, bond_types, bond_conjugated, strict=True): + feat = [ + _BOND_TYPE.get(btype, _BOND_TYPE["SINGLE"]), + _BOND_DIR_NONE, + _BOND_STEREO_NONE, + int(conj), + ] + edges_a.append(a) + edges_b.append(b) + edge_attr.append(feat) + edges_a.append(b) + edges_b.append(a) + edge_attr.append(feat) + + edge_index = [edges_a, edges_b] + edge_type = [feat[0] for feat in edge_attr] + edge_dir_type = [feat[1] for feat in edge_attr] + + return { + "node_attr": node_attr, + "edge_attr": edge_attr, + "edge_index": edge_index, + "node_type": node_type, + "node_chiral_type": [feat[1] for feat in node_attr], + "edge_type": edge_type, + "edge_dire_type": edge_dir_type, + "num_nodes": n, + "num_edges": len(edge_attr), + "conformer": conformer, + } diff --git a/src/step_up/data/splits.py b/src/step_up/data/splits.py new file mode 100644 index 0000000..91e084d --- /dev/null +++ b/src/step_up/data/splits.py @@ -0,0 +1,130 @@ +"""Deterministic train/val/test split helpers.""" + +from __future__ import annotations + +import hashlib +from collections.abc import Mapping, Sequence +from pathlib import Path + +from torch.utils.data import Dataset, Subset + +SPLIT_NAMES = ("train", "val", "test") +# Names other projects use for the same three splits. +_SPLIT_ALIASES = {"valid": "val", "validation": "val", "dev": "val", "eval": "val"} + + +def split_by_labels(dataset: Dataset, labels: Sequence[str]) -> tuple[Subset, Subset, Subset]: + """Partition ``dataset`` by an explicit per-row split name. + + Use this to adopt a published split (``labels[i]`` is item ``i``'s split) + instead of deriving one. ``valid``/``validation``/``dev``/``eval`` are + accepted as spellings of ``val``. + """ + n = len(dataset) # type: ignore[arg-type] + if len(labels) != n: + raise ValueError(f"Got {len(labels)} labels for a dataset of length {n}") + buckets: dict[str, list[int]] = {name: [] for name in SPLIT_NAMES} + for i, raw in enumerate(labels): + name = str(raw).strip().lower() + name = _SPLIT_ALIASES.get(name, name) + if name not in buckets: + raise ValueError( + f"Unknown split name {raw!r} at row {i}; expected one of {SPLIT_NAMES}" + ) + buckets[name].append(i) + return tuple(Subset(dataset, buckets[name]) for name in SPLIT_NAMES) # type: ignore[return-value] + + +_ID_LIST_HEADERS = {"refcode", "id", "mol_id", "identifier", "name", "complex"} + + +def read_id_list(path: str | Path) -> list[str]: + """Read IDs from a one-per-line text file or a single-column CSV. + + A leading line that looks like a column header (``refcode``, ``id``, ...) is + skipped, and only the first comma-separated field of each line is used. + """ + ids: list[str] = [] + for line in Path(path).read_text().splitlines(): + entry = line.split(",")[0].strip() + if entry: + ids.append(entry) + if ids and ids[0].lower() in _ID_LIST_HEADERS: + ids = ids[1:] + return ids + + +def split_by_id_files( + dataset: Dataset, keys: Sequence[str], files: Mapping[str, str | Path] +) -> tuple[Subset, Subset, Subset]: + """Partition ``dataset`` using published ID lists, one file per split. + + ``files`` maps a split name to a file of IDs, and ``keys[i]`` is item ``i``'s + ID. Items listed in none of the files are dropped, which is how a split + published against a slightly different snapshot of a dataset still applies + to ours. + """ + assignment: dict[str, str] = {} + for raw_name, path in files.items(): + name = str(raw_name).strip().lower() + name = _SPLIT_ALIASES.get(name, name) + if name not in SPLIT_NAMES: + raise ValueError(f"Unknown split name {raw_name!r}; expected one of {SPLIT_NAMES}") + for identifier in read_id_list(path): + previous = assignment.get(identifier) + if previous is not None and previous != name: + raise ValueError(f"ID {identifier!r} is listed in both {previous} and {name}") + assignment[identifier] = name + + buckets: dict[str, list[int]] = {name: [] for name in SPLIT_NAMES} + unlisted = 0 + for i, key in enumerate(keys): + name = assignment.get(str(key)) + if name is None: + unlisted += 1 + continue + buckets[name].append(i) + if unlisted: + print( + f"[split] {unlisted} rows are in none of the split files and were dropped", flush=True + ) + return tuple(Subset(dataset, buckets[name]) for name in SPLIT_NAMES) # type: ignore[return-value] + + +def _unit_interval(key: str, seed: int) -> float: + """Map ``(seed, key)`` to a float in [0, 1), identical on every platform and run.""" + digest = hashlib.blake2b(f"{seed}:{key}".encode(), digest_size=8).digest() + return int.from_bytes(digest, "big") / 2**64 + + +def stable_split( + dataset: Dataset, + keys: Sequence[str], + ratios: tuple[float, float, float] = (0.8, 0.1, 0.1), + seed: int = 0, +) -> tuple[Subset, Subset, Subset]: + """Split ``dataset`` into train/val/test by hashing each item's key. + + ``keys[i]`` identifies item ``i``, e.g. a molecule ID or its row number in the + source CSV. An item's split depends only on its own key, ``seed`` and ``ratios``, + so it never moves when other items are added, removed, filtered out, or fail + featurization, and items that share a key always share a split. Split sizes + follow ``ratios`` only approximately, which matters just for tiny datasets. + """ + if abs(sum(ratios) - 1.0) > 1e-6: + raise ValueError(f"Ratios must sum to 1, got {ratios}") + n = len(dataset) # type: ignore[arg-type] + if len(keys) != n: + raise ValueError(f"Got {len(keys)} keys for a dataset of length {n}") + train_idx: list[int] = [] + val_idx: list[int] = [] + test_idx: list[int] = [] + for i, key in enumerate(keys): + u = _unit_interval(key, seed) + if u < ratios[0]: + train_idx.append(i) + elif u < ratios[0] + ratios[1]: + val_idx.append(i) + else: + test_idx.append(i) + return Subset(dataset, train_idx), Subset(dataset, val_idx), Subset(dataset, test_idx) diff --git a/src/step_up/eval/__init__.py b/src/step_up/eval/__init__.py new file mode 100644 index 0000000..a9113d1 --- /dev/null +++ b/src/step_up/eval/__init__.py @@ -0,0 +1 @@ +"""Evaluation utilities for step-up.""" diff --git a/src/step_up/eval/conformer_eval.py b/src/step_up/eval/conformer_eval.py new file mode 100644 index 0000000..c8e0096 --- /dev/null +++ b/src/step_up/eval/conformer_eval.py @@ -0,0 +1,166 @@ +"""Conformer metrics in ReBind's published protocol. + +``external/ReBIND/evaluate.py`` reports three numbers, and the details matter if +you want to compare against the paper: + +- **D-MAE / D-RMSE** pool absolute / squared errors over *every* pairwise + distance in the split and divide by the total count of distances, including + the zero diagonal. This is a distance-weighted average, so larger molecules + count more — unlike the per-batch mean the training loop prints. +- **C-RMSD** is RDKit's ``GetBestRMS`` (optimal alignment, symmetry aware) on + the molecule with hydrogens removed, averaged per molecule. + +For reference, ReBind's paper reports test D-MAE 0.254, D-RMSE 0.446 and +C-RMSD 0.321 on QM9. +""" + +from __future__ import annotations + +import json +from collections.abc import Sequence +from pathlib import Path +from typing import Any + +import torch +from rdkit import Chem, RDLogger +from rdkit.Chem import rdchem, rdMolAlign +from torch.utils.data import DataLoader, Subset +from tqdm import tqdm + +RDLogger.DisableLog("rdApp.*") + + +def kabsch_rmsd(pred: torch.Tensor, target: torch.Tensor) -> float: + """RMSD after optimally aligning ``pred`` onto ``target`` (Kabsch, no symmetry matching). + + Both are ``(n, 3)`` over an individual molecule's real atoms. Doing the + alignment here rather than trusting the model's own matters: ReBind and GTMGC + align inside the prediction head over the *padded* batch tensor, so padding + zeros pull the centroid and rotation off and inflate the RMSD — badly for a + small molecule batched with a large one. + """ + p = pred - pred.mean(dim=0, keepdim=True) + q = target - target.mean(dim=0, keepdim=True) + u, _, vt = torch.linalg.svd(p.T @ q) + # Flip the last axis if the optimal rotation came out as a reflection. + sign = torch.sign(torch.det(vt.T @ u.T)) + correction = torch.diag(torch.tensor([1.0, 1.0, float(sign)], dtype=p.dtype)) + rotation = vt.T @ correction @ u.T + aligned = (rotation @ p.T).T + return float(((aligned - q) ** 2).sum(dim=-1).mean().sqrt()) + + +def _predicted_mol(mol: Chem.Mol, coords: Sequence[Sequence[float]]) -> Chem.Mol: + """Copy ``mol`` and replace its conformer with ``coords``.""" + mol_hat = Chem.Mol(mol) + conformer = rdchem.Conformer(mol.GetNumAtoms()) + for i, position in enumerate(coords): + conformer.SetAtomPosition(i, [float(v) for v in position]) + mol_hat.RemoveAllConformers() + mol_hat.AddConformer(conformer) + return mol_hat + + +def evaluate_split( + model: torch.nn.Module, + dataset: Any, + subset: Subset, + collator: Any, + device: str = "cpu", + batch_size: int = 100, + remove_hs: bool = True, + rmsd_method: str = "auto", +) -> dict[str, float | str]: + """Score ``subset`` with ReBind's metric definitions. + + ``dataset`` must expose ``rdkit_mol(idx)`` for the C-RMSD part; the indices + in ``subset`` index into it. + + ``rmsd_method`` is ``"auto"`` (RDKit ``GetBestRMS`` where a molecule can be + built, else aligned coordinates) or ``"aligned_coords"`` to force the plain + metric everywhere, which is what makes RMSD comparable between the organic + and organometallic sets. + + ``remove_hs`` applies to both RMSD paths — via ``Chem.RemoveHs`` for the + RDKit one and via the atom types for the aligned-coordinate one. It never + touches D-MAE / D-RMSE, which pool over every atom including hydrogen, as + ReBind's ``evaluate.py`` does. + """ + if rmsd_method not in ("auto", "aligned_coords"): + raise ValueError(f"Unknown rmsd_method: {rmsd_method!r}") + model.eval() + loader = DataLoader(subset, batch_size=batch_size, shuffle=False, collate_fn=collator) + total_abs, total_sq, total_dist = 0.0, 0.0, 0 + total_rmsd, n_rmsd, rmsd_failures, n_mol = 0.0, 0, 0, 0 + # What we ask for vs what we end up reporting: "auto" degrades to aligned + # coordinates for rows that have no RDKit molecule (the MOL2 path). + use_rdkit = rmsd_method == "auto" + reported_method = "rdkit_bestrms" if use_rdkit else "aligned_coords" + + position = 0 + for batch in tqdm(loader, desc="evaluating", leave=False, dynamic_ncols=True): + device_batch = {k: (v.to(device) if torch.is_tensor(v) else v) for k, v in batch.items()} + with torch.no_grad(): + out = model(**device_batch) + node_mask = device_batch["node_mask"].bool() + for row in range(node_mask.shape[0]): + keep = node_mask[row] + pred = out.conformer_hat[row][keep].double().cpu() + target = out.conformer[row][keep].double().cpu() + d_pred, d_true = torch.cdist(pred, pred), torch.cdist(target, target) + delta = (d_pred - d_true).abs() + total_abs += float(delta.sum()) + total_sq += float((delta**2).sum()) + total_dist += delta.numel() # n*n, diagonal included, as in ReBind + n_mol += 1 + + mol = None + if use_rdkit: + try: + mol = dataset.rdkit_mol(subset.indices[position + row]) + except NotImplementedError: + # Organometallic (MOL2) rows never build an RDKit molecule. + reported_method = "aligned_coords" + if mol is None: + # Optimal rigid alignment on this molecule's real atoms. Not + # symmetry matched, so only compare to other aligned_coords numbers. + # `node_type` is Z here (the collator shifted it by 1 so 0 can + # mean padding), so hydrogens can be dropped without RDKit — + # which matters: they are a third of the atoms in a metal complex + # and the hardest to place. + if remove_hs: + heavy = device_batch["node_type"][row][keep] != 1 + total_rmsd += kabsch_rmsd(pred[heavy.cpu()], target[heavy.cpu()]) + else: + total_rmsd += kabsch_rmsd(pred, target) + n_rmsd += 1 + else: + ref = _predicted_mol(mol, target.tolist()) + probe = _predicted_mol(mol, pred.tolist()) + try: + if remove_hs: + ref, probe = Chem.RemoveHs(ref), Chem.RemoveHs(probe) + total_rmsd += rdMolAlign.GetBestRMS(probe, ref) + n_rmsd += 1 + except Exception: + # GetBestRMS can fail on molecules RDKit won't match up. + rmsd_failures += 1 + position += node_mask.shape[0] + + metrics: dict[str, float | str] = { + "d_mae": total_abs / max(total_dist, 1), + "d_rmse": (total_sq / max(total_dist, 1)) ** 0.5, + "c_rmsd": total_rmsd / max(n_rmsd, 1), + "c_rmsd_method": reported_method, + "n_molecules": float(n_mol), + "n_rmsd_molecules": float(n_rmsd), + "n_rmsd_failures": float(rmsd_failures), + "remove_hs": float(remove_hs), + } + return metrics + + +def write_metrics(metrics: dict[str, float | str], path: str | Path) -> None: + Path(path).parent.mkdir(parents=True, exist_ok=True) + with open(path, "w") as f: + json.dump(metrics, f, indent=2) diff --git a/src/step_up/eval/metrics.py b/src/step_up/eval/metrics.py new file mode 100644 index 0000000..21c7f49 --- /dev/null +++ b/src/step_up/eval/metrics.py @@ -0,0 +1,92 @@ +"""Conformer-quality metrics matching ReBind's evaluate.py. + +All metrics operate on padded coordinate tensors of shape ``(B, N, 3)`` with a +boolean ``node_mask`` of shape ``(B, N)`` marking valid (non-padding) atoms. +""" + +from __future__ import annotations + +import torch +import torch.nn.functional as F + + +def _pair_mask(node_mask: torch.Tensor) -> torch.Tensor: + m = node_mask.to(torch.bool).unsqueeze(-1) + return (m & m.transpose(-1, -2)).to(torch.float32) + + +def cdist_mae(pred: torch.Tensor, target: torch.Tensor, node_mask: torch.Tensor) -> torch.Tensor: + """Mean absolute error on pairwise distance matrices (ReBind ``D-MAE``).""" + mask = _pair_mask(node_mask) + d_pred = torch.cdist(pred, pred) * mask + d_true = torch.cdist(target, target) * mask + return F.l1_loss(d_pred, d_true, reduction="sum") / mask.sum().clamp_min(1) + + +def cdist_rmse(pred: torch.Tensor, target: torch.Tensor, node_mask: torch.Tensor) -> torch.Tensor: + """Root mean squared error on pairwise distance matrices (ReBind ``D-RMSE``).""" + mask = _pair_mask(node_mask) + d_pred = torch.cdist(pred, pred) * mask + d_true = torch.cdist(target, target) * mask + mse = F.mse_loss(d_pred, d_true, reduction="sum") / mask.sum().clamp_min(1) + return torch.sqrt(mse) + + +def coord_rmsd(pred: torch.Tensor, target: torch.Tensor, node_mask: torch.Tensor) -> torch.Tensor: + """Per-molecule coordinate RMSD averaged over the batch (no Kabsch alignment). + + ReBind aligns predictions to targets *inside* the model head before the loss, + so by the time tensors leave the model, this raw RMSD is already + rotation-aware. For unaligned eval (e.g., a baseline that does no + alignment), use ``kabsch_rmsd_per_mol`` below. + """ + delta = (pred - target).to(torch.float32) + sq = (delta * delta).sum(dim=-1) * node_mask.to(torch.float32) + msd = sq.sum(dim=-1) / node_mask.sum(dim=-1).clamp_min(1) + return torch.sqrt(msd).mean() + + +def per_element_dmae( + pred: torch.Tensor, + target: torch.Tensor, + node_mask: torch.Tensor, + atomic_numbers: torch.Tensor, +) -> dict[int, float]: + """Per-element D-MAE: |d_ij_pred - d_ij_true| averaged over pairs touching Z. + + ``atomic_numbers`` is ``(B, N)`` of **true atomic numbers** (1 for H, 6 for + C, etc.). Padding positions are identified via ``node_mask`` (1 = valid, + 0 = padding) — *not* via a sentinel atomic-number value. + + ReBind uses two encodings, so check which one you have: + + - A collated batch's ``node_type`` is already true atomic numbers (the + collator adds 1, with 0 at padding). Pass it directly. + - A single graph dict's ``node_type`` is ``Z - 1`` (H is 0). Add 1 first. + + Raises ``ValueError`` if a non-padding atom has atomic number < 1, which + catches ``Z - 1`` indices passed by mistake whenever hydrogen is present. + + Returns a dict mapping Z -> mean |Δd| over pairs (i, j) with i or j of + that element (each pair contributes to both endpoint elements). + """ + mask = _pair_mask(node_mask) + diff = (torch.cdist(pred, pred) - torch.cdist(target, target)).abs() * mask + valid = node_mask.to(torch.bool) + + zs = atomic_numbers.to(torch.long) + if (zs[valid] < 1).any(): + raise ValueError( + "atomic_numbers must be true atomic numbers (H = 1), but a non-padding atom " + "has Z < 1. Graph-dict node_type is Z - 1; add 1 before calling." + ) + per_z: dict[int, float] = {} + for z in torch.unique(zs[valid]): + z_int = int(z.item()) + # Pairs where either endpoint is a valid atom of element z. + endpoint = ((zs == z) & valid).to(torch.float32) + endpoint_mask = (endpoint.unsqueeze(-1) + endpoint.unsqueeze(-2)).clamp_max(1.0) + pair_mask = mask * endpoint_mask + denom = pair_mask.sum().clamp_min(1) + per_z[z_int] = float((diff * endpoint_mask).sum() / denom) + return per_z diff --git a/src/step_up/models/__init__.py b/src/step_up/models/__init__.py new file mode 100644 index 0000000..dcc3550 --- /dev/null +++ b/src/step_up/models/__init__.py @@ -0,0 +1,65 @@ +"""Model wrappers for step-up benchmark. + +Each benchmark model is a thin wrapper over a vendored upstream implementation +and exposes the same two entry points, so the training loop stays model-agnostic: +``build_model`` for the network and ``build_model_collator`` for its batching. +""" + +from __future__ import annotations + +from typing import Any + +from .common import N_GLOBAL_FEATURES + +MODEL_NAMES = ("rebind", "gtmgc") + + +def build_model( + name: str, + n_layers: int, + d_model: int, + d_ffn: int, + n_head: int, + dropout: float = 0.0, + n_global_features: int = 0, +) -> Any: + """Build one of the benchmark models by name.""" + if name == "rebind": + from .rebind import build_rebind + + return build_rebind( + n_layers=n_layers, + d_model=d_model, + d_ffn=d_ffn, + n_head=n_head, + dropout=dropout, + n_global_features=n_global_features, + ) + if name == "gtmgc": + from .gtmgc import build_gtmgc + + return build_gtmgc( + n_layers=n_layers, + d_model=d_model, + d_ffn=d_ffn, + n_head=n_head, + dropout=dropout, + n_global_features=n_global_features, + ) + raise ValueError(f"Unknown model: {name!r} (expected one of {MODEL_NAMES})") + + +def build_model_collator(name: str, conditioned: bool = False) -> Any: + """Build the collator that matches ``name``'s expected batch layout.""" + if name == "rebind": + from .rebind import build_collator + + return build_collator(conditioned=conditioned) + if name == "gtmgc": + from .gtmgc import build_gtmgc_collator + + return build_gtmgc_collator(conditioned=conditioned) + raise ValueError(f"Unknown model: {name!r} (expected one of {MODEL_NAMES})") + + +__all__ = ["MODEL_NAMES", "N_GLOBAL_FEATURES", "build_model", "build_model_collator"] diff --git a/src/step_up/models/common.py b/src/step_up/models/common.py new file mode 100644 index 0000000..77ae166 --- /dev/null +++ b/src/step_up/models/common.py @@ -0,0 +1,85 @@ +"""Pieces shared by the vendored model wrappers. + +Both ReBind and GTMGC descend from the same code base, so they need the same +two fixes and the same optional conditioning: + +- the Laplacian positional encoding is added in place upstream, which breaks + autograd in fp32 (see the wrappers for the details), and +- neither model takes molecule-level state, so charge and spin are projected + and added to every atom embedding. +""" + +from __future__ import annotations + +from typing import Any + +import torch + +# Charge (electrons) and unpaired electrons (spin multiplicity - 1). +N_GLOBAL_FEATURES = 2 + + +def add_lap_out_of_place(node_embedding: torch.Tensor, lap: torch.Tensor) -> torch.Tensor: + """Add ``lap`` into the leading channels of ``node_embedding`` without in-place ops.""" + d = node_embedding.shape[-1] + lap_dim = lap.shape[-1] + if lap_dim < d: + lap = torch.nn.functional.pad(lap, (0, d - lap_dim)) + elif lap_dim > d: + lap = lap[..., :d] + return node_embedding + lap + + +def global_condition_mlp(n_features: int, d_model: int) -> torch.nn.Module: + """Project molecule-level scalars to ``d_model``, starting as a no-op. + + The last layer is zero-initialised, so a freshly built conditioned model + behaves exactly like the unconditioned one and learns to use the + conditioning from there. + """ + mlp = torch.nn.Sequential( + torch.nn.Linear(n_features, d_model), + torch.nn.SiLU(), + torch.nn.Linear(d_model, d_model), + ) + torch.nn.init.zeros_(mlp[-1].weight) + torch.nn.init.zeros_(mlp[-1].bias) + return mlp + + +def apply_global_conditioning(node_embedding: torch.Tensor, inputs: dict[str, Any]) -> torch.Tensor: + """Broadcast ``inputs["global_embedding"]`` over atoms, if it is there. + + Padding positions embed to zero, so they are re-masked to stay that way. + """ + global_embedding = inputs.get("global_embedding") + if global_embedding is None: + return node_embedding + node_mask = inputs.get("node_mask") + conditioned = node_embedding + global_embedding.unsqueeze(1) + if node_mask is not None: + conditioned = conditioned * node_mask.unsqueeze(-1) + return conditioned + + +class GlobalConditionCollator: + """Wrap a model's collator and add ``global_features`` of shape ``(B, 2)``. + + Column 0 is the formal charge in electrons, column 1 the number of unpaired + electrons (spin multiplicity - 1), which is 0 for a closed-shell singlet. + Graph dicts without those fields are treated as neutral and closed-shell. + """ + + def __init__(self, base: Any) -> None: + self.base = base + + def __call__(self, mol_sq: Any) -> dict[str, Any]: + batch = self.base(mol_sq) + batch["global_features"] = torch.tensor( + [ + [float(mol.get("charge", 0.0)), float(mol.get("spin_multiplicity", 1.0)) - 1.0] + for mol in mol_sq + ], + dtype=torch.float32, + ) + return batch diff --git a/src/step_up/models/gtmgc.py b/src/step_up/models/gtmgc.py new file mode 100644 index 0000000..47671cb --- /dev/null +++ b/src/step_up/models/gtmgc.py @@ -0,0 +1,271 @@ +"""Thin wrapper around the vendored GTMGC implementation. + +Vendored at ``external/GTMGC`` (Xu et al., ICLR 2024). ReBind was built on this +code base, so the two wrappers look alike. GTMGC's package is *also* called +``models``, which would collide with ReBind's entry on ``sys.path``, so this one +is imported under a private module name instead of by path injection. + +Nothing is imported until ``build_gtmgc()`` or ``get_gtmgc_collator()`` runs. One +runtime patch is applied: ``GTMGCEncoder.forward`` and ``GTMGCDecoder.forward`` +add the Laplacian positional encoding with an in-place slice assignment, which +breaks autograd in fp32 exactly as ReBind's does (upstream trains under fp16 +autocast). Unlike ReBind, nothing reads the tensor between the write and its use, +so the out-of-place replacement is value-identical to upstream. + +Their conformer models embed atoms as Mole-BERT tokenizer ids +(``embed_style="atom_tokenized_ids"``), so batches need per-atom ``input_ids``; +see ``scripts/tokenize_molebert.py``. +""" + +from __future__ import annotations + +import importlib.util +import json +import sys +from pathlib import Path +from typing import Any + +import torch + +from .common import ( + N_GLOBAL_FEATURES, + GlobalConditionCollator, + add_lap_out_of_place, + apply_global_conditioning, + global_condition_mlp, +) + +_GTMGC_ROOT = Path(__file__).resolve().parents[3] / "external" / "GTMGC" +_GTMGC_PROBE = _GTMGC_ROOT / "models" / "gtmgc" / "modeling_gtmgc.py" +_VENDOR_MODULE = "_step_up_vendor_gtmgc" + +_gtmgc: Any = None +_gtmgc_modeling: Any = None +# Upstream ``forward`` methods replaced below, keyed by class, so tests can check +# the patched model against upstream. +_UPSTREAM_FORWARDS: dict[type, Any] = {} +_CONDITIONED_GTMGC: Any = None + +__all__ = ["build_gtmgc", "build_gtmgc_collator", "get_gtmgc_collator"] + + +def _load_gtmgc() -> None: + """Import the vendored GTMGC package and apply the runtime patch. Idempotent.""" + global _gtmgc, _gtmgc_modeling + if _gtmgc is not None: + return + if not _GTMGC_PROBE.exists(): + raise FileNotFoundError( + f"GTMGC submodule files missing at {_GTMGC_PROBE}. " + "Run: git submodule update --init --recursive" + ) + package = _GTMGC_ROOT / "models" + spec = importlib.util.spec_from_file_location( + _VENDOR_MODULE, package / "__init__.py", submodule_search_locations=[str(package)] + ) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + # Register before executing so the package's relative imports resolve. + sys.modules[_VENDOR_MODULE] = module + spec.loader.exec_module(module) + _gtmgc = module + _gtmgc_modeling = sys.modules[f"{_VENDOR_MODULE}.gtmgc.modeling_gtmgc"] + patch_inplace_lap_addition() + + +def _patched_encoder_forward(self, **inputs): + """Out-of-place equivalent of ``GTMGCEncoder.forward``.""" + if self.embed_style == "atom_tokenized_ids": + node_embedding = self.node_embedding(inputs.get("node_input_ids")) + elif self.embed_style == "atom_type_ids": + node_embedding = self.node_embedding(inputs.get("node_type")) + elif self.embed_style == "ogb": + node_embedding = self.ogb_node_embedding(inputs["node_attr"]) + else: + raise ValueError(f"Unknown embed_style: {self.embed_style!r}") + + node_embedding = add_lap_out_of_place(node_embedding, inputs.get("lap_eigenvectors")) + node_embedding = apply_global_conditioning(node_embedding, inputs) + inputs["node_embedding"] = node_embedding + + if self.config.encoder_use_D_in_attn: + # Off in their published conformer configs: this would feed the encoder + # distances from the ground-truth conformer. + conformer = inputs.get("conformer") + distance = torch.cdist(conformer, conformer) + mask = _gtmgc_modeling.make_cdist_mask(inputs.get("node_mask")) + inputs["distance"] = _gtmgc_modeling.compute_distance_residual_bias( + cdist=distance, cdist_mask=mask + ) + + attn_weight_dict: dict = {} + for i, encoder_block in enumerate(self.encoder_blocks): + block_out = encoder_block(**inputs) + node_embedding, attn_weight = block_out["out"], block_out["attn_weight"] + inputs["node_embedding"] = node_embedding + attn_weight_dict[f"encoder_block_{i}"] = attn_weight + return {"node_embedding": node_embedding, "attn_weight_dict": attn_weight_dict} + + +def _patched_decoder_forward(self, **inputs): + """Out-of-place equivalent of ``GTMGCDecoder.forward``.""" + node_embedding = add_lap_out_of_place( + inputs.get("node_embedding"), inputs.get("lap_eigenvectors") + ) + inputs["node_embedding"] = node_embedding + + attn_weight_dict: dict = {} + for i, decoder_block in enumerate(self.decoder_blocks): + block_out = decoder_block(**inputs) + node_embedding, attn_weight = block_out["out"], block_out["attn_weight"] + inputs["node_embedding"] = node_embedding + attn_weight_dict[f"decoder_block_{i}"] = attn_weight + return {"node_embedding": node_embedding, "attn_weight_dict": attn_weight_dict} + + +def patch_inplace_lap_addition() -> None: + """Replace the encoder/decoder forwards with autograd-safe versions.""" + if getattr(_gtmgc_modeling, "_step_up_inplace_patched", False): + return + _UPSTREAM_FORWARDS[_gtmgc_modeling.GTMGCEncoder] = _gtmgc_modeling.GTMGCEncoder.forward + _UPSTREAM_FORWARDS[_gtmgc_modeling.GTMGCDecoder] = _gtmgc_modeling.GTMGCDecoder.forward + _gtmgc_modeling.GTMGCEncoder.forward = _patched_encoder_forward + _gtmgc_modeling.GTMGCDecoder.forward = _patched_decoder_forward + _gtmgc_modeling._step_up_inplace_patched = True + + +def _conditioned_gtmgc_class() -> Any: + """Define (once) the GTMGC subclass that adds charge / spin conditioning.""" + global _CONDITIONED_GTMGC + if _CONDITIONED_GTMGC is not None: + return _CONDITIONED_GTMGC + + class ConditionedGTMGC(_gtmgc.GTMGCForConformerPrediction): # type: ignore[misc, valid-type] + """GTMGC conditioned on molecule-level charge and spin.""" + + def __init__(self, config: Any, n_global_features: int = N_GLOBAL_FEATURES) -> None: + super().__init__(config) + self.global_cond = global_condition_mlp(n_global_features, config.d_model) + + def forward(self, **inputs): + global_features = inputs.get("global_features") + if global_features is not None: + inputs["global_embedding"] = self.global_cond(global_features.to(torch.float32)) + return super().forward(**inputs) + + _CONDITIONED_GTMGC = ConditionedGTMGC + return ConditionedGTMGC + + +def get_gtmgc_collator(): + """Return the (lazily loaded) ``GTMGCCollator`` class.""" + _load_gtmgc() + return _gtmgc.GTMGCCollator + + +def build_gtmgc_collator(conditioned: bool = False): + """Collator instance for the DataLoader, optionally with charge/spin features.""" + base = get_gtmgc_collator()() + return GlobalConditionCollator(base) if conditioned else base + + +def build_gtmgc( + n_layers: int = 6, + d_model: int = 256, + d_ffn: int = 1024, + n_head: int = 8, + atom_vocab_size: int = 513, + dropout: float = 0.0, + embed_style: str = "atom_tokenized_ids", + n_global_features: int = 0, +): + """Instantiate GTMGC for conformer prediction. + + Defaults and attention flags follow their released QM9 checkpoint + (``RichXuOvO/GTMGC-Qm9``): 6 + 6 layers, d_model 256, d_ffn 1024, 8 heads, + Mole-BERT token embeddings. The encoder attends over the adjacency only, the + decoder also over the distance matrix predicted by the first conformer head, + so no ground-truth geometry reaches the encoder. + """ + _load_gtmgc() + config = _gtmgc.GTMGCConfig( + n_encode_layers=n_layers, + n_decode_layers=n_layers, + encoder_use_A_in_attn=True, + encoder_use_D_in_attn=False, + decoder_use_A_in_attn=True, + decoder_use_D_in_attn=True, + embed_style=embed_style, + atom_vocab_size=atom_vocab_size, + d_embed=d_model, + pre_ln=False, + d_q=d_model, + d_k=d_model, + d_v=d_model, + d_model=d_model, + n_head=n_head, + qkv_bias=True, + attn_drop=dropout, + norm_drop=dropout, + ffn_drop=dropout, + d_ffn=d_ffn, + ) + if n_global_features: + return _conditioned_gtmgc_class()(config, n_global_features=n_global_features) + return _gtmgc.GTMGCForConformerPrediction(config) + + +def _load_vendor_weights(model_cls: Any, config_cls: Any, checkpoint: str) -> Any: + """Build a vendored model and load HuggingFace-hosted weights into it. + + ``PreTrainedModel.from_pretrained`` is not usable here: the vendored classes + target transformers 4.32 and current transformers expects model APIs they + don't implement. The two steps it would do for these plain checkpoints — + build the config, load the state dict — are done directly instead. + + ``checkpoint`` is a Hub repo id or a local directory holding ``config.json`` + and ``pytorch_model.bin``. + """ + local = Path(checkpoint) + if local.is_dir(): + config_path, weights_path = local / "config.json", local / "pytorch_model.bin" + else: + from huggingface_hub import hf_hub_download + + config_path = Path(hf_hub_download(checkpoint, "config.json")) + weights_path = Path(hf_hub_download(checkpoint, "pytorch_model.bin")) + + with open(config_path) as handle: + config = config_cls(**json.load(handle)) + model = model_cls(config) + state = torch.load(weights_path, map_location="cpu", weights_only=True) + missing, unexpected = model.load_state_dict(state, strict=False) + if missing: + raise RuntimeError(f"{checkpoint} is missing weights for: {sorted(missing)[:8]}") + if unexpected: + print(f"[gtmgc] ignoring {len(unexpected)} unexpected keys in {checkpoint}", flush=True) + return model + + +def load_molebert_tokenizer(checkpoint: str = "RichXuOvO/MoleBERT-Tokenizer"): + """Load the Mole-BERT atom tokenizer and its collator. + + GTMGC's conformer models embed the tokenizer's per-atom codebook indices. + Returns ``(tokenizer, collator)``; the collator turns graph dicts into the + PyG batch the tokenizer expects. + """ + _load_gtmgc() + tokenizer = _load_vendor_weights( + _gtmgc.MoleBERTTokenizer, _gtmgc.MoleBERTTokenizerConfig, checkpoint + ) + return tokenizer, _gtmgc.MoleBERTTokenizerCollator() + + +def load_pretrained_gtmgc(checkpoint: str): + """Load one of their released checkpoints, e.g. ``RichXuOvO/GTMGC-Qm9``. + + Useful as a reference point: scoring their weights with our evaluation code + should reproduce their published numbers. + """ + _load_gtmgc() + return _load_vendor_weights(_gtmgc.GTMGCForConformerPrediction, _gtmgc.GTMGCConfig, checkpoint) diff --git a/src/step_up/models/lj_params.py b/src/step_up/models/lj_params.py new file mode 100644 index 0000000..dfa068a --- /dev/null +++ b/src/step_up/models/lj_params.py @@ -0,0 +1,123 @@ +"""Universal Force Field Lennard-Jones parameters, keyed the way ReBind keys them. + +ReBind's ``get_sigma_and_epsilon`` looks parameters up by ``node_type``, which is +the atomic number minus one, and its own table stops at index 35 (Kr). Everything +heavier — the 4d/5d metals, the lanthanides, iodine — fell through to a flat +default, which is most of what makes an organometallic complex an organometallic +complex. This table carries the full UFF set through index 102 (Lr). + +Values as compiled by Luis Pinto (``rebind_utils.py``); indices 0-35 are +bit-identical to ReBind's own table, so QM9 behaviour is unchanged. +Sigma in angstrom, epsilon in kcal/mol. +""" + +from __future__ import annotations + +# node_type (Z - 1) -> (sigma, epsilon) +UFF_LJ_PARAMETERS: dict[int, tuple[float, float]] = { + 0: (2.886, 0.0440), # H + 1: (2.362, 0.0560), # He + 2: (2.451, 0.0250), # Li + 3: (2.745, 0.0850), # Be + 4: (3.637, 0.1800), # B + 5: (3.431, 0.1050), # C + 6: (3.260, 0.0690), # N + 7: (3.118, 0.0600), # O + 8: (2.996, 0.0500), # F + 9: (2.889, 0.0420), # Ne + 10: (2.983, 0.0300), # Na + 11: (2.905, 0.1110), # Mg + 12: (4.008, 0.5050), # Al + 13: (3.826, 0.4020), # Si + 14: (3.694, 0.3050), # P + 15: (3.594, 0.2740), # S + 16: (3.516, 0.2270), # Cl + 17: (3.404, 0.1850), # Ar + 18: (3.812, 0.0350), # K + 19: (3.487, 0.2380), # Ca + 20: (3.316, 0.0190), # Sc + 21: (3.294, 0.0170), # Ti + 22: (3.273, 0.0160), # V + 23: (3.249, 0.0150), # Cr + 24: (3.210, 0.0130), # Mn + 25: (3.174, 0.0130), # Fe + 26: (3.144, 0.0130), # Co + 27: (3.116, 0.0130), # Ni + 28: (3.083, 0.0050), # Cu + 29: (3.002, 0.1240), # Zn + 30: (4.383, 0.4150), # Ga + 31: (4.310, 0.3790), # Ge + 32: (4.280, 0.3090), # As + 33: (4.336, 0.2910), # Se + 34: (4.403, 0.2510), # Br + 35: (4.463, 0.2200), # Kr + 36: (4.114, 0.0400), # Rb + 37: (3.641, 0.2350), # Sr + 38: (3.345, 0.0720), # Y + 39: (3.124, 0.0690), # Zr + 40: (3.167, 0.0590), # Nb + 41: (3.167, 0.0560), # Mo + 42: (3.140, 0.0480), # Tc + 43: (3.076, 0.0560), # Ru + 44: (3.053, 0.0530), # Rh + 45: (3.003, 0.0410), # Pd + 46: (3.148, 0.0360), # Ag + 47: (2.848, 0.2280), # Cd + 48: (4.432, 0.4080), # In + 49: (4.295, 0.3810), # Sn + 50: (4.314, 0.3110), # Sb + 51: (4.382, 0.2920), # Te + 52: (4.436, 0.2520), # I + 53: (4.465, 0.2210), # Xe + 54: (4.302, 0.0320), # Cs + 55: (3.666, 0.2200), # Ba + 56: (3.522, 0.0130), # La + 57: (3.493, 0.0130), # Ce + 58: (3.468, 0.0130), # Pr + 59: (3.441, 0.0130), # Nd + 60: (3.415, 0.0130), # Pm + 61: (3.391, 0.0130), # Sm + 62: (3.364, 0.0130), # Eu + 63: (3.340, 0.0130), # Gd + 64: (3.315, 0.0130), # Tb + 65: (3.289, 0.0130), # Dy + 66: (3.265, 0.0130), # Ho + 67: (3.241, 0.0130), # Er + 68: (3.216, 0.0130), # Tm + 69: (3.191, 0.0130), # Yb + 70: (3.167, 0.0130), # Lu + 71: (3.141, 0.0670), # Hf + 72: (3.170, 0.0600), # Ta + 73: (3.168, 0.0540), # W + 74: (3.111, 0.0460), # Re + 75: (3.120, 0.0310), # Os + 76: (3.020, 0.0320), # Ir + 77: (2.754, 0.0800), # Pt + 78: (3.293, 0.0390), # Au + 79: (2.730, 0.4090), # Hg + 80: (4.347, 0.3790), # Tl + 81: (4.355, 0.3450), # Pb + 82: (4.370, 0.2840), # Bi + 83: (4.450, 0.2220), # Po + 84: (4.750, 0.2840), # At + 85: (4.765, 0.2210), # Rn + 86: (3.846, 0.0300), # Fr + 87: (3.659, 0.2110), # Ra + 88: (3.478, 0.0330), # Ac + 89: (3.395, 0.0310), # Th + 90: (3.424, 0.0100), # Pa + 91: (3.397, 0.0110), # U + 92: (3.424, 0.0050), # Np + 93: (3.424, 0.0050), # Pu + 94: (3.381, 0.0140), # Am + 95: (3.326, 0.0140), # Cm + 96: (3.339, 0.0140), # Bk + 97: (3.313, 0.0140), # Cf + 98: (3.286, 0.0140), # Es + 99: (3.281, 0.0140), # Fm + 100: (3.268, 0.0140), # Md + 101: (3.254, 0.0140), # No + 102: (3.240, 0.0070), # Lr +} + +MAX_NODE_TYPE = max(UFF_LJ_PARAMETERS) diff --git a/src/step_up/models/rebind.py b/src/step_up/models/rebind.py new file mode 100644 index 0000000..b8aec49 --- /dev/null +++ b/src/step_up/models/rebind.py @@ -0,0 +1,427 @@ +"""Thin wrapper around the vendored ReBind implementation. + +Vendored at ``external/ReBIND``. Nothing is imported from the submodule until +``build_rebind()`` or ``get_collator()`` is first called. At that point its root +is put on ``sys.path`` (so the vendor's intra-package imports work) and three +runtime patches are applied: + +- ``get_sigma_and_epsilon`` looks parameters up in the full UFF table + (:mod:`step_up.models.lj_params`) instead of raising a KeyError on any atomic + number above 36 — i.e. on most of the organometallic datasets. +- ``Encoder.forward`` / ``Decoder.forward`` add the Laplacian positional + encoding out of place, so the model can be trained in fp32. +- ``REBIND.forward`` clamps predicted distances in the LJ block and scrubs + non-finite values from the rewired adjacencies. + +This is the explicit "hybrid: vendor for now, refactor later" handoff. The next +iteration will copy the model code into ``step_up.models.rebind`` natively and +delete the sys.path hack. +""" + +from __future__ import annotations + +import sys +from pathlib import Path +from typing import Any + +import torch + +from .common import ( + N_GLOBAL_FEATURES, + GlobalConditionCollator, + add_lap_out_of_place, + apply_global_conditioning, + global_condition_mlp, +) +from .lj_params import MAX_NODE_TYPE, UFF_LJ_PARAMETERS + +_REBIND_ROOT = Path(__file__).resolve().parents[3] / "external" / "ReBIND" +# Concrete-file probe: a fresh git checkout without `--recursive` leaves +# `external/ReBIND/` as an empty directory, so `exists()` on the root passes +# but later imports fail with a confusing ModuleNotFoundError. Check for an +# actual vendored file so the error message is actionable. +_REBIND_PROBE = _REBIND_ROOT / "models" / "rebind" / "modeling_rebind.py" + + +def _ensure_rebind_on_path() -> None: + """Verify the submodule is initialized and put its root on ``sys.path``. + + Raises ``FileNotFoundError`` with an actionable message if the submodule + files aren't present. Idempotent. + """ + if not _REBIND_PROBE.exists(): + raise FileNotFoundError( + f"ReBind submodule files missing at {_REBIND_PROBE}. " + "Run: git submodule update --init --recursive" + ) + if str(_REBIND_ROOT) not in sys.path: + sys.path.insert(0, str(_REBIND_ROOT)) + + +# Cached references to the vendored symbols, populated by ``_load_rebind()``. +# Private so that importing them directly fails instead of silently yielding +# ``None`` before the submodule is loaded; use ``build_rebind`` / ``get_collator``. +_REBIND: Any = None +_Collator: Any = None +_REBINDConfig: Any = None +_rebind_utils: Any = None +_rebind_collating: Any = None +_rebind_modeling: Any = None + +# Upstream ``forward`` methods replaced by the patches below, keyed by class, so +# tests can check the patched model against upstream. +_UPSTREAM_FORWARDS: dict[type, Any] = {} + +_CONDITIONED_REBIND: Any = None + +__all__ = ["build_collator", "build_rebind", "get_collator"] + + +def _load_rebind() -> None: + """Import the vendored ReBind modules and apply the runtime patches. + + Deferred until first use so that importing ``step_up.models.rebind`` from a + fresh checkout (without ``--recursive``) doesn't fail at collection time. + Idempotent. + """ + global _REBIND, _Collator, _REBINDConfig + global _rebind_utils, _rebind_collating, _rebind_modeling + if _REBIND is not None: + return + _ensure_rebind_on_path() + # Imports are deferred so the vendored sys.path entry exists first. + from models import REBIND, Collator, REBINDConfig + from models.modules import utils + from models.rebind import collating_rebind, modeling_rebind + + _REBIND = REBIND + _Collator = Collator + _REBINDConfig = REBINDConfig + _rebind_utils = utils + _rebind_collating = collating_rebind + _rebind_modeling = modeling_rebind + + # Apply the three runtime patches we need (defined further down). They + # depend on the vendored modules above so we can only call them now. + patch_lj_parameters() + patch_inplace_lap_addition() + patch_rebind_forward() + + +# --------------------------------------------------------------------------- +# LJ-parameter extension for organometallics. +# --------------------------------------------------------------------------- +# ReBind's `get_sigma_and_epsilon` hardcodes LJ parameters for atomic-number +# indices 0..35 (i.e., Z=1..36, H through Kr), and KeyErrors on anything heavier. +# We swap in the full UFF table, which agrees with theirs exactly over Z=1..36 — +# so QM9 is untouched — and covers the 4d/5d metals, the lanthanides and iodine +# that the organometallic sets are made of. + + +def patch_lj_parameters() -> None: + """Replace ``get_sigma_and_epsilon`` with one that covers the whole table. + + Idempotent: calling twice has no effect beyond the first. + """ + if getattr(_rebind_utils, "_step_up_patched", False): + return + + def patched_get_sigma_and_epsilon( + mol_data: Any, drugs: bool = False + ) -> tuple[torch.Tensor, torch.Tensor]: + # `drugs` is upstream's switch for a differently keyed table; step-up + # never sets it, and `node_type` is always Z - 1 here. + del drugs + eps_list: list[float] = [] + sig_list: list[float] = [] + for atom_id in mol_data.node_type: + idx = int(atom_id.item()) + params = UFF_LJ_PARAMETERS.get(idx) + if params is None: + raise ValueError( + f"No LJ parameters for node_type {idx} (Z={idx + 1}); the UFF table " + f"stops at node_type {MAX_NODE_TYPE}. A larger value means the " + "featurizer emitted a bad atomic number." + ) + sigma, epsilon = params + eps_list.append(epsilon) + sig_list.append(sigma) + return torch.tensor(eps_list), torch.tensor(sig_list) + + _rebind_utils.get_sigma_and_epsilon = patched_get_sigma_and_epsilon + # `from ..modules.utils import get_sigma_and_epsilon` binds the symbol + # directly in the collator module — re-rebind it there too. + _rebind_collating.get_sigma_and_epsilon = patched_get_sigma_and_epsilon + _rebind_utils._step_up_patched = True + + +# --------------------------------------------------------------------------- +# Molecule-level (charge / spin) conditioning +# --------------------------------------------------------------------------- +# Charge and spin enter as two scalars — the formal charge in electrons and the +# number of unpaired electrons (spin multiplicity - 1) — projected by a small MLP +# and added to every atom's embedding. Scalars rather than one-hot categories +# because BOSTMC's low-spin charges span -8..+8 with single-structure tails that +# a category would never learn, and because a scalar lets you ask the trained +# model for a charge/spin combination that never appeared in training. The MLP's +# last layer starts at zero, so a freshly built conditioned model behaves exactly +# like the unconditioned one and learns to use the conditioning from there. + + +def _conditioned_rebind_class() -> Any: + """Define (once) the REBIND subclass that adds global conditioning.""" + global _CONDITIONED_REBIND + if _CONDITIONED_REBIND is not None: + return _CONDITIONED_REBIND + + class ConditionedREBIND(_REBIND): # type: ignore[misc, valid-type] + """REBIND conditioned on molecule-level charge and spin.""" + + def __init__(self, config: Any, n_global_features: int = N_GLOBAL_FEATURES) -> None: + super().__init__(config) + self.global_cond = global_condition_mlp(n_global_features, config.d_model) + + def forward(self, **inputs): + global_features = inputs.get("global_features") + if global_features is not None: + inputs["global_embedding"] = self.global_cond(global_features.to(torch.float32)) + return super().forward(**inputs) + + _CONDITIONED_REBIND = ConditionedREBIND + return ConditionedREBIND + + +def _patched_encoder_forward(self, **inputs): + """Out-of-place equivalent of ``Encoder.forward`` from vendored ReBind. + + Upstream adds the Laplacian positional encoding into ``node_embedding`` with + an in-place slice assignment. In the decoder, that write modifies the encoder + output after ``conformer_head`` has saved it for backward, so fp32 training + fails with autograd's "modified by an inplace operation" error. (Upstream + trains under fp16 autocast, where the error doesn't trigger.) The encoder's + own write is harmless, but both blocks use a zero-padded add for symmetry. + """ + node_attr = inputs.get("node_attr") + node_embedding = self.node_embedding(node_attr) + lap = inputs.get("lap_eigenvectors") + node_embedding = add_lap_out_of_place(node_embedding, lap) + node_embedding = apply_global_conditioning(node_embedding, inputs) + inputs["node_embedding"] = node_embedding + + attn_weight_dict: dict = {} + for i, encoder_block in enumerate(self.encoder_blocks): + block_out = encoder_block(**inputs) + node_embedding, attn_weight = block_out["out"], block_out["attn_weight"] + inputs["node_embedding"] = node_embedding + attn_weight_dict[f"encoder_block_{i}"] = attn_weight + return {"node_embedding": node_embedding, "attn_weight_dict": attn_weight_dict} + + +def _patched_decoder_forward(self, **inputs): + """Out-of-place equivalent of ``Decoder.forward`` from vendored ReBind.""" + node_embedding = inputs.get("node_embedding") + lap = inputs.get("lap_eigenvectors") + node_embedding = add_lap_out_of_place(node_embedding, lap) + inputs["node_embedding"] = node_embedding + + attn_weight_dict: dict = {} + for i, decoder_block in enumerate(self.decoder_blocks): + block_out = decoder_block(**inputs) + node_embedding, attn_weight = block_out["out"], block_out["attn_weight"] + inputs["node_embedding"] = node_embedding + attn_weight_dict[f"decoder_block_{i}"] = attn_weight + return {"node_embedding": node_embedding, "attn_weight_dict": attn_weight_dict} + + +def patch_inplace_lap_addition() -> None: + """Replace ``Encoder.forward`` and ``Decoder.forward`` with autograd-safe versions.""" + if getattr(_rebind_modeling, "_step_up_inplace_patched", False): + return + _UPSTREAM_FORWARDS[_rebind_modeling.Encoder] = _rebind_modeling.Encoder.forward + _UPSTREAM_FORWARDS[_rebind_modeling.Decoder] = _rebind_modeling.Decoder.forward + _rebind_modeling.Encoder.forward = _patched_encoder_forward + _rebind_modeling.Decoder.forward = _patched_decoder_forward + _rebind_modeling._step_up_inplace_patched = True + + +# Minimum predicted interatomic distance, in angstroms, used to clamp the LJ +# denominator. Real bonded distances are >0.7 A; this value is small enough to +# be a no-op for any realistic prediction and large enough that ``s**6`` (which +# scales as ``sigma**6 / D**6``) cannot overflow FP32 in the LJ block. +_LJ_D_MIN = 0.1 + + +def _patched_rebind_forward(self, **inputs): + """Out-of-place + numerically-defensive copy of ``REBIND.forward``. + + Two changes from vendored ReBind: + + 1. The predicted pairwise-distance tensor ``D_cache`` is clamped to a + small positive minimum before being used in the LJ-force expression + ``s = sigma / D_cache``. Without this, two non-bonded atoms whose + predicted coordinates collide produce ``s = inf`` and downstream + ``inf * 0`` NaNs. The diagonal mask only catches ``i == j`` collisions, + not collisions between distinct atoms — which is exactly what kills + full-scale QM9 training after ~100 steps. Clamp threshold is well + below any real interatomic distance so realistic forward passes are + unaffected. + 2. The final attraction / repulsion adjacency tensors are passed through + ``nan_to_num`` as a belt-and-suspenders guard — if anything upstream + still produces NaN, it gets neutralized to 0 before flowing into the + decoder's attention bias. + """ + conformer, node_mask = inputs.get("conformer"), inputs.get("node_mask") + + encoder_out = self.encoder(**inputs) + node_embedding = encoder_out["node_embedding"] + + cache_out = self.conformer_head( + conformer=conformer, + hidden_X=node_embedding, + padding_mask=node_mask, + compute_loss=True, + ) + loss_cache, conformer_cache = cache_out["loss"], cache_out["conformer_hat"] + + # CHANGE (1): clamp_min on the predicted distance matrix. + D_cache = torch.cdist(conformer_cache, conformer_cache).detach().clamp_min(_LJ_D_MIN) + D_M = _rebind_modeling.make_cdist_mask(node_mask) + # NOTE: The variable name ``pred_conformation`` is misleading — this is + # ReBind's intentional design (see vendored ``modeling_rebind.py``). The + # residual head treats ``hidden_X`` (decoder output) and ``conformer_base`` + # (this tensor) as two **hidden states** of identical shape + # ``(B, N, d_model)``, stacks them along a new last dim, computes an + # attention score across the two channels, and projects the weighted sum + # back to coordinates. It is NOT the predicted coordinate tensor + # ``conformer_cache`` (shape ``(B, N, 3)``); using ``conformer_cache`` here + # would crash on the ``torch.stack`` shape mismatch. + # + # Upstream stores the encoder output here, and its decoder then adds the + # Laplacian positional encoding to that same tensor in place, so upstream's + # residual head actually receives ``encoder output + PE``. The out-of-place + # decoder patch no longer mutates it, so the PE is added explicitly here. + inputs["pred_conformation"] = add_lap_out_of_place(node_embedding, inputs["lap_eigenvectors"]) + inputs["node_embedding"] = node_embedding + + sigma, epsilon = inputs.get("sigma"), inputs.get("epsilon") + s = sigma / D_cache + s6 = s**6 + LJ_force_orig = 24 * epsilon * (s6 / D_cache) * (2 * s6 - 1) + LJ_force = torch.abs(LJ_force_orig) + + adj_mask = inputs["adjacency"].bool() + self_mask = ( + torch.eye(D_cache.shape[1], device=D_cache.device) + .bool() + .unsqueeze(0) + .expand(D_cache.shape[0], -1, -1) + ) + LJ_force = LJ_force.masked_fill(adj_mask | self_mask, 0) + LJ_force = LJ_force.masked_fill(~D_M.bool(), 0) + + num_retain_edges = inputs.get("num_near_edges") + ret_val = _rebind_modeling.retain_top_k(LJ_force, num_retain_edges, descending=True) + nonzeros = ret_val.nonzero(as_tuple=False) + ret_val[nonzeros[:, 0], nonzeros[:, 1], nonzeros[:, 2]] = 1 + + LJ_nonzero_mask = LJ_force > 0 + LJ_above_zero_mask = LJ_force_orig > 0 + LJ_below_zero_mask = LJ_force_orig < 0 + + repulsive = LJ_nonzero_mask.float() * LJ_above_zero_mask.float() * ret_val * -1 + attractive = LJ_nonzero_mask.float() * LJ_below_zero_mask.float() * ret_val * 1 + # CHANGE (2): scrub any residual NaN/inf out of the attention biases. + inputs["attraction_adjacency"] = torch.nan_to_num(attractive, nan=0.0, posinf=0.0, neginf=0.0) + inputs["repulsion_adjacency"] = torch.nan_to_num(repulsive, nan=0.0, posinf=0.0, neginf=0.0) + + decoder_out = self.decoder(**inputs) + node_embedding = decoder_out["node_embedding"] + + outputs = self.residual_head( + conformer=conformer, + hidden_X=node_embedding, + padding_mask=node_mask, + compute_loss=True, + conformer_base=inputs["pred_conformation"], + ) + + return _rebind_modeling.ConformerPredictionOutput( + loss=(outputs["loss"] + loss_cache) / 2, + cdist_mae=outputs["cdist_mae"], + cdist_mse=outputs["cdist_mse"], + coord_rmsd=outputs["coord_rmsd"], + conformer=outputs["conformer"], + conformer_hat=outputs["conformer_hat"], + ) + + +def patch_rebind_forward() -> None: + """Replace ``REBIND.forward`` with the numerically-defensive version.""" + if getattr(_rebind_modeling, "_step_up_forward_patched", False): + return + _UPSTREAM_FORWARDS[_rebind_modeling.REBIND] = _rebind_modeling.REBIND.forward + _rebind_modeling.REBIND.forward = _patched_rebind_forward + _rebind_modeling._step_up_forward_patched = True + + +# NOTE: patches are no longer applied at module import. ``_load_rebind()`` +# applies them on first use (i.e. when ``build_rebind()`` or a ``Collator`` +# instance is requested via the lazy accessors below). This lets test +# collection succeed on a fresh checkout where the submodule isn't initialized +# yet — the missing-submodule error fires only when someone actually tries to +# build the model. + + +def get_collator(): + """Return the (lazily loaded) ReBind ``Collator`` class.""" + _load_rebind() + return _Collator + + +def build_collator(conditioned: bool = False): + """Collator instance for the DataLoader. + + With ``conditioned=True`` it also emits the charge / spin features that a + model from ``build_rebind(n_global_features=...)`` expects. + """ + base = get_collator()() + return GlobalConditionCollator(base) if conditioned else base + + +def build_rebind( + n_layers: int = 8, + d_model: int = 512, + d_ffn: int = 1024, + n_head: int = 8, + atom_vocab_size: int = 513, + dropout: float = 0.0, + n_global_features: int = 0, +): + """Instantiate a REBIND model from a flat keyword-style config. + + ``n_global_features > 0`` returns the variant conditioned on molecule-level + charge and spin; pair it with ``build_collator(conditioned=True)``. + """ + _load_rebind() + config = _REBINDConfig( + n_encode_layers=n_layers, + n_decode_layers=n_layers, + embed_style="atom_type_ids", + atom_vocab_size=atom_vocab_size, + d_embed=d_model, + pre_ln=False, + d_q=d_model, + d_k=d_model, + d_v=d_model, + d_model=d_model, + n_head=n_head, + qkv_bias=True, + attn_drop=dropout, + norm_drop=dropout, + ffn_drop=dropout, + dropout=dropout, + d_ffn=d_ffn, + ) + if n_global_features: + return _conditioned_rebind_class()(config, n_global_features=n_global_features) + return _REBIND(config) diff --git a/src/step_up/train.py b/src/step_up/train.py new file mode 100644 index 0000000..b910f15 --- /dev/null +++ b/src/step_up/train.py @@ -0,0 +1,516 @@ +"""Config-driven training loop for step-up. + +The loop is deliberately minimal: it owns dataset construction, the model +(see step_up.models), AdamW with linear warmup and cosine decay, periodic validation, +best-checkpoint saving, and TensorBoard logging. No HuggingFace Trainer, no +accelerate — keeping the control flow legible while we're still iterating on +the model. +""" + +from __future__ import annotations + +import json +import math +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import torch +import yaml +from torch.utils.data import DataLoader, Subset +from torch.utils.tensorboard import SummaryWriter +from tqdm import tqdm + +from .data.csv_dataset import CSVMoleculeDataset +from .data.splits import split_by_id_files, split_by_labels, stable_split +from .models import MODEL_NAMES, N_GLOBAL_FEATURES, build_model, build_model_collator + +# --------------------------------------------------------------------------- +# Config dataclasses +# --------------------------------------------------------------------------- + + +@dataclass +class TrainConfig: + dataset_path: str + dataset_source: str # "smiles" | "mol2" | "sdf" + # Which benchmark model to train (see step_up.models.MODEL_NAMES). + model: str = "rebind" + subset_size: int | None = None + split_ratios: tuple[float, float, float] = (0.8, 0.1, 0.1) + split_seed: int = 0 + # Column with a stable per-molecule ID (e.g. `refcode`). Each molecule's split + # is a hash of this key and `split_seed` (see `stable_split`); without it the + # key is the CSV row number. + id_column: str | None = None + # Column holding a published split name per row (train / val / test). When set, + # it replaces the hashed split, and `split_ratios` / `split_seed` are unused. + split_column: str | None = None + # Published split given as ID lists instead, e.g. + # `split_files: {train: train.txt, val: val.txt, test: test.txt}`. Needs + # `id_column`; rows listed in no file are dropped. Takes precedence over + # `split_column` and the hashed split. + split_files: dict[str, str] | None = None + # Columns holding the molecule's charge (electrons) and spin multiplicity. + # Setting either turns on the model's charge/spin conditioning. + charge_column: str | None = None + spin_column: str | None = None + # CSV of precomputed Mole-BERT token ids (`id,input_ids`), joined on `id_column`. + # GTMGC's published conformer models need it; ReBind does not. + token_file: str | None = None + # Optional CSV column filter (e.g. `filter_column: spinmult, filter_value: 1` + # to restrict BOSTMC to singlets). + filter_column: str | None = None + filter_value: Any = None + # Featurize every row up front and drop the ones that fail. Disable only for + # datasets known to be clean; a bad row then raises mid-training instead. + validate_dataset: bool = True + # Keep featurized graph dicts in memory instead of re-featurizing every epoch. + # Trades memory for speed; recommended for smoke runs. + cache_dataset: bool = False + # Fail validation if more than this fraction of rows are dropped. + max_drop_fraction: float = 0.5 + # Train on heavy atoms only: hydrogens are dropped from the graph, so the + # model never sees or predicts them. A different model, not a different + # metric — the published ReBind and GTMGC setups keep hydrogen explicit. + remove_hs: bool = False + + # Model + n_layers: int = 8 + d_model: int = 512 + d_ffn: int = 1024 + n_head: int = 8 + dropout: float = 0.0 + + # Optimization + epochs: int = 20 + batch_size: int = 100 + eval_batch_size: int = 100 + lr: float = 9e-5 + weight_decay: float = 0.0 + warmup_ratio: float = 0.1 + # AdamW moments. ReBind's script sets beta2=0.99; PyTorch's default is 0.999. + adam_beta1: float = 0.9 + adam_beta2: float = 0.99 + adam_eps: float = 1e-8 + # "linear" reproduces ReBind's `--lr_scheduler_type=linear` (HuggingFace's linear + # warmup then linear decay to zero). "cosine" keeps the decay used before. + lr_schedule: str = "linear" + # ReBind passes `--dataloader_drop_last`, so the last partial training batch is skipped. + drop_last: bool = True + # Global gradient-norm clip. ReBind's published training inherited + # HuggingFace Trainer's default of 1.0; without it the 8-layer / d=512 + # model NaNs out in the first epoch on QM9. + grad_clip: float = 1.0 + # Skip batches whose loss comes back non-finite (the LJ block in ReBind's + # head can divide by predicted distances that are ~0 early in training). + # Abort if more than this many batches are skipped over the whole run. + nan_skip_max: int = 50 + num_workers: int = 0 + + # Runtime + device: str = "cpu" + output_dir: str = "outputs/run" + seed: int = 0 + log_interval: int = 10 + + def __post_init__(self) -> None: + # Catch a typo here rather than partway into a run: the schedule name is + # only consulted once warmup ends. + if self.model not in MODEL_NAMES: + raise ValueError(f"Unknown model: {self.model!r} (expected one of {MODEL_NAMES})") + if self.lr_schedule not in ("linear", "cosine"): + raise ValueError( + f"Unknown lr_schedule: {self.lr_schedule!r} (expected 'linear' or 'cosine')" + ) + + @classmethod + def from_yaml(cls, path: str | Path) -> TrainConfig: + with open(path) as f: + raw = yaml.safe_load(f) or {} + if "split_ratios" in raw: + raw["split_ratios"] = tuple(raw["split_ratios"]) + return cls(**raw) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _set_seed(seed: int) -> None: + import random + + import numpy as np + + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + + +def _move_batch_to_device(batch: dict[str, Any], device: str) -> dict[str, Any]: + return {k: v.to(device) if torch.is_tensor(v) else v for k, v in batch.items()} + + +def _nonfinite_keys(batch: dict[str, Any]) -> list[str]: + """Return floating-point tensors in ``batch`` that contain non-finite values.""" + bad: list[str] = [] + for k, v in batch.items(): + if torch.is_tensor(v) and v.dtype.is_floating_point and not torch.isfinite(v).all(): + bad.append(k) + return bad + + +def _all_params_finite(model: torch.nn.Module) -> bool: + for p in model.parameters(): + if not torch.isfinite(p).all(): + return False + return True + + +def _lr_at_step( + step: int, total_steps: int, warmup_steps: int, base_lr: float, schedule: str = "linear" +) -> float: + """Learning rate at ``step``. + + ``linear`` matches HuggingFace's ``get_linear_schedule_with_warmup``, which is + what ReBind's script requests: the rate ramps from 0 over the warmup and then + decays linearly to 0 at the last step. + """ + if step < warmup_steps: + return base_lr * step / max(warmup_steps, 1) + progress = (step - warmup_steps) / max(total_steps - warmup_steps, 1) + if schedule == "linear": + return base_lr * max(0.0, 1.0 - progress) + if schedule == "cosine": + return base_lr * 0.5 * (1.0 + math.cos(math.pi * progress)) + raise ValueError(f"Unknown lr_schedule: {schedule!r} (expected 'linear' or 'cosine')") + + +def _mean_or_nan(total: float, count: int) -> float: + # NaN rather than 0.0 when nothing was averaged: a 0.0 D-MAE would read as a + # perfect score and win best-checkpoint selection. + return total / count if count else math.nan + + +# --------------------------------------------------------------------------- +# Training loop +# --------------------------------------------------------------------------- + + +def _run_epoch( + model: torch.nn.Module, + loader: DataLoader, + optimizer: torch.optim.Optimizer | None, + device: str, + scheduler_state: dict[str, int], + base_lr: float, + total_steps: int, + warmup_steps: int, + grad_clip: float = 0.0, + nan_skip_max: int = 0, + lr_schedule: str = "linear", +) -> tuple[float, float]: + """Run one pass over ``loader``. Returns (mean_loss, mean_dmae). + + During training, batches whose loss comes back non-finite are skipped + (no backward, no optimizer step). The first occurrence prints whether the + badness is in the input batch or appeared inside the forward pass — useful + to distinguish data corruption from model-side numerical instability. + During evaluation, non-finite batches are excluded from the means with a + warning. If no batch contributes, both means are NaN. + """ + is_train = optimizer is not None + model.train(is_train) + loss_sum, dmae_sum, n = 0.0, 0.0, 0 + pbar = tqdm(loader, leave=False, desc="train" if is_train else "val", dynamic_ncols=True) + for batch in pbar: + batch = _move_batch_to_device(batch, device) + if is_train: + lr_now = _lr_at_step( + scheduler_state["step"], total_steps, warmup_steps, base_lr, lr_schedule + ) + for g in optimizer.param_groups: + g["lr"] = lr_now + optimizer.zero_grad(set_to_none=True) + out = model(**batch) + loss_val = out.loss + if not torch.isfinite(loss_val): + scheduler_state["nan_skipped"] = scheduler_state.get("nan_skipped", 0) + 1 + skipped = scheduler_state["nan_skipped"] + bad_inputs = _nonfinite_keys(batch) + n_atoms = batch["node_mask"].sum(dim=-1).long().tolist() + origin = ( + f"input nonfinite in {bad_inputs}" + if bad_inputs + else "input is clean -> NaN originated in the model forward" + ) + print( + f"WARN: non-finite loss at step {scheduler_state['step']} " + f"(skipped {skipped}/{nan_skip_max}); " + f"batch n_atoms={n_atoms}; {origin}", + flush=True, + ) + if skipped > nan_skip_max: + raise RuntimeError( + f"Exceeded nan_skip_max={nan_skip_max} non-finite batches. " + "Lower lr, raise grad_clip stringency, or filter the dataset." + ) + scheduler_state["step"] += 1 + continue + loss_val.backward() + # clip_grad_norm_ returns the *unclipped* total gradient norm. If + # it's non-finite, the gradients themselves contain NaN/Inf — a + # well-known failure mode of torch.svd on rank-deficient inputs, + # which the Kabsch alignment inside ReBind's conformer head can + # produce. Calling optimizer.step() with NaN gradients would + # poison every parameter with NaN, making every subsequent + # forward pass NaN. Skip the step in that case. + grad_norm = torch.nn.utils.clip_grad_norm_( + model.parameters(), max_norm=grad_clip if grad_clip > 0 else float("inf") + ) + if not torch.isfinite(grad_norm): + scheduler_state["nan_skipped"] = scheduler_state.get("nan_skipped", 0) + 1 + skipped = scheduler_state["nan_skipped"] + print( + f"WARN: non-finite gradient norm ({grad_norm.item()}) at step " + f"{scheduler_state['step']} (skipped {skipped}/{nan_skip_max}). " + f"Likely a degenerate batch through Kabsch SVD.", + flush=True, + ) + optimizer.zero_grad(set_to_none=True) + if skipped > nan_skip_max: + raise RuntimeError( + f"Exceeded nan_skip_max={nan_skip_max} non-finite batches. " + "Lower lr or filter the dataset." + ) + scheduler_state["step"] += 1 + continue + optimizer.step() + # Cheap sanity: any parameter going NaN means we have a bug + # upstream (e.g. another path that bypassed the grad-norm check). + # Fail loudly instead of producing meaningless NaN for thousands + # of steps. + if not _all_params_finite(model): + raise RuntimeError( + f"Model parameters went non-finite after optimizer step at " + f"step {scheduler_state['step']}. This indicates a code " + "bug bypassing the gradient-NaN check." + ) + scheduler_state["step"] += 1 + else: + with torch.no_grad(): + out = model(**batch) + if not torch.isfinite(out.loss): + print("WARN: non-finite loss on an evaluation batch; excluded from metrics.") + continue + loss_sum += float(out.loss.detach()) + dmae_sum += float(out.cdist_mae.detach()) + n += 1 + pbar.set_postfix(loss=f"{loss_sum / n:.4f}", dmae=f"{dmae_sum / n:.4f}") + return _mean_or_nan(loss_sum, n), _mean_or_nan(dmae_sum, n) + + +def build_splits(config: TrainConfig) -> tuple[CSVMoleculeDataset, Subset, Subset, Subset]: + """Build the dataset and its train/val/test subsets, as training does. + + Shared with ``step-up evaluate`` so both see identical splits. + """ + dataset = CSVMoleculeDataset( + path=config.dataset_path, + source=config.dataset_source, # type: ignore[arg-type] + subset_size=config.subset_size, + validate=config.validate_dataset, + max_drop_fraction=config.max_drop_fraction, + filter_column=config.filter_column, + filter_value=config.filter_value, + cache=config.cache_dataset, + id_column=config.id_column, + split_column=config.split_column, + charge_column=config.charge_column, + spin_column=config.spin_column, + token_file=config.token_file, + remove_hs=config.remove_hs, + ) + if config.split_files: + if config.id_column is None: + raise ValueError("split_files needs id_column so rows can be matched to the lists") + train_set, val_set, test_set = split_by_id_files( + dataset, dataset.split_keys(), config.split_files + ) + split_desc = f"published ID lists keyed on {config.id_column!r}" + elif config.split_column is not None: + train_set, val_set, test_set = split_by_labels(dataset, dataset.split_labels()) + split_desc = f"published split from column {config.split_column!r}" + else: + train_set, val_set, test_set = stable_split( + dataset, dataset.split_keys(), ratios=config.split_ratios, seed=config.split_seed + ) + split_desc = f"hashed on {config.id_column or 'CSV row number'}, seed={config.split_seed}" + print( + f"[split] train={len(train_set)} val={len(val_set)} test={len(test_set)} ({split_desc})", + flush=True, + ) + return dataset, train_set, val_set, test_set + + +def train(config: TrainConfig) -> dict[str, Any]: + _set_seed(config.seed) + out_dir = Path(config.output_dir) + out_dir.mkdir(parents=True, exist_ok=True) + with open(out_dir / "config.json", "w") as f: + json.dump(config.__dict__, f, indent=2, default=str) + + dataset, train_set, val_set, test_set = build_splits(config) + if len(train_set) == 0 or len(val_set) == 0: + raise ValueError( + f"Empty train or val split from {len(dataset)} molecules with " + f"split_ratios={config.split_ratios}. Use more data or larger ratios." + ) + conditioned = config.charge_column is not None or config.spin_column is not None + if conditioned: + print( + f"[model] {config.model}: conditioning on molecule charge and unpaired-electron count " + f"(charge_column={config.charge_column!r}, spin_column={config.spin_column!r})", + flush=True, + ) + collator = build_model_collator(config.model, conditioned=conditioned) + + def _make_loader( + subset: Subset, batch_size: int, shuffle: bool, drop_last: bool = False + ) -> DataLoader: + return DataLoader( + subset, + batch_size=batch_size, + shuffle=shuffle, + num_workers=config.num_workers, + collate_fn=collator, + persistent_workers=config.num_workers > 0, + drop_last=drop_last, + ) + + train_loader = _make_loader( + train_set, config.batch_size, shuffle=True, drop_last=config.drop_last + ) + if len(train_loader) == 0: + print( + f"WARN: drop_last would leave no training batches for {len(train_set)} molecules " + f"at batch_size={config.batch_size}; keeping the partial batch.", + flush=True, + ) + train_loader = _make_loader(train_set, config.batch_size, shuffle=True) + val_loader = _make_loader(val_set, config.eval_batch_size, shuffle=False) + + # Model + optimizer + model = build_model( + config.model, + n_layers=config.n_layers, + d_model=config.d_model, + d_ffn=config.d_ffn, + n_head=config.n_head, + dropout=config.dropout, + n_global_features=N_GLOBAL_FEATURES if conditioned else 0, + ).to(config.device) + optimizer = torch.optim.AdamW( + model.parameters(), + lr=config.lr, + betas=(config.adam_beta1, config.adam_beta2), + eps=config.adam_eps, + weight_decay=config.weight_decay, + ) + + total_steps = len(train_loader) * config.epochs + warmup_steps = int(total_steps * config.warmup_ratio) + + writer = SummaryWriter(out_dir / "tb") + scheduler_state: dict[str, int] = {"step": 0} + history: list[dict[str, float]] = [] + best_val = math.inf + best_epoch: int | None = None + t0 = time.time() + for epoch in range(1, config.epochs + 1): + train_loss, train_dmae = _run_epoch( + model, + train_loader, + optimizer, + config.device, + scheduler_state, + config.lr, + total_steps, + warmup_steps, + grad_clip=config.grad_clip, + nan_skip_max=config.nan_skip_max, + lr_schedule=config.lr_schedule, + ) + val_loss, val_dmae = _run_epoch( + model, + val_loader, + None, + config.device, + scheduler_state, + config.lr, + total_steps, + warmup_steps, + ) + writer.add_scalar("train/loss", train_loss, epoch) + writer.add_scalar("train/dmae", train_dmae, epoch) + writer.add_scalar("val/loss", val_loss, epoch) + writer.add_scalar("val/dmae", val_dmae, epoch) + elapsed = time.time() - t0 + print( + f"[epoch {epoch:>3}/{config.epochs}] " + f"train_loss={train_loss:.4f} train_dmae={train_dmae:.4f} " + f"val_loss={val_loss:.4f} val_dmae={val_dmae:.4f} " + f"elapsed={elapsed:.1f}s" + ) + history.append( + { + "epoch": epoch, + "train_loss": train_loss, + "train_dmae": train_dmae, + "val_loss": val_loss, + "val_dmae": val_dmae, + } + ) + if math.isfinite(val_dmae) and val_dmae < best_val: + best_val, best_epoch = val_dmae, epoch + torch.save(model.state_dict(), out_dir / "best.pt") + writer.close() + + with open(out_dir / "history.json", "w") as f: + json.dump(history, f, indent=2) + if best_epoch is None: + raise RuntimeError( + "No epoch produced a finite validation D-MAE, so no checkpoint was saved. " + f"Per-epoch metrics are in {out_dir / 'history.json'}." + ) + + # ------------------------------------------------------------------ + # Test-set evaluation on this run's best checkpoint (selected by val D-MAE). + # ------------------------------------------------------------------ + test_metrics: dict[str, float] = {} + if len(test_set) == 0: + print("WARN: test split is empty; skipping test evaluation.", flush=True) + else: + model.load_state_dict(torch.load(out_dir / "best.pt", map_location=config.device)) + test_loader = _make_loader(test_set, config.eval_batch_size, shuffle=False) + test_loss, test_dmae = _run_epoch( + model, + test_loader, + None, + config.device, + scheduler_state, + config.lr, + total_steps, + warmup_steps, + ) + test_metrics = {"test_loss": test_loss, "test_dmae": test_dmae} + with open(out_dir / "test_metrics.json", "w") as f: + json.dump({**test_metrics, "best_epoch": best_epoch}, f, indent=2) + print( + f"[test] test_loss={test_loss:.4f} test_dmae={test_dmae:.4f} " + f"(checkpoint from epoch {best_epoch})" + ) + + return {"history": history, "best_val_dmae": best_val, "best_epoch": best_epoch, **test_metrics} diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..09a357f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,64 @@ +"""Shared pytest fixtures. + +Tests run against tiny CSV fixtures committed under ``tests/fixtures/``, so the +suite behaves the same on every machine, including CI runners that don't have +the full QM9-full.csv / tmQMg-full.csv / BOSTMC datasets. A missing fixture is +an error rather than a skip: a skipped test would let CI pass without +exercising the featurization, MOL2 parser, LJ patch, or model forward pass. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +FIXTURE_DIR = Path(__file__).parent / "fixtures" + + +def _fixture(name: str) -> Path: + path = FIXTURE_DIR / name + if not path.exists(): + pytest.fail(f"committed test fixture missing: {path}", pytrace=False) + return path + + +@pytest.fixture(scope="session") +def qm9_path() -> Path: + return _fixture("qm9_mini.csv") + + +@pytest.fixture(scope="session") +def qm9_sdf_path() -> Path: + """QM9 records in ReBind's published form: ``mol_id, split, sdf`` molblocks. + + Five molecules copied from gdb9.sdf (the HuggingFace ``RichXuOvO/HFQm9`` + copy that ReBind and GTMGC used), carrying their published split labels. + """ + return _fixture("qm9_sdf_mini.csv") + + +@pytest.fixture(scope="session") +def qm9_sdf_tokens_path() -> Path: + """Mole-BERT token ids for the molecules in ``qm9_sdf_mini.csv``. + + Committed so the GTMGC tests don't need the tokenizer checkpoint; regenerate + with ``scripts/tokenize_molebert.py``. + """ + return _fixture("qm9_sdf_mini_molebert_tokens.csv") + + +@pytest.fixture(scope="session") +def tmqmg_path() -> Path: + return _fixture("tmqmg_mini.csv") + + +@pytest.fixture(scope="session") +def bostmc_path() -> Path: + """BOSTMC-shaped CSV with columns ``refcode, xyz, charge, spinmult, mol2``. + + The structures are the public tmQMg complexes from ``tmqmg_mini.csv`` with + made-up ``refcode`` / ``charge`` / ``spinmult`` values (singlets, then + doublets), so no BOSTMC data is committed to the repo. + """ + return _fixture("bostmc_mini.csv") diff --git a/tests/fixtures/bostmc_mini.csv b/tests/fixtures/bostmc_mini.csv new file mode 100644 index 0000000..246d708 --- /dev/null +++ b/tests/fixtures/bostmc_mini.csv @@ -0,0 +1,925 @@ +refcode,xyz,charge,spinmult,mol2 +MINI01,"54 +10/06/2025 18:27, XYZ structure generated by mol3D Class, molSimplify +C -1.090100 2.615300 -0.569700 +C -2.208300 3.468000 -0.460700 +H -2.101700 4.537300 -0.696800 +C -3.432400 2.935300 -0.055700 +H -4.312700 3.585400 0.049000 +C -3.531800 1.557800 0.177900 +H -4.496900 1.104000 0.433800 +C -2.387000 0.761200 0.035100 +C -2.409200 -0.706900 0.028900 +C -3.577900 -1.468100 0.170100 +H -4.527400 -0.986300 0.432700 +C -3.522300 -2.846700 -0.072300 +H -4.422700 -3.469300 0.029200 +C -2.315200 -3.415500 -0.478500 +H -2.241600 -4.486900 -0.717900 +C -1.171100 -2.597200 -0.582200 +C 0.902400 0.067900 2.039200 +C 1.267700 -1.219100 1.502200 +C 2.222700 -1.325500 0.413300 +C 2.704600 -0.124000 -0.206600 +C 2.315800 1.177900 0.280600 +C 1.408100 1.260700 1.399900 +C -0.010100 0.203900 3.235600 +H -0.976300 0.681000 2.973300 +H 0.462400 0.839500 4.011000 +H -0.235900 -0.767300 3.705600 +C 0.669300 -2.459800 2.117900 +H 1.215200 -2.724900 3.047500 +H 0.721900 -3.324600 1.438400 +H -0.392500 -2.311200 2.384900 +C 2.770200 -2.652200 -0.052100 +H 2.566800 -2.828300 -1.123800 +H 2.362100 -3.502600 0.517100 +H 3.870300 -2.663500 0.089500 +C 3.617700 -0.186400 -1.399400 +H 3.148900 0.332900 -2.258500 +H 3.837700 -1.217000 -1.716900 +H 4.577700 0.319100 -1.171400 +C 2.925300 2.385200 -0.381900 +H 4.029300 2.350400 -0.279500 +H 2.584900 3.335300 0.058700 +H 2.692800 2.403300 -1.463100 +C 0.975800 2.587500 1.969200 +H 1.515600 2.768400 2.922500 +H -0.104300 2.601400 2.204600 +H 1.186900 3.430300 1.294300 +Cl 0.438500 0.028000 -2.519800 +N -1.162100 1.301800 -0.255300 +N -1.202400 -1.282500 -0.270000 +O 0.110500 3.048000 -0.971600 +H 0.041900 3.979600 -1.257000 +O 0.019400 -3.068900 -0.973100 +H -0.076000 -3.999300 -1.254900 +Ru 0.463300 -0.012400 -0.141900 +",0,1,"@MOLECULE + +54 59 1 +SMALL +PartialCharges +**** +Generated from molSimplify + +@ATOM +1 C1 -1.0901 2.6153 -0.5697 C.3 1 RES1 0.0 +2 C2 -2.2083 3.468 -0.4607 C.2 1 RES1 0.0 +3 H1 -2.1017 4.5373 -0.6968 H 1 RES1 0.0 +4 C3 -3.4324 2.9353 -0.0557 C.2 1 RES1 0.0 +5 H2 -4.3127 3.5854 0.049 H 1 RES1 0.0 +6 C4 -3.5318 1.5578 0.1779 C.2 1 RES1 0.0 +7 H3 -4.4969 1.104 0.4338 H 1 RES1 0.0 +8 C5 -2.387 0.7612 0.0351 C.2 1 RES1 0.0 +9 C6 -2.4092 -0.7069 0.0289 C.3 1 RES1 0.0 +10 C7 -3.5779 -1.4681 0.1701 C.2 1 RES1 0.0 +11 H4 -4.5274 -0.9863 0.4327 H 1 RES1 0.0 +12 C8 -3.5223 -2.8467 -0.0723 C.2 1 RES1 0.0 +13 H5 -4.4227 -3.4693 0.0292 H 1 RES1 0.0 +14 C9 -2.3152 -3.4155 -0.4785 C.2 1 RES1 0.0 +15 H6 -2.2416 -4.4869 -0.7179 H 1 RES1 0.0 +16 C10 -1.1711 -2.5972 -0.5822 C.2 1 RES1 0.0 +17 C11 0.9024 0.0679 2.0392 C.3 1 RES1 0.0 +18 C12 1.2677 -1.2191 1.5022 C.3 1 RES1 0.0 +19 C13 2.2227 -1.3255 0.4133 C.2 1 RES1 0.0 +20 C14 2.7046 -0.124 -0.2066 C.2 1 RES1 0.0 +21 C15 2.3158 1.1779 0.2806 C.3 1 RES1 0.0 +22 C16 1.4081 1.2607 1.3999 C.3 1 RES1 0.0 +23 C17 -0.0101 0.2039 3.2356 C.3 1 RES1 0.0 +24 H7 -0.9763 0.681 2.9733 H 1 RES1 0.0 +25 H8 0.4624 0.8395 4.011 H 1 RES1 0.0 +26 H9 -0.2359 -0.7673 3.7056 H 1 RES1 0.0 +27 C18 0.6693 -2.4598 2.1179 C.3 1 RES1 0.0 +28 H10 1.2152 -2.7249 3.0475 H 1 RES1 0.0 +29 H11 0.7219 -3.3246 1.4384 H 1 RES1 0.0 +30 H12 -0.3925 -2.3112 2.3849 H 1 RES1 0.0 +31 C19 2.7702 -2.6522 -0.0521 C.3 1 RES1 0.0 +32 H13 2.5668 -2.8283 -1.1238 H 1 RES1 0.0 +33 H14 2.3621 -3.5026 0.5171 H 1 RES1 0.0 +34 H15 3.8703 -2.6635 0.0895 H 1 RES1 0.0 +35 C20 3.6177 -0.1864 -1.3994 C.3 1 RES1 0.0 +36 H16 3.1489 0.3329 -2.2585 H 1 RES1 0.0 +37 H17 3.8377 -1.217 -1.7169 H 1 RES1 0.0 +38 H18 4.5777 0.3191 -1.1714 H 1 RES1 0.0 +39 C21 2.9253 2.3852 -0.3819 C.3 1 RES1 0.0 +40 H19 4.0293 2.3504 -0.2795 H 1 RES1 0.0 +41 H20 2.5849 3.3353 0.0587 H 1 RES1 0.0 +42 H21 2.6928 2.4033 -1.4631 H 1 RES1 0.0 +43 C22 0.9758 2.5875 1.9692 C.3 1 RES1 0.0 +44 H22 1.5156 2.7684 2.9225 H 1 RES1 0.0 +45 H23 -0.1043 2.6014 2.2046 H 1 RES1 0.0 +46 H24 1.1869 3.4303 1.2943 H 1 RES1 0.0 +47 Cl1 0.4385 0.028 -2.5198 Cl 1 RES1 0.0 +48 N1 -1.1621 1.3018 -0.2553 N.pl3 1 RES1 0.0 +49 N2 -1.2024 -1.2825 -0.27 N.pl3 1 RES1 0.0 +50 O1 0.1105 3.048 -0.9716 O.3 1 RES1 0.0 +51 H25 0.0419 3.9796 -1.257 H 1 RES1 0.0 +52 O2 0.0194 -3.0689 -0.9731 O.2 1 RES1 0.0 +53 H26 -0.076 -3.9993 -1.2549 H 1 RES1 0.0 +54 Ru1 0.4633 -0.0124 -0.1419 Ru 1 RES1 0.0 +@BOND +1 1 2 1 +1 1 48 1 +1 1 50 1 +1 2 3 1 +1 2 4 2 +1 4 5 1 +1 4 6 1 +1 6 7 1 +1 6 8 2 +1 8 9 1 +1 8 48 1 +1 9 10 1 +1 9 49 1 +1 10 11 1 +1 10 12 2 +1 12 13 1 +1 12 14 1 +1 14 15 1 +1 14 16 2 +1 16 49 1 +1 16 52 1 +1 17 18 1 +1 17 22 1 +1 17 23 1 +1 17 54 1 +1 18 19 1 +1 18 27 1 +1 18 54 1 +1 19 20 2 +1 19 31 1 +1 20 21 1 +1 20 35 1 +1 21 22 1 +1 21 39 1 +1 21 54 1 +1 22 43 1 +1 23 24 1 +1 23 25 1 +1 23 26 1 +1 27 28 1 +1 27 29 1 +1 27 30 1 +1 31 32 1 +1 31 33 1 +1 31 34 1 +1 35 36 1 +1 35 37 1 +1 35 38 1 +1 39 40 1 +1 39 41 1 +1 39 42 1 +1 43 44 1 +1 43 45 1 +1 43 46 1 +1 47 54 1 +1 48 54 1 +1 49 54 1 +1 50 51 1 +1 52 53 1 +@SUBSTRUCTURE +1 RES1 54 + +" +MINI02,"61 +10/06/2025 18:27, XYZ structure generated by mol3D Class, molSimplify +Re 0.045000 -1.706400 -1.296200 +O 0.147600 -2.802800 -2.581900 +O -1.453000 -2.470800 -0.174100 +O -1.494100 -0.522400 -1.910000 +O 1.520400 -0.376200 -1.776100 +O 1.522600 -2.326100 -0.051600 +O -0.156500 -0.173600 0.427500 +C -2.589800 -1.773000 -0.254600 +C -2.611100 -0.704900 -1.202400 +C -3.739800 0.131500 -1.291200 +Cl -3.726000 1.431600 -2.448000 +C -4.850000 -0.075500 -0.433600 +Cl -6.216600 1.000700 -0.511700 +C -4.826800 -1.127900 0.504400 +Cl -6.149700 -1.334300 1.622300 +C -3.700600 -1.986800 0.583600 +Cl -3.625600 -3.263000 1.763300 +C 2.649900 -1.615100 -0.139700 +C 3.781600 -1.824000 0.671000 +Cl 3.717400 -3.068600 1.886000 +C 4.917500 -0.987800 0.541800 +Cl 6.289100 -1.221800 1.591800 +C 4.911200 0.075800 -0.383100 +Cl 6.279700 1.145500 -0.503600 +C 3.767000 0.303700 -1.184300 +Cl 3.691400 1.657500 -2.280600 +C 2.650700 -0.544000 -1.080500 +P -0.098900 1.323400 0.775400 +C -1.213600 1.640900 2.183800 +C -0.942800 2.614300 3.164600 +C -1.870800 2.837000 4.194100 +C -3.062700 2.093200 4.243800 +C -3.327900 1.116600 3.269400 +C -2.402100 0.887300 2.242100 +C 1.581700 1.788300 1.291200 +C 2.282900 0.849900 2.078700 +C 3.597700 1.122700 2.476100 +C 4.218000 2.319700 2.080400 +C 3.524100 3.247600 1.289400 +C 2.202800 2.987000 0.893600 +C -0.612400 2.474600 -0.542900 +C -0.047600 2.333100 -1.828700 +C -0.424800 3.220600 -2.845200 +C -1.361100 4.236300 -2.594400 +C -1.931500 4.366800 -1.319200 +C -1.560200 3.487500 -0.291700 +H -0.005000 3.190200 3.123500 +H -1.660300 3.594000 4.965500 +H -3.787900 2.274200 5.052500 +H -4.255300 0.524000 3.295100 +H -2.593900 0.117400 1.482400 +H 1.807200 -0.107300 2.341600 +H 4.157700 0.376200 3.058000 +H 5.265800 2.507500 2.358100 +H 4.023500 4.168700 0.953000 +H 1.662500 3.697500 0.250200 +H 0.674500 1.525300 -2.029700 +H 0.012700 3.104100 -3.848200 +H -1.659300 4.922800 -3.402300 +H -2.681200 5.148500 -1.123200 +H -2.019200 3.580500 0.703500 +",0,1,"@MOLECULE + +61 67 1 +SMALL +PartialCharges +**** +Generated from molSimplify + +@ATOM +1 Re1 0.045 -1.7064 -1.2962 Re 1 RES1 0.0 +2 O1 0.1476 -2.8028 -2.5819 O.3 1 RES1 0.0 +3 O2 -1.453 -2.4708 -0.1741 O.3 1 RES1 0.0 +4 O3 -1.4941 -0.5224 -1.91 O.3 1 RES1 0.0 +5 O4 1.5204 -0.3762 -1.7761 O.3 1 RES1 0.0 +6 O5 1.5226 -2.3261 -0.0516 O.3 1 RES1 0.0 +7 O6 -0.1565 -0.1736 0.4275 O.3 1 RES1 0.0 +8 C1 -2.5898 -1.773 -0.2546 C.ar 1 RES1 0.0 +9 C2 -2.6111 -0.7049 -1.2024 C.ar 1 RES1 0.0 +10 C3 -3.7398 0.1315 -1.2912 C.ar 1 RES1 0.0 +11 Cl1 -3.726 1.4316 -2.448 Cl 1 RES1 0.0 +12 C4 -4.85 -0.0755 -0.4336 C.ar 1 RES1 0.0 +13 Cl2 -6.2166 1.0007 -0.5117 Cl 1 RES1 0.0 +14 C5 -4.8268 -1.1279 0.5044 C.ar 1 RES1 0.0 +15 Cl3 -6.1497 -1.3343 1.6223 Cl 1 RES1 0.0 +16 C6 -3.7006 -1.9868 0.5836 C.ar 1 RES1 0.0 +17 Cl4 -3.6256 -3.263 1.7633 Cl 1 RES1 0.0 +18 C7 2.6499 -1.6151 -0.1397 C.ar 1 RES1 0.0 +19 C8 3.7816 -1.824 0.671 C.ar 1 RES1 0.0 +20 Cl5 3.7174 -3.0686 1.886 Cl 1 RES1 0.0 +21 C9 4.9175 -0.9878 0.5418 C.ar 1 RES1 0.0 +22 Cl6 6.2891 -1.2218 1.5918 Cl 1 RES1 0.0 +23 C10 4.9112 0.0758 -0.3831 C.ar 1 RES1 0.0 +24 Cl7 6.2797 1.1455 -0.5036 Cl 1 RES1 0.0 +25 C11 3.767 0.3037 -1.1843 C.ar 1 RES1 0.0 +26 Cl8 3.6914 1.6575 -2.2806 Cl 1 RES1 0.0 +27 C12 2.6507 -0.544 -1.0805 C.ar 1 RES1 0.0 +28 P1 -0.0989 1.3234 0.7754 P.3 1 RES1 0.0 +29 C13 -1.2136 1.6409 2.1838 C.ar 1 RES1 0.0 +30 C14 -0.9428 2.6143 3.1646 C.ar 1 RES1 0.0 +31 C15 -1.8708 2.837 4.1941 C.ar 1 RES1 0.0 +32 C16 -3.0627 2.0932 4.2438 C.ar 1 RES1 0.0 +33 C17 -3.3279 1.1166 3.2694 C.ar 1 RES1 0.0 +34 C18 -2.4021 0.8873 2.2421 C.ar 1 RES1 0.0 +35 C19 1.5817 1.7883 1.2912 C.ar 1 RES1 0.0 +36 C20 2.2829 0.8499 2.0787 C.ar 1 RES1 0.0 +37 C21 3.5977 1.1227 2.4761 C.ar 1 RES1 0.0 +38 C22 4.218 2.3197 2.0804 C.ar 1 RES1 0.0 +39 C23 3.5241 3.2476 1.2894 C.ar 1 RES1 0.0 +40 C24 2.2028 2.987 0.8936 C.ar 1 RES1 0.0 +41 C25 -0.6124 2.4746 -0.5429 C.ar 1 RES1 0.0 +42 C26 -0.0476 2.3331 -1.8287 C.ar 1 RES1 0.0 +43 C27 -0.4248 3.2206 -2.8452 C.ar 1 RES1 0.0 +44 C28 -1.3611 4.2363 -2.5944 C.ar 1 RES1 0.0 +45 C29 -1.9315 4.3668 -1.3192 C.ar 1 RES1 0.0 +46 C30 -1.5602 3.4875 -0.2917 C.ar 1 RES1 0.0 +47 H1 -0.005 3.1902 3.1235 H 1 RES1 0.0 +48 H2 -1.6603 3.594 4.9655 H 1 RES1 0.0 +49 H3 -3.7879 2.2742 5.0525 H 1 RES1 0.0 +50 H4 -4.2553 0.524 3.2951 H 1 RES1 0.0 +51 H5 -2.5939 0.1174 1.4824 H 1 RES1 0.0 +52 H6 1.8072 -0.1073 2.3416 H 1 RES1 0.0 +53 H7 4.1577 0.3762 3.058 H 1 RES1 0.0 +54 H8 5.2658 2.5075 2.3581 H 1 RES1 0.0 +55 H9 4.0235 4.1687 0.953 H 1 RES1 0.0 +56 H10 1.6625 3.6975 0.2502 H 1 RES1 0.0 +57 H11 0.6745 1.5253 -2.0297 H 1 RES1 0.0 +58 H12 0.0127 3.1041 -3.8482 H 1 RES1 0.0 +59 H13 -1.6593 4.9228 -3.4023 H 1 RES1 0.0 +60 H14 -2.6812 5.1485 -1.1232 H 1 RES1 0.0 +61 H15 -2.0192 3.5805 0.7035 H 1 RES1 0.0 +@BOND +1 1 2 1 +1 1 3 1 +1 1 4 1 +1 1 5 1 +1 1 6 1 +1 1 7 1 +1 3 8 1 +1 4 9 1 +1 5 27 1 +1 6 18 1 +1 7 28 1 +1 8 9 2 +1 8 16 1 +1 9 10 1 +1 10 11 1 +1 10 12 2 +1 12 13 1 +1 12 14 1 +1 14 15 1 +1 14 16 2 +1 16 17 1 +1 18 19 2 +1 18 27 1 +1 19 20 1 +1 19 21 1 +1 21 22 1 +1 21 23 2 +1 23 24 1 +1 23 25 1 +1 25 26 1 +1 25 27 2 +1 28 29 1 +1 28 35 1 +1 28 41 1 +1 29 30 2 +1 29 34 1 +1 30 31 1 +1 30 47 1 +1 31 32 2 +1 31 48 1 +1 32 33 1 +1 32 49 1 +1 33 34 2 +1 33 50 1 +1 34 51 1 +1 35 36 2 +1 35 40 1 +1 36 37 1 +1 36 52 1 +1 37 38 2 +1 37 53 1 +1 38 39 1 +1 38 54 1 +1 39 40 2 +1 39 55 1 +1 40 56 1 +1 41 42 2 +1 41 46 1 +1 42 43 1 +1 42 57 1 +1 43 44 2 +1 43 58 1 +1 44 45 1 +1 44 59 1 +1 45 46 2 +1 45 60 1 +1 46 61 1 +@SUBSTRUCTURE +1 RES1 61 + +" +MINI03,"37 +10/06/2025 18:27, XYZ structure generated by mol3D Class, molSimplify +Zn -2.075600 -0.249800 -1.186800 +Cl -0.346900 -0.981000 -2.329000 +Cl -4.255500 -0.075600 -1.460400 +O 6.266400 1.047200 -0.566500 +O 6.085000 -1.109400 -0.885800 +N -1.597600 1.506300 -0.079000 +N -1.737500 -1.217200 0.693600 +N 0.133900 0.210300 0.887500 +N 5.625400 -0.005700 -0.587900 +C -2.404500 2.582600 -0.115900 +H -3.220400 2.530700 -0.856700 +C -2.232500 3.663600 0.756400 +H -2.894900 4.538000 0.687600 +C -1.222800 3.586800 1.727400 +H -1.076600 4.403700 2.450000 +C -0.421200 2.438700 1.794900 +H 0.342300 2.315500 2.575000 +C -0.628700 1.418400 0.850800 +C -0.549700 -0.972900 1.275400 +C -0.035000 -1.818600 2.278400 +H 0.934300 -1.592700 2.743600 +C -0.794100 -2.927600 2.665000 +H -0.415900 -3.603600 3.446600 +C -2.050700 -3.152800 2.074200 +H -2.677700 -4.005800 2.368800 +C -2.488600 -2.261000 1.091700 +H -3.459200 -2.361700 0.577100 +C 1.496700 0.180700 0.512000 +C 2.292000 1.350800 0.508000 +H 1.862100 2.322400 0.781100 +C 3.638400 1.290200 0.141100 +H 4.274600 2.184900 0.135700 +C 4.197800 0.059700 -0.222400 +C 3.419700 -1.106200 -0.263700 +H 3.881500 -2.042700 -0.602800 +C 2.072700 -1.042300 0.087100 +H 1.439400 -1.931400 -0.024300 +",1,1,"@MOLECULE + +37 40 1 +SMALL +PartialCharges +**** +Generated from molSimplify + +@ATOM +1 Zn1 -2.0756 -0.2498 -1.1868 Zn 1 RES1 0.0 +2 Cl1 -0.3469 -0.981 -2.329 Cl 1 RES1 0.0 +3 Cl2 -4.2555 -0.0756 -1.4604 Cl 1 RES1 0.0 +4 O1 6.2664 1.0472 -0.5665 O.2 1 RES1 0.0 +5 O2 6.085 -1.1094 -0.8858 O.2 1 RES1 0.0 +6 N1 -1.5976 1.5063 -0.079 N.pl3 1 RES1 0.0 +7 N2 -1.7375 -1.2172 0.6936 N.pl3 1 RES1 0.0 +8 N3 0.1339 0.2103 0.8875 N.pl3 1 RES1 0.0 +9 N4 5.6254 -0.0057 -0.5879 N.pl3 1 RES1 0.0 +10 C1 -2.4045 2.5826 -0.1159 C.3 1 RES1 0.0 +11 H1 -3.2204 2.5307 -0.8567 H 1 RES1 0.0 +12 C2 -2.2325 3.6636 0.7564 C.2 1 RES1 0.0 +13 H2 -2.8949 4.538 0.6876 H 1 RES1 0.0 +14 C3 -1.2228 3.5868 1.7274 C.2 1 RES1 0.0 +15 H3 -1.0766 4.4037 2.45 H 1 RES1 0.0 +16 C4 -0.4212 2.4387 1.7949 C.2 1 RES1 0.0 +17 H4 0.3423 2.3155 2.575 H 1 RES1 0.0 +18 C5 -0.6287 1.4184 0.8508 C.2 1 RES1 0.0 +19 C6 -0.5497 -0.9729 1.2754 C.3 1 RES1 0.0 +20 C7 -0.035 -1.8186 2.2784 C.2 1 RES1 0.0 +21 H5 0.9343 -1.5927 2.7436 H 1 RES1 0.0 +22 C8 -0.7941 -2.9276 2.665 C.2 1 RES1 0.0 +23 H6 -0.4159 -3.6036 3.4466 H 1 RES1 0.0 +24 C9 -2.0507 -3.1528 2.0742 C.2 1 RES1 0.0 +25 H7 -2.6777 -4.0058 2.3688 H 1 RES1 0.0 +26 C10 -2.4886 -2.261 1.0917 C.2 1 RES1 0.0 +27 H8 -3.4592 -2.3617 0.5771 H 1 RES1 0.0 +28 C11 1.4967 0.1807 0.512 C.ar 1 RES1 0.0 +29 C12 2.292 1.3508 0.508 C.ar 1 RES1 0.0 +30 H9 1.8621 2.3224 0.7811 H 1 RES1 0.0 +31 C13 3.6384 1.2902 0.1411 C.ar 1 RES1 0.0 +32 H10 4.2746 2.1849 0.1357 H 1 RES1 0.0 +33 C14 4.1978 0.0597 -0.2224 C.ar 1 RES1 0.0 +34 C15 3.4197 -1.1062 -0.2637 C.ar 1 RES1 0.0 +35 H11 3.8815 -2.0427 -0.6028 H 1 RES1 0.0 +36 C16 2.0727 -1.0423 0.0871 C.ar 1 RES1 0.0 +37 H12 1.4394 -1.9314 -0.0243 H 1 RES1 0.0 +@BOND +1 1 2 1 +1 1 3 1 +1 1 6 1 +1 1 7 1 +1 4 9 2 +1 5 9 2 +1 6 10 1 +1 6 18 1 +1 7 19 1 +1 7 26 1 +1 8 18 1 +1 8 19 1 +1 8 28 1 +1 9 33 1 +1 10 11 1 +1 10 12 1 +1 12 13 1 +1 12 14 2 +1 14 15 1 +1 14 16 1 +1 16 17 1 +1 16 18 2 +1 19 20 1 +1 20 21 1 +1 20 22 2 +1 22 23 1 +1 22 24 1 +1 24 25 1 +1 24 26 2 +1 26 27 1 +1 28 29 2 +1 28 36 1 +1 29 30 1 +1 29 31 1 +1 31 32 1 +1 31 33 2 +1 33 34 1 +1 34 35 1 +1 34 36 2 +1 36 37 1 +@SUBSTRUCTURE +1 RES1 37 + +" +MINI04,"48 +10/06/2025 18:27, XYZ structure generated by mol3D Class, molSimplify +Rh 0.033400 1.177600 -0.275300 +O 2.257100 3.238700 -0.525000 +O -2.087900 3.333000 -0.625200 +N 2.370500 -1.043800 0.026600 +N 1.128000 -1.142100 -1.745900 +N -1.241600 -1.039400 -1.759300 +N -2.494200 -0.849800 -0.000400 +C 1.336600 -0.417400 -0.602300 +C 2.809400 -2.129600 -0.731100 +H 3.641700 -2.766900 -0.426900 +C 2.016400 -2.202900 -1.844600 +H 2.022900 -2.904200 -2.684600 +C 2.985800 -0.636300 1.343900 +C 4.038000 0.450400 1.072200 +H 4.814800 0.081200 0.373700 +H 4.536700 0.732400 2.019600 +H 3.588800 1.362700 0.641000 +C 1.875700 -0.133700 2.277600 +H 1.140000 -0.935900 2.486300 +H 1.331800 0.734900 1.849300 +H 2.323500 0.184800 3.238200 +C 3.655300 -1.863300 1.982800 +H 2.947900 -2.709500 2.088600 +H 4.004200 -1.585500 2.995200 +H 4.546900 -2.203000 1.420700 +C -0.043600 -0.889500 -2.576500 +H -0.070900 -1.612500 -3.411100 +H 0.005100 0.145300 -2.967800 +C -1.399300 -0.310100 -0.608300 +C -3.005900 -1.895900 -0.770000 +H -3.889600 -2.466400 -0.476900 +C -2.212000 -2.023200 -1.876300 +H -2.274100 -2.705600 -2.729600 +C -2.979700 -0.603500 1.409300 +C -2.569700 0.785300 1.898700 +H -3.058600 1.586700 1.313500 +H -1.470800 0.935000 1.861700 +H -2.890700 0.893100 2.952200 +C -2.335800 -1.688500 2.291500 +H -2.607000 -2.706000 1.947100 +H -2.681300 -1.577700 3.337500 +H -1.231300 -1.595900 2.279000 +C -4.513200 -0.712100 1.431800 +H -4.974800 -0.019000 0.701400 +H -4.876900 -0.437300 2.439900 +H -4.875500 -1.738600 1.230800 +C 1.425000 2.451900 -0.367300 +C -1.282700 2.530200 -0.420600 +",-1,2,"@MOLECULE + +48 50 1 +SMALL +PartialCharges +**** +Generated from molSimplify + +@ATOM +1 Rh1 0.0334 1.1776 -0.2753 Rh 1 RES1 0.0 +2 O1 2.2571 3.2387 -0.525 O.3 1 RES1 0.0 +3 O2 -2.0879 3.333 -0.6252 O.3 1 RES1 0.0 +4 N1 2.3705 -1.0438 0.0266 N.pl3 1 RES1 0.0 +5 N2 1.128 -1.1421 -1.7459 N.pl3 1 RES1 0.0 +6 N3 -1.2416 -1.0394 -1.7593 N.pl3 1 RES1 0.0 +7 N4 -2.4942 -0.8498 -0.0004 N.pl3 1 RES1 0.0 +8 C1 1.3366 -0.4174 -0.6023 C.3 1 RES1 0.0 +9 C2 2.8094 -2.1296 -0.7311 C.2 1 RES1 0.0 +10 H1 3.6417 -2.7669 -0.4269 H 1 RES1 0.0 +11 C3 2.0164 -2.2029 -1.8446 C.2 1 RES1 0.0 +12 H2 2.0229 -2.9042 -2.6846 H 1 RES1 0.0 +13 C4 2.9858 -0.6363 1.3439 C.3 1 RES1 0.0 +14 C5 4.038 0.4504 1.0722 C.3 1 RES1 0.0 +15 H3 4.8148 0.0812 0.3737 H 1 RES1 0.0 +16 H4 4.5367 0.7324 2.0196 H 1 RES1 0.0 +17 H5 3.5888 1.3627 0.641 H 1 RES1 0.0 +18 C6 1.8757 -0.1337 2.2776 C.3 1 RES1 0.0 +19 H6 1.14 -0.9359 2.4863 H 1 RES1 0.0 +20 H7 1.3318 0.7349 1.8493 H 1 RES1 0.0 +21 H8 2.3235 0.1848 3.2382 H 1 RES1 0.0 +22 C7 3.6553 -1.8633 1.9828 C.3 1 RES1 0.0 +23 H9 2.9479 -2.7095 2.0886 H 1 RES1 0.0 +24 H10 4.0042 -1.5855 2.9952 H 1 RES1 0.0 +25 H11 4.5469 -2.203 1.4207 H 1 RES1 0.0 +26 C8 -0.0436 -0.8895 -2.5765 C.3 1 RES1 0.0 +27 H12 -0.0709 -1.6125 -3.4111 H 1 RES1 0.0 +28 H13 0.0051 0.1453 -2.9678 H 1 RES1 0.0 +29 C9 -1.3993 -0.3101 -0.6083 C.2 1 RES1 0.0 +30 C10 -3.0059 -1.8959 -0.77 C.2 1 RES1 0.0 +31 H14 -3.8896 -2.4664 -0.4769 H 1 RES1 0.0 +32 C11 -2.212 -2.0232 -1.8763 C.2 1 RES1 0.0 +33 H15 -2.2741 -2.7056 -2.7296 H 1 RES1 0.0 +34 C12 -2.9797 -0.6035 1.4093 C.3 1 RES1 0.0 +35 C13 -2.5697 0.7853 1.8987 C.3 1 RES1 0.0 +36 H16 -3.0586 1.5867 1.3135 H 1 RES1 0.0 +37 H17 -1.4708 0.935 1.8617 H 1 RES1 0.0 +38 H18 -2.8907 0.8931 2.9522 H 1 RES1 0.0 +39 C14 -2.3358 -1.6885 2.2915 C.3 1 RES1 0.0 +40 H19 -2.607 -2.706 1.9471 H 1 RES1 0.0 +41 H20 -2.6813 -1.5777 3.3375 H 1 RES1 0.0 +42 H21 -1.2313 -1.5959 2.279 H 1 RES1 0.0 +43 C15 -4.5132 -0.7121 1.4318 C.3 1 RES1 0.0 +44 H22 -4.9748 -0.019 0.7014 H 1 RES1 0.0 +45 H23 -4.8769 -0.4373 2.4399 H 1 RES1 0.0 +46 H24 -4.8755 -1.7386 1.2308 H 1 RES1 0.0 +47 C16 1.425 2.4519 -0.3673 C.3 1 RES1 0.0 +48 C17 -1.2827 2.5302 -0.4206 C.3 1 RES1 0.0 +@BOND +1 1 8 1 +1 1 29 2 +1 1 47 1 +1 1 48 1 +1 2 47 1 +1 3 48 1 +1 4 8 1 +1 4 9 1 +1 4 13 1 +1 5 8 1 +1 5 11 1 +1 5 26 1 +1 6 26 1 +1 6 29 1 +1 6 32 1 +1 7 29 1 +1 7 30 1 +1 7 34 1 +1 9 10 1 +1 9 11 2 +1 11 12 1 +1 13 14 1 +1 13 18 1 +1 13 22 1 +1 14 15 1 +1 14 16 1 +1 14 17 1 +1 18 19 1 +1 18 20 1 +1 18 21 1 +1 22 23 1 +1 22 24 1 +1 22 25 1 +1 26 27 1 +1 26 28 1 +1 30 31 1 +1 30 32 2 +1 32 33 1 +1 34 35 1 +1 34 39 1 +1 34 43 1 +1 35 36 1 +1 35 37 1 +1 35 38 1 +1 39 40 1 +1 39 41 1 +1 39 42 1 +1 43 44 1 +1 43 45 1 +1 43 46 1 +@SUBSTRUCTURE +1 RES1 48 + +" +MINI05,"75 +10/06/2025 18:27, XYZ structure generated by mol3D Class, molSimplify +Ti -0.000000 0.933900 -0.000000 +Cl -0.786400 -0.659000 -1.538300 +N 4.754200 -0.646200 -0.024300 +C 2.133200 0.628900 -1.117600 +C 1.301000 1.421200 -1.968400 +C 0.994700 2.638900 -1.308900 +C 1.654300 2.621000 -0.037100 +C 2.355600 1.392300 0.071600 +C 2.658000 -0.743900 -1.410100 +C 4.194600 -0.853500 -1.350200 +C 4.498700 -1.736100 0.932000 +C 4.451200 -1.222700 2.376100 +C 5.435700 -2.952700 0.798600 +C 6.081700 -0.009300 0.017500 +C 5.957300 1.512800 -0.147200 +C 7.120000 -0.580600 -0.970800 +H 0.918400 1.106500 -2.945400 +H 0.398900 3.460300 -1.719800 +H 1.658200 3.429300 0.701500 +H 2.954100 1.051500 0.919500 +H 2.313600 -1.057900 -2.415200 +H 2.199200 -1.460200 -0.697000 +H 4.482300 -1.843700 -1.791700 +H 4.621500 -0.088500 -2.030400 +H 3.470000 -2.086200 0.703600 +H 5.424300 -0.799300 2.701800 +H 4.206300 -2.051100 3.070800 +H 3.668800 -0.447600 2.490200 +H 5.081600 -3.790000 1.433900 +H 6.469600 -2.708600 1.120300 +H 5.478400 -3.323100 -0.245900 +H 6.472500 -0.194300 1.040000 +H 6.951700 2.003000 -0.108300 +H 5.320200 1.944400 0.648700 +H 5.494700 1.772800 -1.122300 +H 8.118100 -0.134900 -0.782900 +H 6.853300 -0.349300 -2.022500 +H 7.216500 -1.679100 -0.880500 +Cl 0.786300 -0.658900 1.538400 +C -2.133200 0.629000 1.117600 +C -1.301000 1.421300 1.968400 +C -0.994700 2.639000 1.308800 +H -0.918400 1.106700 2.945400 +C -1.654300 2.621000 0.037000 +C -2.355700 1.392400 -0.071700 +C -2.658000 -0.743800 1.410200 +C -4.194700 -0.853400 1.350300 +N -4.754200 -0.646300 0.024300 +C -4.498700 -1.736200 -0.931900 +C -4.451100 -1.223000 -2.376000 +H -5.424100 -0.799700 -2.701900 +H -4.206200 -2.051500 -3.070600 +H -3.668500 -0.448000 -2.490100 +C -5.435800 -2.952800 -0.798400 +H -5.081700 -3.790200 -1.433500 +H -6.469600 -2.708700 -1.120200 +H -5.478600 -3.323100 0.246200 +H -3.470000 -2.086400 -0.703300 +C -6.081700 -0.009300 -0.017600 +C -5.957100 1.512900 0.146900 +H -6.951500 2.003100 0.107800 +H -5.320000 1.944300 -0.649100 +H -5.494500 1.773000 1.121900 +C -7.120000 -0.580300 0.970800 +H -8.118100 -0.134600 0.782700 +H -6.853400 -0.348800 2.022400 +H -7.216600 -1.678800 0.880600 +H -6.472500 -0.194400 -1.040100 +H -4.482400 -1.843500 1.791900 +H -4.621500 -0.088300 2.030400 +H -2.313700 -1.057700 2.415300 +H -2.199200 -1.460200 0.697100 +H -1.658200 3.429300 -0.701600 +H -2.954100 1.051500 -0.919500 +H -0.398900 3.460400 1.719600 +",0,2,"@MOLECULE + +75 78 1 +SMALL +PartialCharges +**** +Generated from molSimplify + +@ATOM +1 Ti1 -0.0 0.9339 -0.0 Ti 1 RES1 0.0 +2 Cl1 -0.7864 -0.659 -1.5383 Cl 1 RES1 0.0 +3 N1 4.7542 -0.6462 -0.0243 N.3 1 RES1 0.0 +4 C1 2.1332 0.6289 -1.1176 C.2 1 RES1 0.0 +5 C2 1.301 1.4212 -1.9684 C.2 1 RES1 0.0 +6 C3 0.9947 2.6389 -1.3089 C.3 1 RES1 0.0 +7 C4 1.6543 2.621 -0.0371 C.3 1 RES1 0.0 +8 C5 2.3556 1.3923 0.0716 C.3 1 RES1 0.0 +9 C6 2.658 -0.7439 -1.4101 C.3 1 RES1 0.0 +10 C7 4.1946 -0.8535 -1.3502 C.3 1 RES1 0.0 +11 C8 4.4987 -1.7361 0.932 C.3 1 RES1 0.0 +12 C9 4.4512 -1.2227 2.3761 C.3 1 RES1 0.0 +13 C10 5.4357 -2.9527 0.7986 C.3 1 RES1 0.0 +14 C11 6.0817 -0.0093 0.0175 C.3 1 RES1 0.0 +15 C12 5.9573 1.5128 -0.1472 C.3 1 RES1 0.0 +16 C13 7.12 -0.5806 -0.9708 C.3 1 RES1 0.0 +17 H1 0.9184 1.1065 -2.9454 H 1 RES1 0.0 +18 H2 0.3989 3.4603 -1.7198 H 1 RES1 0.0 +19 H3 1.6582 3.4293 0.7015 H 1 RES1 0.0 +20 H4 2.9541 1.0515 0.9195 H 1 RES1 0.0 +21 H5 2.3136 -1.0579 -2.4152 H 1 RES1 0.0 +22 H6 2.1992 -1.4602 -0.697 H 1 RES1 0.0 +23 H7 4.4823 -1.8437 -1.7917 H 1 RES1 0.0 +24 H8 4.6215 -0.0885 -2.0304 H 1 RES1 0.0 +25 H9 3.47 -2.0862 0.7036 H 1 RES1 0.0 +26 H10 5.4243 -0.7993 2.7018 H 1 RES1 0.0 +27 H11 4.2063 -2.0511 3.0708 H 1 RES1 0.0 +28 H12 3.6688 -0.4476 2.4902 H 1 RES1 0.0 +29 H13 5.0816 -3.79 1.4339 H 1 RES1 0.0 +30 H14 6.4696 -2.7086 1.1203 H 1 RES1 0.0 +31 H15 5.4784 -3.3231 -0.2459 H 1 RES1 0.0 +32 H16 6.4725 -0.1943 1.04 H 1 RES1 0.0 +33 H17 6.9517 2.003 -0.1083 H 1 RES1 0.0 +34 H18 5.3202 1.9444 0.6487 H 1 RES1 0.0 +35 H19 5.4947 1.7728 -1.1223 H 1 RES1 0.0 +36 H20 8.1181 -0.1349 -0.7829 H 1 RES1 0.0 +37 H21 6.8533 -0.3493 -2.0225 H 1 RES1 0.0 +38 H22 7.2165 -1.6791 -0.8805 H 1 RES1 0.0 +39 Cl2 0.7863 -0.6589 1.5384 Cl 1 RES1 0.0 +40 C14 -2.1332 0.629 1.1176 C.2 1 RES1 0.0 +41 C15 -1.301 1.4213 1.9684 C.2 1 RES1 0.0 +42 C16 -0.9947 2.639 1.3088 C.3 1 RES1 0.0 +43 H23 -0.9184 1.1067 2.9454 H 1 RES1 0.0 +44 C17 -1.6543 2.621 0.037 C.3 1 RES1 0.0 +45 C18 -2.3557 1.3924 -0.0717 C.3 1 RES1 0.0 +46 C19 -2.658 -0.7438 1.4102 C.3 1 RES1 0.0 +47 C20 -4.1947 -0.8534 1.3503 C.3 1 RES1 0.0 +48 N2 -4.7542 -0.6463 0.0243 N.3 1 RES1 0.0 +49 C21 -4.4987 -1.7362 -0.9319 C.3 1 RES1 0.0 +50 C22 -4.4511 -1.223 -2.376 C.3 1 RES1 0.0 +51 H24 -5.4241 -0.7997 -2.7019 H 1 RES1 0.0 +52 H25 -4.2062 -2.0515 -3.0706 H 1 RES1 0.0 +53 H26 -3.6685 -0.448 -2.4901 H 1 RES1 0.0 +54 C23 -5.4358 -2.9528 -0.7984 C.3 1 RES1 0.0 +55 H27 -5.0817 -3.7902 -1.4335 H 1 RES1 0.0 +56 H28 -6.4696 -2.7087 -1.1202 H 1 RES1 0.0 +57 H29 -5.4786 -3.3231 0.2462 H 1 RES1 0.0 +58 H30 -3.47 -2.0864 -0.7033 H 1 RES1 0.0 +59 C24 -6.0817 -0.0093 -0.0176 C.3 1 RES1 0.0 +60 C25 -5.9571 1.5129 0.1469 C.3 1 RES1 0.0 +61 H31 -6.9515 2.0031 0.1078 H 1 RES1 0.0 +62 H32 -5.32 1.9443 -0.6491 H 1 RES1 0.0 +63 H33 -5.4945 1.773 1.1219 H 1 RES1 0.0 +64 C26 -7.12 -0.5803 0.9708 C.3 1 RES1 0.0 +65 H34 -8.1181 -0.1346 0.7827 H 1 RES1 0.0 +66 H35 -6.8534 -0.3488 2.0224 H 1 RES1 0.0 +67 H36 -7.2166 -1.6788 0.8806 H 1 RES1 0.0 +68 H37 -6.4725 -0.1944 -1.0401 H 1 RES1 0.0 +69 H38 -4.4824 -1.8435 1.7919 H 1 RES1 0.0 +70 H39 -4.6215 -0.0883 2.0304 H 1 RES1 0.0 +71 H40 -2.3137 -1.0577 2.4153 H 1 RES1 0.0 +72 H41 -2.1992 -1.4602 0.6971 H 1 RES1 0.0 +73 H42 -1.6582 3.4293 -0.7016 H 1 RES1 0.0 +74 H43 -2.9541 1.0515 -0.9195 H 1 RES1 0.0 +75 H44 -0.3989 3.4604 1.7196 H 1 RES1 0.0 +@BOND +1 1 2 1 +1 1 6 1 +1 1 7 1 +1 1 39 1 +1 1 42 1 +1 1 44 1 +1 3 10 1 +1 3 11 1 +1 3 14 1 +1 4 5 2 +1 4 8 1 +1 4 9 1 +1 5 6 1 +1 5 17 1 +1 6 7 1 +1 6 18 1 +1 7 8 1 +1 7 19 1 +1 8 20 1 +1 9 10 1 +1 9 21 1 +1 9 22 1 +1 10 23 1 +1 10 24 1 +1 11 12 1 +1 11 13 1 +1 11 25 1 +1 12 26 1 +1 12 27 1 +1 12 28 1 +1 13 29 1 +1 13 30 1 +1 13 31 1 +1 14 15 1 +1 14 16 1 +1 14 32 1 +1 15 33 1 +1 15 34 1 +1 15 35 1 +1 16 36 1 +1 16 37 1 +1 16 38 1 +1 40 41 2 +1 40 45 1 +1 40 46 1 +1 41 42 1 +1 41 43 1 +1 42 44 1 +1 42 75 1 +1 44 45 1 +1 44 73 1 +1 45 74 1 +1 46 47 1 +1 46 71 1 +1 46 72 1 +1 47 48 1 +1 47 69 1 +1 47 70 1 +1 48 49 1 +1 48 59 1 +1 49 50 1 +1 49 54 1 +1 49 58 1 +1 50 51 1 +1 50 52 1 +1 50 53 1 +1 54 55 1 +1 54 56 1 +1 54 57 1 +1 59 60 1 +1 59 64 1 +1 59 68 1 +1 60 61 1 +1 60 62 1 +1 60 63 1 +1 64 65 1 +1 64 66 1 +1 64 67 1 +@SUBSTRUCTURE +1 RES1 75 + +" diff --git a/tests/fixtures/qm9_mini.csv b/tests/fixtures/qm9_mini.csv new file mode 100644 index 0000000..1bf24fe --- /dev/null +++ b/tests/fixtures/qm9_mini.csv @@ -0,0 +1,411 @@ +smiles,xyz +c1cncc(c1CO)F,"15 +gdb_133127 +O 0.019300 1.431200 0.007600 +C 0.019300 0.017400 0.001500 +C -1.403400 -0.477500 0.012100 +C -2.512000 0.364200 0.023200 +C -3.788500 -0.193600 0.032200 +N -4.027700 -1.507200 0.030900 +C -2.969600 -2.321000 0.020300 +C -1.669600 -1.842400 0.010900 +F -0.641300 -2.703900 0.000400 +H 0.932500 1.730400 0.002000 +H 0.531000 -0.385500 -0.886900 +H 0.546800 -0.393300 0.877100 +H -2.368100 1.436900 0.024800 +H -4.662900 0.452000 0.040900 +H -3.147000 -3.393400 0.019100 +" +O=CC12C3OC1COC23,"15 +gdb_48553 +O -0.129900 0.007800 -0.118300 +C -0.020600 1.198700 0.064000 +C 1.242800 1.925200 -0.011800 +C 1.934900 2.990900 0.857900 +O 3.173900 2.303000 0.866500 +C 2.682400 1.403300 -0.186300 +C 3.139100 1.993000 -1.509000 +O 2.517800 3.293500 -1.541700 +C 1.535100 3.323300 -0.547400 +H -0.902900 1.832500 0.308700 +H 1.605800 3.491000 1.762200 +H 2.907800 0.357200 0.011700 +H 4.223500 2.124100 -1.548200 +H 2.805400 1.390000 -2.365300 +H 0.728500 4.022900 -0.734200 +" +CC(C)(C)C(O)C1CN1,"24 +gdb_60526 +C -0.034500 1.518500 -0.093600 +C 0.051000 -0.013200 0.042000 +C 0.387100 -0.368000 1.502800 +C -1.300300 -0.636800 -0.349300 +C 1.159900 -0.518800 -0.927500 +O 2.433300 0.059300 -0.623300 +C 1.275400 -2.028200 -1.031400 +C 0.796700 -2.745500 -2.240900 +N 2.236900 -2.594800 -1.990700 +H 0.935500 1.979400 0.106900 +H -0.342000 1.806800 -1.105300 +H -0.768300 1.930000 0.607600 +H -0.376100 0.037400 2.174900 +H 0.425200 -1.449100 1.671700 +H 1.347100 0.062500 1.805300 +H -1.545500 -0.426300 -1.396500 +H -1.304300 -1.722700 -0.216700 +H -2.104800 -0.222100 0.267400 +H 0.923600 -0.128500 -1.927000 +H 2.820900 -0.439700 0.103500 +H 1.259300 -2.565200 -0.084100 +H 0.419100 -3.761000 -2.159300 +H 0.336100 -2.156300 -3.030100 +H 2.618800 -1.840300 -2.557800 +" +C#CC1COC(=N1)C#C,"14 +gdb_35654 +C 0.015200 -1.192300 0.045100 +C 0.022800 0.008400 0.013000 +C -0.004500 1.466600 0.001100 +C -0.595200 2.083800 1.310400 +O 0.413500 3.018000 1.742200 +C 1.454800 2.868200 0.877300 +N 1.351000 2.055200 -0.098500 +C 2.597500 3.671700 1.154200 +C 3.555800 4.354300 1.394800 +H 0.020900 -2.254200 0.062800 +H -0.576300 1.797400 -0.875400 +H -1.525500 2.631100 1.145600 +H -0.739700 1.337300 2.094700 +H 4.408500 4.954300 1.599800 +" +CC1C2CC3C2CC13,"20 +gdb_18927 +C -0.079000 1.524800 0.021400 +C 0.094500 0.006900 0.030600 +C 1.057100 -0.614500 -1.012400 +C 2.425200 0.027900 -0.661500 +C 2.284000 -0.800200 0.622300 +C 1.520800 -1.899300 -0.185100 +C 0.585600 -2.121100 1.012300 +C 0.829700 -0.607600 1.249700 +H 0.844100 2.059800 0.258900 +H -0.427300 1.877300 -0.956300 +H -0.825700 1.824700 0.764700 +H -0.900200 -0.444700 -0.064200 +H 0.709100 -0.687500 -2.043400 +H 3.230300 -0.377600 -1.282800 +H 2.515400 1.116300 -0.625800 +H 3.102800 -1.021500 1.306400 +H 2.024000 -2.696600 -0.731800 +H -0.445100 -2.458000 0.858800 +H 1.070100 -2.752700 1.763400 +H 0.676900 -0.144200 2.224900 +" +O=C1C=CC2(NC12)C#N,"13 +gdb_43271 +O 0.039700 -0.016500 -0.015200 +C -0.013300 1.186100 0.012500 +C -1.226500 2.051000 0.092200 +C -0.895100 3.343200 -0.006200 +C 0.597500 3.510900 -0.099800 +N 1.283200 3.004300 1.130600 +C 1.178900 2.139200 -0.065200 +C 1.181800 4.584700 -0.849300 +N 1.645300 5.464100 -1.440900 +H -2.218800 1.642700 0.229600 +H -1.566900 4.191600 0.018600 +H 2.229100 3.388100 1.169000 +H 2.068500 1.858300 -0.615400 +" +O=CC1CC(C1)C1CC1,"21 +gdb_49907 +O -0.084200 -0.095100 -0.220800 +C -0.056900 1.083300 0.028100 +C 1.173200 1.945500 0.005900 +C 0.967400 3.271000 -0.801000 +C 0.731200 4.040000 0.531000 +C 1.321400 2.820000 1.295700 +C 1.376800 5.385000 0.711300 +C 0.644900 6.647700 0.324800 +C 0.890500 6.334800 1.779400 +H -0.993800 1.627400 0.303300 +H 2.027700 1.326400 -0.271500 +H 1.895700 3.574800 -1.292300 +H 0.164400 3.285300 -1.543000 +H -0.343200 4.126700 0.731100 +H 0.795000 2.481800 2.192400 +H 2.375300 2.963800 1.548700 +H 2.450000 5.386800 0.530600 +H 1.209900 7.452200 -0.132500 +H -0.368700 6.550000 -0.050300 +H 0.041800 6.026900 2.381700 +H 1.623600 6.925200 2.317700 +" +FC1=NC(=N)NN=C1,"11 +gdb_23823 +F 0.002200 0.074200 0.003600 +C -0.051600 1.395400 0.011400 +N -1.196900 1.957500 0.029900 +C -1.226500 3.354700 0.037400 +N -2.254100 4.101900 0.055700 +N 0.039000 3.980500 0.023400 +N 1.215100 3.382000 0.004300 +C 1.210800 2.081000 -0.002400 +H -3.092900 3.525400 0.064300 +H 0.051400 4.990800 0.028600 +H 2.157900 1.559800 -0.018100 +" +CN=CN1C=CCC1=O,"17 +gdb_97298 +C -0.057300 1.468900 0.054100 +N 0.052300 0.025000 0.087000 +C -1.015600 -0.643000 -0.035300 +N -1.013500 -2.030500 -0.016800 +C 0.120600 -2.857400 0.134200 +C -0.217400 -4.147800 0.110200 +C -1.706900 -4.267300 -0.071300 +C -2.174900 -2.810400 -0.148400 +O -3.291600 -2.375900 -0.291100 +H 0.557800 1.864800 -0.762300 +H -1.087900 1.834000 -0.077100 +H 0.343600 1.888000 0.984200 +H -2.023800 -0.236300 -0.164500 +H 1.079500 -2.376400 0.245300 +H 0.464500 -4.978800 0.205500 +H -2.002400 -4.794700 -0.985900 +H -2.217500 -4.771900 0.757300 +" +c1c(ocn1)CCCO,"18 +gdb_33215 +O -0.084900 1.464900 0.277500 +C 0.005900 0.059700 0.111400 +C -1.411600 -0.495100 0.063400 +C -1.436200 -2.024100 -0.112600 +C -2.808800 -2.594600 -0.144700 +C -3.558400 -3.250600 -1.064800 +N -4.808500 -3.547400 -0.530300 +C -4.755700 -3.064300 0.665400 +O -3.581900 -2.469600 0.984900 +H 0.806500 1.824100 0.307500 +H 0.539200 -0.200100 -0.818500 +H 0.559500 -0.407400 0.943100 +H -1.930500 -0.219700 0.987400 +H -1.953200 -0.016700 -0.759800 +H -0.938500 -2.304800 -1.047500 +H -0.864100 -2.493700 0.699300 +H -3.290900 -3.532400 -2.071000 +H -5.516700 -3.075300 1.429700 +" +C(C#N)N=C(C#N)N,"12 +gdb_9018 +N 0.259800 1.055100 0.866200 +C 0.166800 -0.125800 0.175400 +N 1.073800 -0.811800 -0.411800 +C 2.451400 -0.345100 -0.413400 +C 3.006200 -0.004500 0.908100 +N 3.381500 0.290700 1.961500 +C -1.193500 -0.627900 0.061000 +N -2.285900 -0.997200 -0.007200 +H 1.150300 1.307400 1.271000 +H -0.546900 1.346000 1.396200 +H 3.076200 -1.128700 -0.852000 +H 2.558500 0.542500 -1.055700 +" +CC1C2C(CO)CN12,"19 +gdb_18157 +C -0.088600 1.538400 0.334500 +C 0.055000 0.072800 -0.008300 +C 0.546800 -0.552700 -1.258000 +C 1.736000 0.134400 -1.927600 +C 1.549200 1.399300 -2.746500 +O 2.831200 1.769900 -3.237000 +C 2.408800 0.137700 -0.531600 +N 1.306900 -0.705800 0.067300 +H -1.045400 1.916400 -0.041700 +H -0.085800 1.667500 1.422600 +H 0.700500 2.168600 -0.075200 +H -0.740200 -0.520500 0.444000 +H 0.019900 -1.395400 -1.698500 +H 2.254100 -0.585100 -2.568800 +H 0.847000 1.192200 -3.571700 +H 1.109900 2.206700 -2.144300 +H 2.740000 2.586800 -3.735900 +H 3.350800 -0.410400 -0.491700 +H 2.552400 1.108400 -0.048300 +" +OCC1N=COC2CC12,"18 +gdb_112093 +O 0.117300 1.338100 -0.157500 +C 0.054800 -0.069200 -0.125800 +C -1.385600 -0.551000 0.111300 +N -2.222500 0.073000 -0.914000 +C -3.135600 -0.585700 -1.486600 +O -3.488100 -1.883400 -1.337500 +C -2.660900 -2.692200 -0.525400 +C -1.254600 -2.957800 -0.978700 +C -1.523800 -2.059300 0.216300 +H -0.631600 1.609400 -0.706300 +H 0.432800 -0.515300 -1.060700 +H 0.701500 -0.411700 0.689900 +H -1.707100 -0.117600 1.068500 +H -3.781600 -0.116600 -2.226000 +H -3.252700 -3.480400 -0.077100 +H -0.834300 -3.941700 -0.804500 +H -0.944400 -2.491700 -1.907800 +H -1.293100 -2.471600 1.193800 +" +CC1=C(O)OC(=N)C=N1,"15 +gdb_26884 +C 0.015000 1.545900 0.047000 +C -0.020400 0.045800 0.032200 +C -1.159400 -0.673400 -0.101500 +O -2.385000 -0.149500 -0.236000 +O -1.177200 -2.024500 -0.113300 +C 0.012600 -2.755400 0.015600 +N 0.013600 -4.021600 0.006600 +C 1.198000 -1.920700 0.155400 +N 1.178500 -0.633100 0.162500 +H 0.642900 1.925100 -0.766500 +H -0.985800 1.967400 -0.062700 +H 0.445500 1.912400 0.985200 +H -3.017200 -0.875900 -0.312500 +H -0.927200 -4.399800 -0.102300 +H 2.142000 -2.447300 0.258000 +" +COC(C)CCCC#N,"22 +gdb_101878 +C -0.002200 1.417100 -0.131700 +O -0.034700 0.029600 0.110900 +C -1.322300 -0.569000 0.019100 +C -1.767100 -0.748100 -1.435000 +C -1.230500 -1.907500 0.755700 +C -0.887400 -1.752300 2.239400 +C -0.728600 -3.116300 2.938700 +C -0.416600 -2.988200 4.360800 +N -0.173000 -2.869400 5.484200 +H 1.018100 1.753500 0.068800 +H -0.252900 1.673000 -1.171200 +H -0.691100 1.961700 0.532800 +H -2.052800 0.075000 0.540600 +H -2.757600 -1.211800 -1.479200 +H -1.828300 0.210300 -1.957900 +H -1.059300 -1.386900 -1.972700 +H -2.184200 -2.437900 0.641800 +H -0.465300 -2.518000 0.258500 +H 0.038800 -1.180500 2.337500 +H -1.670900 -1.179200 2.748500 +H -1.646900 -3.707800 2.839800 +H 0.069200 -3.696700 2.460200 +" +CC(C#N)C1(O)CC1,"17 +gdb_9785 +C 0.078300 1.453200 -0.197600 +C 0.020600 -0.067300 0.046100 +C 0.709800 -0.416600 1.296000 +N 1.265300 -0.686500 2.272500 +C -1.415900 -0.598400 0.057100 +O -2.236900 -0.027100 1.034700 +C -2.045000 -1.011300 -1.250800 +C -1.668500 -2.047600 -0.207200 +H 1.111800 1.803700 -0.240800 +H -0.411800 1.694600 -1.145300 +H -0.423700 1.991900 0.610400 +H 0.556200 -0.573800 -0.765800 +H -2.494200 0.853700 0.741200 +H -3.099800 -0.792800 -1.373700 +H -1.458300 -0.952800 -2.161800 +H -0.821200 -2.696100 -0.399900 +H -2.473600 -2.501800 0.357400 +" +Cc1nc(co1)O,"12 +gdb_848 +C -0.008700 1.439000 -0.007300 +C -0.052300 -0.046000 -0.003800 +N -1.063000 -0.820500 -0.274000 +C -0.567000 -2.100200 -0.123900 +O -1.341900 -3.181900 -0.326000 +C 0.738600 -2.051100 0.235400 +O 1.073800 -0.713600 0.313500 +H 0.733600 1.808900 -0.722000 +H -0.990800 1.823500 -0.282900 +H 0.263100 1.826700 0.979900 +H -2.215300 -2.852000 -0.569200 +H 1.505900 -2.769400 0.454000 +" +C1C2COC3COC12C3,"19 +gdb_39267 +C -0.047900 1.452200 0.043600 +C 1.335900 0.804100 -0.058000 +C 2.194100 0.441100 -1.243700 +O 2.303900 -1.001500 -1.311600 +C 1.093100 -1.724900 -1.040200 +C 0.875400 -2.013600 0.483500 +O -0.015200 -0.979200 0.981400 +C 0.069700 -0.021700 -0.043900 +C -0.118800 -0.829000 -1.316000 +H -0.259800 1.913200 1.001700 +H -0.488500 1.965400 -0.806300 +H 1.856000 0.789700 0.895300 +H 1.795900 0.841300 -2.185700 +H 3.223500 0.794500 -1.148200 +H 1.159200 -2.647300 -1.621200 +H 0.395600 -2.982100 0.654400 +H 1.834400 -1.979900 1.012100 +H -0.050600 -0.277900 -2.253500 +H -1.063600 -1.378100 -1.285800 +" +CC(C)(CC1CC1)C#C,"23 +gdb_57623 +C -0.091400 1.555800 -0.084000 +C 0.014100 0.018000 0.028700 +C 0.887800 -0.354200 1.249500 +C -1.395900 -0.642800 0.131800 +C -2.237300 -0.242400 1.326700 +C -3.186600 0.930900 1.262000 +C -3.728300 -0.475400 1.290500 +C 0.656800 -0.503700 -1.183700 +C 1.174700 -0.936600 -2.179200 +H 0.901800 1.999800 -0.196300 +H -0.689300 1.844200 -0.953400 +H -0.559600 1.973200 0.811000 +H 0.468900 0.066100 2.168600 +H 0.958500 -1.439900 1.364400 +H 1.900700 0.039300 1.128700 +H -1.948300 -0.413500 -0.787600 +H -1.252300 -1.730500 0.137600 +H -1.772700 -0.414900 2.293200 +H -3.308300 1.549400 2.144700 +H -3.252400 1.477900 0.327400 +H -4.154200 -0.872200 0.374500 +H -4.218400 -0.822800 2.193300 +H 1.635300 -1.316400 -3.057200 +" +CCCOC1CC1CC,"25 +gdb_122524 +C 0.378600 1.236200 0.486700 +C -0.197600 0.056100 -0.299500 +C 0.838200 -0.605500 -1.197500 +O 0.219400 -1.678100 -1.887800 +C 1.081400 -2.360300 -2.741600 +C 1.989300 -3.430300 -2.178900 +C 0.777400 -3.801400 -3.017700 +C -0.411900 -4.490200 -2.379500 +C -0.210700 -6.002100 -2.230400 +H 1.207900 0.921200 1.129800 +H 0.757800 2.016000 -0.183100 +H -0.382500 1.691400 1.126900 +H -0.595800 -0.699900 0.387200 +H -1.038000 0.385900 -0.920800 +H 1.243600 0.124600 -1.918800 +H 1.688700 -0.976200 -0.602800 +H 1.466500 -1.750200 -3.561000 +H 2.985600 -3.553200 -2.589900 +H 1.912400 -3.612500 -1.111500 +H 0.992400 -4.129200 -4.032600 +H -0.605200 -4.035600 -1.403100 +H -1.306900 -4.296300 -2.984700 +H 0.660500 -6.223200 -1.604800 +H -1.084700 -6.474700 -1.771200 +H -0.047200 -6.479600 -3.203200 +" diff --git a/tests/fixtures/qm9_sdf_mini.csv b/tests/fixtures/qm9_sdf_mini.csv new file mode 100644 index 0000000..dfea4c6 --- /dev/null +++ b/tests/fixtures/qm9_sdf_mini.csv @@ -0,0 +1,107 @@ +mol_id,split,sdf +gdb_1,train,"gdb_1 + -OEChem-03231823243D + + 5 4 0 0 0 0 0 0 0999 V2000 + -0.0127 1.0858 0.0080 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.0022 -0.0060 0.0020 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1.0117 1.4638 0.0003 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.5408 1.4475 -0.8766 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.5238 1.4379 0.9064 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 0 0 0 + 1 3 1 0 0 0 0 + 1 4 1 0 0 0 0 + 1 5 1 0 0 0 0 +M END +" +gdb_2,train,"gdb_2 + -OEChem-03231823233D + + 4 3 0 0 0 0 0 0 0999 V2000 + -0.0404 1.0241 0.0626 N 0 0 0 0 0 0 0 0 0 0 0 0 + 0.0173 0.0125 -0.0274 H 0 0 0 0 0 0 0 0 0 0 0 0 + 0.9158 1.3587 -0.0288 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.5203 1.3435 -0.7755 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 0 0 0 + 1 3 1 0 0 0 0 + 1 4 1 0 0 0 0 +M END +" +gdb_11,test,"gdb_11 + -OEChem-03231823243D + + 7 6 0 0 0 0 0 0 0999 V2000 + -0.0029 1.5099 0.0087 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.0261 0.0033 -0.0375 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.9423 -0.6551 -0.4568 O 0 0 0 0 0 0 0 0 0 0 0 0 + 0.9228 1.9263 -0.3915 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.8620 1.8785 -0.5648 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.1505 1.8439 1.0429 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.8944 -0.4864 0.3577 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 0 0 0 + 1 4 1 0 0 0 0 + 1 5 1 0 0 0 0 + 1 6 1 0 0 0 0 + 2 3 2 0 0 0 0 + 2 7 1 0 0 0 0 +M END +" +gdb_22,val,"gdb_22 + -OEChem-03231823253D + + 12 11 0 0 0 0 0 0 0999 V2000 + -0.0332 1.5478 -0.0044 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.0111 0.0186 0.0168 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.7094 -0.5360 1.2398 C 0 0 0 0 0 0 0 0 0 0 0 0 + -1.3322 -0.5171 0.0524 O 0 0 0 0 0 0 0 0 0 0 0 0 + 0.9822 1.9572 -0.0318 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.5639 1.9209 -0.8885 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.5408 1.9334 0.8858 H 0 0 0 0 0 0 0 0 0 0 0 0 + 0.5104 -0.3377 -0.8882 H 0 0 0 0 0 0 0 0 0 0 0 0 + 0.2224 -0.1879 2.1565 H 0 0 0 0 0 0 0 0 0 0 0 0 + 0.6824 -1.6289 1.2331 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1.7543 -0.2125 1.2552 H 0 0 0 0 0 0 0 0 0 0 0 0 + -1.8182 -0.1558 -0.6955 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 0 0 0 + 1 5 1 0 0 0 0 + 1 6 1 0 0 0 0 + 1 7 1 0 0 0 0 + 2 3 1 0 0 0 0 + 2 4 1 0 0 0 0 + 2 8 1 0 0 0 0 + 3 9 1 0 0 0 0 + 3 10 1 0 0 0 0 + 3 11 1 0 0 0 0 + 4 12 1 0 0 0 0 +M END +" +gdb_47,val,"gdb_47 + -OEChem-03231823243D + + 12 12 0 0 0 0 0 0 0999 V2000 + -0.0254 1.5403 -0.0422 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.5256 1.5042 0.0412 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.4215 0.0615 0.6084 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.0264 -0.0107 0.0494 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.4659 1.9964 0.8496 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.4790 2.0003 -0.9237 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1.9825 1.5138 -0.9530 H 0 0 0 0 0 0 0 0 0 0 0 0 + 2.0219 2.2599 0.6552 H 0 0 0 0 0 0 0 0 0 0 0 0 + 2.1429 -0.6796 0.2553 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1.4311 0.0575 1.7026 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.7905 -0.4853 0.6700 H 0 0 0 0 0 0 0 0 0 0 0 0 + -0.0523 -0.4724 -0.9424 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 0 0 0 + 1 4 1 0 0 0 0 + 1 5 1 0 0 0 0 + 1 6 1 0 0 0 0 + 2 3 1 0 0 0 0 + 2 7 1 0 0 0 0 + 2 8 1 0 0 0 0 + 3 4 1 0 0 0 0 + 3 9 1 0 0 0 0 + 3 10 1 0 0 0 0 + 4 11 1 0 0 0 0 + 4 12 1 0 0 0 0 +M END +" diff --git a/tests/fixtures/qm9_sdf_mini_molebert_tokens.csv b/tests/fixtures/qm9_sdf_mini_molebert_tokens.csv new file mode 100644 index 0000000..3234466 --- /dev/null +++ b/tests/fixtures/qm9_sdf_mini_molebert_tokens.csv @@ -0,0 +1,6 @@ +id,input_ids +gdb_1,50 468 468 468 468 +gdb_2,163 427 427 427 +gdb_11,50 41 334 467 467 467 504 +gdb_22,50 38 50 366 422 422 422 424 422 422 422 503 +gdb_47,50 50 50 50 422 422 422 422 422 422 422 422 diff --git a/tests/fixtures/tmqmg_mini.csv b/tests/fixtures/tmqmg_mini.csv new file mode 100644 index 0000000..145fee3 --- /dev/null +++ b/tests/fixtures/tmqmg_mini.csv @@ -0,0 +1,925 @@ +xyz,mol2 +"54 +10/06/2025 18:27, XYZ structure generated by mol3D Class, molSimplify +C -1.090100 2.615300 -0.569700 +C -2.208300 3.468000 -0.460700 +H -2.101700 4.537300 -0.696800 +C -3.432400 2.935300 -0.055700 +H -4.312700 3.585400 0.049000 +C -3.531800 1.557800 0.177900 +H -4.496900 1.104000 0.433800 +C -2.387000 0.761200 0.035100 +C -2.409200 -0.706900 0.028900 +C -3.577900 -1.468100 0.170100 +H -4.527400 -0.986300 0.432700 +C -3.522300 -2.846700 -0.072300 +H -4.422700 -3.469300 0.029200 +C -2.315200 -3.415500 -0.478500 +H -2.241600 -4.486900 -0.717900 +C -1.171100 -2.597200 -0.582200 +C 0.902400 0.067900 2.039200 +C 1.267700 -1.219100 1.502200 +C 2.222700 -1.325500 0.413300 +C 2.704600 -0.124000 -0.206600 +C 2.315800 1.177900 0.280600 +C 1.408100 1.260700 1.399900 +C -0.010100 0.203900 3.235600 +H -0.976300 0.681000 2.973300 +H 0.462400 0.839500 4.011000 +H -0.235900 -0.767300 3.705600 +C 0.669300 -2.459800 2.117900 +H 1.215200 -2.724900 3.047500 +H 0.721900 -3.324600 1.438400 +H -0.392500 -2.311200 2.384900 +C 2.770200 -2.652200 -0.052100 +H 2.566800 -2.828300 -1.123800 +H 2.362100 -3.502600 0.517100 +H 3.870300 -2.663500 0.089500 +C 3.617700 -0.186400 -1.399400 +H 3.148900 0.332900 -2.258500 +H 3.837700 -1.217000 -1.716900 +H 4.577700 0.319100 -1.171400 +C 2.925300 2.385200 -0.381900 +H 4.029300 2.350400 -0.279500 +H 2.584900 3.335300 0.058700 +H 2.692800 2.403300 -1.463100 +C 0.975800 2.587500 1.969200 +H 1.515600 2.768400 2.922500 +H -0.104300 2.601400 2.204600 +H 1.186900 3.430300 1.294300 +Cl 0.438500 0.028000 -2.519800 +N -1.162100 1.301800 -0.255300 +N -1.202400 -1.282500 -0.270000 +O 0.110500 3.048000 -0.971600 +H 0.041900 3.979600 -1.257000 +O 0.019400 -3.068900 -0.973100 +H -0.076000 -3.999300 -1.254900 +Ru 0.463300 -0.012400 -0.141900 +","@MOLECULE + +54 59 1 +SMALL +PartialCharges +**** +Generated from molSimplify + +@ATOM +1 C1 -1.0901 2.6153 -0.5697 C.3 1 RES1 0.0 +2 C2 -2.2083 3.468 -0.4607 C.2 1 RES1 0.0 +3 H1 -2.1017 4.5373 -0.6968 H 1 RES1 0.0 +4 C3 -3.4324 2.9353 -0.0557 C.2 1 RES1 0.0 +5 H2 -4.3127 3.5854 0.049 H 1 RES1 0.0 +6 C4 -3.5318 1.5578 0.1779 C.2 1 RES1 0.0 +7 H3 -4.4969 1.104 0.4338 H 1 RES1 0.0 +8 C5 -2.387 0.7612 0.0351 C.2 1 RES1 0.0 +9 C6 -2.4092 -0.7069 0.0289 C.3 1 RES1 0.0 +10 C7 -3.5779 -1.4681 0.1701 C.2 1 RES1 0.0 +11 H4 -4.5274 -0.9863 0.4327 H 1 RES1 0.0 +12 C8 -3.5223 -2.8467 -0.0723 C.2 1 RES1 0.0 +13 H5 -4.4227 -3.4693 0.0292 H 1 RES1 0.0 +14 C9 -2.3152 -3.4155 -0.4785 C.2 1 RES1 0.0 +15 H6 -2.2416 -4.4869 -0.7179 H 1 RES1 0.0 +16 C10 -1.1711 -2.5972 -0.5822 C.2 1 RES1 0.0 +17 C11 0.9024 0.0679 2.0392 C.3 1 RES1 0.0 +18 C12 1.2677 -1.2191 1.5022 C.3 1 RES1 0.0 +19 C13 2.2227 -1.3255 0.4133 C.2 1 RES1 0.0 +20 C14 2.7046 -0.124 -0.2066 C.2 1 RES1 0.0 +21 C15 2.3158 1.1779 0.2806 C.3 1 RES1 0.0 +22 C16 1.4081 1.2607 1.3999 C.3 1 RES1 0.0 +23 C17 -0.0101 0.2039 3.2356 C.3 1 RES1 0.0 +24 H7 -0.9763 0.681 2.9733 H 1 RES1 0.0 +25 H8 0.4624 0.8395 4.011 H 1 RES1 0.0 +26 H9 -0.2359 -0.7673 3.7056 H 1 RES1 0.0 +27 C18 0.6693 -2.4598 2.1179 C.3 1 RES1 0.0 +28 H10 1.2152 -2.7249 3.0475 H 1 RES1 0.0 +29 H11 0.7219 -3.3246 1.4384 H 1 RES1 0.0 +30 H12 -0.3925 -2.3112 2.3849 H 1 RES1 0.0 +31 C19 2.7702 -2.6522 -0.0521 C.3 1 RES1 0.0 +32 H13 2.5668 -2.8283 -1.1238 H 1 RES1 0.0 +33 H14 2.3621 -3.5026 0.5171 H 1 RES1 0.0 +34 H15 3.8703 -2.6635 0.0895 H 1 RES1 0.0 +35 C20 3.6177 -0.1864 -1.3994 C.3 1 RES1 0.0 +36 H16 3.1489 0.3329 -2.2585 H 1 RES1 0.0 +37 H17 3.8377 -1.217 -1.7169 H 1 RES1 0.0 +38 H18 4.5777 0.3191 -1.1714 H 1 RES1 0.0 +39 C21 2.9253 2.3852 -0.3819 C.3 1 RES1 0.0 +40 H19 4.0293 2.3504 -0.2795 H 1 RES1 0.0 +41 H20 2.5849 3.3353 0.0587 H 1 RES1 0.0 +42 H21 2.6928 2.4033 -1.4631 H 1 RES1 0.0 +43 C22 0.9758 2.5875 1.9692 C.3 1 RES1 0.0 +44 H22 1.5156 2.7684 2.9225 H 1 RES1 0.0 +45 H23 -0.1043 2.6014 2.2046 H 1 RES1 0.0 +46 H24 1.1869 3.4303 1.2943 H 1 RES1 0.0 +47 Cl1 0.4385 0.028 -2.5198 Cl 1 RES1 0.0 +48 N1 -1.1621 1.3018 -0.2553 N.pl3 1 RES1 0.0 +49 N2 -1.2024 -1.2825 -0.27 N.pl3 1 RES1 0.0 +50 O1 0.1105 3.048 -0.9716 O.3 1 RES1 0.0 +51 H25 0.0419 3.9796 -1.257 H 1 RES1 0.0 +52 O2 0.0194 -3.0689 -0.9731 O.2 1 RES1 0.0 +53 H26 -0.076 -3.9993 -1.2549 H 1 RES1 0.0 +54 Ru1 0.4633 -0.0124 -0.1419 Ru 1 RES1 0.0 +@BOND +1 1 2 1 +1 1 48 1 +1 1 50 1 +1 2 3 1 +1 2 4 2 +1 4 5 1 +1 4 6 1 +1 6 7 1 +1 6 8 2 +1 8 9 1 +1 8 48 1 +1 9 10 1 +1 9 49 1 +1 10 11 1 +1 10 12 2 +1 12 13 1 +1 12 14 1 +1 14 15 1 +1 14 16 2 +1 16 49 1 +1 16 52 1 +1 17 18 1 +1 17 22 1 +1 17 23 1 +1 17 54 1 +1 18 19 1 +1 18 27 1 +1 18 54 1 +1 19 20 2 +1 19 31 1 +1 20 21 1 +1 20 35 1 +1 21 22 1 +1 21 39 1 +1 21 54 1 +1 22 43 1 +1 23 24 1 +1 23 25 1 +1 23 26 1 +1 27 28 1 +1 27 29 1 +1 27 30 1 +1 31 32 1 +1 31 33 1 +1 31 34 1 +1 35 36 1 +1 35 37 1 +1 35 38 1 +1 39 40 1 +1 39 41 1 +1 39 42 1 +1 43 44 1 +1 43 45 1 +1 43 46 1 +1 47 54 1 +1 48 54 1 +1 49 54 1 +1 50 51 1 +1 52 53 1 +@SUBSTRUCTURE +1 RES1 54 + +" +"61 +10/06/2025 18:27, XYZ structure generated by mol3D Class, molSimplify +Re 0.045000 -1.706400 -1.296200 +O 0.147600 -2.802800 -2.581900 +O -1.453000 -2.470800 -0.174100 +O -1.494100 -0.522400 -1.910000 +O 1.520400 -0.376200 -1.776100 +O 1.522600 -2.326100 -0.051600 +O -0.156500 -0.173600 0.427500 +C -2.589800 -1.773000 -0.254600 +C -2.611100 -0.704900 -1.202400 +C -3.739800 0.131500 -1.291200 +Cl -3.726000 1.431600 -2.448000 +C -4.850000 -0.075500 -0.433600 +Cl -6.216600 1.000700 -0.511700 +C -4.826800 -1.127900 0.504400 +Cl -6.149700 -1.334300 1.622300 +C -3.700600 -1.986800 0.583600 +Cl -3.625600 -3.263000 1.763300 +C 2.649900 -1.615100 -0.139700 +C 3.781600 -1.824000 0.671000 +Cl 3.717400 -3.068600 1.886000 +C 4.917500 -0.987800 0.541800 +Cl 6.289100 -1.221800 1.591800 +C 4.911200 0.075800 -0.383100 +Cl 6.279700 1.145500 -0.503600 +C 3.767000 0.303700 -1.184300 +Cl 3.691400 1.657500 -2.280600 +C 2.650700 -0.544000 -1.080500 +P -0.098900 1.323400 0.775400 +C -1.213600 1.640900 2.183800 +C -0.942800 2.614300 3.164600 +C -1.870800 2.837000 4.194100 +C -3.062700 2.093200 4.243800 +C -3.327900 1.116600 3.269400 +C -2.402100 0.887300 2.242100 +C 1.581700 1.788300 1.291200 +C 2.282900 0.849900 2.078700 +C 3.597700 1.122700 2.476100 +C 4.218000 2.319700 2.080400 +C 3.524100 3.247600 1.289400 +C 2.202800 2.987000 0.893600 +C -0.612400 2.474600 -0.542900 +C -0.047600 2.333100 -1.828700 +C -0.424800 3.220600 -2.845200 +C -1.361100 4.236300 -2.594400 +C -1.931500 4.366800 -1.319200 +C -1.560200 3.487500 -0.291700 +H -0.005000 3.190200 3.123500 +H -1.660300 3.594000 4.965500 +H -3.787900 2.274200 5.052500 +H -4.255300 0.524000 3.295100 +H -2.593900 0.117400 1.482400 +H 1.807200 -0.107300 2.341600 +H 4.157700 0.376200 3.058000 +H 5.265800 2.507500 2.358100 +H 4.023500 4.168700 0.953000 +H 1.662500 3.697500 0.250200 +H 0.674500 1.525300 -2.029700 +H 0.012700 3.104100 -3.848200 +H -1.659300 4.922800 -3.402300 +H -2.681200 5.148500 -1.123200 +H -2.019200 3.580500 0.703500 +","@MOLECULE + +61 67 1 +SMALL +PartialCharges +**** +Generated from molSimplify + +@ATOM +1 Re1 0.045 -1.7064 -1.2962 Re 1 RES1 0.0 +2 O1 0.1476 -2.8028 -2.5819 O.3 1 RES1 0.0 +3 O2 -1.453 -2.4708 -0.1741 O.3 1 RES1 0.0 +4 O3 -1.4941 -0.5224 -1.91 O.3 1 RES1 0.0 +5 O4 1.5204 -0.3762 -1.7761 O.3 1 RES1 0.0 +6 O5 1.5226 -2.3261 -0.0516 O.3 1 RES1 0.0 +7 O6 -0.1565 -0.1736 0.4275 O.3 1 RES1 0.0 +8 C1 -2.5898 -1.773 -0.2546 C.ar 1 RES1 0.0 +9 C2 -2.6111 -0.7049 -1.2024 C.ar 1 RES1 0.0 +10 C3 -3.7398 0.1315 -1.2912 C.ar 1 RES1 0.0 +11 Cl1 -3.726 1.4316 -2.448 Cl 1 RES1 0.0 +12 C4 -4.85 -0.0755 -0.4336 C.ar 1 RES1 0.0 +13 Cl2 -6.2166 1.0007 -0.5117 Cl 1 RES1 0.0 +14 C5 -4.8268 -1.1279 0.5044 C.ar 1 RES1 0.0 +15 Cl3 -6.1497 -1.3343 1.6223 Cl 1 RES1 0.0 +16 C6 -3.7006 -1.9868 0.5836 C.ar 1 RES1 0.0 +17 Cl4 -3.6256 -3.263 1.7633 Cl 1 RES1 0.0 +18 C7 2.6499 -1.6151 -0.1397 C.ar 1 RES1 0.0 +19 C8 3.7816 -1.824 0.671 C.ar 1 RES1 0.0 +20 Cl5 3.7174 -3.0686 1.886 Cl 1 RES1 0.0 +21 C9 4.9175 -0.9878 0.5418 C.ar 1 RES1 0.0 +22 Cl6 6.2891 -1.2218 1.5918 Cl 1 RES1 0.0 +23 C10 4.9112 0.0758 -0.3831 C.ar 1 RES1 0.0 +24 Cl7 6.2797 1.1455 -0.5036 Cl 1 RES1 0.0 +25 C11 3.767 0.3037 -1.1843 C.ar 1 RES1 0.0 +26 Cl8 3.6914 1.6575 -2.2806 Cl 1 RES1 0.0 +27 C12 2.6507 -0.544 -1.0805 C.ar 1 RES1 0.0 +28 P1 -0.0989 1.3234 0.7754 P.3 1 RES1 0.0 +29 C13 -1.2136 1.6409 2.1838 C.ar 1 RES1 0.0 +30 C14 -0.9428 2.6143 3.1646 C.ar 1 RES1 0.0 +31 C15 -1.8708 2.837 4.1941 C.ar 1 RES1 0.0 +32 C16 -3.0627 2.0932 4.2438 C.ar 1 RES1 0.0 +33 C17 -3.3279 1.1166 3.2694 C.ar 1 RES1 0.0 +34 C18 -2.4021 0.8873 2.2421 C.ar 1 RES1 0.0 +35 C19 1.5817 1.7883 1.2912 C.ar 1 RES1 0.0 +36 C20 2.2829 0.8499 2.0787 C.ar 1 RES1 0.0 +37 C21 3.5977 1.1227 2.4761 C.ar 1 RES1 0.0 +38 C22 4.218 2.3197 2.0804 C.ar 1 RES1 0.0 +39 C23 3.5241 3.2476 1.2894 C.ar 1 RES1 0.0 +40 C24 2.2028 2.987 0.8936 C.ar 1 RES1 0.0 +41 C25 -0.6124 2.4746 -0.5429 C.ar 1 RES1 0.0 +42 C26 -0.0476 2.3331 -1.8287 C.ar 1 RES1 0.0 +43 C27 -0.4248 3.2206 -2.8452 C.ar 1 RES1 0.0 +44 C28 -1.3611 4.2363 -2.5944 C.ar 1 RES1 0.0 +45 C29 -1.9315 4.3668 -1.3192 C.ar 1 RES1 0.0 +46 C30 -1.5602 3.4875 -0.2917 C.ar 1 RES1 0.0 +47 H1 -0.005 3.1902 3.1235 H 1 RES1 0.0 +48 H2 -1.6603 3.594 4.9655 H 1 RES1 0.0 +49 H3 -3.7879 2.2742 5.0525 H 1 RES1 0.0 +50 H4 -4.2553 0.524 3.2951 H 1 RES1 0.0 +51 H5 -2.5939 0.1174 1.4824 H 1 RES1 0.0 +52 H6 1.8072 -0.1073 2.3416 H 1 RES1 0.0 +53 H7 4.1577 0.3762 3.058 H 1 RES1 0.0 +54 H8 5.2658 2.5075 2.3581 H 1 RES1 0.0 +55 H9 4.0235 4.1687 0.953 H 1 RES1 0.0 +56 H10 1.6625 3.6975 0.2502 H 1 RES1 0.0 +57 H11 0.6745 1.5253 -2.0297 H 1 RES1 0.0 +58 H12 0.0127 3.1041 -3.8482 H 1 RES1 0.0 +59 H13 -1.6593 4.9228 -3.4023 H 1 RES1 0.0 +60 H14 -2.6812 5.1485 -1.1232 H 1 RES1 0.0 +61 H15 -2.0192 3.5805 0.7035 H 1 RES1 0.0 +@BOND +1 1 2 1 +1 1 3 1 +1 1 4 1 +1 1 5 1 +1 1 6 1 +1 1 7 1 +1 3 8 1 +1 4 9 1 +1 5 27 1 +1 6 18 1 +1 7 28 1 +1 8 9 2 +1 8 16 1 +1 9 10 1 +1 10 11 1 +1 10 12 2 +1 12 13 1 +1 12 14 1 +1 14 15 1 +1 14 16 2 +1 16 17 1 +1 18 19 2 +1 18 27 1 +1 19 20 1 +1 19 21 1 +1 21 22 1 +1 21 23 2 +1 23 24 1 +1 23 25 1 +1 25 26 1 +1 25 27 2 +1 28 29 1 +1 28 35 1 +1 28 41 1 +1 29 30 2 +1 29 34 1 +1 30 31 1 +1 30 47 1 +1 31 32 2 +1 31 48 1 +1 32 33 1 +1 32 49 1 +1 33 34 2 +1 33 50 1 +1 34 51 1 +1 35 36 2 +1 35 40 1 +1 36 37 1 +1 36 52 1 +1 37 38 2 +1 37 53 1 +1 38 39 1 +1 38 54 1 +1 39 40 2 +1 39 55 1 +1 40 56 1 +1 41 42 2 +1 41 46 1 +1 42 43 1 +1 42 57 1 +1 43 44 2 +1 43 58 1 +1 44 45 1 +1 44 59 1 +1 45 46 2 +1 45 60 1 +1 46 61 1 +@SUBSTRUCTURE +1 RES1 61 + +" +"37 +10/06/2025 18:27, XYZ structure generated by mol3D Class, molSimplify +Zn -2.075600 -0.249800 -1.186800 +Cl -0.346900 -0.981000 -2.329000 +Cl -4.255500 -0.075600 -1.460400 +O 6.266400 1.047200 -0.566500 +O 6.085000 -1.109400 -0.885800 +N -1.597600 1.506300 -0.079000 +N -1.737500 -1.217200 0.693600 +N 0.133900 0.210300 0.887500 +N 5.625400 -0.005700 -0.587900 +C -2.404500 2.582600 -0.115900 +H -3.220400 2.530700 -0.856700 +C -2.232500 3.663600 0.756400 +H -2.894900 4.538000 0.687600 +C -1.222800 3.586800 1.727400 +H -1.076600 4.403700 2.450000 +C -0.421200 2.438700 1.794900 +H 0.342300 2.315500 2.575000 +C -0.628700 1.418400 0.850800 +C -0.549700 -0.972900 1.275400 +C -0.035000 -1.818600 2.278400 +H 0.934300 -1.592700 2.743600 +C -0.794100 -2.927600 2.665000 +H -0.415900 -3.603600 3.446600 +C -2.050700 -3.152800 2.074200 +H -2.677700 -4.005800 2.368800 +C -2.488600 -2.261000 1.091700 +H -3.459200 -2.361700 0.577100 +C 1.496700 0.180700 0.512000 +C 2.292000 1.350800 0.508000 +H 1.862100 2.322400 0.781100 +C 3.638400 1.290200 0.141100 +H 4.274600 2.184900 0.135700 +C 4.197800 0.059700 -0.222400 +C 3.419700 -1.106200 -0.263700 +H 3.881500 -2.042700 -0.602800 +C 2.072700 -1.042300 0.087100 +H 1.439400 -1.931400 -0.024300 +","@MOLECULE + +37 40 1 +SMALL +PartialCharges +**** +Generated from molSimplify + +@ATOM +1 Zn1 -2.0756 -0.2498 -1.1868 Zn 1 RES1 0.0 +2 Cl1 -0.3469 -0.981 -2.329 Cl 1 RES1 0.0 +3 Cl2 -4.2555 -0.0756 -1.4604 Cl 1 RES1 0.0 +4 O1 6.2664 1.0472 -0.5665 O.2 1 RES1 0.0 +5 O2 6.085 -1.1094 -0.8858 O.2 1 RES1 0.0 +6 N1 -1.5976 1.5063 -0.079 N.pl3 1 RES1 0.0 +7 N2 -1.7375 -1.2172 0.6936 N.pl3 1 RES1 0.0 +8 N3 0.1339 0.2103 0.8875 N.pl3 1 RES1 0.0 +9 N4 5.6254 -0.0057 -0.5879 N.pl3 1 RES1 0.0 +10 C1 -2.4045 2.5826 -0.1159 C.3 1 RES1 0.0 +11 H1 -3.2204 2.5307 -0.8567 H 1 RES1 0.0 +12 C2 -2.2325 3.6636 0.7564 C.2 1 RES1 0.0 +13 H2 -2.8949 4.538 0.6876 H 1 RES1 0.0 +14 C3 -1.2228 3.5868 1.7274 C.2 1 RES1 0.0 +15 H3 -1.0766 4.4037 2.45 H 1 RES1 0.0 +16 C4 -0.4212 2.4387 1.7949 C.2 1 RES1 0.0 +17 H4 0.3423 2.3155 2.575 H 1 RES1 0.0 +18 C5 -0.6287 1.4184 0.8508 C.2 1 RES1 0.0 +19 C6 -0.5497 -0.9729 1.2754 C.3 1 RES1 0.0 +20 C7 -0.035 -1.8186 2.2784 C.2 1 RES1 0.0 +21 H5 0.9343 -1.5927 2.7436 H 1 RES1 0.0 +22 C8 -0.7941 -2.9276 2.665 C.2 1 RES1 0.0 +23 H6 -0.4159 -3.6036 3.4466 H 1 RES1 0.0 +24 C9 -2.0507 -3.1528 2.0742 C.2 1 RES1 0.0 +25 H7 -2.6777 -4.0058 2.3688 H 1 RES1 0.0 +26 C10 -2.4886 -2.261 1.0917 C.2 1 RES1 0.0 +27 H8 -3.4592 -2.3617 0.5771 H 1 RES1 0.0 +28 C11 1.4967 0.1807 0.512 C.ar 1 RES1 0.0 +29 C12 2.292 1.3508 0.508 C.ar 1 RES1 0.0 +30 H9 1.8621 2.3224 0.7811 H 1 RES1 0.0 +31 C13 3.6384 1.2902 0.1411 C.ar 1 RES1 0.0 +32 H10 4.2746 2.1849 0.1357 H 1 RES1 0.0 +33 C14 4.1978 0.0597 -0.2224 C.ar 1 RES1 0.0 +34 C15 3.4197 -1.1062 -0.2637 C.ar 1 RES1 0.0 +35 H11 3.8815 -2.0427 -0.6028 H 1 RES1 0.0 +36 C16 2.0727 -1.0423 0.0871 C.ar 1 RES1 0.0 +37 H12 1.4394 -1.9314 -0.0243 H 1 RES1 0.0 +@BOND +1 1 2 1 +1 1 3 1 +1 1 6 1 +1 1 7 1 +1 4 9 2 +1 5 9 2 +1 6 10 1 +1 6 18 1 +1 7 19 1 +1 7 26 1 +1 8 18 1 +1 8 19 1 +1 8 28 1 +1 9 33 1 +1 10 11 1 +1 10 12 1 +1 12 13 1 +1 12 14 2 +1 14 15 1 +1 14 16 1 +1 16 17 1 +1 16 18 2 +1 19 20 1 +1 20 21 1 +1 20 22 2 +1 22 23 1 +1 22 24 1 +1 24 25 1 +1 24 26 2 +1 26 27 1 +1 28 29 2 +1 28 36 1 +1 29 30 1 +1 29 31 1 +1 31 32 1 +1 31 33 2 +1 33 34 1 +1 34 35 1 +1 34 36 2 +1 36 37 1 +@SUBSTRUCTURE +1 RES1 37 + +" +"48 +10/06/2025 18:27, XYZ structure generated by mol3D Class, molSimplify +Rh 0.033400 1.177600 -0.275300 +O 2.257100 3.238700 -0.525000 +O -2.087900 3.333000 -0.625200 +N 2.370500 -1.043800 0.026600 +N 1.128000 -1.142100 -1.745900 +N -1.241600 -1.039400 -1.759300 +N -2.494200 -0.849800 -0.000400 +C 1.336600 -0.417400 -0.602300 +C 2.809400 -2.129600 -0.731100 +H 3.641700 -2.766900 -0.426900 +C 2.016400 -2.202900 -1.844600 +H 2.022900 -2.904200 -2.684600 +C 2.985800 -0.636300 1.343900 +C 4.038000 0.450400 1.072200 +H 4.814800 0.081200 0.373700 +H 4.536700 0.732400 2.019600 +H 3.588800 1.362700 0.641000 +C 1.875700 -0.133700 2.277600 +H 1.140000 -0.935900 2.486300 +H 1.331800 0.734900 1.849300 +H 2.323500 0.184800 3.238200 +C 3.655300 -1.863300 1.982800 +H 2.947900 -2.709500 2.088600 +H 4.004200 -1.585500 2.995200 +H 4.546900 -2.203000 1.420700 +C -0.043600 -0.889500 -2.576500 +H -0.070900 -1.612500 -3.411100 +H 0.005100 0.145300 -2.967800 +C -1.399300 -0.310100 -0.608300 +C -3.005900 -1.895900 -0.770000 +H -3.889600 -2.466400 -0.476900 +C -2.212000 -2.023200 -1.876300 +H -2.274100 -2.705600 -2.729600 +C -2.979700 -0.603500 1.409300 +C -2.569700 0.785300 1.898700 +H -3.058600 1.586700 1.313500 +H -1.470800 0.935000 1.861700 +H -2.890700 0.893100 2.952200 +C -2.335800 -1.688500 2.291500 +H -2.607000 -2.706000 1.947100 +H -2.681300 -1.577700 3.337500 +H -1.231300 -1.595900 2.279000 +C -4.513200 -0.712100 1.431800 +H -4.974800 -0.019000 0.701400 +H -4.876900 -0.437300 2.439900 +H -4.875500 -1.738600 1.230800 +C 1.425000 2.451900 -0.367300 +C -1.282700 2.530200 -0.420600 +","@MOLECULE + +48 50 1 +SMALL +PartialCharges +**** +Generated from molSimplify + +@ATOM +1 Rh1 0.0334 1.1776 -0.2753 Rh 1 RES1 0.0 +2 O1 2.2571 3.2387 -0.525 O.3 1 RES1 0.0 +3 O2 -2.0879 3.333 -0.6252 O.3 1 RES1 0.0 +4 N1 2.3705 -1.0438 0.0266 N.pl3 1 RES1 0.0 +5 N2 1.128 -1.1421 -1.7459 N.pl3 1 RES1 0.0 +6 N3 -1.2416 -1.0394 -1.7593 N.pl3 1 RES1 0.0 +7 N4 -2.4942 -0.8498 -0.0004 N.pl3 1 RES1 0.0 +8 C1 1.3366 -0.4174 -0.6023 C.3 1 RES1 0.0 +9 C2 2.8094 -2.1296 -0.7311 C.2 1 RES1 0.0 +10 H1 3.6417 -2.7669 -0.4269 H 1 RES1 0.0 +11 C3 2.0164 -2.2029 -1.8446 C.2 1 RES1 0.0 +12 H2 2.0229 -2.9042 -2.6846 H 1 RES1 0.0 +13 C4 2.9858 -0.6363 1.3439 C.3 1 RES1 0.0 +14 C5 4.038 0.4504 1.0722 C.3 1 RES1 0.0 +15 H3 4.8148 0.0812 0.3737 H 1 RES1 0.0 +16 H4 4.5367 0.7324 2.0196 H 1 RES1 0.0 +17 H5 3.5888 1.3627 0.641 H 1 RES1 0.0 +18 C6 1.8757 -0.1337 2.2776 C.3 1 RES1 0.0 +19 H6 1.14 -0.9359 2.4863 H 1 RES1 0.0 +20 H7 1.3318 0.7349 1.8493 H 1 RES1 0.0 +21 H8 2.3235 0.1848 3.2382 H 1 RES1 0.0 +22 C7 3.6553 -1.8633 1.9828 C.3 1 RES1 0.0 +23 H9 2.9479 -2.7095 2.0886 H 1 RES1 0.0 +24 H10 4.0042 -1.5855 2.9952 H 1 RES1 0.0 +25 H11 4.5469 -2.203 1.4207 H 1 RES1 0.0 +26 C8 -0.0436 -0.8895 -2.5765 C.3 1 RES1 0.0 +27 H12 -0.0709 -1.6125 -3.4111 H 1 RES1 0.0 +28 H13 0.0051 0.1453 -2.9678 H 1 RES1 0.0 +29 C9 -1.3993 -0.3101 -0.6083 C.2 1 RES1 0.0 +30 C10 -3.0059 -1.8959 -0.77 C.2 1 RES1 0.0 +31 H14 -3.8896 -2.4664 -0.4769 H 1 RES1 0.0 +32 C11 -2.212 -2.0232 -1.8763 C.2 1 RES1 0.0 +33 H15 -2.2741 -2.7056 -2.7296 H 1 RES1 0.0 +34 C12 -2.9797 -0.6035 1.4093 C.3 1 RES1 0.0 +35 C13 -2.5697 0.7853 1.8987 C.3 1 RES1 0.0 +36 H16 -3.0586 1.5867 1.3135 H 1 RES1 0.0 +37 H17 -1.4708 0.935 1.8617 H 1 RES1 0.0 +38 H18 -2.8907 0.8931 2.9522 H 1 RES1 0.0 +39 C14 -2.3358 -1.6885 2.2915 C.3 1 RES1 0.0 +40 H19 -2.607 -2.706 1.9471 H 1 RES1 0.0 +41 H20 -2.6813 -1.5777 3.3375 H 1 RES1 0.0 +42 H21 -1.2313 -1.5959 2.279 H 1 RES1 0.0 +43 C15 -4.5132 -0.7121 1.4318 C.3 1 RES1 0.0 +44 H22 -4.9748 -0.019 0.7014 H 1 RES1 0.0 +45 H23 -4.8769 -0.4373 2.4399 H 1 RES1 0.0 +46 H24 -4.8755 -1.7386 1.2308 H 1 RES1 0.0 +47 C16 1.425 2.4519 -0.3673 C.3 1 RES1 0.0 +48 C17 -1.2827 2.5302 -0.4206 C.3 1 RES1 0.0 +@BOND +1 1 8 1 +1 1 29 2 +1 1 47 1 +1 1 48 1 +1 2 47 1 +1 3 48 1 +1 4 8 1 +1 4 9 1 +1 4 13 1 +1 5 8 1 +1 5 11 1 +1 5 26 1 +1 6 26 1 +1 6 29 1 +1 6 32 1 +1 7 29 1 +1 7 30 1 +1 7 34 1 +1 9 10 1 +1 9 11 2 +1 11 12 1 +1 13 14 1 +1 13 18 1 +1 13 22 1 +1 14 15 1 +1 14 16 1 +1 14 17 1 +1 18 19 1 +1 18 20 1 +1 18 21 1 +1 22 23 1 +1 22 24 1 +1 22 25 1 +1 26 27 1 +1 26 28 1 +1 30 31 1 +1 30 32 2 +1 32 33 1 +1 34 35 1 +1 34 39 1 +1 34 43 1 +1 35 36 1 +1 35 37 1 +1 35 38 1 +1 39 40 1 +1 39 41 1 +1 39 42 1 +1 43 44 1 +1 43 45 1 +1 43 46 1 +@SUBSTRUCTURE +1 RES1 48 + +" +"75 +10/06/2025 18:27, XYZ structure generated by mol3D Class, molSimplify +Ti -0.000000 0.933900 -0.000000 +Cl -0.786400 -0.659000 -1.538300 +N 4.754200 -0.646200 -0.024300 +C 2.133200 0.628900 -1.117600 +C 1.301000 1.421200 -1.968400 +C 0.994700 2.638900 -1.308900 +C 1.654300 2.621000 -0.037100 +C 2.355600 1.392300 0.071600 +C 2.658000 -0.743900 -1.410100 +C 4.194600 -0.853500 -1.350200 +C 4.498700 -1.736100 0.932000 +C 4.451200 -1.222700 2.376100 +C 5.435700 -2.952700 0.798600 +C 6.081700 -0.009300 0.017500 +C 5.957300 1.512800 -0.147200 +C 7.120000 -0.580600 -0.970800 +H 0.918400 1.106500 -2.945400 +H 0.398900 3.460300 -1.719800 +H 1.658200 3.429300 0.701500 +H 2.954100 1.051500 0.919500 +H 2.313600 -1.057900 -2.415200 +H 2.199200 -1.460200 -0.697000 +H 4.482300 -1.843700 -1.791700 +H 4.621500 -0.088500 -2.030400 +H 3.470000 -2.086200 0.703600 +H 5.424300 -0.799300 2.701800 +H 4.206300 -2.051100 3.070800 +H 3.668800 -0.447600 2.490200 +H 5.081600 -3.790000 1.433900 +H 6.469600 -2.708600 1.120300 +H 5.478400 -3.323100 -0.245900 +H 6.472500 -0.194300 1.040000 +H 6.951700 2.003000 -0.108300 +H 5.320200 1.944400 0.648700 +H 5.494700 1.772800 -1.122300 +H 8.118100 -0.134900 -0.782900 +H 6.853300 -0.349300 -2.022500 +H 7.216500 -1.679100 -0.880500 +Cl 0.786300 -0.658900 1.538400 +C -2.133200 0.629000 1.117600 +C -1.301000 1.421300 1.968400 +C -0.994700 2.639000 1.308800 +H -0.918400 1.106700 2.945400 +C -1.654300 2.621000 0.037000 +C -2.355700 1.392400 -0.071700 +C -2.658000 -0.743800 1.410200 +C -4.194700 -0.853400 1.350300 +N -4.754200 -0.646300 0.024300 +C -4.498700 -1.736200 -0.931900 +C -4.451100 -1.223000 -2.376000 +H -5.424100 -0.799700 -2.701900 +H -4.206200 -2.051500 -3.070600 +H -3.668500 -0.448000 -2.490100 +C -5.435800 -2.952800 -0.798400 +H -5.081700 -3.790200 -1.433500 +H -6.469600 -2.708700 -1.120200 +H -5.478600 -3.323100 0.246200 +H -3.470000 -2.086400 -0.703300 +C -6.081700 -0.009300 -0.017600 +C -5.957100 1.512900 0.146900 +H -6.951500 2.003100 0.107800 +H -5.320000 1.944300 -0.649100 +H -5.494500 1.773000 1.121900 +C -7.120000 -0.580300 0.970800 +H -8.118100 -0.134600 0.782700 +H -6.853400 -0.348800 2.022400 +H -7.216600 -1.678800 0.880600 +H -6.472500 -0.194400 -1.040100 +H -4.482400 -1.843500 1.791900 +H -4.621500 -0.088300 2.030400 +H -2.313700 -1.057700 2.415300 +H -2.199200 -1.460200 0.697100 +H -1.658200 3.429300 -0.701600 +H -2.954100 1.051500 -0.919500 +H -0.398900 3.460400 1.719600 +","@MOLECULE + +75 78 1 +SMALL +PartialCharges +**** +Generated from molSimplify + +@ATOM +1 Ti1 -0.0 0.9339 -0.0 Ti 1 RES1 0.0 +2 Cl1 -0.7864 -0.659 -1.5383 Cl 1 RES1 0.0 +3 N1 4.7542 -0.6462 -0.0243 N.3 1 RES1 0.0 +4 C1 2.1332 0.6289 -1.1176 C.2 1 RES1 0.0 +5 C2 1.301 1.4212 -1.9684 C.2 1 RES1 0.0 +6 C3 0.9947 2.6389 -1.3089 C.3 1 RES1 0.0 +7 C4 1.6543 2.621 -0.0371 C.3 1 RES1 0.0 +8 C5 2.3556 1.3923 0.0716 C.3 1 RES1 0.0 +9 C6 2.658 -0.7439 -1.4101 C.3 1 RES1 0.0 +10 C7 4.1946 -0.8535 -1.3502 C.3 1 RES1 0.0 +11 C8 4.4987 -1.7361 0.932 C.3 1 RES1 0.0 +12 C9 4.4512 -1.2227 2.3761 C.3 1 RES1 0.0 +13 C10 5.4357 -2.9527 0.7986 C.3 1 RES1 0.0 +14 C11 6.0817 -0.0093 0.0175 C.3 1 RES1 0.0 +15 C12 5.9573 1.5128 -0.1472 C.3 1 RES1 0.0 +16 C13 7.12 -0.5806 -0.9708 C.3 1 RES1 0.0 +17 H1 0.9184 1.1065 -2.9454 H 1 RES1 0.0 +18 H2 0.3989 3.4603 -1.7198 H 1 RES1 0.0 +19 H3 1.6582 3.4293 0.7015 H 1 RES1 0.0 +20 H4 2.9541 1.0515 0.9195 H 1 RES1 0.0 +21 H5 2.3136 -1.0579 -2.4152 H 1 RES1 0.0 +22 H6 2.1992 -1.4602 -0.697 H 1 RES1 0.0 +23 H7 4.4823 -1.8437 -1.7917 H 1 RES1 0.0 +24 H8 4.6215 -0.0885 -2.0304 H 1 RES1 0.0 +25 H9 3.47 -2.0862 0.7036 H 1 RES1 0.0 +26 H10 5.4243 -0.7993 2.7018 H 1 RES1 0.0 +27 H11 4.2063 -2.0511 3.0708 H 1 RES1 0.0 +28 H12 3.6688 -0.4476 2.4902 H 1 RES1 0.0 +29 H13 5.0816 -3.79 1.4339 H 1 RES1 0.0 +30 H14 6.4696 -2.7086 1.1203 H 1 RES1 0.0 +31 H15 5.4784 -3.3231 -0.2459 H 1 RES1 0.0 +32 H16 6.4725 -0.1943 1.04 H 1 RES1 0.0 +33 H17 6.9517 2.003 -0.1083 H 1 RES1 0.0 +34 H18 5.3202 1.9444 0.6487 H 1 RES1 0.0 +35 H19 5.4947 1.7728 -1.1223 H 1 RES1 0.0 +36 H20 8.1181 -0.1349 -0.7829 H 1 RES1 0.0 +37 H21 6.8533 -0.3493 -2.0225 H 1 RES1 0.0 +38 H22 7.2165 -1.6791 -0.8805 H 1 RES1 0.0 +39 Cl2 0.7863 -0.6589 1.5384 Cl 1 RES1 0.0 +40 C14 -2.1332 0.629 1.1176 C.2 1 RES1 0.0 +41 C15 -1.301 1.4213 1.9684 C.2 1 RES1 0.0 +42 C16 -0.9947 2.639 1.3088 C.3 1 RES1 0.0 +43 H23 -0.9184 1.1067 2.9454 H 1 RES1 0.0 +44 C17 -1.6543 2.621 0.037 C.3 1 RES1 0.0 +45 C18 -2.3557 1.3924 -0.0717 C.3 1 RES1 0.0 +46 C19 -2.658 -0.7438 1.4102 C.3 1 RES1 0.0 +47 C20 -4.1947 -0.8534 1.3503 C.3 1 RES1 0.0 +48 N2 -4.7542 -0.6463 0.0243 N.3 1 RES1 0.0 +49 C21 -4.4987 -1.7362 -0.9319 C.3 1 RES1 0.0 +50 C22 -4.4511 -1.223 -2.376 C.3 1 RES1 0.0 +51 H24 -5.4241 -0.7997 -2.7019 H 1 RES1 0.0 +52 H25 -4.2062 -2.0515 -3.0706 H 1 RES1 0.0 +53 H26 -3.6685 -0.448 -2.4901 H 1 RES1 0.0 +54 C23 -5.4358 -2.9528 -0.7984 C.3 1 RES1 0.0 +55 H27 -5.0817 -3.7902 -1.4335 H 1 RES1 0.0 +56 H28 -6.4696 -2.7087 -1.1202 H 1 RES1 0.0 +57 H29 -5.4786 -3.3231 0.2462 H 1 RES1 0.0 +58 H30 -3.47 -2.0864 -0.7033 H 1 RES1 0.0 +59 C24 -6.0817 -0.0093 -0.0176 C.3 1 RES1 0.0 +60 C25 -5.9571 1.5129 0.1469 C.3 1 RES1 0.0 +61 H31 -6.9515 2.0031 0.1078 H 1 RES1 0.0 +62 H32 -5.32 1.9443 -0.6491 H 1 RES1 0.0 +63 H33 -5.4945 1.773 1.1219 H 1 RES1 0.0 +64 C26 -7.12 -0.5803 0.9708 C.3 1 RES1 0.0 +65 H34 -8.1181 -0.1346 0.7827 H 1 RES1 0.0 +66 H35 -6.8534 -0.3488 2.0224 H 1 RES1 0.0 +67 H36 -7.2166 -1.6788 0.8806 H 1 RES1 0.0 +68 H37 -6.4725 -0.1944 -1.0401 H 1 RES1 0.0 +69 H38 -4.4824 -1.8435 1.7919 H 1 RES1 0.0 +70 H39 -4.6215 -0.0883 2.0304 H 1 RES1 0.0 +71 H40 -2.3137 -1.0577 2.4153 H 1 RES1 0.0 +72 H41 -2.1992 -1.4602 0.6971 H 1 RES1 0.0 +73 H42 -1.6582 3.4293 -0.7016 H 1 RES1 0.0 +74 H43 -2.9541 1.0515 -0.9195 H 1 RES1 0.0 +75 H44 -0.3989 3.4604 1.7196 H 1 RES1 0.0 +@BOND +1 1 2 1 +1 1 6 1 +1 1 7 1 +1 1 39 1 +1 1 42 1 +1 1 44 1 +1 3 10 1 +1 3 11 1 +1 3 14 1 +1 4 5 2 +1 4 8 1 +1 4 9 1 +1 5 6 1 +1 5 17 1 +1 6 7 1 +1 6 18 1 +1 7 8 1 +1 7 19 1 +1 8 20 1 +1 9 10 1 +1 9 21 1 +1 9 22 1 +1 10 23 1 +1 10 24 1 +1 11 12 1 +1 11 13 1 +1 11 25 1 +1 12 26 1 +1 12 27 1 +1 12 28 1 +1 13 29 1 +1 13 30 1 +1 13 31 1 +1 14 15 1 +1 14 16 1 +1 14 32 1 +1 15 33 1 +1 15 34 1 +1 15 35 1 +1 16 36 1 +1 16 37 1 +1 16 38 1 +1 40 41 2 +1 40 45 1 +1 40 46 1 +1 41 42 1 +1 41 43 1 +1 42 44 1 +1 42 75 1 +1 44 45 1 +1 44 73 1 +1 45 74 1 +1 46 47 1 +1 46 71 1 +1 46 72 1 +1 47 48 1 +1 47 69 1 +1 47 70 1 +1 48 49 1 +1 48 59 1 +1 49 50 1 +1 49 54 1 +1 49 58 1 +1 50 51 1 +1 50 52 1 +1 50 53 1 +1 54 55 1 +1 54 56 1 +1 54 57 1 +1 59 60 1 +1 59 64 1 +1 59 68 1 +1 60 61 1 +1 60 62 1 +1 60 63 1 +1 64 65 1 +1 64 66 1 +1 64 67 1 +@SUBSTRUCTURE +1 RES1 75 + +" diff --git a/tests/test_conditioning.py b/tests/test_conditioning.py new file mode 100644 index 0000000..5d463d6 --- /dev/null +++ b/tests/test_conditioning.py @@ -0,0 +1,67 @@ +"""Molecule-level charge / spin conditioning.""" + +from __future__ import annotations + +import pandas as pd +import torch +from torch.utils.data import DataLoader, Subset + +from step_up.data.csv_dataset import CSVMoleculeDataset +from step_up.models.rebind import N_GLOBAL_FEATURES, build_collator, build_rebind + + +def _dataset(path) -> CSVMoleculeDataset: + return CSVMoleculeDataset(path, "mol2", charge_column="charge", spin_column="spinmult") + + +def _batch(path, n: int) -> dict: + loader = DataLoader( + Subset(_dataset(path), list(range(n))), + batch_size=n, + collate_fn=build_collator(conditioned=True), + ) + return next(iter(loader)) + + +def test_dataset_attaches_charge_and_spin(bostmc_path) -> None: + df = pd.read_csv(bostmc_path) + ds = _dataset(bostmc_path) + assert [ds[i]["charge"] for i in range(len(ds))] == df["charge"].astype(float).tolist() + assert [ds[i]["spin_multiplicity"] for i in range(len(ds))] == ( + df["spinmult"].astype(float).tolist() + ) + + +def test_collator_emits_charge_and_unpaired_electron_count(bostmc_path) -> None: + df = pd.read_csv(bostmc_path) + batch = _batch(bostmc_path, len(df)) + # Column 1 is unpaired electrons, i.e. multiplicity - 1: 0 for a singlet. + expected = torch.tensor( + [[float(c), float(s) - 1.0] for c, s in zip(df["charge"], df["spinmult"], strict=True)] + ) + assert batch["global_features"].shape == (len(df), N_GLOBAL_FEATURES) + torch.testing.assert_close(batch["global_features"], expected) + + +def test_conditioning_is_inert_at_init_then_changes_predictions(bostmc_path) -> None: + """Zero-initialised output layer means a fresh model matches the unconditioned one.""" + batch = _batch(bostmc_path, 2) + torch.manual_seed(0) + model = build_rebind( + n_layers=1, d_model=32, d_ffn=64, n_head=4, n_global_features=N_GLOBAL_FEATURES + ) + model.eval() + without_conditioning = {k: v for k, v in batch.items() if k != "global_features"} + with torch.no_grad(): + conditioned = model(**batch) + plain = model(**without_conditioning) + torch.testing.assert_close(conditioned.conformer_hat, plain.conformer_hat) + + # Once the layer is non-zero, the molecule-level features reach the output. + torch.nn.init.normal_(model.global_cond[-1].weight, std=0.5) + with torch.no_grad(): + neutral = model(**{**batch, "global_features": torch.zeros_like(batch["global_features"])}) + charged = model( + **{**batch, "global_features": torch.full_like(batch["global_features"], 2.0)} + ) + assert not torch.allclose(neutral.conformer_hat, charged.conformer_hat) diff --git a/tests/test_conformer_eval.py b/tests/test_conformer_eval.py new file mode 100644 index 0000000..57d3f5e --- /dev/null +++ b/tests/test_conformer_eval.py @@ -0,0 +1,118 @@ +"""Conformer metrics in ReBind's published protocol.""" + +from __future__ import annotations + +import math +from types import SimpleNamespace + +import pytest +import torch +from torch.utils.data import Subset + +from step_up.data.csv_dataset import CSVMoleculeDataset +from step_up.eval.conformer_eval import evaluate_split, kabsch_rmsd +from step_up.models.rebind import build_rebind, get_collator + + +class _PerfectModel(torch.nn.Module): + """Predicts the target conformer exactly.""" + + def forward(self, **batch): + conformer = batch["conformer"] + return SimpleNamespace(conformer_hat=conformer, conformer=conformer) + + +def test_perfect_prediction_scores_zero(qm9_sdf_path) -> None: + ds = CSVMoleculeDataset(qm9_sdf_path, "sdf") + subset = Subset(ds, list(range(len(ds)))) + metrics = evaluate_split(_PerfectModel(), ds, subset, get_collator()(), batch_size=2) + assert metrics["n_molecules"] == len(ds) + # Every fixture molecule reaches the RDKit C-RMSD path. + assert metrics["n_rmsd_molecules"] == len(ds) + assert metrics["n_rmsd_failures"] == 0 + assert metrics["d_mae"] == 0.0 + assert metrics["d_rmse"] == 0.0 + assert metrics["c_rmsd"] < 1e-6 + + +def test_untrained_model_gives_finite_metrics(qm9_sdf_path) -> None: + ds = CSVMoleculeDataset(qm9_sdf_path, "sdf") + subset = Subset(ds, list(range(len(ds)))) + torch.manual_seed(0) + model = build_rebind(n_layers=1, d_model=32, d_ffn=64, n_head=4) + metrics = evaluate_split(model, ds, subset, get_collator()(), batch_size=2) + assert metrics["d_mae"] > 0.0 + assert all(math.isfinite(metrics[key]) for key in ("d_mae", "d_rmse", "c_rmsd")) + + +def test_mol2_rows_fall_back_to_aligned_coordinate_rmsd(bostmc_path) -> None: + """Organometallic rows have no RDKit molecule, so C-RMSD uses aligned coordinates.""" + ds = CSVMoleculeDataset(bostmc_path, "mol2") + subset = Subset(ds, list(range(len(ds)))) + metrics = evaluate_split(_PerfectModel(), ds, subset, get_collator()(), batch_size=2) + assert metrics["c_rmsd_method"] == "aligned_coords" + assert metrics["n_rmsd_molecules"] == len(ds) + assert metrics["d_mae"] == 0.0 + assert metrics["c_rmsd"] < 1e-6 + + +def test_forcing_aligned_coords_skips_rdkit(qm9_sdf_path) -> None: + """QM9 rows do build RDKit molecules, so the option has to override that. + + Forcing the plain metric is what makes RMSD comparable with the MOL2 sets. + """ + ds = CSVMoleculeDataset(qm9_sdf_path, "sdf") + subset = Subset(ds, list(range(len(ds)))) + auto = evaluate_split(_PerfectModel(), ds, subset, get_collator()(), batch_size=2) + forced = evaluate_split( + _PerfectModel(), ds, subset, get_collator()(), batch_size=2, rmsd_method="aligned_coords" + ) + assert auto["c_rmsd_method"] == "rdkit_bestrms" + assert forced["c_rmsd_method"] == "aligned_coords" + assert forced["n_rmsd_molecules"] == len(ds) + + with pytest.raises(ValueError, match="Unknown rmsd_method"): + evaluate_split(_PerfectModel(), ds, subset, get_collator()(), rmsd_method="kabsch") + + +def test_aligned_coords_rmsd_can_drop_hydrogens(bostmc_path) -> None: + """The MOL2 path has no RDKit molecule, so it must drop Hs by atom type. + + Organometallic RMSD was being reported over every atom while the QM9 C-RMSD + excluded hydrogen, which is not a comparison. + """ + ds = CSVMoleculeDataset(bostmc_path, "mol2") + subset = Subset(ds, list(range(len(ds)))) + torch.manual_seed(0) + model = build_rebind(n_layers=1, d_model=32, d_ffn=64, n_head=4) + heavy = evaluate_split(model, ds, subset, get_collator()(), batch_size=2, remove_hs=True) + everything = evaluate_split(model, ds, subset, get_collator()(), batch_size=2, remove_hs=False) + + assert heavy["c_rmsd_method"] == everything["c_rmsd_method"] == "aligned_coords" + assert heavy["c_rmsd"] != everything["c_rmsd"], "the fixture molecules have hydrogens" + # Dropping atoms changes only the RMSD; the distance metrics pool over all + # atoms either way, as ReBind's evaluate.py does. + assert heavy["d_mae"] == everything["d_mae"] + assert heavy["d_rmse"] == everything["d_rmse"] + + +def test_kabsch_rmsd_is_invariant_to_rigid_motion() -> None: + """A rotated and translated copy of a structure has zero RMSD.""" + torch.manual_seed(0) + target = torch.randn(12, 3, dtype=torch.float64) + angle = torch.tensor(0.7, dtype=torch.float64) + rotation = torch.tensor( + [ + [torch.cos(angle), -torch.sin(angle), 0.0], + [torch.sin(angle), torch.cos(angle), 0.0], + [0.0, 0.0, 1.0], + ], + dtype=torch.float64, + ) + moved = (rotation @ target.T).T + torch.tensor([3.0, -1.0, 2.0], dtype=torch.float64) + assert kabsch_rmsd(moved, target) < 1e-10 + # A reflection is not a rotation, so it must not be aligned away. + mirrored = target * torch.tensor([1.0, 1.0, -1.0], dtype=torch.float64) + assert kabsch_rmsd(mirrored, target) > 0.1 + # Displacing every atom by a fixed amount along one axis shows up in full. + assert kabsch_rmsd(target + torch.tensor([0.0, 0.0, 0.0]), target) < 1e-10 diff --git a/tests/test_csv_dataset.py b/tests/test_csv_dataset.py new file mode 100644 index 0000000..3d96cec --- /dev/null +++ b/tests/test_csv_dataset.py @@ -0,0 +1,182 @@ +"""Verify each CSV path produces valid ReBind graph dicts on a small subset.""" + +from __future__ import annotations + +import pandas as pd +import pytest + +from step_up.data import featurize +from step_up.data.csv_dataset import CSVMoleculeDataset +from step_up.data.splits import split_by_labels + + +def _check_graph_dict(g: dict) -> None: + required = {"node_attr", "edge_attr", "edge_index", "node_type", "conformer"} + missing = required - g.keys() + assert not missing, f"graph dict missing keys: {missing}" + n = g["num_nodes"] + assert n > 0 + assert len(g["node_type"]) == n + assert len(g["node_attr"]) == n + assert len(g["conformer"]) == n + # edge_index shape: 2 x num_edges (lists) + assert len(g["edge_index"]) == 2 + assert len(g["edge_index"][0]) == len(g["edge_index"][1]) + + +def test_qm9_smoke(qm9_path) -> None: + ds = CSVMoleculeDataset(qm9_path, source="smiles", subset_size=5) + assert len(ds) == 5 + for i in range(len(ds)): + _check_graph_dict(ds[i]) + + +def test_tmqmg_smoke(tmqmg_path) -> None: + ds = CSVMoleculeDataset(tmqmg_path, source="mol2", subset_size=5) + assert len(ds) == 5 + elements_seen: set[int] = set() + for i in range(len(ds)): + g = ds[i] + _check_graph_dict(g) + elements_seen.update(g["node_type"]) + # tmQMg always includes a transition metal. node_type is Z-1, so 3d/4d/5d + # metals show up at indices >= 20. Confirm at least one heavy element + # made it through the direct MOL2 parser without being silently dropped. + assert max(elements_seen) >= 20, f"no heavy elements seen: {sorted(elements_seen)}" + + +def test_bostmc_smoke(bostmc_path) -> None: + ds = CSVMoleculeDataset(bostmc_path, source="mol2") + elements_seen: set[int] = set() + for i in range(len(ds)): + g = ds[i] + _check_graph_dict(g) + elements_seen.update(g["node_type"]) + # Every complex in the fixture contains a d-block metal. + assert max(elements_seen) >= 20, f"no transition metals seen: {sorted(elements_seen)}" + + +def test_bostmc_spin_filter(bostmc_path) -> None: + """The filter_column knob should leave only matching rows in the dataset.""" + unfiltered = CSVMoleculeDataset(bostmc_path, source="mol2", validate=False) + assert len(unfiltered) > 0 + singlets = CSVMoleculeDataset( + bostmc_path, source="mol2", filter_column="spinmult", filter_value=1 + ) + doublets = CSVMoleculeDataset( + bostmc_path, source="mol2", filter_column="spinmult", filter_value=2 + ) + # The mini fixture is built to contain both — filter must split them. + assert len(singlets) > 0 + assert len(doublets) > 0 + assert len(singlets) + len(doublets) <= len(unfiltered) + + +def test_missing_submodule_error_is_not_swallowed(qm9_path, monkeypatch, tmp_path) -> None: + """Without the ReBind checkout, validation must surface the submodule error. + + Otherwise every row is counted as a featurization failure and the user sees + a misleading "Validation dropped all rows" error instead. + """ + monkeypatch.setattr(featurize, "_rebind_utils", None) + monkeypatch.setattr(featurize, "_REBIND_UTILS_PATH", tmp_path / "missing" / "utils.py") + with pytest.raises(FileNotFoundError, match="git submodule update --init --recursive"): + CSVMoleculeDataset(qm9_path, source="smiles", subset_size=2) + + +def test_split_keys_come_from_the_csv_not_dataset_positions(bostmc_path) -> None: + """Keys are CSV row numbers (or id_column values), unchanged by filtering.""" + df = pd.read_csv(bostmc_path) + doublet_rows = df.index[df["spinmult"] == 2] + assert doublet_rows[0] > 0, "fixture should have doublets after the first row" + by_row = CSVMoleculeDataset(bostmc_path, "mol2", filter_column="spinmult", filter_value=2) + assert by_row.split_keys() == [str(i) for i in doublet_rows] + by_id = CSVMoleculeDataset( + bostmc_path, "mol2", filter_column="spinmult", filter_value=2, id_column="refcode" + ) + assert by_id.split_keys() == df.loc[doublet_rows, "refcode"].tolist() + + +def test_sdf_source_uses_published_bonds_and_split(qm9_sdf_path) -> None: + """The sdf source featurizes molblocks and can read a published split column.""" + ds = CSVMoleculeDataset(qm9_sdf_path, "sdf", id_column="mol_id", split_column="split") + df = pd.read_csv(qm9_sdf_path) + assert len(ds) == len(df) + for i in range(len(ds)): + _check_graph_dict(ds[i]) + assert ds.split_keys() == df["mol_id"].tolist() + assert ds.split_labels() == df["split"].tolist() + + train, val, test = split_by_labels(ds, ds.split_labels()) + assert (len(train), len(val), len(test)) == (2, 2, 1) + # Methane from gdb9.sdf: 4 C-H bonds, stored as two directed edges each. + methane = ds[0] + assert methane["num_nodes"] == 5 + assert methane["num_edges"] == 8 + + +# The numH atom feature is column 4 of ReBind's 9-column atom feature vector, +# and the degree is column 2. +_NUMH_COLUMN = 4 +_DEGREE_COLUMN = 2 + + +@pytest.mark.parametrize( + ("fixture_name", "source"), + [("qm9_sdf_path", "sdf"), ("tmqmg_path", "mol2"), ("bostmc_path", "mol2")], +) +def test_remove_hs_drops_hydrogens_from_every_source(fixture_name, source, request) -> None: + """Training without hydrogens has to work the same on both featurization paths.""" + path = request.getfixturevalue(fixture_name) + full = CSVMoleculeDataset(path, source=source) + heavy = CSVMoleculeDataset(path, source=source, remove_hs=True) + assert len(full) == len(heavy) + + saw_hydrogen = False + for i in range(len(full)): + before, after = full[i], heavy[i] + _check_graph_dict(after) + n_h = sum(1 for z in before["node_type"] if z == 0) # node_type is Z - 1 + saw_hydrogen = saw_hydrogen or n_h > 0 + assert after["num_nodes"] == before["num_nodes"] - n_h + assert 0 not in after["node_type"], "a hydrogen survived the strip" + # Bonds to a dropped atom go too, and the survivors stay in range. + assert all(idx < after["num_nodes"] for side in after["edge_index"] for idx in side) + # Hydrogen count survives as an atom feature, so the graph still knows + # how many there were — only where they were is gone. (RDKit reports 0 + # while the hydrogens are explicit atoms and fills the count in once they + # are removed, so this is checked against the bond graph, not against the + # feature in `before`.) + attached = [0] * before["num_nodes"] + for a, b in zip(*before["edge_index"], strict=True): + if before["node_type"][b] == 0: + attached[a] += 1 + assert [row[_NUMH_COLUMN] for row in after["node_attr"]] == [ + count for count, z in zip(attached, before["node_type"], strict=True) if z != 0 + ] + assert saw_hydrogen, "fixture has no hydrogens, so this proves nothing" + + +def test_remove_hs_reports_the_heavy_atom_degree(qm9_sdf_path) -> None: + """Degree must count surviving bonds, or it would disagree with the edges.""" + heavy = CSVMoleculeDataset(qm9_sdf_path, source="sdf", remove_hs=True) + for i in range(len(heavy)): + graph = heavy[i] + counted = [0] * graph["num_nodes"] + for node in graph["edge_index"][0]: + counted[node] += 1 + assert [row[_DEGREE_COLUMN] for row in graph["node_attr"]] == counted + + +def test_remove_hs_keeps_rdkit_mol_aligned_with_the_graph(qm9_sdf_path) -> None: + """C-RMSD writes predicted coordinates onto this molecule by atom index. + + If it still carried hydrogens while the graph did not, every coordinate + after the first hydrogen would land on the wrong atom. + """ + heavy = CSVMoleculeDataset(qm9_sdf_path, source="sdf", id_column="mol_id", remove_hs=True) + for i in range(len(heavy)): + assert heavy.rdkit_mol(i).GetNumAtoms() == heavy[i]["num_nodes"] + + full = CSVMoleculeDataset(qm9_sdf_path, source="sdf", id_column="mol_id") + assert full.rdkit_mol(0).GetNumAtoms() == full[0]["num_nodes"] diff --git a/tests/test_dump_structures.py b/tests/test_dump_structures.py new file mode 100644 index 0000000..66e22d0 --- /dev/null +++ b/tests/test_dump_structures.py @@ -0,0 +1,155 @@ +"""The per-molecule structure dump: one CSV row per test molecule.""" + +from __future__ import annotations + +import csv +import importlib.util +import sys +from pathlib import Path + +import pandas as pd +import pytest +import torch +import yaml + +from step_up.models import N_GLOBAL_FEATURES, build_model + +_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "dump_structures.py" + + +def _load_script(): + spec = importlib.util.spec_from_file_location("_dump_structures", str(_SCRIPT)) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules["_dump_structures"] = module + spec.loader.exec_module(module) + return module + + +def _write_config(tmp_path: Path, dataset_path: Path, **overrides) -> Path: + config = { + "dataset_path": str(dataset_path), + "dataset_source": "mol2", + "id_column": "refcode", + "charge_column": "charge", + "spin_column": "spinmult", + "split_ratios": [0.4, 0.3, 0.3], + "split_seed": 0, + "n_layers": 1, + "d_model": 32, + "d_ffn": 64, + "n_head": 4, + "epochs": 1, + "batch_size": 2, + "eval_batch_size": 2, + "num_workers": 0, + "device": "cpu", + "output_dir": str(tmp_path / "run"), + "seed": 0, + } + config.update(overrides) + path = tmp_path / "config.yaml" + path.write_text(yaml.safe_dump(config)) + return path + + +def _untrained_checkpoint(tmp_path: Path, conditioned: bool) -> Path: + torch.manual_seed(0) + model = build_model( + "rebind", + n_layers=1, + d_model=32, + d_ffn=64, + n_head=4, + n_global_features=N_GLOBAL_FEATURES if conditioned else 0, + ) + path = tmp_path / "run" / "best.pt" + path.parent.mkdir(parents=True, exist_ok=True) + torch.save(model.state_dict(), path) + return path + + +def _parse_xyz(block: str) -> tuple[list[str], list[list[float]]]: + lines = block.splitlines() + count = int(lines[0]) + symbols, coords = [], [] + for line in lines[2 : 2 + count]: + parts = line.split() + symbols.append(parts[0]) + coords.append([float(v) for v in parts[1:4]]) + assert len(symbols) == count, "count line disagrees with the number of atom lines" + return symbols, coords + + +def test_dump_writes_one_row_per_molecule_with_both_structures(bostmc_path, tmp_path) -> None: + module = _load_script() + config_path = _write_config(tmp_path, bostmc_path) + _untrained_checkpoint(tmp_path, conditioned=True) + + assert module.main(["-c", str(config_path), "--split", "test"]) == 0 + out = tmp_path / "run" / "structures_test.csv" + frame = pd.read_csv(out) + assert list(frame.columns) == list(module._COLUMNS) + assert len(frame) > 0 + + for _, row in frame.iterrows(): + true_symbols, true_coords = _parse_xyz(row["xyz_true"]) + pred_symbols, pred_coords = _parse_xyz(row["xyz_pred"]) + # Same atoms, same order, so the two blocks overlay directly. + assert true_symbols == pred_symbols + assert len(true_coords) == len(pred_coords) == row["n_atoms"] + assert row["elements"].split() == true_symbols + # Real coordinates, not padding zeros. + assert any(any(v != 0.0 for v in xyz) for xyz in true_coords) + assert all(row[key] >= 0.0 for key in ("d_mae", "d_rmse", "rmsd")) + # BOSTMC states both, so neither may be blank. + assert not pd.isna(row["charge"]) + assert not pd.isna(row["spin_multiplicity"]) + + +def test_dump_records_the_true_charge_and_spin(bostmc_path, tmp_path) -> None: + """The point of the file is downstream analysis, so these must be the data's.""" + module = _load_script() + config_path = _write_config(tmp_path, bostmc_path) + _untrained_checkpoint(tmp_path, conditioned=True) + module.main(["-c", str(config_path), "--split", "test"]) + + dumped = pd.read_csv(tmp_path / "run" / "structures_test.csv").set_index("id") + source = pd.read_csv(bostmc_path).set_index("refcode") + for key, row in dumped.iterrows(): + assert row["charge"] == pytest.approx(float(source.loc[key, "charge"])) + assert row["spin_multiplicity"] == pytest.approx(float(source.loc[key, "spinmult"])) + + +def test_dump_leaves_charge_and_spin_blank_when_the_data_is_silent(qm9_sdf_path, tmp_path) -> None: + """An unconditioned dataset must not report a charge it was never given.""" + module = _load_script() + config_path = _write_config( + tmp_path, + qm9_sdf_path, + dataset_source="sdf", + id_column="mol_id", + charge_column=None, + spin_column=None, + ) + _untrained_checkpoint(tmp_path, conditioned=False) + module.main(["-c", str(config_path), "--split", "test"]) + + with open(tmp_path / "run" / "structures_test.csv") as handle: + rows = list(csv.DictReader(handle)) + assert rows + for row in rows: + assert row["charge"] == "" + assert row["spin_multiplicity"] == "" + + +def test_predicted_block_is_rigidly_aligned_not_rebuilt(bostmc_path, tmp_path) -> None: + """Alignment may rotate and translate the prediction, never reshape it.""" + module = _load_script() + torch.manual_seed(0) + pred = torch.randn(10, 3, dtype=torch.float64) + target = torch.randn(10, 3, dtype=torch.float64) + moved = module._aligned(pred, target) + torch.testing.assert_close(torch.cdist(moved, moved), torch.cdist(pred, pred)) + # And it lands on the target's centroid rather than the origin. + torch.testing.assert_close(moved.mean(dim=0), target.mean(dim=0)) diff --git a/tests/test_forward.py b/tests/test_forward.py new file mode 100644 index 0000000..7528175 --- /dev/null +++ b/tests/test_forward.py @@ -0,0 +1,166 @@ +"""Forward pass on a 4-molecule QM9 batch through a tiny ReBind.""" + +from __future__ import annotations + +import ast +from pathlib import Path + +import pytest +import torch +from torch.utils.data import DataLoader, Subset + +from step_up.data.csv_dataset import CSVMoleculeDataset +from step_up.models import rebind +from step_up.models.lj_params import UFF_LJ_PARAMETERS +from step_up.models.rebind import build_rebind, get_collator + + +def test_forward_tiny_qm9(qm9_path) -> None: + ds = CSVMoleculeDataset(qm9_path, source="smiles", subset_size=8) + subset = Subset(ds, [0, 1, 2, 3]) + loader = DataLoader(subset, batch_size=4, shuffle=False, collate_fn=get_collator()()) + batch = next(iter(loader)) + + model = build_rebind(n_layers=2, d_model=32, d_ffn=64, n_head=4) + model.eval() + with torch.no_grad(): + out = model(**batch) + + assert out.loss.dim() == 0 + assert torch.isfinite(out.loss) + assert out.conformer_hat.shape == out.conformer.shape + b, n, _ = out.conformer_hat.shape + assert b == 4 + assert n > 0 + assert torch.isfinite(out.conformer_hat).all() + + +def test_forward_with_backward(qm9_path) -> None: + """End-to-end train-mode forward+backward through the patched REBIND.forward. + + Specifically exercises the residual_head path that uses + ``inputs["pred_conformation"] = node_embedding`` as ``conformer_base`` — + catches future regressions where someone might be tempted to switch this + to ``conformer_cache`` (which would crash on a shape mismatch). + """ + ds = CSVMoleculeDataset(qm9_path, source="smiles", subset_size=8) + subset = Subset(ds, [0, 1, 2, 3]) + loader = DataLoader(subset, batch_size=4, shuffle=False, collate_fn=get_collator()()) + batch = next(iter(loader)) + + model = build_rebind(n_layers=2, d_model=32, d_ffn=64, n_head=4) + model.train() + out = model(**batch) + assert torch.isfinite(out.loss) + out.loss.backward() + # At least one parameter must have a finite, nonzero gradient. + finite_grad = any( + p.grad is not None and torch.isfinite(p.grad).all() and (p.grad != 0).any() + for p in model.parameters() + ) + assert finite_grad, "no parameter received a finite, nonzero gradient" + + +def test_patched_forward_matches_upstream(qm9_path, monkeypatch) -> None: + """The patched model must reproduce upstream ReBind's outputs exactly. + + Upstream's in-place writes only break autograd, so under ``no_grad`` the + original methods run and the two forwards can be compared directly. This + guards the residual head's ``conformer_base``, which upstream builds through + an in-place write in its decoder. The LJ distance clamp is the one intended + numerical difference, so it is disabled here. + """ + ds = CSVMoleculeDataset(qm9_path, source="smiles", subset_size=8) + subset = Subset(ds, [0, 1, 2, 3]) + loader = DataLoader(subset, batch_size=4, shuffle=False, collate_fn=get_collator()()) + batch = next(iter(loader)) + + torch.manual_seed(0) + model = build_rebind(n_layers=2, d_model=32, d_ffn=64, n_head=4) + model.eval() + monkeypatch.setattr(rebind, "_LJ_D_MIN", 0.0) + with torch.no_grad(): + patched = model(**batch) + for cls, upstream_forward in rebind._UPSTREAM_FORWARDS.items(): + monkeypatch.setattr(cls, "forward", upstream_forward) + upstream = model(**batch) + + torch.testing.assert_close(patched.loss, upstream.loss) + torch.testing.assert_close(patched.conformer_hat, upstream.conformer_hat) + + +def test_lj_patch_supports_heavy_z(tmqmg_path) -> None: + """Verify that the LJ patch lets tmQMg (Z up to 80) pass through Collator.""" + ds = CSVMoleculeDataset(tmqmg_path, source="mol2", subset_size=2) + subset = Subset(ds, [0, 1]) + loader = DataLoader(subset, batch_size=2, shuffle=False, collate_fn=get_collator()()) + # This would KeyError pre-patch on transition-metal indices. + batch = next(iter(loader)) + assert "sigma" in batch and "epsilon" in batch + assert torch.isfinite(batch["sigma"]).all() + assert torch.isfinite(batch["epsilon"]).all() + + # Element-specific, not a flat fallback. The collator combines pairs + # (Lorentz-Berthelot), so each atom's own parameters sit on the diagonal. + node_mask = batch["node_mask"].bool() + seen = set() + for row in range(node_mask.shape[0]): + keep = node_mask[row] + node_types = batch["node_type"][row][keep].tolist() + sigmas = batch["sigma"][row][keep][:, keep].diagonal().tolist() + epsilons = batch["epsilon"][row][keep][:, keep].diagonal().tolist() + for node_type, sigma, epsilon in zip(node_types, sigmas, epsilons, strict=True): + # The collator shifts node_type by 1 so 0 can mean padding. + expected = UFF_LJ_PARAMETERS[int(node_type) - 1] + assert sigma == pytest.approx(expected[0], rel=1e-5) + assert epsilon == pytest.approx(expected[1], rel=1e-5) + seen.add(int(node_type)) + assert max(seen) - 1 > 35, "the fixture should contain a metal beyond ReBind's own table" + + +def test_uff_table_agrees_with_rebinds_own_values() -> None: + """Our table must extend upstream's, not restate it differently. + + ReBind defines its parameters inside the function body, so we read them out + of the vendored source: over Z=1..36 the two tables have to match exactly, or + swapping ours in would quietly change the published QM9 setup. + """ + source = ( + Path(__file__).resolve().parents[1] + / "external" + / "ReBIND" + / "models" + / "modules" + / "utils.py" + ).read_text() + start = source.index("lj_parameters = {") + end = source.index("\n }", start) + len("\n }") + upstream = ast.literal_eval(source[start + len("lj_parameters = ") : end]) + + assert upstream, "failed to parse ReBind's LJ table" + for node_type, params in upstream.items(): + assert UFF_LJ_PARAMETERS[node_type] == (params["sigma"], params["epsilon"]) + assert max(UFF_LJ_PARAMETERS) > max(upstream) + + +def test_single_heavy_atom_molecules_survive_the_forward_pass(qm9_sdf_path) -> None: + """Methane without its hydrogens is one atom and no bonds. + + QM9 has a handful of these, and a one-node graph is where a Laplacian + positional encoding would divide by zero if the degree were not floored. + """ + ds = CSVMoleculeDataset(qm9_sdf_path, source="sdf", remove_hs=True) + assert min(ds[i]["num_nodes"] for i in range(len(ds))) == 1 + loader = DataLoader( + Subset(ds, list(range(len(ds)))), batch_size=len(ds), collate_fn=get_collator()() + ) + batch = next(iter(loader)) + assert torch.isfinite(batch["lap_eigenvectors"]).all() + + torch.manual_seed(0) + model = build_rebind(n_layers=1, d_model=32, d_ffn=64, n_head=4) + model.eval() + with torch.no_grad(): + out = model(**batch) + assert torch.isfinite(out.loss) + assert torch.isfinite(out.conformer_hat).all() diff --git a/tests/test_gtmgc.py b/tests/test_gtmgc.py new file mode 100644 index 0000000..9d71656 --- /dev/null +++ b/tests/test_gtmgc.py @@ -0,0 +1,76 @@ +"""The vendored GTMGC wrapper.""" + +from __future__ import annotations + +import torch +from torch.utils.data import DataLoader, Subset + +from step_up.data.csv_dataset import CSVMoleculeDataset +from step_up.models import build_model, build_model_collator +from step_up.models import gtmgc as gtmgc_module + + +def _tokenized_batch(qm9_sdf_path, tokens_path, n: int = 4) -> dict: + ds = CSVMoleculeDataset(qm9_sdf_path, "sdf", id_column="mol_id", token_file=tokens_path) + loader = DataLoader( + Subset(ds, list(range(n))), batch_size=n, collate_fn=build_model_collator("gtmgc") + ) + return next(iter(loader)) + + +def test_tokens_reach_the_batch_as_node_input_ids(qm9_sdf_path, qm9_sdf_tokens_path) -> None: + """GTMGC embeds Mole-BERT ids, so the collator must receive them from the dataset.""" + ds = CSVMoleculeDataset(qm9_sdf_path, "sdf", id_column="mol_id", token_file=qm9_sdf_tokens_path) + methane = ds[0] + assert len(methane["input_ids"]) == methane["num_nodes"] + + batch = _tokenized_batch(qm9_sdf_path, qm9_sdf_tokens_path) + assert "node_input_ids" in batch + # The collator shifts ids by 1 so 0 can mean padding. + assert batch["node_input_ids"][0][0].item() == methane["input_ids"][0] + 1 + assert batch["node_input_ids"].max().item() <= 512 + + +def test_forward_and_backward_in_fp32(qm9_sdf_path, qm9_sdf_tokens_path) -> None: + batch = _tokenized_batch(qm9_sdf_path, qm9_sdf_tokens_path) + torch.manual_seed(0) + model = build_model("gtmgc", n_layers=2, d_model=64, d_ffn=128, n_head=4) + model.train() + out = model(**batch) + assert torch.isfinite(out.loss) + assert out.conformer_hat.shape == out.conformer.shape + # Upstream's in-place Laplacian write breaks this; our patch is what makes fp32 work. + out.loss.backward() + assert any( + p.grad is not None and torch.isfinite(p.grad).all() and (p.grad != 0).any() + for p in model.parameters() + ) + + +def test_patched_forward_matches_upstream(qm9_sdf_path, qm9_sdf_tokens_path, monkeypatch) -> None: + """The out-of-place patch must not change what the model computes.""" + batch = _tokenized_batch(qm9_sdf_path, qm9_sdf_tokens_path) + torch.manual_seed(0) + model = build_model("gtmgc", n_layers=2, d_model=64, d_ffn=128, n_head=4) + model.eval() + with torch.no_grad(): + patched = model(**batch) + for cls, upstream_forward in gtmgc_module._UPSTREAM_FORWARDS.items(): + monkeypatch.setattr(cls, "forward", upstream_forward) + upstream = model(**batch) + torch.testing.assert_close(patched.loss, upstream.loss) + torch.testing.assert_close(patched.conformer_hat, upstream.conformer_hat) + + +def test_atom_type_embedding_needs_no_tokenizer(qm9_sdf_path) -> None: + """Their ablation embed_style runs off atom types, which we always have.""" + ds = CSVMoleculeDataset(qm9_sdf_path, "sdf") + loader = DataLoader(Subset(ds, [0, 1]), batch_size=2, collate_fn=build_model_collator("gtmgc")) + torch.manual_seed(0) + model = gtmgc_module.build_gtmgc( + n_layers=1, d_model=32, d_ffn=64, n_head=4, embed_style="atom_type_ids", atom_vocab_size=119 + ) + model.eval() + with torch.no_grad(): + out = model(**next(iter(loader))) + assert torch.isfinite(out.loss) diff --git a/tests/test_metrics.py b/tests/test_metrics.py new file mode 100644 index 0000000..cfc4afd --- /dev/null +++ b/tests/test_metrics.py @@ -0,0 +1,81 @@ +"""Verify D-MAE / D-RMSE / coord-RMSD have expected mathematical behavior.""" + +from __future__ import annotations + +import pytest +import torch + +from step_up.data.csv_dataset import CSVMoleculeDataset +from step_up.eval.metrics import cdist_mae, cdist_rmse, coord_rmsd, per_element_dmae +from step_up.models.rebind import get_collator + + +def _toy_batch() -> tuple[torch.Tensor, torch.Tensor]: + # Two molecules: first has 3 valid atoms, second has 4. + coords = torch.zeros(2, 4, 3, dtype=torch.float32) + coords[0, 0] = torch.tensor([0.0, 0.0, 0.0]) + coords[0, 1] = torch.tensor([1.0, 0.0, 0.0]) + coords[0, 2] = torch.tensor([0.0, 1.0, 0.0]) + coords[1, 0] = torch.tensor([0.0, 0.0, 0.0]) + coords[1, 1] = torch.tensor([2.0, 0.0, 0.0]) + coords[1, 2] = torch.tensor([0.0, 2.0, 0.0]) + coords[1, 3] = torch.tensor([0.0, 0.0, 2.0]) + mask = torch.tensor([[1, 1, 1, 0], [1, 1, 1, 1]], dtype=torch.float32) + return coords, mask + + +def test_zero_when_identical() -> None: + coords, mask = _toy_batch() + assert cdist_mae(coords, coords, mask).item() == 0.0 + assert cdist_rmse(coords, coords, mask).item() == 0.0 + assert coord_rmsd(coords, coords, mask).item() == 0.0 + + +def test_dmae_positive_on_perturbation() -> None: + coords, mask = _toy_batch() + noisy = coords + 0.1 * torch.randn_like(coords) * mask.unsqueeze(-1) + assert cdist_mae(noisy, coords, mask).item() > 0.0 + assert cdist_rmse(noisy, coords, mask).item() > 0.0 + + +def test_per_element_dmae_groups_by_z() -> None: + coords, mask = _toy_batch() + pred = coords + 0.5 # uniform shift; cdist is translation-invariant -> 0 + # True atomic numbers: first mol = C, N, O (Z = 6, 7, 8); padding atom is + # whatever — the function uses node_mask, not a sentinel Z. Second mol = + # H, H, H, H (Z = 1) — H must NOT be silently dropped just because Z=1 + # is small. + z = torch.tensor([[6, 7, 8, 0], [1, 1, 1, 1]], dtype=torch.long) + per = per_element_dmae(pred, coords, mask, z) + # Uniform translation leaves pairwise distances unchanged -> 0 per element. + assert set(per.keys()) == {6, 7, 8, 1}, f"hydrogen must appear in keys: {per.keys()}" + for z_val, v in per.items(): + assert v == 0.0, f"element {z_val} should have 0 D-MAE under translation" + + +def test_per_element_dmae_nonzero_on_perturbation() -> None: + coords, mask = _toy_batch() + z = torch.tensor([[6, 7, 8, 0], [1, 1, 1, 1]], dtype=torch.long) + noisy = coords + 0.1 * torch.randn_like(coords) * mask.unsqueeze(-1) + per = per_element_dmae(noisy, coords, mask, z) + # All real elements should pick up some error. + for z_val in (1, 6, 7, 8): + assert per[z_val] > 0.0, f"element {z_val} should have nonzero D-MAE" + + +def test_per_element_dmae_takes_collated_node_type(qm9_path) -> None: + """A collated batch's node_type is already Z (H = 1) and can be passed directly.""" + ds = CSVMoleculeDataset(qm9_path, source="smiles", subset_size=4) + batch = get_collator()()([ds[i] for i in range(len(ds))]) + coords = batch["conformer"] + per = per_element_dmae(coords, coords, batch["node_mask"], batch["node_type"]) + assert 1 in per, f"hydrogen missing, so node_type wasn't read as true Z: {sorted(per)}" + assert set(per) <= {1, 6, 7, 8, 9} # QM9 elements: H, C, N, O, F + + +def test_per_element_dmae_rejects_z_minus_1_indices() -> None: + coords, mask = _toy_batch() + # Graph-dict style Z - 1 indices: C, N, O (+ padding), then four hydrogens as 0. + z_minus_1 = torch.tensor([[5, 6, 7, 0], [0, 0, 0, 0]], dtype=torch.long) + with pytest.raises(ValueError, match="true atomic numbers"): + per_element_dmae(coords, coords, mask, z_minus_1) diff --git a/tests/test_mol2.py b/tests/test_mol2.py new file mode 100644 index 0000000..1dfe9d7 --- /dev/null +++ b/tests/test_mol2.py @@ -0,0 +1,61 @@ +"""Ring and aromaticity features from the direct MOL2 parser.""" + +from __future__ import annotations + +from step_up.data.mol2 import mol2_to_graph_dict + +# Column positions in ``node_attr`` (see the ``step_up.data.mol2`` docstring). +IS_AROMATIC = 7 +IS_IN_RING = 8 + +# Synthetic complex: Co bound to a pyridine N, a chloride, and both carbons of an +# ethylene (eta-2). It has two independent cycles, the aromatic pyridine ring and +# the Co-C=C metallacycle, while the Co-N and Co-Cl bonds are bridges. Atom +# order: Co, N, C x5 (pyridine), Cl, C x2 (ethylene). +SYNTHETIC_COMPLEX = """\ +@MOLECULE +synthetic +10 11 1 +SMALL +NO_CHARGES + +@ATOM +1 Co1 0.000 0.000 0.000 Co.oh +2 N1 2.000 0.000 0.000 N.ar +3 C1 2.700 1.200 0.000 C.ar +4 C2 4.100 1.200 0.000 C.ar +5 C3 4.800 0.000 0.000 C.ar +6 C4 4.100 -1.200 0.000 C.ar +7 C5 2.700 -1.200 0.000 C.ar +8 Cl1 -2.200 0.000 0.000 Cl +9 C6 0.000 1.400 1.400 C.2 +10 C7 0.000 0.000 2.000 C.2 +@BOND +1 1 2 1 +2 2 3 ar +3 3 4 ar +4 4 5 ar +5 5 6 ar +6 6 7 ar +7 7 2 ar +8 1 8 1 +9 1 9 1 +10 1 10 1 +11 9 10 2 +""" + + +def test_ring_and_aromatic_flags() -> None: + g = mol2_to_graph_dict(SYNTHETIC_COMPLEX) + # "Co.oh" carries a geometry suffix; the element must still parse as Co (Z=27). + assert g["node_type"][0] == 26 + assert [atom[IS_AROMATIC] for atom in g["node_attr"]] == [0, 1, 1, 1, 1, 1, 1, 0, 0, 0] + # Ring membership comes from bridge detection, so the metallacycle counts as a + # ring and the chloride (a bridge) does not. + assert [atom[IS_IN_RING] for atom in g["node_attr"]] == [1, 1, 1, 1, 1, 1, 1, 0, 1, 1] + + # Bond features are [type, dir, stereo, is_conjugated], stored per direction. + bonds = dict(zip(zip(*g["edge_index"], strict=True), g["edge_attr"], strict=True)) + assert bonds[(1, 2)] == bonds[(2, 1)] == [3, 0, 0, 1] # aromatic N-C, conjugated + assert bonds[(8, 9)] == [1, 0, 0, 0] # C=C double + assert bonds[(0, 7)] == [0, 0, 0, 0] # Co-Cl single diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 7ee65a0..0cc4935 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -1,2 +1,7 @@ +"""Package-level import smoke test.""" + + def test_package_imports() -> None: - pass + import step_up + + assert step_up.__version__ diff --git a/tests/test_splits.py b/tests/test_splits.py new file mode 100644 index 0000000..5f88d24 --- /dev/null +++ b/tests/test_splits.py @@ -0,0 +1,92 @@ +"""Stable, hash-based train/val/test splits.""" + +from __future__ import annotations + +import random + +import pytest + +from step_up.data.splits import split_by_id_files, split_by_labels, stable_split + +RATIOS = (0.8, 0.1, 0.1) + + +def _membership(keys: list[str], seed: int = 0) -> dict[str, str]: + """Map each key to the name of the split it lands in.""" + subsets = stable_split(list(range(len(keys))), keys, ratios=RATIOS, seed=seed) + return { + keys[i]: name + for name, subset in zip(("train", "val", "test"), subsets, strict=True) + for i in subset.indices + } + + +def test_membership_is_independent_of_other_rows() -> None: + keys = [f"mol{i}" for i in range(2000)] + full = _membership(keys) + # Drop a third of the rows (as filtering or failed featurization would) and shuffle. + kept = [key for i, key in enumerate(keys) if i % 3] + random.Random(0).shuffle(kept) + assert _membership(kept) == {key: full[key] for key in kept} + + +def test_proportions_seed_and_shared_keys() -> None: + keys = [f"mol{i}" for i in range(20000)] + membership = _membership(keys) + for name, ratio in zip(("train", "val", "test"), RATIOS, strict=True): + fraction = sum(split == name for split in membership.values()) / len(keys) + assert abs(fraction - ratio) < 0.01, (name, fraction) + assert _membership(keys, seed=1) != membership + # Rows sharing a key (e.g. duplicated molecules) always share a split. + subsets = stable_split(list(range(4)), ["a", "b", "a", "b"], ratios=RATIOS) + split_of = {i: s for s, subset in enumerate(subsets) for i in subset.indices} + assert split_of[0] == split_of[2] + assert split_of[1] == split_of[3] + + +def test_rejects_bad_inputs() -> None: + with pytest.raises(ValueError, match="sum to 1"): + stable_split([0, 1], ["a", "b"], ratios=(0.5, 0.5, 0.5)) + with pytest.raises(ValueError, match="keys"): + stable_split([0, 1], ["a"], ratios=RATIOS) + + +def test_split_by_labels_accepts_published_names() -> None: + labels = ["train", "valid", "test", "TRAIN", "Validation", " test "] + train, val, test = split_by_labels(list(range(len(labels))), labels) + assert train.indices == [0, 3] + assert val.indices == [1, 4] + assert test.indices == [2, 5] + + +def test_split_by_labels_rejects_bad_input() -> None: + with pytest.raises(ValueError, match="Unknown split name"): + split_by_labels([0, 1], ["train", "holdout"]) + with pytest.raises(ValueError, match="labels"): + split_by_labels([0, 1], ["train"]) + + +def test_split_by_id_files_applies_published_lists(tmp_path) -> None: + """IDs come from txt or csv lists; rows in no list are dropped.""" + (tmp_path / "train.txt").write_text("AAA\nBBB\n") + (tmp_path / "val.csv").write_text("refcode\nCCC\n") + (tmp_path / "test.csv").write_text("refcode\nDDD\n") + keys = ["AAA", "CCC", "ZZZ", "DDD", "BBB"] # ZZZ is in no list + files = { + "train": tmp_path / "train.txt", + "valid": tmp_path / "val.csv", + "test": tmp_path / "test.csv", + } + train, val, test = split_by_id_files(list(range(len(keys))), keys, files) + assert train.indices == [0, 4] + assert val.indices == [1] + assert test.indices == [3] + + +def test_split_by_id_files_rejects_an_id_in_two_lists(tmp_path) -> None: + (tmp_path / "train.txt").write_text("AAA\n") + (tmp_path / "test.txt").write_text("AAA\n") + with pytest.raises(ValueError, match="listed in both"): + split_by_id_files( + [0], ["AAA"], {"train": tmp_path / "train.txt", "test": tmp_path / "test.txt"} + ) diff --git a/tests/test_train.py b/tests/test_train.py new file mode 100644 index 0000000..09effe0 --- /dev/null +++ b/tests/test_train.py @@ -0,0 +1,107 @@ +"""End-to-end checks of the training loop on the committed fixtures.""" + +from __future__ import annotations + +import json +import math +from pathlib import Path +from types import SimpleNamespace + +import pytest +import torch + +from step_up import train as train_module +from step_up.train import TrainConfig, train + + +def _tiny_config(qm9_path: Path, tmp_path: Path, **overrides) -> TrainConfig: + # (0.6, 0.2, 0.2) with split_seed 0 puts 11 / 4 / 5 of the 20 fixture rows in + # train / val / test. + fields = { + "dataset_path": str(qm9_path), + "dataset_source": "smiles", + "split_ratios": (0.6, 0.2, 0.2), + "n_layers": 1, + "d_model": 32, + "d_ffn": 64, + "n_head": 4, + "epochs": 1, + "batch_size": 4, + "eval_batch_size": 4, + "device": "cpu", + "output_dir": str(tmp_path / "run"), + } + return TrainConfig(**{**fields, **overrides}) + + +def test_train_writes_checkpoint_and_test_metrics(qm9_path, tmp_path) -> None: + result = train(_tiny_config(qm9_path, tmp_path)) + assert result["best_epoch"] == 1 + assert math.isfinite(result["best_val_dmae"]) + assert (tmp_path / "run" / "best.pt").exists() + metrics = json.loads((tmp_path / "run" / "test_metrics.json").read_text()) + assert math.isfinite(metrics["test_dmae"]) + assert metrics["best_epoch"] == 1 + + +def test_train_rejects_empty_val_split(qm9_path, tmp_path) -> None: + with pytest.raises(ValueError, match="Empty train or val split"): + train(_tiny_config(qm9_path, tmp_path, split_ratios=(1.0, 0.0, 0.0))) + + +def test_epoch_with_every_batch_skipped_reports_nan() -> None: + class NaNModel(torch.nn.Module): + def forward(self, **batch): + nan = torch.tensor(math.nan) + return SimpleNamespace(loss=nan, cdist_mae=nan) + + loader = [{"node_mask": torch.ones(1, 3)}] * 2 + loss, dmae = train_module._run_epoch(NaNModel(), loader, None, "cpu", {"step": 0}, 1e-3, 1, 0) + assert math.isnan(loss) + assert math.isnan(dmae) + + +def test_train_fails_without_a_finite_validation_epoch(qm9_path, tmp_path, monkeypatch) -> None: + real_run_epoch = train_module._run_epoch + + def nan_validation(model, loader, optimizer, *args, **kwargs): + if optimizer is None: + return math.nan, math.nan + return real_run_epoch(model, loader, optimizer, *args, **kwargs) + + monkeypatch.setattr(train_module, "_run_epoch", nan_validation) + with pytest.raises(RuntimeError, match="No epoch produced a finite validation D-MAE"): + train(_tiny_config(qm9_path, tmp_path)) + assert not (tmp_path / "run" / "best.pt").exists() + assert not (tmp_path / "run" / "test_metrics.json").exists() + + +def test_lr_schedule_matches_the_paper_setting() -> None: + """`linear` is HuggingFace's warmup-then-decay, which ReBind's script requests.""" + total, warmup, base = 100, 10, 9e-5 + linear = [train_module._lr_at_step(s, total, warmup, base, "linear") for s in range(total + 1)] + assert linear[0] == 0.0 + assert linear[warmup] == pytest.approx(base) + assert linear[warmup + (total - warmup) // 2] == pytest.approx(base / 2) + assert linear[total] == pytest.approx(0.0) + assert all(a >= b for a, b in zip(linear[warmup:], linear[warmup + 1 :], strict=False)) + + cosine = [train_module._lr_at_step(s, total, warmup, base, "cosine") for s in range(total + 1)] + assert cosine[warmup] == pytest.approx(base) + assert cosine[total] == pytest.approx(0.0, abs=1e-12) + + # An unknown name is rejected at config load, and by the helper itself. + with pytest.raises(ValueError, match="Unknown lr_schedule"): + TrainConfig(dataset_path="x.csv", dataset_source="sdf", lr_schedule="triangular") + with pytest.raises(ValueError, match="Unknown lr_schedule"): + train_module._lr_at_step(warmup + 1, total, warmup, base, "triangular") + + +def test_drop_last_keeps_a_partial_batch_rather_than_training_on_nothing( + qm9_path, tmp_path, capsys +) -> None: + config = _tiny_config(qm9_path, tmp_path, batch_size=64, eval_batch_size=64) + assert config.drop_last, "ReBind's script drops the last partial batch" + result = train(config) + assert result["best_epoch"] == 1 + assert "keeping the partial batch" in capsys.readouterr().out diff --git a/uv.lock b/uv.lock index 4c6b6ee..7d23b8e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,172 @@ version = 1 revision = 3 requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "(python_full_version < '3.14' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] + +[[package]] +name = "absl-py" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "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 = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "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 = "cfgv" @@ -11,6 +177,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -122,6 +373,234 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[package]] +name = "grpcio" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, +] + +[[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.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, +] + +[[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.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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 = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/b6/e22bd20a25299c34b8c5922c1545a6320825b13906eb0f7298edfd034a0b/huggingface_hub-1.15.0.tar.gz", hash = "sha256:28abfdddda3927fd4de6a63cf26ab012498a2c24dae52baf150c5c6edf98a1d5", size = 784100, upload-time = "2026-05-15T11:42:52.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/11/0b64cc9024329b76d7547c19a67604a61d21d3ba678a69d1b220c29d5112/huggingface_hub-1.15.0-py3-none-any.whl", hash = "sha256:a4a59af04cbc41a3fe3fec429b171ef994ef8c971eda10136746f408dd4e3744", size = 663602, upload-time = "2026-05-15T11:42:50.487Z" }, +] + [[package]] name = "identify" version = "2.6.19" @@ -131,6 +610,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, ] +[[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" @@ -140,6 +628,241 @@ 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 = "lightning-utilities" +version = "0.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/45/7fa8f56b17dc0f0a41ec70dd307ecd6787254483549843bef4c30ab5adce/lightning_utilities-0.15.3.tar.gz", hash = "sha256:792ae0204c79f6859721ac7f386c237a33b0ed06ba775009cb894e010a842033", size = 33553, upload-time = "2026-02-22T14:48:53.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/f4/ead6e0e37209b07c9baa3e984ccdb0348ca370b77cea3aaea8ddbb097e00/lightning_utilities-0.15.3-py3-none-any.whl", hash = "sha256:6c55f1bee70084a1cbeaa41ada96e4b3a0fea5909e844dd335bd80f5a73c5f91", size = 31906, upload-time = "2026-02-22T14:48:52.488Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[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 = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -149,6 +872,185 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "numpy" +version = "2.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/8e/b8041bc719f056afd864478029d52214789341ac6583437b0ee5031e9530/numpy-2.4.5.tar.gz", hash = "sha256:ca670567a5683b7c1670ec03e0ddd5862e10934e92a70751d68d7b7b74ca7f9f", size = 20735669, upload-time = "2026-05-15T20:25:19.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/18/3275231e98620002681c922e792db04d72c356e9d8073c387344fc0e4ff1/numpy-2.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:654fb8674b61b1c4bd568f944d13a908566fdcb0d797303521d4149d16da05ef", size = 16689166, upload-time = "2026-05-15T20:22:50.761Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/000aab6a16bdec53307f0f72546b57a3ac9266a62d8c257bee97d85fd078/numpy-2.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4cd9f6fa7ce10dc4627f2bb81dd9075dab67e94632e04c2b638e12575ddaa862", size = 14699514, upload-time = "2026-05-15T20:22:53.678Z" }, + { url = "https://files.pythonhosted.org/packages/47/cc/ddaf3af9c46966fef5be879256f213d85a0c56c75d07a3b7defec7cf6b4c/numpy-2.4.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:4f5bc96d35d94e4ceab8b38a92241b4611e95dc44e63b9f1fa2a331858ee3507", size = 5204601, upload-time = "2026-05-15T20:22:56.257Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/627fadd11959b3c7759008f34c92a35af8ff942dd8284a66ced648bbe516/numpy-2.4.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4bb33e900ee81730ad77a258965134aa8ceac805124f7e5229347beda4b8d0aa", size = 6551360, upload-time = "2026-05-15T20:22:58.334Z" }, + { url = "https://files.pythonhosted.org/packages/a1/47/0728b986b8682d742ff68c16baa5af9d185484abfc635c5cc700f44e62be/numpy-2.4.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32f8f852273ef32b291201ac2a2c97629c4a1ee8632bb670e3443eaa09fc2e72", size = 15671157, upload-time = "2026-05-15T20:23:01.081Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0b/b905ae82d9419dc38123523862db64978ca2954b69609c3ae8fdaca1084c/numpy-2.4.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685681e956fc8dcb75adc6ff26694e1dfd738b24bd8d4696c51ca0110157f912", size = 16645703, upload-time = "2026-05-15T20:23:04.358Z" }, + { url = "https://files.pythonhosted.org/packages/5f/24/e27fc3f5236b4118ed9eed67111675f5c61a07ea333acec87c869c3b359d/numpy-2.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f64dd84b277a737eb59513f6b9bb6195bf41ab11941ef15b2562dbab43fa8ef", size = 17021018, upload-time = "2026-05-15T20:23:07.021Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a7/9041af38d527ab80a06a93570a77e29425b41507ad41f6acf5da78cfb4a4/numpy-2.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b42d9496f79e3a728192f05a42d86e36163217b7cdecb3813d0028a0aa6b72d7", size = 18368768, upload-time = "2026-05-15T20:23:09.44Z" }, + { url = "https://files.pythonhosted.org/packages/49/82/326a014442f32c2663434fd424d9298791f47f8a0f17585ad60519a5606e/numpy-2.4.5-cp312-cp312-win32.whl", hash = "sha256:86d980970f5110595ca14855768073b08585fc1acc36895de303e039e7dee4a5", size = 5962819, upload-time = "2026-05-15T20:23:11.631Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/cbf5d391b0b3a5e8cad264603e2fae256b0bde8ce43566b13b78faedc659/numpy-2.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:3333dba6a4e611d666f69e177ba8fe4140366ff681a5feb2374d3fd4fff3acb6", size = 12321621, upload-time = "2026-05-15T20:23:14.305Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d0/0f18909d9bc37a5f3f969fc737d2bb5df9f2ff295f71b467e6f52a0d6c4e/numpy-2.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:4593d197270b894efeb538dcbe227e4bcf1c77f88c4c6bf933ead812cfaa4453", size = 10221430, upload-time = "2026-05-15T20:23:16.887Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a4/fb50657c7cab297bf34edcd60a074cb0647f61771430d6363575274160fe/numpy-2.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ef248460b645c102026b82337cc4e88231909c66dd77b59ec6d6cac7e44f277", size = 16684760, upload-time = "2026-05-15T20:23:19.436Z" }, + { url = "https://files.pythonhosted.org/packages/3e/43/87e731299b9408eda705b3b9cb31c7bceb9347d2af9cbb16b2b1e4b5bc0f/numpy-2.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4603622bdcdbf8dccb1d9d5b21d16a7aa4e473ae6c8e14048d846fd4ca2907a0", size = 14694117, upload-time = "2026-05-15T20:23:21.832Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/0b2bb8acea222e9dd6e582afc2bc553b89b8833cbdccc68e68f050fb31f8/numpy-2.4.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6c18d49c67689c562854b53fdc433b93e47c12952aa6fa6d59f185e1a5992419", size = 5199141, upload-time = "2026-05-15T20:23:24.066Z" }, + { url = "https://files.pythonhosted.org/packages/39/60/b6972b5d47033d90000f0097c81a98b9486589a2d7003bf725bff275cb0d/numpy-2.4.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b1c663ddc641f4192e90511bec61a09bc231e3bbdb996cdc6edbcaa0e528d685", size = 6546954, upload-time = "2026-05-15T20:23:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e9/ed667cb12c11ca0adde431f685d3a5dd78e6f78b27228c581c8415198e9e/numpy-2.4.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93793222b524f692f12b2f8752ce8b1d9d9125b2bfd5dbf0fb69c92c5e1ce86c", size = 15669430, upload-time = "2026-05-15T20:23:28.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/e5/679f6ffeb01294b0008e5ada4a113cb47617bc0e1819a529fd7973c6d7f4/numpy-2.4.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1616bde34b2bcba2fa9bde06217ce00da4f3d1bdfb264d54525a99e8fe170d83", size = 16633390, upload-time = "2026-05-15T20:23:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/36/46/42bfffc9a780ec902ccd7470d3219192ee82b7b442710307dd85b4d121b0/numpy-2.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09d7d97da1c2c62f4818b3e150a57572ff8dcf1cf5ac501aac832ffd4ebd9566", size = 17020709, upload-time = "2026-05-15T20:23:34.08Z" }, + { url = "https://files.pythonhosted.org/packages/44/00/3e840bfee0cc6cec22209f2c97057f26eeb30de031e4933b4dfc0395416c/numpy-2.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d68d0b355ab2e39fe0de59001d7151dfdbbb880ef67baeed806661e03df5097", size = 18357818, upload-time = "2026-05-15T20:23:36.965Z" }, + { url = "https://files.pythonhosted.org/packages/72/cb/3447b400b9da84134575486f0f656541559b00d4b262477bce9b678bbca8/numpy-2.4.5-cp313-cp313-win32.whl", hash = "sha256:fe28b64777ddfa0eca9b5f51474034ebe3dcb8324f48f27b28f479085673ae33", size = 5961114, upload-time = "2026-05-15T20:23:39.586Z" }, + { url = "https://files.pythonhosted.org/packages/28/f9/a90d2220ffcdc0798f5d55bb5d5463cd6254ec9ef43f384dae80217d7a2f/numpy-2.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:fb4a6c9c537d6ccec9cc4aeae4261bd3cc79b070c67ddc0646f5b1c07fddde42", size = 12318553, upload-time = "2026-05-15T20:23:41.436Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/96f531fb3234545315152d34efdf3de7daee81254448447eb619e8d16967/numpy-2.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:6d7df2da2e7ea0624a43aa368104b3a3ce14aae98ad4bb2c9a93fecef76f1c97", size = 10222200, upload-time = "2026-05-15T20:23:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f4/a291caab5a3c520babf93ff77c54fd5fdb1ebbc3296cee2eb2146ce773b1/numpy-2.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:2a235607a18df941760a695927051af4b1cd5d3ee85840d0e2af816785771feb", size = 14821438, upload-time = "2026-05-15T20:23:45.911Z" }, + { url = "https://files.pythonhosted.org/packages/85/26/13dbb1159b864370568e7309063fd72667984df89db74e9caeb175d067c7/numpy-2.4.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:58dcf64969d870f36bc7fbd557d2617e997db7dc06261b6e3327148ea460d0a4", size = 5326663, upload-time = "2026-05-15T20:23:48.18Z" }, + { url = "https://files.pythonhosted.org/packages/7c/99/d233408072a0e019e2288e27edd23f7d572ccd4a73d1539baa3270ede85d/numpy-2.4.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:235f54b0156274d8fa3155db3ed6d2f401c7e8f3367c90db0a12f02a58fde6ed", size = 6646874, upload-time = "2026-05-15T20:23:49.856Z" }, + { url = "https://files.pythonhosted.org/packages/c5/00/eeb6f193dfe767725e952e0464f3e51f44145c5dd261cd7389aa36ac0713/numpy-2.4.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3b5bb65437a3555c648e706475db01c645559ca80dc8b03e4f202ea757e0d6", size = 15728147, upload-time = "2026-05-15T20:23:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c9/b8ed039f1fde1b13a8807c893e7e2f9432a379f4d6401edecf0028da5b2c/numpy-2.4.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f09a7e5f017d7098c66522097c96257411c9620c0926212200d66bc8cee3976", size = 16681770, upload-time = "2026-05-15T20:23:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/11/5b/0198ef6cb7016eca6d895d392106012138127fab23f46637e76d5e25c9f5/numpy-2.4.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:993a88d8fdd8554466a8765cd8bacd97ba56b70ca6b0a04bcdca77f5afed4222", size = 17086218, upload-time = "2026-05-15T20:23:56.646Z" }, + { url = "https://files.pythonhosted.org/packages/f0/fe/8821f3cfc660ae84c92ee158505941874b62c56a42e035a41425228cd8cf/numpy-2.4.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:84f58bed609b5669f5ad3d597901a4f1f86ee5b3c3708aaa55f05b4fe6e0f656", size = 18403542, upload-time = "2026-05-15T20:23:59.173Z" }, + { url = "https://files.pythonhosted.org/packages/0e/00/e64ecaf498865e7b091f57658b2c522503e5d1b70e43b807f5f8247e1d88/numpy-2.4.5-cp313-cp313t-win32.whl", hash = "sha256:7200c58f3f933ca61e66346667dcc8510bb111995e9ce15398a731e6a4afa4bb", size = 6084903, upload-time = "2026-05-15T20:24:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/20/c0/354997dedaf74e8311c2cf9a6027b476fd8d424cb92189cc0ae2b25f501c/numpy-2.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c26c71080d35db5002102f5d9ff614d45de02aa1f7802943e691e063e5ee93bc", size = 12458420, upload-time = "2026-05-15T20:24:03.735Z" }, + { url = "https://files.pythonhosted.org/packages/66/dc/917ee5ea4a31ca1a6e4c9a85386477efa318dcc60db257c5ef4adda096c1/numpy-2.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:2caa576d1707b275cba1aeb60a5c50daa6fa2a3f28ecb08123bc05fd439005db", size = 10291826, upload-time = "2026-05-15T20:24:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c1/3be0bf102fc17cff5bd142e3be0bfffabec6fa46da0a462396c76b0765d0/numpy-2.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:889ca2c072315de638a5194a772aa1fa2df92bdd6175f6a222d4784040424b61", size = 16683455, upload-time = "2026-05-15T20:24:08.988Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3e/0742d724901fa36bc54b338c6e62e463a7601180da896aa44978f0adf004/numpy-2.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:89e89304fb1f8c3f0ecfa4a7d48f311dd79771336a940e920159d643d1307e77", size = 14704577, upload-time = "2026-05-15T20:24:11.542Z" }, + { url = "https://files.pythonhosted.org/packages/25/1c/196c610ff4c6782d697ba780ebdc1616be143213701bf22c1a270f3bf7dd/numpy-2.4.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:144fcc5a3a17679b2b82543b4a2d8dd29937230a7af13232b5f753872feb6361", size = 5209756, upload-time = "2026-05-15T20:24:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/52/c0/23fb1bc506f774e03db66219a2830e720f4d3dbcaaddf855a7ff7bb6d96f/numpy-2.4.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:398bb16772b265b9fa5c07b07072646ea97137c10ffb62a9a087b277fc825c29", size = 6543937, upload-time = "2026-05-15T20:24:16.223Z" }, + { url = "https://files.pythonhosted.org/packages/9f/49/db4662c26e68520afcc84d672a6f9f5294063dee0e57a46d61afdaa7f9ed/numpy-2.4.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb352e7b8876da1249e72254736d6c58c505fa4e58a3d7e30efca241ca9ca9ce", size = 15685292, upload-time = "2026-05-15T20:24:17.978Z" }, + { url = "https://files.pythonhosted.org/packages/43/80/1315439acedd8398319bac177d6de3d48ab39c62cc0c810f74f0a9a73996/numpy-2.4.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7341b08ff8124d7353939778e2707b8732d03c78c1c30e0815aba2dacbe1245a", size = 16638528, upload-time = "2026-05-15T20:24:20.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/81/364388600932618fe735d97fdd2437cb8dd87a23377ac11d8b9d5db098b7/numpy-2.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:deb01226f012539f3945261ffe1c10aec081a0fa0a5c925419933c70f3ae2d23", size = 17036709, upload-time = "2026-05-15T20:24:22.949Z" }, + { url = "https://files.pythonhosted.org/packages/32/4a/a1185b18a94a6d9587e54b437e7d0ba36ecf6e614f1bea03f5249912c64e/numpy-2.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d888bdf7335f76878c3c7b264ac1ff089863e211ec81249f9fb5795c2183dc25", size = 18363254, upload-time = "2026-05-15T20:24:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8e/95c1d2ed15ae97750ede8c8a0ac487c9c01207afff430f47078b1d9d7dc5/numpy-2.4.5-cp314-cp314-win32.whl", hash = "sha256:15f90d1256e9b2320aff24fde44815b787ab6d7c49a1a11bfd8138b321c5f080", size = 6010184, upload-time = "2026-05-15T20:24:27.852Z" }, + { url = "https://files.pythonhosted.org/packages/aa/92/d063df4d63d988b20d881856c74df76c0c1786229bb870f3a52af0981d4d/numpy-2.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4bd2cd4ef9c0afa87de73723c0a33c0edff62143e1432917458e26d3d195d87f", size = 12450344, upload-time = "2026-05-15T20:24:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/3d/64/c0ae481f7c3b2f85869bcd8fc5d30aa7c96b394162eef9c9315957f115c5/numpy-2.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:db304568c650e9d7039744d3575d0d287754debb2057d7c7b8cdfdc2c487a957", size = 10495674, upload-time = "2026-05-15T20:24:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/57/89/c5a4c677acf17aa50ba09a15e61812f90baac42bb6ca38d112e005858351/numpy-2.4.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de2883e0d2c63eae1bab1a84b390dca74aabb3d20ea1f5d58f360853c83abf3", size = 14824078, upload-time = "2026-05-15T20:24:34.669Z" }, + { url = "https://files.pythonhosted.org/packages/e7/52/57e7144284f6b51ba93523e495ff239260b1ecd5257e3700a436332e5688/numpy-2.4.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:06760fe73ae5005008748d182de612c733542af3cde063d532cd2127561b27be", size = 5329246, upload-time = "2026-05-15T20:24:36.957Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b3/09dbce80fd4a7db4318f2fc01eec0ae76f29306442b5a32d4b811d082cdf/numpy-2.4.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:4b51a01745cb04cc19278482207444b4d30728ce91c28d27a3bfae5fc6ff24c7", size = 6649877, upload-time = "2026-05-15T20:24:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/dbdb23e82d540b757690ef13f011c386fca6a63848eec6136baf8ce7cbed/numpy-2.4.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a05636d7937d0936f271e5ba957fa8d746b5be3c2025caa1a2508f4fe521d40", size = 15730534, upload-time = "2026-05-15T20:24:41.168Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bd/68f6e9b3c20decf40ac06708a7b506757e3a8588efed32988d1b747316be/numpy-2.4.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b86f56048ed09c3bbe48962a7dff077c2fd3274f8cf981800f3b38eac49cc3", size = 16679741, upload-time = "2026-05-15T20:24:44.874Z" }, + { url = "https://files.pythonhosted.org/packages/39/1d/0fcac0b6b4ea1b50ca8fca05a34bed5c8d56e34c1cb5ffb04cf76109ac3c/numpy-2.4.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:130d58151c4db23e9fa860b84784e219a3aa3e030acc88a493ea37006c4dfd4c", size = 17085598, upload-time = "2026-05-15T20:24:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e8/a472b2564cf6cc498ad7aa9741d9832648221b8ab8cc0dbef41faa248ede/numpy-2.4.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d475afc8cbe935ff5944f753d863bba774d7f4e1feaaa4102901e3e053ca5963", size = 18403855, upload-time = "2026-05-15T20:24:50.474Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a4/da82196f8cc4bd28ecf17bd57008c84f3d4696caf06753d9bad45e4ad749/numpy-2.4.5-cp314-cp314t-win32.whl", hash = "sha256:27f4a6dc26353a860b348961b9aa9e009835688b435cfa105e873b8dc2c726f5", size = 6156900, upload-time = "2026-05-15T20:24:53.134Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/860959b91a73d9a085006554fa3850da51a7ffab64599bac5097243438ab/numpy-2.4.5-cp314-cp314t-win_amd64.whl", hash = "sha256:76ac6e90f5e226011c88f9b7040a4bcae612518bc7e9adc127e697a13b28ad1a", size = 12638906, upload-time = "2026-05-15T20:24:55.009Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2a/bbd3097913083ad07c0f28fc9629666221fc18923e17ce97ae22a5dccdd6/numpy-2.4.5-cp314-cp314t-win_arm64.whl", hash = "sha256:7c392e2c1bf596701d3c6832be7567eab5d5b0a13865036c33365ee097d37f8b", size = 10565875, upload-time = "2026-05-15T20:24:57.425Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.4.5.8" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/71/1c91302526c45ab494c23f61c7a84aa568b8c1f9d196efa5993957faf906/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b", size = 363438805, upload-time = "2024-04-03T20:57:06.025Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/42/f4f60238e8194a3106d06a058d494b18e006c10bb2b915655bd9f6ea4cb1/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb", size = 13813957, upload-time = "2024-04-03T20:55:01.564Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/14/91ae57cd4db3f9ef7aa99f4019cfa8d54cb4caa7e00975df6467e9725a9f/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338", size = 24640306, upload-time = "2024-04-03T20:56:01.463Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/27/1795d86fe88ef397885f2e580ac37628ed058a92ed2c39dc8eac3adf0619/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5", size = 883737, upload-time = "2024-04-03T20:54:51.355Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.1.0.70" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741, upload-time = "2024-04-22T15:24:15.253Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117, upload-time = "2024-04-03T20:57:40.402Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.5.147" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/6d/44ad094874c6f1b9c654f8ed939590bdc408349f137f9b98a3a23ccec411/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b", size = 56305206, upload-time = "2024-04-03T20:58:08.722Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.6.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057, upload-time = "2024-04-03T20:58:28.735Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.3.1.170" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763, upload-time = "2024-04-03T20:58:59.995Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/a8/bcbb63b53a4b1234feeafb65544ee55495e1bb37ec31b999b963cbccfd1d/nvidia_cusparselt_cu12-0.6.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:df2c24502fd76ebafe7457dbc4716b2fec071aabaed4fb7691a201cde03704d9", size = 150057751, upload-time = "2024-07-23T02:35:53.074Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.21.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/99/12cd266d6233f47d00daf3a72739872bdc10267d0383508b0b9c84a18bb6/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0", size = 188654414, upload-time = "2024-04-03T15:32:57.427Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/ff/847841bacfbefc97a00036e0fce5a0f086b640756dc38caea5e1bb002655/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57", size = 21066810, upload-time = "2024-04-03T20:59:46.957Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/20/199b8713428322a2f22b722c62b8cc278cc53dffa9705d744484b5035ee9/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a", size = 99144, upload-time = "2024-04-03T20:56:12.406Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -158,6 +1060,127 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "pandas" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, + { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + [[package]] name = "platformdirs" version = "4.9.6" @@ -192,6 +1215,143 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "protobuf" +version = "7.34.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, + { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -201,6 +1361,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pytest" version = "9.0.3" @@ -231,6 +1400,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-discovery" version = "1.3.0" @@ -290,6 +1471,145 @@ wheels = [ { 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 = "rdkit" +version = "2026.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/ee/e41dc13201769a593143141e214cea54dece91d2ee9553239fed3c5181ce/rdkit-2026.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3371395b23bdd03441ad4fdd575b4bad2d44dfa5a0e3a76c572db60b05d4f4b7", size = 29985222, upload-time = "2026-05-16T11:46:53.707Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b7/74bfd5b9a71439e9257342941e59d0c750450335a91a6963e536545ced23/rdkit-2026.3.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3083c723f5cd22b084d18661725103738f18b7d38e0876899f61a9a03b6cbb57", size = 35576172, upload-time = "2026-05-16T11:46:57.721Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/75b061afd7b84216caee4b5050db42be7c5ac11097b8166cd509db53b6a4/rdkit-2026.3.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5197a104b9ddf18b08388649b91abe8737cc02503e02137c05f7b2bea4961aae", size = 37090229, upload-time = "2026-05-16T11:47:01.797Z" }, + { url = "https://files.pythonhosted.org/packages/55/35/8f361ea5324593638112b605667fa7728d33e738110fb793351d2e7c629a/rdkit-2026.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:119b8ca7394b136ddb414a6e15fbdfbcbb8398925abbb823e853f273977211a9", size = 24613234, upload-time = "2026-05-16T11:47:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/b3e8469291a9725eda29e911987f39be8a625b78f78d64bdc731ddda852f/rdkit-2026.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c30bf6846b2f55a24b0e2beba7f0480dd948a73bfc13ad95cdb29f839586a497", size = 29984328, upload-time = "2026-05-16T11:47:10.123Z" }, + { url = "https://files.pythonhosted.org/packages/f2/46/47f8ab91a05a6b060852a8484b555256a8f32b740c67560b00560ef56eb5/rdkit-2026.3.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fb14f634f630cac27e6484bf01a789a2ae8e8dbea96c87e365ff956b31c384a2", size = 35575509, upload-time = "2026-05-16T11:47:15.281Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ed/568c508712bb4f1b94b9b85f3a514aedcd37da73fb08d6ef22a3b0981698/rdkit-2026.3.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9908eff69e89da927006b9513adaf7985b46bf0b213cf204a2529d367e932dda", size = 37089613, upload-time = "2026-05-16T11:47:20.002Z" }, + { url = "https://files.pythonhosted.org/packages/e7/39/8e095f9406d7416b9c1284817c62049dccc801008ee98a8aaa7a68c609bf/rdkit-2026.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:b4a3e15fd1e9986284288362ef415478d5a895c9409c72a3fa9320e728b76ca3", size = 24612214, upload-time = "2026-05-16T11:47:23.628Z" }, + { url = "https://files.pythonhosted.org/packages/bc/eb/8325049d449e901fa6ff28f1f5ff9f98aa95faf2e280cf03d9dc582bf310/rdkit-2026.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07ec533914be1a72e00beb24dfda409e3264bf4f0945e062cb616765381998e5", size = 30002251, upload-time = "2026-05-16T11:47:27.52Z" }, + { url = "https://files.pythonhosted.org/packages/62/7b/edc80ca5ce1343ab15686caf645e43a254ea2b8e7af3a4034655c355d7d5/rdkit-2026.3.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:146829850a725db91d6042a4c46fab2169de7ca8db22f0c889ab29a1646175d1", size = 35612930, upload-time = "2026-05-16T11:47:31.945Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c7/85d8c89905663550a5962d754aeb44f2dfc81b2968f83d82dd56692dd763/rdkit-2026.3.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d9deadcf76f9f8d836a3362be1f606ae74bd88fd622cd77a3ddaae71fcad83d9", size = 37102451, upload-time = "2026-05-16T11:47:36.843Z" }, + { url = "https://files.pythonhosted.org/packages/c9/79/dab566de8d9bd89cd1b627c4f493fa2936114c2004e547069625763353fa/rdkit-2026.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:5190d910e086f54ec640234afd3f3a26882a82f69d7f5d6a75748f5cf9177e93", size = 25097379, upload-time = "2026-05-16T11:47:41.396Z" }, +] + +[[package]] +name = "regex" +version = "2026.5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, + { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, + { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, + { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, + { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, + { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "ruff" version = "0.15.12" @@ -315,10 +1635,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[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 = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "step-up" version = "0.1.0" source = { editable = "." } +dependencies = [ + { name = "numpy" }, + { name = "pandas" }, + { name = "pyyaml" }, + { name = "rdkit" }, + { name = "tensorboard" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" }, + { name = "torch", version = "2.6.0+cu124", source = { registry = "https://download.pytorch.org/whl/cu124" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "torch-geometric" }, + { name = "torchmetrics" }, + { name = "tqdm" }, + { name = "transformers" }, +] [package.dev-dependencies] dev = [ @@ -329,6 +1711,19 @@ dev = [ ] [package.metadata] +requires-dist = [ + { name = "numpy", specifier = ">=1.26" }, + { name = "pandas", specifier = ">=2.2" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "rdkit", specifier = ">=2024.3.3" }, + { name = "tensorboard", specifier = ">=2.16" }, + { name = "torch", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'", specifier = ">=2.6,<2.7" }, + { name = "torch", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=2.6,<2.7", index = "https://download.pytorch.org/whl/cu124" }, + { name = "torch-geometric", specifier = ">=2.3" }, + { name = "torchmetrics", specifier = ">=1.0" }, + { name = "tqdm", specifier = ">=4.66" }, + { name = "transformers", specifier = ">=4.43" }, +] [package.metadata.requires-dev] dev = [ @@ -338,6 +1733,262 @@ dev = [ { name = "ruff", specifier = ">=0.15.12" }, ] +[[package]] +name = "sympy" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040, upload-time = "2024-07-19T09:26:51.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177, upload-time = "2024-07-19T09:26:48.863Z" }, +] + +[[package]] +name = "tensorboard" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "grpcio" }, + { name = "markdown" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "setuptools" }, + { name = "tensorboard-data-server" }, + { name = "werkzeug" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680, upload-time = "2025-07-17T19:20:49.638Z" }, +] + +[[package]] +name = "tensorboard-data-server" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356, upload-time = "2023-10-23T21:23:32.16Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598, upload-time = "2023-10-23T21:23:33.714Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "torch" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "(python_full_version < '3.14' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "filelock", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" }, + { name = "fsspec", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" }, + { name = "jinja2", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" }, + { name = "networkx", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" }, + { name = "setuptools", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" }, + { name = "sympy", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" }, + { name = "typing-extensions", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/0c52d708144c2deb595cd22819a609f78fdd699b95ff6f0ebcd456e3c7c1/torch-2.6.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:2bb8987f3bb1ef2675897034402373ddfc8f5ef0e156e2d8cfc47cacafdda4a9", size = 766624563, upload-time = "2025-01-29T16:23:19.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/d6/455ab3fbb2c61c71c8842753b566012e1ed111e7a4c82e0e1c20d0c76b62/torch-2.6.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b789069020c5588c70d5c2158ac0aa23fd24a028f34a8b4fcb8fcb4d7efcf5fb", size = 95607867, upload-time = "2025-01-29T16:25:55.649Z" }, + { url = "https://files.pythonhosted.org/packages/18/cf/ae99bd066571656185be0d88ee70abc58467b76f2f7c8bfeb48735a71fe6/torch-2.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:7e1448426d0ba3620408218b50aa6ada88aeae34f7a239ba5431f6c8774b1239", size = 204120469, upload-time = "2025-01-29T16:24:01.821Z" }, + { url = "https://files.pythonhosted.org/packages/81/b4/605ae4173aa37fb5aa14605d100ff31f4f5d49f617928c9f486bb3aaec08/torch-2.6.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:9a610afe216a85a8b9bc9f8365ed561535c93e804c2a317ef7fabcc5deda0989", size = 66532538, upload-time = "2025-01-29T16:24:18.976Z" }, + { url = "https://files.pythonhosted.org/packages/24/85/ead1349fc30fe5a32cadd947c91bda4a62fbfd7f8c34ee61f6398d38fb48/torch-2.6.0-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:4874a73507a300a5d089ceaff616a569e7bb7c613c56f37f63ec3ffac65259cf", size = 766626191, upload-time = "2025-01-29T16:17:26.26Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b0/26f06f9428b250d856f6d512413e9e800b78625f63801cbba13957432036/torch-2.6.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a0d5e1b9874c1a6c25556840ab8920569a7a4137afa8a63a32cee0bc7d89bd4b", size = 95611439, upload-time = "2025-01-29T16:21:21.061Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9c/fc5224e9770c83faed3a087112d73147cd7c7bfb7557dcf9ad87e1dda163/torch-2.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:510c73251bee9ba02ae1cb6c9d4ee0907b3ce6020e62784e2d7598e0cfa4d6cc", size = 204126475, upload-time = "2025-01-29T16:21:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/8b/d60c0491ab63634763be1537ad488694d316ddc4a20eaadd639cedc53971/torch-2.6.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:ff96f4038f8af9f7ec4231710ed4549da1bdebad95923953a25045dcf6fd87e2", size = 66536783, upload-time = "2025-01-29T16:22:08.559Z" }, +] + +[[package]] +name = "torch" +version = "2.6.0+cu124" +source = { registry = "https://download.pytorch.org/whl/cu124" } +resolution-markers = [ + "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "filelock", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "fsspec", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "jinja2", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "networkx", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "sympy", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu124/torch-2.6.0%2Bcu124-cp312-cp312-linux_x86_64.whl", hash = "sha256:a393b506844035c0dac2f30ea8478c343b8e95a429f06f3b3cadfc7f53adb597", upload-time = "2025-01-30T00:57:08Z" }, + { url = "https://download-r2.pytorch.org/whl/cu124/torch-2.6.0%2Bcu124-cp313-cp313-linux_x86_64.whl", hash = "sha256:0f3bc53c988ce9568cd876a2a5316761e84a8704135ec8068f5f81b4417979cb", upload-time = "2025-01-30T00:59:14Z" }, + { url = "https://download-r2.pytorch.org/whl/cu124/torch-2.6.0%2Bcu124-cp313-cp313t-linux_x86_64.whl", hash = "sha256:35cba404c0d742406cdcba1609085874bc60facdfbc50e910c47a92405fef44c", upload-time = "2025-01-30T01:01:25Z" }, +] + +[[package]] +name = "torch-geometric" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "numpy" }, + { name = "psutil" }, + { name = "pyparsing" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/63/b210152635902da7fe79fcdd16517fae108f457a0ed22c737e702a9afbae/torch_geometric-2.7.0.tar.gz", hash = "sha256:f9099e4aece1a9f618c84dbaac33a77f43139736698c7e8bddf3301ef1f2e8d4", size = 876725, upload-time = "2025-10-15T20:48:03.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/4dffd7300500465e0b4a2ae917dcb2ce771de0b9a772670365799a27c024/torch_geometric-2.7.0-py3-none-any.whl", hash = "sha256:6e0cd3ad824d484651ef5d308fc66c687bfcf5ba040d56d1e0fe0f81f365e292", size = 1275346, upload-time = "2025-10-15T20:48:01.949Z" }, +] + +[[package]] +name = "torchmetrics" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lightning-utilities" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" }, + { name = "torch", version = "2.6.0+cu124", source = { registry = "https://download.pytorch.org/whl/cu124" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/34/39b8b749333db56c0585d7a11fa62a283c087bb1dfc897d69fb8cedbefb1/torchmetrics-1.9.0.tar.gz", hash = "sha256:a488609948600df52d3db4fcdab02e62aab2a85ef34da67037dc3e65b8512faa", size = 581765, upload-time = "2026-03-09T17:41:22.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/a2/c7f6ebf546f8f644edf0f999aa98ece106986a77a7b922316bf6414ff825/torchmetrics-1.9.0-py3-none-any.whl", hash = "sha256:bfdcbff3dd1d96b3374bb2496eb39f23c4b28b8a845b6a18c313688e0d2d9ca1", size = 983384, upload-time = "2026-03-09T17:41:19.756Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "transformers" +version = "5.8.1" +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/e7/e6/4134ea2fbea322cddc7ffc94a0d8ee47fe32ce8e876b320cd37d88edfc4d/transformers-5.8.1.tar.gz", hash = "sha256:4dd5b6de4105725104d84fd6abd74b305f4debfc251b38c648ee5dd087cf543b", size = 8532019, upload-time = "2026-05-13T03:21:57.234Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b1/8be7e7ef0b5200491312201918b6125ef9c9df9dd0f0240ccef9ac824e6b/transformers-5.8.1-py3-none-any.whl", hash = "sha256:5340fb95962162cdfdae5cc91d7f8fedd92ed75216c1154c5e1f590fcf56dd0e", size = 10632882, upload-time = "2026-05-13T03:21:52.876Z" }, +] + +[[package]] +name = "triton" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/00/59500052cb1cf8cf5316be93598946bc451f14072c6ff256904428eaf03c/triton-3.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d9b215efc1c26fa7eefb9a157915c92d52e000d2bf83e5f69704047e63f125c", size = 253159365, upload-time = "2025-01-22T19:13:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/37a3384d1e2e9320331baca41e835e90a3767303642c7a80d4510152cbcf/triton-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5dfa23ba84541d7c0a531dfce76d8bcd19159d50a4a8b14ad01e91734a5c1b0", size = 253154278, upload-time = "2025-01-22T19:13:54.221Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[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 = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + [[package]] name = "virtualenv" version = "21.3.1" @@ -352,3 +2003,232 @@ sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b855 wheels = [ { url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" }, ] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] + +[[package]] +name = "xxhash" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/2f/e183a1b407002f5af81822bee18b61cdb94b8670208ef34734d8d2b8ebe9/xxhash-3.7.0.tar.gz", hash = "sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae", size = 82022, upload-time = "2026-04-25T11:10:32.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/8a/51a14cdef4728c6c2337db8a7d8704422cc65676d9199d77215464c880af/xxhash-3.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:082c87bfdd2b9f457606c7a4a53457f4c4b48b0cdc48de0277f4349d79bb3d7a", size = 33357, upload-time = "2026-04-25T11:06:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/0c2c933809421ffd9bf42b59315552c143c755db5d9a816b2f1ae273e884/xxhash-3.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e7ce913b61f35b0c1c839a49ac9c8e75dd8d860150688aed353b0ce1bf409d8", size = 30869, upload-time = "2026-04-25T11:06:21.989Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/89d5fdd6ee12d70ba99451de46dd0e8010167468dcd913ec855653f4dd50/xxhash-3.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3beb1de3b1e9694fcdd853e570ee64c631c7062435d2f8c69c1adf809bc086f0", size = 194100, upload-time = "2026-04-25T11:06:23.586Z" }, + { url = "https://files.pythonhosted.org/packages/87/ee/2f9f2ed993e77206d1e66991290a1ebe22e843351ca3ebec8e49e01ba186/xxhash-3.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3e7b689c3bce16699efcf736066f5c6cc4472c3840fe4b22bd8279daf4abdac", size = 212977, upload-time = "2026-04-25T11:06:25.019Z" }, + { url = "https://files.pythonhosted.org/packages/de/60/5a91644615a9e9d4e42c2e9925f1908e3a24e4e691d9de7340d565bea024/xxhash-3.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a6545e6b409e3d5cbafc850fb84c55a1ca26ed15a6b11e3bf07a0e0cd84517c8", size = 236373, upload-time = "2026-04-25T11:06:26.482Z" }, + { url = "https://files.pythonhosted.org/packages/22/c0/f3a9384eaaed9d14d4d062a5d953aa0da489bfe9747877aa994caa87cd0b/xxhash-3.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:31ab1461c77a11461d703c88eb949e132a1c6515933cf675d97ec680f4bd18de", size = 212229, upload-time = "2026-04-25T11:06:28.065Z" }, + { url = "https://files.pythonhosted.org/packages/2e/67/02f07a9fd79726804190f2172c4894c3ed9a4ebccaca05653c84beb58025/xxhash-3.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c4d596b7676f811172687ec567cbafb9e4dea2f9be1bbb4f622410cb7f40f40", size = 445462, upload-time = "2026-04-25T11:06:30.048Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/558f5a90c0672fc9b4402dc25d87ac5b7406616e8969430c9ca4e52ee74d/xxhash-3.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13805f0461cba0a857924e70ff91ae6d52d2598f79a884e788db80532614a4a1", size = 193932, upload-time = "2026-04-25T11:06:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/aaa09cd58661d32044dbbad7df55bbe22a623032b810e7ed3b8c569a2a6f/xxhash-3.7.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d398f372496152f1c6933a33566373f8d1b37b98b8c9d608fa6edc0976f23b2", size = 284807, upload-time = "2026-04-25T11:06:33.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f3/53df3719ab127a02c174f0c1c74924fcd110866e89c966bc7909cfa8fa84/xxhash-3.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d610aa62cdb7d4d497740741772a24a794903bf3e79eaa51d2e800082abe11e5", size = 210445, upload-time = "2026-04-25T11:06:35.488Z" }, + { url = "https://files.pythonhosted.org/packages/72/33/d219975c0e8b6fa2eb9ccd486fe47e21bf1847985b878dd2fbc3126e0d5c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:073c23900a9fbf3d26616c17c830db28af9803677cd5b33aea3224d824111514", size = 241273, upload-time = "2026-04-25T11:06:37.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/50/49b1afe610eb3964cedcb90a4d4c3d46a261ee8669cbd4f060652619ae3c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:418a463c3e6a590c0cdc890f8be19adb44a8c8acd175ca5b2a6de77e61d0b386", size = 197950, upload-time = "2026-04-25T11:06:39.148Z" }, + { url = "https://files.pythonhosted.org/packages/c6/75/5f42a1a4c78717d906a4b6a140c6dbf837ab1f547a54d23c4e2903310936/xxhash-3.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:03f8ff4474ee61c845758ce00711d7087a770d77efb36f7e74a6e867301000b8", size = 210709, upload-time = "2026-04-25T11:06:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/8a/85/237e446c25abced71e9c53d269f2cef5bab8a82b3f88a12e00c5368e7368/xxhash-3.7.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:44fba4a5f1d179b7ddc7b3dc40f56f9209046421679b57025d4d8821b376fd8d", size = 275345, upload-time = "2026-04-25T11:06:42.525Z" }, + { url = "https://files.pythonhosted.org/packages/62/34/c2c26c0a6a9cc739bc2a5f0ae03ba8b87deb12b8bce35f7ac495e790dc6d/xxhash-3.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31e3516a0f829d06ded4a2c0f3c7c5561993256bfa1c493975fb9dc7bfa828a1", size = 414056, upload-time = "2026-04-25T11:06:44.343Z" }, + { url = "https://files.pythonhosted.org/packages/a0/aa/5c58e9bc8071b8afd8dcf297ff362f723c4892168faba149f19904132bf4/xxhash-3.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b59ee2ac81de57771a09ecad09191e840a1d2fae1ef684208320591055768f83", size = 191485, upload-time = "2026-04-25T11:06:46.262Z" }, + { url = "https://files.pythonhosted.org/packages/d4/69/a929cf9d1e2e65a48b818cdce72cb6b69eab2e6877f21436d0a1942aff43/xxhash-3.7.0-cp312-cp312-win32.whl", hash = "sha256:74bbd92f8c7fcc397ba0a11bfdc106bc72ad7f11e3a60277753f87e7532b4d81", size = 30671, upload-time = "2026-04-25T11:06:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/104b41a8947f4e1d4a66ce1e628eea752f37d1890bfd7453559ca7a3d950/xxhash-3.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:7bd7bc82dd4f185f28f35193c2e968ef46131628e3cac62f639dadf321cba4d1", size = 31514, upload-time = "2026-04-25T11:06:49.279Z" }, + { url = "https://files.pythonhosted.org/packages/98/a0/1fd0ea1f1b886d9e7c73f0397571e22333a7d79e31da6d7127c2a4a71d75/xxhash-3.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:7d7148180ec99ba36585b42c8c5de25e9b40191613bc4be68909b4d25a77a852", size = 27761, upload-time = "2026-04-25T11:06:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/d5174b4c36d10f64d4ca7050563138c5a599efb01a765858ddefc9c1202a/xxhash-3.7.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:4b6d6b33f141158692bd4eafbb96edbc5aa0dabdb593a962db01a91983d4f8fa", size = 36813, upload-time = "2026-04-25T11:06:51.73Z" }, + { url = "https://files.pythonhosted.org/packages/41/d0/abc6c9d347ba1f1e1e1d98125d0881a0452c7f9a76a9dd03a7b5d2197f23/xxhash-3.7.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:845d347df254d6c619f616afa921331bada8614b8d373d58725c663ba97c3605", size = 35121, upload-time = "2026-04-25T11:06:53.048Z" }, + { url = "https://files.pythonhosted.org/packages/bf/11/4cc834eb3d79f2f2b3a6ef7324195208bcdfbdcf7534d2b17267aa5f3a8f/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fddbbb69a6fff4f421e7a0d1fa28f894b20112e9e3fab306af451e2dfd0e459b", size = 29624, upload-time = "2026-04-25T11:06:54.311Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/e97d3e7b635fe73a1dfb1e91f805324dd6d930bb42041cbf18f183bc0b6d/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:54876a4e45101cec2bf8f31a973cda073a23e2e108538dad224ba07f85f22487", size = 30638, upload-time = "2026-04-25T11:06:55.864Z" }, + { url = "https://files.pythonhosted.org/packages/f4/40/d84951d80c35db1f4c40a29a64a8520eea5d56e764c603906b4fe763580f/xxhash-3.7.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:0c72fe9c7e3d6dfd7f1e21e224a877917fa09c465694ba4e06464b9511b65544", size = 33323, upload-time = "2026-04-25T11:06:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/c7dc6558d97e9ab023f663d69ab28b340ed9bf4d2d94f2c259cf896bb354/xxhash-3.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a6d73a830b17ef49bc04e00182bd839164c1b3c59c127cd7c54fcb10c7ed8ee8", size = 33362, upload-time = "2026-04-25T11:06:58.656Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6e/46b84017b1301d54091430353d4ad5901654a3e0871649877a416f7f1644/xxhash-3.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c3b07cf3362086d8f126c6aecd8e5e9396ad8b2f2219ea7e49a8250c318acd", size = 30874, upload-time = "2026-04-25T11:06:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/df/5e/8f9158e3ab906ad3fec51e09b5ea0093e769f12207bfa42a368ca204e7ab/xxhash-3.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50e879ebbac351c81565ca108db766d7832f5b8b6a5b14b8c0151f7190028e3d", size = 194185, upload-time = "2026-04-25T11:07:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/f3/29/a804ded9f5d3d3758292678d23e7528b08fda7b7e750688d08b052322475/xxhash-3.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:921c14e93817842dd0dd9f372890a0f0c72e534650b6ab13c5be5cd0db11d47e", size = 213033, upload-time = "2026-04-25T11:07:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/91/1ce5a7d2fdc975267320e2c78fc1cecfe7ab735ccbcf6993ec5dd541cb2c/xxhash-3.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e64a7c9d7dfca3e0fafcbc5e455519090706a3e36e95d655cec3e04e79f95aaa", size = 236140, upload-time = "2026-04-25T11:07:05.396Z" }, + { url = "https://files.pythonhosted.org/packages/34/04/fd595a4fd8617b05fa27bd9b684ecb4985bfed27917848eea85d54036d06/xxhash-3.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2220af08163baf5fa36c2b8af079dc2cbe6e66ae061385267f9472362dfd53c6", size = 212291, upload-time = "2026-04-25T11:07:06.966Z" }, + { url = "https://files.pythonhosted.org/packages/03/fb/f1a379cbc372ae5b9f4ab36154c48a849ca6ebe3ac477067a57865bf3bc6/xxhash-3.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f14bb8b22a4a91325813e3d553b8963c10cf8c756cff65ee50c194431296c655", size = 445532, upload-time = "2026-04-25T11:07:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/65/59/172424b79f8cfd4b6d8a122b2193e6b8ad4b11f7159bb3b6f9b3191329bb/xxhash-3.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496736f86a9bedaf64b0dc70e3539d0766df01c71ea22032698e88f3f04a1ce9", size = 193990, upload-time = "2026-04-25T11:07:10.315Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/aeac22161d953f139f07ba5586cb4a17c5b7b6dff985122803bb12933500/xxhash-3.7.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0ff71596bd79816975b3de7130ab1ff4541410285a3c084584eeb1c8239996fd", size = 284876, upload-time = "2026-04-25T11:07:12.15Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/4fd0b59e7a02242953da05ff679fbb961b0a4368eac97a217e11dae110c1/xxhash-3.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1ad86695c19b1d46fe106925db3c7a37f16be37669dcf58dcc70a9dd6e324676", size = 210495, upload-time = "2026-04-25T11:07:13.952Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fb/976a3165c728c7faf74aa1b5ab3cf6a85e6d731612894741840524c7d28c/xxhash-3.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:970f9f8c50961d639cbd0d988c96f80ddf66006de93641719282c4fe7a87c5e6", size = 241331, upload-time = "2026-04-25T11:07:15.557Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2c/6763d5901d53ac9e6ba296e5717ae599025c9d268396e8faa8b4b0a8e0ac/xxhash-3.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5886ad85e9e347911783760a1d16cb6b393e8f9e3b52c982568226cb56927bdc", size = 198037, upload-time = "2026-04-25T11:07:17.563Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/876e722d533833f5f9a83473e6ba993e48745701096944e77bbecf29b2c3/xxhash-3.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6e934bbae1e0ec74e27d5f0d7f37ef547ce5ff9f0a7e63fb39e559fc99526734", size = 210744, upload-time = "2026-04-25T11:07:19.055Z" }, + { url = "https://files.pythonhosted.org/packages/21/e6/d7e7baef7ce24166b4668d3c48557bb35a23b92ecadcac7e7718d099ab69/xxhash-3.7.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3b6b3d28228af044ebcded71c4a3dd86e1dbd7e2f4645bf40f7b5da65bb5fb5a", size = 275406, upload-time = "2026-04-25T11:07:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/92/fe/198b3763b2e01ca908f2154969a2352ec99bda892b574a11a9a151c5ede4/xxhash-3.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6be4d70d9ab76c9f324ead9c01af6ff52c324745ea0c3731682a0cf99720f1fe", size = 414125, upload-time = "2026-04-25T11:07:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6d/019a11affd5a5499137cacca53808659964785439855b5aa40dfd3412916/xxhash-3.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:151d7520838d4465461a0b7f4ae488b3b00de16183dd3214c1a6b14bf89d7fb6", size = 191555, upload-time = "2026-04-25T11:07:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/76/21/b96d58568df2d01533244c3e0e5cbdd0c8b2b25c4bec4d72f19259a292d7/xxhash-3.7.0-cp313-cp313-win32.whl", hash = "sha256:d798c1e291bffb8e37b5bbe0dda77fc767cd19e89cadaf66e6ed5d0ff88c9fe6", size = 30668, upload-time = "2026-04-25T11:07:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/99/57/d849a8d3afa1f8f4bc6a831cd89f49f9706fbbad94d2975d6140a171988c/xxhash-3.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:875811ba23c543b1a1c3143c926e43996eb27ebb8f52d3500744aa608c275aed", size = 31524, upload-time = "2026-04-25T11:07:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/81/52/bacc753e92dee78b058af8dcef0a50815f5f860986c664a92d75f965b6a5/xxhash-3.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:54a675cb300dda83d71daae2a599389d22db8021a0f8db0dd659e14626eb3ecc", size = 27768, upload-time = "2026-04-25T11:07:29.113Z" }, + { url = "https://files.pythonhosted.org/packages/1c/47/ddbd683b7fc7e592c1a8d9d65f73ce9ab513f082b3967eee2baf549b8fc6/xxhash-3.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a3b19a42111c4057c1547a4a1396a53961dca576a0f6b82bfa88a2d1561764b2", size = 33576, upload-time = "2026-04-25T11:07:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/07/f2/36d3310161db7f72efb4562aadde0ed429f1d0531782dd6345b12d2da527/xxhash-3.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8f4608a06e4d61b7a3425665a46d00e0579122e1a2fae97a0c52953a3aad9aa3", size = 31123, upload-time = "2026-04-25T11:07:31.989Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3f/75937a5c69556ed213021e43cbedd84c8e0279d0d74e7d41a255d84ba4b1/xxhash-3.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad37c7792479e49cf96c1ab25517d7003fe0d93687a772ba19a097d235bbe41e", size = 196491, upload-time = "2026-04-25T11:07:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/f10d7ff8c7a733d4403a43b9de18c8fabc005f98cec054644f04418659ee/xxhash-3.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc026e3b89d98e30a8288c95cb696e77d150b3f0fb7a51f73dcd49ee6b5577fa", size = 215793, upload-time = "2026-04-25T11:07:34.919Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fd/778f60aa295f58907938f030a8b514611f391405614a525cccd2ffc00eb5/xxhash-3.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9b31ab1f28b078a6a1ac1a54eb35e7d5390deddd56870d0be3a0a733d1c321c", size = 237993, upload-time = "2026-04-25T11:07:36.638Z" }, + { url = "https://files.pythonhosted.org/packages/70/f5/736db5de387b4a540e37a05b84b40dc58a1ce974bfd2b4e5754ce29b68c3/xxhash-3.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bb5fd680c038fd5229e44e9c493782f90df9bef632fd0499d442374688ff70b", size = 214887, upload-time = "2026-04-25T11:07:38.564Z" }, + { url = "https://files.pythonhosted.org/packages/4d/aa/09a095f22fdb9a27fbb716841fbff52119721f9ca4261952d07a912f7839/xxhash-3.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030c0fd688fce3569fbb49a2feefd4110cbb0b650186fb4610759ecfac677548", size = 448407, upload-time = "2026-04-25T11:07:40.552Z" }, + { url = "https://files.pythonhosted.org/packages/74/8a/b745efeeca9e34a91c26fdc97ad8514c43d5a81ac78565cba80a1353870a/xxhash-3.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b1bde10324f4c31812ae0d0502e92d916ae8917cad7209353f122b8b8f610c3", size = 196119, upload-time = "2026-04-25T11:07:42.101Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5c/0cfceb024af90c191f665c7933b1f318ee234f4797858383bebd1881d52f/xxhash-3.7.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:503722d52a615f2604f5e7611de7d43878df010dc0053094ef91cb9a9ac3d987", size = 286751, upload-time = "2026-04-25T11:07:43.568Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0a/0793e405dc3cf8f4ebe2c1acec1e4e4608cd9e7e50ea691dabbc2a95ccbb/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c72500a3b6d6c30ebfc135035bcace9eb5884f2dc220804efcaaba43e9f611dd", size = 212961, upload-time = "2026-04-25T11:07:45.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/721118ffc63bfff94aa565bcf2555a820f9f4bdb0f001e0d609bdfad70de/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:43475925a766d01ca8cd9a857fd87f3d50406983c8506a4c07c4df12adcc867f", size = 243703, upload-time = "2026-04-25T11:07:47.053Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/16f6267160488b8276fd3d449d425712512add292ba545c1b6946bfdb7dd/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8d09dfd2ab135b985daf868b594315ebe11ad86cd9fea46e6c69f19b28f7d25a", size = 200894, upload-time = "2026-04-25T11:07:48.657Z" }, + { url = "https://files.pythonhosted.org/packages/2d/94/80ba841287fd97e3e9cac1d228788c8ef623746f570404961eec748ecb5c/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c50269d0055ac1faecfd559886d2cbe4b730de236585aba0e873f9d9dadbe585", size = 213357, upload-time = "2026-04-25T11:07:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/106d4067130c59f1e18a55ffadcd876d8c68534883a1e02685b29d3d8153/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1910df4756a5ab58cfad8744fc2d0f23926e3efcc346ee76e87b974abab922f4", size = 277600, upload-time = "2026-04-25T11:07:51.745Z" }, + { url = "https://files.pythonhosted.org/packages/c5/86/a081dd30da71d720b2612a792bfd55e45fa9a07ac76a0507f60487473c25/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d006faf3b491957efcb433489be3c149efe4787b7063d5cddb8ddaefdc60e0c1", size = 416980, upload-time = "2026-04-25T11:07:53.504Z" }, + { url = "https://files.pythonhosted.org/packages/35/29/1a95221a029a3c1293773869e1ab47b07cbbdd82444a42809e8c60156626/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:abb65b4e947e958f7b3b0d71db3ce447d1bc5f37f5eab871ce7223bda8768a04", size = 193840, upload-time = "2026-04-25T11:07:55.103Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/db909dd0823285de2286f67e10ee4d81e96ad35d7d8e964ecb07fccd8af9/xxhash-3.7.0-cp313-cp313t-win32.whl", hash = "sha256:178959906cb1716a1ce08e0d69c82886c70a15a6f2790fc084fdd146ca30cd49", size = 30966, upload-time = "2026-04-25T11:07:56.524Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ff/d705b15b22f21ee106adce239cb65d35067a158c630b240270f09b17c2e6/xxhash-3.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2524a1e20d4c231d13b50f7cf39e44265b055669a64a7a4b9a2a44faa03f19b6", size = 31784, upload-time = "2026-04-25T11:07:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1f/b2cf83c3638fd0588e0b17f22e5a9400bdfb1a3e3755324ac0aee2250b88/xxhash-3.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:37d994d0ffe81ef087bb330d392caa809bb5853c77e22ea3f71db024a0543dba", size = 27932, upload-time = "2026-04-25T11:07:59.109Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cc/431db584f6fbb9312e40a173af027644e5580d39df1f73603cbb9dca4d6b/xxhash-3.7.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:8c5fcfd806c335bfa2adf1cd0b3110a44fc7b6995c3a648c27489bae85801465", size = 36644, upload-time = "2026-04-25T11:08:00.658Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/255ec513e0a705d1f9a61413e78dfce4e3235203f0ed525a24c2b4b56345/xxhash-3.7.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:506a0b488f190f0a06769575e30caf71615c898ed93ab18b0dbcb6dec5c3713c", size = 35003, upload-time = "2026-04-25T11:08:02.338Z" }, + { url = "https://files.pythonhosted.org/packages/68/70/c55fc33c93445b44d8fc5a17b41ed99e3cebe92bcf8396809e63fc9a1165/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ec68dbba21532c0173a9872298e65c89749f7c9d21538c3a78b5bb6105871568", size = 29655, upload-time = "2026-04-25T11:08:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/c2/72/ff8de73df000d74467d12a59ce6d6e2b2a368b978d41ab7b1fba5ed442be/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa77e7ec1450d415d20129961814787c9abd9a07f98872f070b1fe96c5084611", size = 30664, upload-time = "2026-04-25T11:08:05.011Z" }, + { url = "https://files.pythonhosted.org/packages/b6/91/08416d9bd9bc3bf39d831abe8a5631ac2db5141dfd6fe81c3fe59a1f9264/xxhash-3.7.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fe32736295ea38e43e7d9424053c8c47c9f64fecfc7c895fb3da9b30b131c9ee", size = 33317, upload-time = "2026-04-25T11:08:06.413Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3b/86b1caa4dee10a99f4bf9521e623359341c5e50d05158fa10c275b2bd079/xxhash-3.7.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ab9dd2c83c4bbd63e422181a76f13502d049d3ddcac9a1bdc29196263d692bb8", size = 33457, upload-time = "2026-04-25T11:08:08.099Z" }, + { url = "https://files.pythonhosted.org/packages/ed/38/98ea14ad1517e1461292a65906951458d520689782bfbae111050145bdba/xxhash-3.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3afec3a336a2286601a437cb07562ab0227685e6fbb9ec17e8c18457ff348ecf", size = 30894, upload-time = "2026-04-25T11:08:09.429Z" }, + { url = "https://files.pythonhosted.org/packages/61/a2/074654d0b893606541199993c7db70067d9fc63b748e0d60020a52a1bd36/xxhash-3.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:565df64437a9390f84465dcca33e7377114c7ede8d05cd2cf20081f831ea788e", size = 194409, upload-time = "2026-04-25T11:08:10.91Z" }, + { url = "https://files.pythonhosted.org/packages/e2/26/6d2a1afc468189f77ca28c32e1c83e1b9da1178231e05641dbc1b350e332/xxhash-3.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12eca820a5d558633d423bf8bb78ce72a55394823f64089247f788a7e0ae691e", size = 213135, upload-time = "2026-04-25T11:08:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0e/d8aecf95e09c42547453137be74d2f7b8b14e08f5177fa2fab6144a19061/xxhash-3.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f262b8f7599516567e070abf607b9af649052b2c4bd6f9be02b0cb41b7024805", size = 236379, upload-time = "2026-04-25T11:08:14.206Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/8140e8210536b3dd0cc816c4faaeb5ba6e63e8125ab25af4bcddd6a037b3/xxhash-3.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1598916cb197681e03e601901e4ab96a9a963de398c59d0964f8a6f44a2b361", size = 212447, upload-time = "2026-04-25T11:08:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/462001d2903b4bee5a5689598a0a55e5e7cd1ac7f4247a5545cff10d3ebb/xxhash-3.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:322b2f0622230f526aeb1738149948a7ae357a9e2ceb1383c6fd1fdaecdafa16", size = 445660, upload-time = "2026-04-25T11:08:17.441Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/2bd1ed7f8689b20e51727952cac8329d50c694dc32b2eba06ba5bc742b37/xxhash-3.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cc22070880cc57b830a65cde4e65fa884c6d9b28ae4803b5ee05911e7bafba", size = 194076, upload-time = "2026-04-25T11:08:19.134Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6e/692302cd0a5f4ac4e6289f37fa888dc2e1e07750b68fe3e4bfe939b8cea3/xxhash-3.7.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb5a888a968b2434abf9ecda357b5d43f10d7b5a6da6fdbbe036208473aff0e2", size = 284990, upload-time = "2026-04-25T11:08:20.618Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/e54b159b3d9df7999d2a7c676ce7b323d1b5588a64f8f51ed8172567bd87/xxhash-3.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a999771ff97bec27d18341be4f3a36b163bb1ac41ec17bef6d2dabd84acd33c7", size = 210590, upload-time = "2026-04-25T11:08:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/0e0df1a3a196ced4ca71de76d65ead25d8e87bbfb87b64306ea47a40c00d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ed4a6efe2dee1655adb73e7ad40c6aa955a6892422b1e3b95de6a34de56e3cbb", size = 241442, upload-time = "2026-04-25T11:08:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a9/d917a7a814e90b218f8a0d37967105eea91bf752c3303683c99a1f7bfc1f/xxhash-3.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fd17f14ac0faa12126c2f9ca774a8cf342957265ec3c8669c144e5e6cdb478c", size = 198356, upload-time = "2026-04-25T11:08:25.99Z" }, + { url = "https://files.pythonhosted.org/packages/89/5e/f2ba1877c39469abbefc72991d6ebdcbd4c0880db01ae8cb1f553b0c537d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:05fd1254268c59b5cb2a029dfc204275e9fc52de2913f1e53aa8d01442c96b4d", size = 210898, upload-time = "2026-04-25T11:08:27.608Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/be56b58e73de531f39a10de1355bb77ceb663900dc4bf2d6d3002a9c3f9e/xxhash-3.7.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a2eae53197c6276d5b317f75a1be226bbf440c20b58bf525f36b5d0e1f657ca6", size = 275519, upload-time = "2026-04-25T11:08:29.301Z" }, + { url = "https://files.pythonhosted.org/packages/92/e2/17ddc85d5765b9c709f192009ed8f5a1fc876f4eb35bba7c307b5b1169f9/xxhash-3.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bfe6f92e3522dcbe8c4281efd74fa7542a336cb00b0e3272c4ec0edabeaeaf67", size = 414191, upload-time = "2026-04-25T11:08:31.16Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/85f5b79f4bf1ec7ba052491164adfd4f4e9519f5dc7246de4fbd64a1bd56/xxhash-3.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7ab9a49c410d8c6c786ab99e79c529938d894c01433130353dd0fe999111077a", size = 191604, upload-time = "2026-04-25T11:08:32.862Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d0/6127b623aa4cca18d8b7743592b048d689fd6c6e37ff26a22cddf6cd9d7f/xxhash-3.7.0-cp314-cp314-win32.whl", hash = "sha256:040ea63668f9185b92bc74942df09c7e65703deed71431333678fc6e739a9955", size = 31271, upload-time = "2026-04-25T11:08:34.651Z" }, + { url = "https://files.pythonhosted.org/packages/64/4f/44fc4788568004c43921701cbc127f48218a1eede2c9aea231115323564d/xxhash-3.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2a61e2a3fb23c892496d587b470dee7fa1b58b248a187719c65ea8e94ec13257", size = 32284, upload-time = "2026-04-25T11:08:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/6d/77/18bb895eb60a49453d16e17d67990e5caff557c78eafc90ad4e2eabf4570/xxhash-3.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:c7741c7524961d8c0cb4d4c21b28957ff731a3fd5b5cd8b856dc80a40e9e5acc", size = 28701, upload-time = "2026-04-25T11:08:37.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/a0/46f72244570c550fbbb7db1ef554183dd5ebe9136385f30e032b781ae8f6/xxhash-3.7.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:fc84bf7aa7592f31ec63a3e7b11d624f468a3f19f5238cec7282a42e838ab1d7", size = 33646, upload-time = "2026-04-25T11:08:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3a/453846a7eceea11e75def361eed01ec6a0205b9822c19927ed364ccae7cc/xxhash-3.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f1563fdc8abfc389748e6932c7e4e99c89a53e4ec37d4563c24fc06f5e5644b", size = 31125, upload-time = "2026-04-25T11:08:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3e/49434aba738885d512f9e486db1bdd19db28dfa40372b56da26ef7a4e738/xxhash-3.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d415f18becf6f153046ab6adc97da77e3643a0ee205dae61c4012604113a020", size = 196633, upload-time = "2026-04-25T11:08:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e9/006cb6127baeb9f8abe6d15e62faa01349f09b34e2bfd65175b2422d026b/xxhash-3.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb16aa13ed175bc9be5c2491ba031b85a9b51c4ed90e0b3d4ebe63cf3fb54f8e", size = 215899, upload-time = "2026-04-25T11:08:43.645Z" }, + { url = "https://files.pythonhosted.org/packages/27/e4/cc57d72e66df0ae29b914335f1c6dcf61e8f3746ddf0ae3c471aa4f15e00/xxhash-3.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f9fd595f1e5941b3d7863e4774e4b30caa6731fc34b9277da032295aa5656ee5", size = 238116, upload-time = "2026-04-25T11:08:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/af/78/3531d4a3fd8a0038cc6be1f265a69c1b3587f557a10b677dd736de2202c1/xxhash-3.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1295325c5a98d552333fa53dc2b026b0ef0ec9c8e73ca3a952990b4c7d65d459", size = 215012, upload-time = "2026-04-25T11:08:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f6/259fb1eaaec921f59b17203b0daee69829761226d3b980d5191d7723dd83/xxhash-3.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3573a651d146912da9daa9e29e5fbc45994420daaa9ef1e2fa5823e1dc485513", size = 448534, upload-time = "2026-04-25T11:08:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7b/16/a66d0eaf6a7e68532c07714361ddc904c663ec940f3b028c1ae4a21a7b9d/xxhash-3.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ec1e080a3d02d94ea9335bfab0e3374b877e25411422c18f51a943fa4b46381", size = 196217, upload-time = "2026-04-25T11:08:50.805Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ef/d2efc7fc51756dc52509109d1a25cefc859d74bc4b19a167b12dbd8c2786/xxhash-3.7.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84415265192072d8638a3afc3c1bc5995e310570cd9acb54dc46d3939e364fe0", size = 286906, upload-time = "2026-04-25T11:08:52.418Z" }, + { url = "https://files.pythonhosted.org/packages/fc/67/25decd1d4a4018582ec4db2a868a2b7e40640f4adb20dfeb19ac923aa825/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d4dea659b57443989ef32f4295104fd6912c73d0bf26d1d148bb88a9f159b02", size = 213057, upload-time = "2026-04-25T11:08:54.105Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5d/17651eb29d06786cdc40c60ae3d27d645aa5d61d2eca6237a7ba0b94789b/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05ece0fe4d9c9c2728912d1981ae1566cfc83a011571b24732cbf76e1fb70dca", size = 243886, upload-time = "2026-04-25T11:08:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d4/174d9cf7502243d586e6a9ae842b1ae23026620995114f85f1380e588bc9/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fd880353cf1ffaf321bc18dd663e111976dbd0d3bbd8a66d58d2b470dfa7f396", size = 201015, upload-time = "2026-04-25T11:08:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/91/8c/2254e2d06c3ac5e6fe22eaf3da791b87ea823ae9f2c17b4af66755c5752d/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4e15cc9e2817f6481160f930c62842b3ff419e20e13072bcbab12230943092bc", size = 213457, upload-time = "2026-04-25T11:08:59.826Z" }, + { url = "https://files.pythonhosted.org/packages/79/a2/e3daa762545921173e3360f3b4ff7fc63c2d27359f7230ec1a7a74e117f6/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:90b9d1a8bd37d768ffc92a1f651ec69afc532a96fa1ac2ea7abbed5d630b3237", size = 277738, upload-time = "2026-04-25T11:09:01.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4c/e186da2c46b87f5204640e008d42730bf3c1ee9f0efb71ae1ebcdfeac681/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:157c49475b34ecea8809e51123d9769a534e139d1247942f7a4bc67710bb2533", size = 417127, upload-time = "2026-04-25T11:09:03.592Z" }, + { url = "https://files.pythonhosted.org/packages/17/28/3798e15007a3712d0da3d3fe70f8e11916569858b5cc371053bc26270832/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a6ddec83325685e729ca119d1f5c518ec39294212ecd770e60693cdc5f7eb79", size = 193962, upload-time = "2026-04-25T11:09:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/ad/95/a26baa93b5241fd7630998816a4ec47a5a0bad193b3f8fc8f3593e1a4a67/xxhash-3.7.0-cp314-cp314t-win32.whl", hash = "sha256:a04a6cab47e2166435aaf5b9e5ee41d1532cc8300efdef87f2a4d0acb7db19ed", size = 31643, upload-time = "2026-04-25T11:09:08.153Z" }, + { url = "https://files.pythonhosted.org/packages/44/36/5454f13c447e395f9b06a3e91274c59f503d31fad84e1836efe3bdb71f6a/xxhash-3.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8653dd7c2eda020545bb2c71c7f7039b53fe7434d0fc1a0a9deb79ab3f1a4fc1", size = 32522, upload-time = "2026-04-25T11:09:09.534Z" }, + { url = "https://files.pythonhosted.org/packages/74/35/698e7e3ff38e22992ea24870a511d8762474fb6783627a2910ff22a185c2/xxhash-3.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:468f0fc114faaa4b36699f8e328bbc3bb11dc418ba94ac52c26dd736d4b6c637", size = 28807, upload-time = "2026-04-25T11:09:11.234Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +]