ML-based recommendation feed for Bluesky posts

Switch to memory mapped dataset and get training scripts set up

+432 -63
+1 -1
.gitignore
··· 1 1 .env 2 2 **/__pycache__/** 3 3 data/* 4 - .vscode 4 + .vscode
+1
data/training/.gitignore
··· 1 + ./*
+1
pyproject.toml
··· 13 13 "rich>=14.1.0", 14 14 "ruff>=0.13.2", 15 15 "torch>=2.8.0", 16 + "torchdata>=0.11.0", 16 17 ]
scripts/__init__.py

This is a binary file and will not be displayed.

+83 -21
scripts/split_follows.py
··· 6 6 import random 7 7 import sys 8 8 9 - import pandas as pd 9 + import numpy as np 10 10 11 11 12 12 logger = logging.getLogger(__name__) ··· 37 37 val_file_idx = 1 38 38 train_follows: list[tuple[int, int]] = [] 39 39 val_follows: list[tuple[int, int]] = [] 40 + train_files = [] 41 + val_files = [] 40 42 for file in os.listdir(follow_dir): 41 43 source_did = file[:-3] # Remove .gz extension 42 44 if source_did not in did_id_map: ··· 50 52 if random.random() < val_split: 51 53 val_follows.append((did_id_map[source_did], did_id_map[target_did])) 52 54 if len(val_follows) >= file_size: 53 - logger.info(f"Saving validation file: {val_file_idx}") 54 - val_df = pd.DataFrame( 55 - val_follows, columns=["source", "target"], index=None 56 - ) 57 - val_df.to_parquet( 58 - os.path.join(output_dir, f"val_follows_{val_file_idx}.parquet") 55 + filename = f"val_follows_{val_file_idx}.data" 56 + logger.info(f"Saving validation file: {filename}") 57 + val_df = np.array(val_follows, dtype="int64") 58 + val_file_df = np.memmap( 59 + os.path.join(output_dir, filename), 60 + dtype="int64", 61 + mode="w+", 62 + shape=val_df.shape, 59 63 ) 64 + val_file_df[:] = val_df[:] 65 + val_file_df.flush() 60 66 val_follows = [] 67 + val_files.append( 68 + { 69 + "filename": filename, 70 + "shape": val_df.shape, 71 + "dtype": "int64", 72 + } 73 + ) 61 74 val_file_idx += 1 62 75 del val_df 76 + del val_file_df 63 77 else: 64 78 train_follows.append((did_id_map[source_did], did_id_map[target_did])) 65 79 if len(train_follows) >= file_size: 66 - logger.info(f"Saving training file: {train_file_idx}") 67 - train_df = pd.DataFrame( 68 - train_follows, columns=["source", "target"], index=None 69 - ) 70 - train_df.to_parquet( 71 - os.path.join( 72 - output_dir, f"train_follows_{train_file_idx}.parquet" 73 - ) 80 + filename = f"train_follows_{train_file_idx}.data" 81 + logger.info(f"Saving training file: {filename}") 82 + train_df = np.array(train_follows, dtype="int64") 83 + train_file_df = np.memmap( 84 + os.path.join(output_dir, filename), 85 + dtype="int64", 86 + mode="w+", 87 + shape=train_df.shape, 74 88 ) 89 + train_file_df[:] = train_df[:] 90 + train_file_df.flush() 75 91 train_follows = [] 92 + train_files.append( 93 + { 94 + "filename": filename, 95 + "shape": train_df.shape, 96 + "dtype": "int64", 97 + } 98 + ) 76 99 train_file_idx += 1 77 100 del train_df 101 + del train_file_df 78 102 79 103 logger.info("Saving remnant files...") 80 104 if len(val_follows): 81 - val_df = pd.DataFrame(val_follows, columns=["source", "target"], index=None) 82 - val_df.to_parquet( 83 - os.path.join(output_dir, f"val_follows_{val_file_idx}.parquet") 105 + filename = f"val_follows_{val_file_idx}.data" 106 + val_df = np.array(val_follows, dtype="int64") 107 + val_file_df = np.memmap( 108 + os.path.join(output_dir, filename), 109 + dtype="int64", 110 + mode="w+", 111 + shape=val_df.shape, 112 + ) 113 + val_file_df[:] = val_df[:] 114 + val_file_df.flush() 115 + val_files.append( 116 + { 117 + "filename": filename, 118 + "shape": val_df.shape, 119 + "dtype": "int64", 120 + } 84 121 ) 85 122 86 123 if len(train_follows): 87 - train_df = pd.DataFrame(train_follows, columns=["source", "target"], index=None) 88 - train_df.to_parquet( 89 - os.path.join(output_dir, f"train_follows_{train_file_idx}.parquet") 124 + filename = f"train_follows_{train_file_idx}.data" 125 + train_df = np.array(train_follows, dtype="int64") 126 + train_file_df = np.memmap( 127 + os.path.join(output_dir, filename), 128 + dtype="int64", 129 + mode="w+", 130 + shape=train_df.shape, 131 + ) 132 + train_file_df[:] = train_df[:] 133 + train_file_df.flush() 134 + train_files.append( 135 + { 136 + "filename": filename, 137 + "shape": train_df.shape, 138 + "dtype": "int64", 139 + } 90 140 ) 91 141 92 142 logger.info("Finished reading follows") ··· 95 145 logger.info("Writing id map...") 96 146 with open(os.path.join(output_dir, "did_id_map.json"), "w") as out_file: 97 147 out_file.write(json.dumps(did_id_map)) 148 + 149 + # Write out file metadata 150 + logger.info("Writing dataset metadata") 151 + with open(os.path.join(output_dir, "metadata.json"), "w") as out_file: 152 + out_file.write( 153 + json.dumps( 154 + { 155 + "did_id_map": "did_id_map.json", 156 + "splits": {"training": train_files, "validation": val_files}, 157 + } 158 + ) 159 + ) 98 160 99 161 100 162 if __name__ == "__main__":
scripts/training/__init__.py

This is a binary file and will not be displayed.

+72 -27
scripts/training/data.py
··· 1 1 import json 2 + import glob 3 + import os 2 4 import random 3 5 4 - import polars as pl 5 - from torch import Dataset 6 + import numpy as np 7 + import torch 8 + from torch.utils.data import Dataset 6 9 7 10 8 11 class FollowDataset(Dataset): 9 12 def __init__( 10 - self, did_id_map_path: str, parquet_file_path, negative_sample_count: int 13 + self, 14 + dataset_path: str, 15 + split: str, 16 + negative_sample_count: int, 11 17 ): 12 - with open(did_id_map_path, "r") as in_file: 18 + with open(os.path.join(dataset_path, "metadata.json"), "r") as in_file: 19 + metadata = json.load(in_file) 20 + 21 + with open(os.path.join(dataset_path, metadata["did_id_map"]), "r") as in_file: 13 22 self.did_id_map: dict[str, int] = json.load(in_file) 14 - self.id_arr: list[int] = list(self.did_id_map.keys()) 15 23 16 - self.follows = pl.read_parquet(parquet_file_path) 17 - self.follow_map: dict[int, list[int]] = {} 18 - for row in self.follows.rows(): 19 - if row[0] not in self.follow_map: 20 - self.follow_map[row[0]] = [] 21 - self.follow_map[row[0]].append(row[1]) 24 + if split not in metadata["splits"]: 25 + raise ValueError(f"Could not find split in metadata file: {split}") 26 + split_files = metadata["splits"][split] 27 + 28 + self.parquet_files: list[tuple[str, str, int, int]] = [] 29 + for file in split_files: 30 + file_idx = file["filename"].split("_")[2].split(".")[0] 31 + self.parquet_files.append((file["filename"], file["dtype"], file_idx, file["shape"][0])) # type: ignore 32 + 33 + self.parquet_files.sort(key=lambda x: x[1]) 34 + self.dataframes: list[np.ndarray] = [] 35 + for filename, dtype, _, row_count in self.parquet_files: 36 + self.dataframes.append( 37 + np.memmap( 38 + os.path.join(dataset_path, filename), 39 + dtype=dtype, 40 + mode="r", 41 + shape=(row_count, 2), 42 + ) 43 + ) 22 44 23 45 self.negative_sample_count = negative_sample_count 24 46 25 - def __len__(self): 26 - return self.follows.shape[0] 47 + def __len__(self) -> int: 48 + return sum((row_count for _, _, _, row_count in self.parquet_files)) 49 + 50 + def num_users(self) -> int: 51 + return len(self.did_id_map) 52 + 53 + def _idx_to_row(self, idx: int) -> tuple[int, int]: 54 + if idx < 0 or idx >= len(self): 55 + raise IndexError(f"Invalid index: {idx}") 56 + 57 + # Find which file contains index 58 + row_index_total = 0 59 + effective_idx = idx 60 + i = 0 61 + for i, (_, _, _, row_count) in enumerate(self.parquet_files): 62 + row_index_total += row_count 63 + if idx < row_index_total: 64 + break 65 + effective_idx -= row_count 27 66 28 - def __get_item__(self, idx: int) -> list[tuple[int, int]]: 67 + row = self.dataframes[i][effective_idx] 68 + return (row[0].item(), row[1].item()) 69 + 70 + def __getitem__(self, idx: int) -> tuple[list[tuple[int, int]], list[int]]: 29 71 """ 30 72 Grab follow connection and create negative samples (follows that dont exist) 31 73 """ 32 - sample = self.follows.item(idx) 33 - negative_samples = [] 74 + samples = [self._idx_to_row(idx)] 75 + 34 76 for _ in range(self.negative_sample_count): 35 - attempt = 0 36 - while attempt < 5: 37 - rand_idx = random.randint(0, len(self.id_arr) - 1) 38 - rand_user = self.id_arr[rand_idx] 39 - if rand_user not in self.follow_map[sample[0]]: 40 - break 41 - attempt += 1 42 - if attempt == 5: 43 - raise Exception(f"Failed to find negative sample for id: {sample[0]}") 44 - negative_samples.append((sample[0], rand_user)) 77 + rand_idx = random.randrange(0, len(self.did_id_map)) 78 + # Assume that users arent connected (roughly 1/700 chance they are) 79 + samples.append((samples[0][0], rand_idx)) 45 80 46 - return [(sample[0], sample[1])] + negative_samples 81 + labels = [-1] * len(samples) 82 + labels[0] = 1 83 + return samples, labels 84 + 85 + @staticmethod 86 + def collate_rows( 87 + batch: list[tuple[list[tuple[int, int]], list[int]]], 88 + ) -> tuple[torch.Tensor, torch.Tensor]: 89 + follows = torch.concat(tuple(torch.IntTensor(follow) for follow, _ in batch)) 90 + labels = torch.concat(tuple(torch.IntTensor(label) for _, label in batch)) 91 + return (follows, labels)
+47 -14
scripts/training/models.py
··· 1 1 import torch 2 2 from torch import nn 3 + from torch.utils.data import DataLoader 4 + import torch.nn.functional as F 5 + import lightning as pyl 3 6 7 + from scripts.training.data import FollowDataset 4 8 5 - class FollowEmbed(nn.Module): 9 + 10 + class UserEmbedding(nn.Module): 6 11 def __init__(self, num_embeds: int, size: int): 7 12 super().__init__() 8 - self.source_embed = nn.Embedding(num_embeddings=num_embeds, embedding_dim=size) 9 - self.target_embed = nn.Embedding(num_embeddings=num_embeds, embedding_dim=size) 10 - self.decision_layer = nn.Linear(1, 1) 13 + self.embed = nn.Embedding( 14 + num_embeddings=num_embeds, embedding_dim=size, dtype=torch.bfloat16 15 + ) 11 16 12 17 def forward(self, batch): 13 - source: torch.tensor = self.source_embed(batch[:, 0]) 14 - target: torch.tensor = self.target_embed(batch[:, 1]) 15 - return torch.bmm(source.unsqueeze(1), target.unsqueeze(2)).squeeze() 18 + return self.embed(batch) 19 + 20 + 21 + class FollowEmbedModule(pyl.LightningModule): 22 + def __init__(self, source_embedding, target_embedding): 23 + super().__init__() 24 + self.source_embed = source_embedding 25 + self.target_embed = target_embedding 26 + 27 + def training_step(self, batch, _): 28 + x, y = batch 29 + rotated_x = x.view(x.size(0), -1) 30 + source = self.source_embed(rotated_x[:, 0]) 31 + target: torch.Tensor = self.target_embed(rotated_x[:, 1]) 32 + dot = torch.bmm(source.unsqueeze(1), target.unsqueeze(2)).squeeze() 33 + loss = -1 * torch.mean(torch.log(F.sigmoid(dot * y))) 34 + self.log("train_loss", loss, prog_bar=False) 35 + return loss 16 36 37 + def validation_step(self, batch, _): 38 + x, y = batch 39 + rotated_x = x.view(x.size(0), -1) 40 + source = self.source_embed(rotated_x[:, 0]) 41 + target: torch.Tensor = self.target_embed(rotated_x[:, 1]) 42 + dot = torch.bmm(source.unsqueeze(1), target.unsqueeze(2)).squeeze() 43 + loss = -1 * torch.mean(torch.log(F.sigmoid(dot * y))) 44 + self.log("val_loss", loss, prog_bar=False) 45 + return loss 17 46 18 - if __name__ == "__main__": 19 - embeds = FollowEmbed(10, 4) 20 - x = torch.tensor([[1, 2], [3, 4], [5, 6]]) 21 - print(f"Input tensor: {x.shape}") 22 - y = embeds(x) 23 - print(f"Output shape: {y.shape}") 24 - print(y) 47 + def on_validation_epoch_end(self): 48 + avg_loss = self.trainer.callback_metrics.get("val_loss") 49 + if avg_loss is not None: 50 + print(f"\nValidation Loss: {avg_loss:.4f}\n") 51 + avg_loss = self.trainer.callback_metrics.get("train_loss") 52 + if avg_loss is not None: 53 + print(f"\nTraining Loss: {avg_loss:.4f}\n") 54 + 55 + def configure_optimizers(self): 56 + optimizers = torch.optim.Adagrad(self.parameters(), lr=1e-1) 57 + return optimizers
+113
scripts/training/train.py
··· 1 + from argparse import ArgumentParser 2 + from datetime import datetime 3 + import os 4 + from typing import Optional 5 + 6 + import lightning as pyl 7 + from lightning.pytorch.loggers import WandbLogger 8 + from lightning.pytorch.callbacks import ModelCheckpoint 9 + import torch 10 + from torchdata.stateful_dataloader import StatefulDataLoader 11 + 12 + from scripts.training.data import FollowDataset 13 + from scripts.training.models import UserEmbedding, FollowEmbedModule 14 + 15 + 16 + PROJECT = "goodposts-followers" 17 + SAVE_DIR = "./data/training" 18 + NAME = "following-embedding" 19 + 20 + 21 + def main(run_id: Optional[str], model_checkpoint: Optional[str]): 22 + if run_id is None: 23 + run_id = NAME + str(int(datetime.now().timestamp())) 24 + 25 + print("Loading validation dataset") 26 + val_dataset = FollowDataset( 27 + dataset_path="data/processed/numpy/", 28 + split="validation", 29 + negative_sample_count=0, 30 + ) 31 + 32 + print("Loading training dataset") 33 + train_dataset = FollowDataset( 34 + dataset_path="data/processed/numpy/", 35 + split="training", 36 + negative_sample_count=2, 37 + ) 38 + 39 + val_dataloader = StatefulDataLoader( 40 + val_dataset, 41 + pin_memory=True, 42 + collate_fn=val_dataset.collate_rows, 43 + batch_size=1024, 44 + num_workers=7, 45 + ) 46 + 47 + train_dataloader = StatefulDataLoader( 48 + train_dataset, 49 + pin_memory=True, 50 + collate_fn=train_dataset.collate_rows, 51 + batch_size=1024, 52 + num_workers=7, 53 + ) 54 + 55 + print("Instantiating embedding model") 56 + wandb_logger = WandbLogger( 57 + project=PROJECT, 58 + save_dir=SAVE_DIR, 59 + name=NAME, 60 + id=run_id, 61 + resume="allow", 62 + ) 63 + 64 + checkpoint_callback = ModelCheckpoint( 65 + dirpath=os.path.join(SAVE_DIR, "checkpoints", run_id), 66 + filename="{epoch}-{step}", 67 + save_last=True, 68 + save_top_k=1, 69 + monitor="val_loss", 70 + ) 71 + 72 + embedding = FollowEmbedModule( 73 + source_embedding=UserEmbedding(train_dataset.num_users(), 64), 74 + target_embedding=UserEmbedding(train_dataset.num_users(), 64), 75 + ) 76 + 77 + torch.set_float32_matmul_precision("medium") 78 + trainer = pyl.Trainer( 79 + logger=wandb_logger, 80 + callbacks=[checkpoint_callback], 81 + accelerator="gpu", 82 + devices=1, 83 + precision="bf16", 84 + max_epochs=2, 85 + limit_train_batches=0.01, 86 + limit_val_batches=1_000, 87 + val_check_interval=0.05, 88 + ) 89 + trainer.fit( 90 + model=embedding, 91 + train_dataloaders=train_dataloader, 92 + val_dataloaders=val_dataloader, 93 + ckpt_path=model_checkpoint, 94 + ) 95 + 96 + 97 + if __name__ == "__main__": 98 + parser = ArgumentParser() 99 + parser.add_argument( 100 + "-r", 101 + "--run-id", 102 + dest="run_id", 103 + help="ID for training run", 104 + ) 105 + parser.add_argument( 106 + "-m", 107 + "--model-checkpoint", 108 + dest="model_checkpoint", 109 + help="Path to checkpoint with run data", 110 + ) 111 + 112 + args = parser.parse_args() 113 + main(run_id=args.run_id, model_checkpoint=args.model_checkpoint)
+114
uv.lock
··· 300 300 ] 301 301 302 302 [[package]] 303 + name = "charset-normalizer" 304 + version = "3.4.3" 305 + source = { registry = "https://pypi.org/simple" } 306 + sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } 307 + wheels = [ 308 + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, 309 + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, 310 + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, 311 + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, 312 + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, 313 + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, 314 + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, 315 + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, 316 + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, 317 + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, 318 + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, 319 + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, 320 + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, 321 + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, 322 + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, 323 + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, 324 + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, 325 + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, 326 + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, 327 + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, 328 + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, 329 + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, 330 + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, 331 + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, 332 + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, 333 + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, 334 + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, 335 + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, 336 + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, 337 + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, 338 + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, 339 + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, 340 + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, 341 + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, 342 + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, 343 + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, 344 + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, 345 + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, 346 + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, 347 + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, 348 + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, 349 + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, 350 + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, 351 + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, 352 + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, 353 + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, 354 + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, 355 + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, 356 + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, 357 + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, 358 + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, 359 + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, 360 + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, 361 + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, 362 + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, 363 + { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" }, 364 + { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" }, 365 + { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" }, 366 + { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" }, 367 + { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" }, 368 + { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" }, 369 + { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" }, 370 + { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" }, 371 + { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" }, 372 + { url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" }, 373 + { url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" }, 374 + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, 375 + ] 376 + 377 + [[package]] 303 378 name = "click" 304 379 version = "8.1.8" 305 380 source = { registry = "https://pypi.org/simple" } ··· 572 647 { name = "rich" }, 573 648 { name = "ruff" }, 574 649 { name = "torch" }, 650 + { name = "torchdata" }, 575 651 ] 576 652 577 653 [package.metadata] ··· 584 660 { name = "rich", specifier = ">=14.1.0" }, 585 661 { name = "ruff", specifier = ">=0.13.2" }, 586 662 { name = "torch", specifier = ">=2.8.0" }, 663 + { name = "torchdata", specifier = ">=0.11.0" }, 587 664 ] 588 665 589 666 [[package]] ··· 1882 1959 ] 1883 1960 1884 1961 [[package]] 1962 + name = "requests" 1963 + version = "2.32.5" 1964 + source = { registry = "https://pypi.org/simple" } 1965 + dependencies = [ 1966 + { name = "certifi" }, 1967 + { name = "charset-normalizer" }, 1968 + { name = "idna" }, 1969 + { name = "urllib3" }, 1970 + ] 1971 + sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } 1972 + wheels = [ 1973 + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, 1974 + ] 1975 + 1976 + [[package]] 1885 1977 name = "rich" 1886 1978 version = "14.1.0" 1887 1979 source = { registry = "https://pypi.org/simple" } ··· 2018 2110 ] 2019 2111 2020 2112 [[package]] 2113 + name = "torchdata" 2114 + version = "0.11.0" 2115 + source = { registry = "https://pypi.org/simple" } 2116 + dependencies = [ 2117 + { name = "requests" }, 2118 + { name = "torch" }, 2119 + { name = "urllib3" }, 2120 + ] 2121 + wheels = [ 2122 + { url = "https://files.pythonhosted.org/packages/95/d4/af694ef718aedbe95a72760ab9ff7a6a7a44ace2d7f70c27bfeb67c5c503/torchdata-0.11.0-py3-none-any.whl", hash = "sha256:52b940fbbe0e00fb21cabddf528449d1bec5bfb0d0823b7487b15f951658ee33", size = 61968, upload-time = "2025-02-20T22:26:30.666Z" }, 2123 + ] 2124 + 2125 + [[package]] 2021 2126 name = "torchmetrics" 2022 2127 version = "1.8.2" 2023 2128 source = { registry = "https://pypi.org/simple" } ··· 2091 2196 sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } 2092 2197 wheels = [ 2093 2198 { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, 2199 + ] 2200 + 2201 + [[package]] 2202 + name = "urllib3" 2203 + version = "2.5.0" 2204 + source = { registry = "https://pypi.org/simple" } 2205 + sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } 2206 + wheels = [ 2207 + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, 2094 2208 ] 2095 2209 2096 2210 [[package]]