From ef3536f8dfdedd06e1cd8750d3e042467281094b Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Tue, 13 May 2025 22:22:48 -0300 Subject: [PATCH 01/69] Dockerization --- Dockerfile | 45 +++++++++++++++ README.docker.md | 67 ++++++++++++++++++++++ run_docker.sh | 49 ++++++++++++++++ run_yourbench.py | 141 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 302 insertions(+) create mode 100644 Dockerfile create mode 100644 README.docker.md create mode 100755 run_docker.sh create mode 100644 run_yourbench.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..caa83c91 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + curl \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Copy all yourbench files +COPY . . + +# Install python3-pip +RUN apt-get update && apt-get install -y --no-install-recommends python3-pip && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install dependencies and yourbench in editable mode +RUN pip install --upgrade pip && \ + pip install boto3 pyyaml awscli && \ + pip install -e . + +# Verify installation +RUN yourbench --version || echo "Yourbench installation verification failed but continuing build" + +# Environment variables (will be overridden at runtime) +ENV INPUT_S3_BUCKET="" +ENV INPUT_S3_KEY="" +ENV OUTPUT_S3_BUCKET="" +ENV OUTPUT_S3_KEY="" +ENV OPENROUTER_API_KEY="" +ENV AWS_ACCESS_KEY_ID="" +ENV AWS_SECRET_ACCESS_KEY="" +ENV AWS_DEFAULT_REGION="us-east-1" +ENV WORKDIR="/app" + +# Create a startup script to run the processing workflow +RUN echo '#!/bin/bash\n\ +echo "Running yourbench workflow..."\n\ +exec python run_yourbench.py' > /app/entrypoint.sh \ + && chmod +x /app/entrypoint.sh + +# Use the startup script as entry point +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/README.docker.md b/README.docker.md new file mode 100644 index 00000000..953df1b0 --- /dev/null +++ b/README.docker.md @@ -0,0 +1,67 @@ +# YourbenchProcessor Docker Container + +This Docker container automates the process of: +1. Downloading data from AWS S3 +2. Processing with yourbench +3. Uploading results back to AWS S3 + +## Required Environment Variables + +The container requires the following environment variables: + +- `INPUT_S3_BUCKET`: S3 bucket name for input data +- `INPUT_S3_KEY`: S3 object key for input data (ZIP file) +- `OUTPUT_S3_BUCKET`: S3 bucket name for output results +- `OUTPUT_S3_KEY`: S3 object key for output results +- `OPENROUTER_API_KEY`: API key for OpenRouter +- `AWS_ACCESS_KEY_ID`: AWS access key with S3 permissions +- `AWS_SECRET_ACCESS_KEY`: AWS secret key with S3 permissions +- `AWS_DEFAULT_REGION`: AWS region (default: us-east-1) + +## Building the Docker Image + +```bash +docker build -t yourbench-processor . +``` + +## Running the Container + +```bash +docker run -e INPUT_S3_BUCKET=your-input-bucket \ + -e INPUT_S3_KEY=input/data.zip \ + -e OUTPUT_S3_BUCKET=your-output-bucket \ + -e OUTPUT_S3_KEY=output/results.zip \ + -e OPENROUTER_API_KEY=your-openrouter-key \ + -e AWS_ACCESS_KEY_ID=your-aws-key-id \ + -e AWS_SECRET_ACCESS_KEY=your-aws-secret \ + -e AWS_DEFAULT_REGION=us-east-1 \ + yourbench-processor +``` + +## Process Flow + +1. Downloads the specified zip file from S3 +2. Extracts contents to `task/data/raw` directory +3. Creates a `config.yaml` file in `task/dataset` directory +4. Runs yourbench with the created config +5. Zips the `task/dataset` directory +6. Uploads the zipped results back to S3 + +## Local Testing + +For local testing without Docker: + +```bash +# Set environment variables +export INPUT_S3_BUCKET=your-input-bucket +export INPUT_S3_KEY=input/data.zip +export OUTPUT_S3_BUCKET=your-output-bucket +export OUTPUT_S3_KEY=output/results.zip +export OPENROUTER_API_KEY=your-openrouter-key +export AWS_ACCESS_KEY_ID=your-aws-key-id +export AWS_SECRET_ACCESS_KEY=your-aws-secret +export AWS_DEFAULT_REGION=us-east-1 + +# Run the script +python run_yourbench.py +``` diff --git a/run_docker.sh b/run_docker.sh new file mode 100755 index 00000000..385d8cf8 --- /dev/null +++ b/run_docker.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Script to build and run the yourbench Docker container +set -e + +# Load environment variables from .env if present +if [ -f .env ]; then + set -a + . .env + set +a +fi + +# Build the Docker image +echo "Building Docker image..." +docker build -t yourbench-processor . + +# Check if environment variables are set +if [ -z "$INPUT_S3_BUCKET" ] || [ -z "$INPUT_S3_KEY" ] || [ -z "$OUTPUT_S3_BUCKET" ] || [ -z "$OUTPUT_S3_KEY" ] || [ -z "$OPENROUTER_API_KEY" ]; then + echo "Error: Required environment variables are not set." + echo "Please set these variables before running:" + echo " - INPUT_S3_BUCKET: S3 bucket containing input data" + echo " - INPUT_S3_KEY: S3 key for input data zip file" + echo " - OUTPUT_S3_BUCKET: S3 bucket for output data" + echo " - OUTPUT_S3_KEY: S3 key for output data" + echo " - OPENROUTER_API_KEY: API key for OpenRouter" + echo "" + echo "Example:" + echo " export INPUT_S3_BUCKET=my-input-bucket" + echo " export INPUT_S3_KEY=input/data.zip" + echo " export OUTPUT_S3_BUCKET=my-output-bucket" + echo " export OUTPUT_S3_KEY=output/results.zip" + echo " export OPENROUTER_API_KEY=your-api-key" + exit 1 +fi + +# Run the Docker container +echo "Running yourbench processor Docker container..." +docker run --rm \ + -e INPUT_S3_BUCKET="$INPUT_S3_BUCKET" \ + -e INPUT_S3_KEY="$INPUT_S3_KEY" \ + -e OUTPUT_S3_BUCKET="$OUTPUT_S3_BUCKET" \ + -e OUTPUT_S3_KEY="$OUTPUT_S3_KEY" \ + -e OPENROUTER_API_KEY="$OPENROUTER_API_KEY" \ + -e AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \ + -e AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \ + -e AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-us-east-1}" \ + yourbench-processor + +echo "Yourbench processing complete!" diff --git a/run_yourbench.py b/run_yourbench.py new file mode 100644 index 00000000..3e7d198f --- /dev/null +++ b/run_yourbench.py @@ -0,0 +1,141 @@ +import os +import boto3 +import zipfile +import yaml +import logging +from pathlib import Path + +# Setup logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +def download_from_s3(bucket_name, object_key, local_path): + """Download file from S3 bucket""" + logger.info(f"Downloading {object_key} from bucket {bucket_name} to {local_path}") + s3_client = boto3.client('s3') + s3_client.download_file(bucket_name, object_key, local_path) + logger.info("Download completed") + +def unzip_file(zip_path, extract_dir): + """Unzip file to specified directory""" + logger.info(f"Extracting {zip_path} to {extract_dir}") + os.makedirs(extract_dir, exist_ok=True) + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(extract_dir) + logger.info("Extraction completed") + +def create_config_file(config_content, config_path): + """Create config.yaml file""" + logger.info(f"Creating config file at {config_path}") + os.makedirs(os.path.dirname(config_path), exist_ok=True) + with open(config_path, 'w') as f: + yaml.dump(yaml.safe_load(config_content), f) + logger.info("Config file created") + +def run_yourbench(config_path): + """Run yourbench with the provided config using direct Python API call.""" + logger.info(f"Running yourbench with config {config_path}") + try: + from yourbench.main import main as yourbench_main + import sys + + # Simulate CLI arguments for Typer + sys_argv_backup = sys.argv.copy() + sys.argv = ["yourbench", "run", "--config", str(config_path)] + try: + yourbench_main() + except SystemExit as e: + logger.info(f"yourbench exited with code {e.code} (caught SystemExit, continuing)") + finally: + sys.argv = sys_argv_backup + logger.info("yourbench execution completed successfully") + return "Execution completed successfully" + except Exception as e: + logger.error(f"Error during yourbench execution: {str(e)}") + raise + +def zip_directory(dir_path, output_path): + """Zip directory to output path""" + logger.info(f"Zipping directory {dir_path} to {output_path}") + with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + for root, _, files in os.walk(dir_path): + for file in files: + file_path = os.path.join(root, file) + zipf.write(file_path, os.path.relpath(file_path, os.path.join(dir_path, '..'))) + logger.info("Zipping completed") + +def upload_to_s3(local_path, bucket_name, object_key): + """Upload file to S3 bucket""" + logger.info(f"Uploading {local_path} to bucket {bucket_name} as {object_key}") + s3_client = boto3.client('s3') + s3_client.upload_file(local_path, bucket_name, object_key) + logger.info("Upload completed") + +def main(): + # Get environment variables + input_bucket = os.environ.get('INPUT_S3_BUCKET') + input_key = os.environ.get('INPUT_S3_KEY') + output_bucket = os.environ.get('OUTPUT_S3_BUCKET') + output_key = os.environ.get('OUTPUT_S3_KEY') + + if not all([input_bucket, input_key, output_bucket, output_key]): + logger.error("Missing required environment variables") + raise ValueError("Required environment variables are missing") + + # Define local paths + base_dir = Path(os.environ.get('WORKDIR', '/app')) + download_path = base_dir / "input.zip" + raw_data_dir = base_dir / "task/data/raw" + dataset_dir = base_dir / "task/dataset" + config_path = dataset_dir / "config.yaml" + output_zip_path = base_dir / "output.zip" + + # Create required directories + os.makedirs(raw_data_dir, exist_ok=True) + os.makedirs(dataset_dir, exist_ok=True) + + # Step 1: Download file from S3 + download_from_s3(input_bucket, input_key, download_path) + + # Step 2: Unzip file to raw data directory + unzip_file(download_path, raw_data_dir) + + # Step 3: Create config.yaml + config_content = """ +hf_configuration: + hf_dataset_name: task + +local_dataset_dir: task/dataset + +model_list: + - model_name: openai/gpt-4.1 + provider: null + base_url: "https://openrouter.ai/api/v1" + api_key: $OPENROUTER_API_KEY + max_concurrent_requests: 10 + +pipeline: + ingestion: + source_documents_dir: task/data/raw + output_dir: task/data/processed + upload_ingest_to_hub: + summarization: + chunking: + single_shot_question_generation: + multi_hop_question_generation: + lighteval: + citation_score_filtering: +""" + create_config_file(config_content, config_path) + + # Step 4: Run yourbench + run_yourbench(config_path) + + # Step 5: Zip dataset directory and upload to S3 + zip_directory(dataset_dir, output_zip_path) + upload_to_s3(output_zip_path, output_bucket, output_key) + + logger.info("All tasks completed successfully") + +if __name__ == "__main__": + main() From 0212ddb347744c3f1c12d214e11852e1c0e6a6a2 Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Wed, 14 May 2025 14:48:42 -0300 Subject: [PATCH 02/69] Added excels and JSONL to the output zip file --- run_yourbench.py | 64 +++++++- yourbench/utils/convert_to_atlas_module.py | 173 +++++++++++++++++++++ yourbench/utils/convert_to_excel_module.py | 46 ++++++ 3 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 yourbench/utils/convert_to_atlas_module.py create mode 100644 yourbench/utils/convert_to_excel_module.py diff --git a/run_yourbench.py b/run_yourbench.py index 3e7d198f..ed1fbcb7 100644 --- a/run_yourbench.py +++ b/run_yourbench.py @@ -5,10 +5,31 @@ import logging from pathlib import Path +from yourbench.utils.convert_to_excel_module import convert_datasets_to_excel +from yourbench.utils.convert_to_atlas_module import convert_dataset + # Setup logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) +def zip_multiple_directories(directories, base_dir, output_zip_path): + """ + Zip multiple directories into a single zip archive, preserving their structure relative to base_dir. + Args: + directories (list): List of Path or str directories to include. + base_dir (Path or str): Base directory for relative paths in zip. + output_zip_path (Path or str): Path to the output zip file. + """ + logger.info(f"Zipping directories {directories} into {output_zip_path}") + with zipfile.ZipFile(output_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + for directory in directories: + for root, _, files in os.walk(directory): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, base_dir) + zipf.write(file_path, arcname) + logger.info("Zipping completed for: " + ", ".join([str(d) for d in directories])) + def download_from_s3(bucket_name, object_key, local_path): """Download file from S3 bucket""" logger.info(f"Downloading {object_key} from bucket {bucket_name} to {local_path}") @@ -88,6 +109,7 @@ def main(): raw_data_dir = base_dir / "task/data/raw" dataset_dir = base_dir / "task/dataset" config_path = dataset_dir / "config.yaml" + excel_dir = base_dir / "task/excel" output_zip_path = base_dir / "output.zip" # Create required directories @@ -131,10 +153,46 @@ def main(): # Step 4: Run yourbench run_yourbench(config_path) - # Step 5: Zip dataset directory and upload to S3 - zip_directory(dataset_dir, output_zip_path) + # Step 5: Convert datasets to Excel + convert_datasets_to_excel(str(dataset_dir), str(excel_dir), logger=logger) + + # Step 6: Convert to Atlas format + try: + atlas_dir = base_dir / "task/atlas_dataset" + lighteval_path = dataset_dir / "lighteval" + if lighteval_path.exists(): + logger.info(f"Converting lighteval dataset to Atlas format") + atlas_output = convert_dataset( + hf_path=str(lighteval_path), + name="atlas_dataset", + system_prompt=( + "You are an expert answering benchmark questions. " + "Give a very concise answer." + ), + full_description="Dataset for evaluating built-in knowledge", + short_description="Fact-based knowledge", + category="YourBench", + output_dir=str(base_dir / "task"), # creates task/atlas_dataset/ + ) + logger.info(f"Atlas conversion completed.") + else: + logger.warning(f"Lighteval dataset not found at {lighteval_path}, skipping Atlas conversion") + except Exception as e: + logger.error(f"Atlas conversion failed: {e}") + logger.warning("Continuing with the rest of the pipeline") + + # Step 7: Zip the dataset, excel, and atlas directories into a single archive + directories_to_zip = [dataset_dir, excel_dir] + atlas_dataset_dir = base_dir / "task/atlas_dataset" + if atlas_dataset_dir.exists(): + logger.info(f"Including Atlas dataset directory in zip: {atlas_dataset_dir}") + directories_to_zip.append(atlas_dataset_dir) + zip_multiple_directories(directories_to_zip, base_dir, output_zip_path) + + # Step 8: Upload the combined zip to S3 upload_to_s3(output_zip_path, output_bucket, output_key) - + logger.info("Combined zip uploaded to S3 successfully") + logger.info("All tasks completed successfully") if __name__ == "__main__": diff --git a/yourbench/utils/convert_to_atlas_module.py b/yourbench/utils/convert_to_atlas_module.py new file mode 100644 index 00000000..a57744b7 --- /dev/null +++ b/yourbench/utils/convert_to_atlas_module.py @@ -0,0 +1,173 @@ +from pathlib import Path +import json, textwrap +from datasets import load_from_disk + + +def _scorer_yaml(name: str) -> str: + """Return the complete .yaml text, nothing omitted.""" + return textwrap.dedent( + f"""\ + name: {name} + + scorer: + type: llm_judge + options: + regex_pattern: '' + judge_model: google/gemini-flash-1.5-8b + judge_prompt: |- + Your job is to look at a question, a gold target, and a predicted answer, and then assign a grade of either ["CORRECT", "INCORRECT", "NOT_ATTEMPTED"]. + First, I will give examples of each grade, and then you will grade a new example. + + The following are examples of CORRECT predicted answers. + ``` + Question: What are the names of Barack Obama's children? + Gold target: Malia Obama and Sasha Obama + Predicted answer 1: sasha and malia obama + Predicted answer 2: most people would say Malia and Sasha, but I'm not sure and would have to double check + Predicted answer 3: Barack Obama has two daughters: Malia and Sasha. + ``` + These predicted answers are all CORRECT because: + - They fully contain the important information in the gold target + - They do not contain information that contradicts the gold target + - Capitalization, punctuation, grammar, and order don't matter + - Hedging is okay if the correct answer is included without contradictions + + The following are examples of INCORRECT predicted answers. + ``` + Question: What are the names of Barack Obama's children? + Gold target: Malia and Sasha + Predicted answer 1: Malia. + Predicted answer 2: Malia, Sasha, and Susan. + Predicted answer 3: Barack Obama does not have any children. + ``` + These predicted answers are all INCORRECT because: + - They contain factual contradictions with the gold target + - Even hedged incorrect statements are considered incorrect + + The following are examples of NOT_ATTEMPTED predicted answers. + ``` + Question: What are the names of Barack Obama's children? + Gold target: Malia and Sasha + Predicted answer 1: I don't know. + Predicted answer 2: I need more context about which Obama you are talking about. + Predicted answer 3: Barack Obama has two children, but I don't recall their names. + ``` + These predicted answers are all NOT_ATTEMPTED because: + - They don't include the required information + - They don't contradict the gold target + + Important notes: + - Numbers must match to the last significant figure in the gold target + - Only information directly asked in the question is required + - Information clearly inferred from the question can be omitted + - Name typos are acceptable if the identity is clear + + Grade the following as either A (CORRECT), B (INCORRECT), or C (NOT_ATTEMPTED): + ``` + Question: {{prompt}} + Gold target: {{truth}} + Predicted answer: {{response}} + ``` + Return your response in this exact format: + Grade: [A/B/C] + + criteria: + grade: + description: "Grade for the answer (A=CORRECT, B=INCORRECT, C=NOT_ATTEMPTED)" + weight: 1.0 + pattern: "Grade: (?:\\\\[)?([ABC])(?:\\\\])?" + type: mapped + options: + A: 1.0 # CORRECT + B: 0.0 # INCORRECT + C: 0.0 # NOT_ATTEMPTED + + categories: + - general + subsets: + - default + """ + ) + + +def _metadata_yaml( + name: str, + full_desc: str, + short_desc: str, + category: str, + n_rows: int, +) -> str: + return textwrap.dedent( + f"""\ + name: {full_desc} + key: {name} + full_description: {full_desc} + short_description: {short_desc} + subsets: + - default + categories: + - {category} + key_takeaways: + additional_insights: + - "" + prompt_count: {n_rows} + """ + ) + + +def convert_dataset( + hf_path: str | Path, + name: str, + system_prompt: str, + full_description: str = "Knowledge-oriented evaluation dataset", + short_description: str = "Knowledge eval", + category: str = "General", + output_dir: str | Path = ".", +): + """ + Convert a HF dataset with `question` and `ground_truth_answer` columns into: + + // + ├─ -formatted.jsonl + ├─ .yaml + └─ metadata.yaml + """ + ds = load_from_disk(str(hf_path)) + out_root = Path(output_dir).expanduser().resolve() / name + out_root.mkdir(parents=True, exist_ok=True) + + # 1. formatted JSONL + with (out_root / f"{name}-formatted.jsonl").open("w", encoding="utf-8") as f: + for i, rec in enumerate(ds): + f.write( + json.dumps( + { + "id": f"{name}{i:06d}", + "input": [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": rec["question"]}, + ], + "truth": rec["ground_truth_answer"], + "subset": "default", + }, + ensure_ascii=False, + ) + + "\n" + ) + + # 2. scorer YAML + (out_root / f"{name}.yaml").write_text(_scorer_yaml(name), encoding="utf-8") + + # 3. metadata YAML + (out_root / "metadata.yaml").write_text( + _metadata_yaml( + name, + full_description, + short_description, + category, + len(ds), + ), + encoding="utf-8", + ) + + # print(f"✔ Export complete → {out_root}") diff --git a/yourbench/utils/convert_to_excel_module.py b/yourbench/utils/convert_to_excel_module.py new file mode 100644 index 00000000..fa05fe24 --- /dev/null +++ b/yourbench/utils/convert_to_excel_module.py @@ -0,0 +1,46 @@ +import os +import json +import numpy as np +import re +from datasets import load_from_disk +import pandas as pd + +def convert_datasets_to_excel(dataset_dir, excel_dir=None, logger=None): + """ + Convert all relevant dataset subsets in dataset_dir to Excel files in excel_dir. + If excel_dir is None, will create 'excel' subdir in dataset_dir's parent. + """ + if excel_dir is None: + excel_dir = os.path.join(os.path.dirname(dataset_dir), "excel") + subsets = [ + "ingested", "summarized", "chunked", "single_shot_questions", "multi_hop_questions", "lighteval" + ] + dataset_paths = {k: os.path.join(dataset_dir, k) for k in subsets} + + def clean_illegal_chars(val): + if isinstance(val, str): + return re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F]", "", val) + return val + + os.makedirs(excel_dir, exist_ok=True) + for title, path in dataset_paths.items(): + try: + if not os.path.exists(path): + if logger: + logger.warning(f"Dataset subset '{title}' not found at {path}, skipping.") + continue + ds = load_from_disk(str(path)) + df = ds.to_pandas() + df = df.map(clean_illegal_chars) + if "citations" in df.columns: + df["citations"] = df["citations"].apply(lambda x: json.dumps(x.tolist()) if isinstance(x, np.ndarray) else str(x)) + excel_path = os.path.join(excel_dir, f"{title}.xlsx") + df.to_excel(excel_path, index=False) + if logger: + logger.info(f"Converted {title} to {excel_path}") + except Exception as e: + if logger: + logger.warning(f"Failed to convert {title} to Excel: {e}") + continue + if logger: + logger.info("Excel conversion completed.") From 87f430ca811a5b9ec0c92393d165f6c11557ef23 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 14 May 2025 19:00:42 -0500 Subject: [PATCH 03/69] Fix pipeline integration tests with proper mocking --- .github/workflows/ci.yaml | 39 +++ tests/integration/test_pipeline.py | 379 +++++++++++++++++++++++++++++ 2 files changed, 418 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 tests/integration/test_pipeline.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..ca45031a --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,39 @@ +name: YourBench CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.9, 3.10, 3.11, 3.12] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: | + uv pip install -e . + uv pip install pytest pytest-cov + + - name: Run tests + run: | + python -m pytest tests/ --cov=yourbench --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + fail_ci_if_error: false diff --git a/tests/integration/test_pipeline.py b/tests/integration/test_pipeline.py new file mode 100644 index 00000000..a706e68e --- /dev/null +++ b/tests/integration/test_pipeline.py @@ -0,0 +1,379 @@ +import os +import shutil +import tempfile +from unittest.mock import MagicMock, patch, call +from dataclasses import asdict +import json + +import pytest +from datasets import Dataset + + +# Fixture for temporary directory +@pytest.fixture +def temp_dir(): + dir_path = tempfile.mkdtemp() + yield dir_path + shutil.rmtree(dir_path) + + +# Fixture for mock configuration +@pytest.fixture +def mock_config(temp_dir): + return { + "settings": {"debug": False}, + "hf_configuration": { + "token": "fake_token", + "hf_organization": "fake_org", + "private": True, + "hf_dataset_name": "fake_dataset", + "concat_if_exist": False, + }, + "local_dataset_dir": temp_dir, + "model_list": [ + { + "model_name": "fake_model", + "provider": None, + "api_key": "fake_key", + "base_url": "http://localhost:8000/v1", + "max_concurrent_requests": 1, + } + ], + "model_roles": { + "ingestion": ["fake_model"], + "summarization": ["fake_model"], + "chunking": ["fake_model"], + "single_shot_question_generation": ["fake_model"], + "multi_hop_question_generation": ["fake_model"], + }, + "pipeline": { + "ingestion": { + "run": True, + "source_documents_dir": os.path.join(temp_dir, "raw"), + "output_dir": os.path.join(temp_dir, "processed"), + }, + "upload_ingest_to_hub": {"run": False, "source_documents_dir": os.path.join(temp_dir, "processed")}, + "summarization": {"run": True}, + "chunking": { + "run": True, + "chunking_configuration": { + "l_min_tokens": 64, + "l_max_tokens": 128, + "tau_threshold": 0.8, + "h_min": 2, + "h_max": 5, + "num_multihops_factor": 2, + "chunking_mode": "fast_chunking", + }, + }, + "single_shot_question_generation": { + "run": True, + "additional_instructions": "Generate questions to test a curious adult", + "chunk_sampling": {"mode": "count", "value": 1, "random_seed": 123}, + }, + "multi_hop_question_generation": { + "run": True, + "additional_instructions": "Generate questions to test a curious adult", + "chunk_sampling": {"mode": "count", "value": 1, "random_seed": 42}, + }, + "lighteval": {"run": True}, + }, + } + + +# Test for ingestion stage with mocked components +@pytest.mark.parametrize("mock_no_docs", [False, True]) +def test_ingestion_stage(mock_config, temp_dir, mock_no_docs): + """ + Test the ingestion stage of the YourBench pipeline. + + Verifies that the ingestion stage correctly processes source documents. + """ + # Create test document structure + raw_dir = mock_config["pipeline"]["ingestion"]["source_documents_dir"] + output_dir = mock_config["pipeline"]["ingestion"]["output_dir"] + os.makedirs(raw_dir, exist_ok=True) + os.makedirs(output_dir, exist_ok=True) + + # Create a test document only if not testing the no-docs case + if not mock_no_docs: + with open(os.path.join(raw_dir, "test_doc.txt"), "w") as f: + f.write("This is a test document for ingestion.") + + # Mock the core functionality instead of just the MarkItDown class + with ( + patch("yourbench.pipeline.ingestion.MarkItDown") as mock_markitdown, + patch("yourbench.pipeline.ingestion._convert_document_to_markdown") as mock_convert + ): + # Configure mocks + mock_markitdown_instance = MagicMock() + mock_markitdown.return_value = mock_markitdown_instance + mock_convert.return_value = True + + # Import the run function after mocking + from yourbench.pipeline.ingestion import run + + # Run the ingestion stage + run(mock_config) + + # Verify behavior + if mock_no_docs: + mock_convert.assert_not_called() + else: + mock_convert.assert_called() + + +# Test for summarization stage +def test_summarization_stage(mock_config): + """ + Test the summarization stage of the YourBench pipeline. + + Verifies that summarization correctly calls inference and processes the results. + """ + # Mock Dataset loading and saving + mock_dataset = Dataset.from_dict({ + "document_id": ["doc1", "doc2"], + "document_text": ["This is document 1", "This is document 2"], + "document_filename": ["doc1.md", "doc2.md"] + }) + + # Setup mocks + with ( + patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, + patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, + patch("yourbench.pipeline.summarization.run_inference") as mock_run_inference, + patch("yourbench.pipeline.summarization.extract_content_from_xml_tags") as mock_extract + ): + # Configure mocks + mock_run_inference.return_value = {"fake_model": ["Summary for doc1", "Summary for doc2"]} + mock_extract.side_effect = lambda text, tag: f"Summary for doc{text.split('doc')[1].split('<')[0]}" if tag == "final_summary" else None + + # Import the summarization run function + from yourbench.pipeline.summarization import run + + # Run the summarization stage + run(mock_config) + + # Verify the summarization stage ran as expected + mock_load.assert_called_once() + assert mock_run_inference.call_count == 1 + mock_save.assert_called_once() + + +# Test for chunking stage +def test_chunking_stage(mock_config): + """ + Test the chunking stage of the YourBench pipeline. + + Verifies that documents are properly chunked according to the configuration. + """ + # Mock Dataset loading and saving + mock_dataset = Dataset.from_dict({ + "document_id": ["doc1", "doc2"], + "document_text": ["This is document 1 with enough text to be chunked properly", "This is document 2 which also has sufficient text for chunking"], + "document_summary": ["Summary 1", "Summary 2"] + }) + + # Mock functions and dependencies + with ( + patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, + patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, + patch("yourbench.pipeline.chunking._compute_info_density_metrics") as mock_metrics, + patch("yourbench.pipeline.chunking.split_into_token_chunks") as mock_split + ): + # Configure mock returns + mock_split.return_value = ["Chunk 1", "Chunk 2"] + mock_metrics.return_value = [] + + # Import the chunking run function + from yourbench.pipeline.chunking import run + + # Run the chunking stage + run(mock_config) + + # Verify the chunking stage behavior + mock_load.assert_called_once() + assert mock_split.call_count > 0 + mock_save.assert_called_once() + + +# Test for single-shot question generation stage +def test_single_shot_question_generation_stage(mock_config): + """ + Test the single-shot question generation stage of the YourBench pipeline. + + Verifies that questions are generated for single chunks of text. + """ + # Mock dataset with chunks + chunks = [{"chunk_id": "chunk1", "chunk_text": "This is chunk 1"}] + mock_dataset = Dataset.from_dict({ + "document_id": ["doc1"], + "document_summary": ["Document 1 summary"], + "document_filename": ["doc1.md"], + "chunks": [chunks] + }) + + # Setup mocks + with ( + patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, + patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, + patch("yourbench.utils.inference_engine.run_inference") as mock_run_inference, + patch("yourbench.pipeline.single_shot_question_generation.parse_qa_pairs_from_response") as mock_parse + ): + # Configure mocks + mock_run_inference.return_value = {"fake_model": ["Question generation response"]} + mock_parse.return_value = [ + { + "question": "Test question?", + "answer": "Test answer", + "estimated_difficulty": 5, + "question_type": "factual", + "thought_process": "Reasoning", + "citations": ["citation"] + } + ] + + # Import run function + from yourbench.pipeline.single_shot_question_generation import run + + # Run the stage + run(mock_config) + + # Verify behavior + mock_load.assert_called_once() + mock_run_inference.assert_called_once() + mock_parse.assert_called_once() + mock_save.assert_called_once() + + +# Test for multi-hop question generation stage +def test_multi_hop_question_generation_stage(mock_config): + """ + Test the multi-hop question generation stage of the YourBench pipeline. + + Verifies that questions are generated requiring reasoning across multiple chunks. + """ + # Mock dataset with chunks and valid multihop_chunks format + chunks = [{"chunk_id": "chunk1", "chunk_text": "This is chunk 1"}] + # Correct format for multihop_chunks + multihop_chunks = [{ + "multihop_id": "mh1", + "source_chunks": [ + {"chunk_id": "chunk1", "chunk_text": "Text 1"}, + {"chunk_id": "chunk2", "chunk_text": "Text 2"} + ] + }] + mock_dataset = Dataset.from_dict({ + "document_id": ["doc1"], + "document_summary": ["Document 1 summary"], + "chunks": [chunks], + "multihop_chunks": [multihop_chunks] + }) + + # Setup mocks + with ( + patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, + patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, + patch("yourbench.utils.inference_engine.run_inference") as mock_run_inference, + patch("yourbench.pipeline.multi_hop_question_generation.parse_qa_pairs_from_response") as mock_parse, + # Mock the chunk sampling function to bypass the empty multihop check + patch("yourbench.pipeline.multi_hop_question_generation._multihop_chunk_sampling_and_calls") as mock_sampling + ): + # Configure mocks + mock_run_inference.return_value = {"fake_model": ["Multi-hop question generation response"]} + mock_parse.return_value = [ + { + "question": "Multi-hop test question?", + "answer": "Multi-hop test answer", + "estimated_difficulty": 7, + "question_type": "reasoning", + "thought_process": "Complex reasoning", + "citations": ["citation1", "citation2"] + } + ] + mock_sampling.return_value = ( + [MagicMock()], # Mock inference calls list + [(0, "doc1", ["chunk1", "chunk2"])] # Mock call index mapping + ) + + # Import run function + from yourbench.pipeline.multi_hop_question_generation import run + + # Run the stage + run(mock_config) + + # Verify behavior + mock_load.assert_called_once() + mock_run_inference.assert_called_once() + mock_save.assert_called_once() + + +# Test for lighteval stage +def test_lighteval_stage(mock_config): + """ + Test the lighteval stage of the YourBench pipeline. + + Verifies that the stage combines questions into a unified dataset for evaluation. + """ + # Mock single-shot and multi-hop datasets + single_shot_ds = Dataset.from_dict({ + "document_id": ["doc1"], + "chunk_id": ["chunk1"], + "question": ["Single-shot question?"], + "self_answer": ["Single-shot answer"], + "estimated_difficulty": [5], + "self_assessed_question_type": ["factual"], + "generating_model": ["fake_model"], + "additional_instructions": ["Generate questions"] + }) + + multi_hop_ds = Dataset.from_dict({ + "document_id": ["doc1"], + "source_chunk_ids": [["chunk1", "chunk2"]], + "question": ["Multi-hop question?"], + "self_answer": ["Multi-hop answer"], + "estimated_difficulty": [7], + "self_assessed_question_type": ["reasoning"], + "generating_model": ["fake_model"], + "additional_instructions": ["Generate questions"] + }) + + chunked_ds = Dataset.from_dict({ + "document_id": ["doc1"], + "document_text": ["Full document text"], + "chunks": [[{"chunk_id": "chunk1", "chunk_text": "Chunk 1 text"}]] + }) + + summarized_ds = Dataset.from_dict({ + "document_id": ["doc1"], + "document_summary": ["Document 1 summary"] + }) + + # Setup mocks + with ( + patch("yourbench.utils.dataset_engine.custom_load_dataset") as mock_load, + patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save + ): + # Configure mock to return different datasets based on the subset parameter + def load_dataset_side_effect(config, subset): + if subset == "single_shot_questions": + return single_shot_ds + elif subset == "multi_hop_questions": + return multi_hop_ds + elif subset == "chunked": + return chunked_ds + elif subset == "summarized": + return summarized_ds + return Dataset.from_dict({}) + + mock_load.side_effect = load_dataset_side_effect + + # Import run function + from yourbench.pipeline.lighteval import run + + # Run the stage + run(mock_config) + + # Verify behavior + assert mock_load.call_count == 4 + mock_save.assert_called_once() From 1cfec04083c85095602ed24686f3ff53d24a2eb0 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 14 May 2025 19:03:28 -0500 Subject: [PATCH 04/69] remove unsupported --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ca45031a..5a631277 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.9, 3.10, 3.11, 3.12] + python-version: [3.12] steps: - uses: actions/checkout@v3 From 5f1915c2e75347e9a94e8737b50758dc6a60fd6d Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 14 May 2025 19:05:02 -0500 Subject: [PATCH 05/69] Fix CI workflow by adding virtual environment creation step --- .github/workflows/ci.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5a631277..d1ea7969 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,6 +23,9 @@ jobs: - name: Install uv run: pip install uv + - name: Create virtual environment + run: uv venv + - name: Install dependencies run: | uv pip install -e . From 769fb9e1e81aaf1b64624d5bd496cdf8f3729910 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 14 May 2025 19:06:42 -0500 Subject: [PATCH 06/69] Fix CI workflow: add permissions and activate virtual environment --- .github/workflows/ci.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d1ea7969..205c9e96 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,6 +6,9 @@ on: pull_request: branches: [ main ] +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest @@ -28,11 +31,13 @@ jobs: - name: Install dependencies run: | + . .venv/bin/activate uv pip install -e . uv pip install pytest pytest-cov - name: Run tests run: | + . .venv/bin/activate python -m pytest tests/ --cov=yourbench --cov-report=xml - name: Upload coverage to Codecov From 24a1a3d17562c2ffcac243fe92723e7ba5429e45 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 14 May 2025 19:08:01 -0500 Subject: [PATCH 07/69] CQ --- README.md | 11 +-- tests/integration/test_pipeline.py | 146 +++++++++++++++-------------- 2 files changed, 80 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 1f961225..3ceeb832 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,5 @@ - -
- @@ -33,6 +24,8 @@ GitHub Repo stars + + CI Status

diff --git a/tests/integration/test_pipeline.py b/tests/integration/test_pipeline.py index a706e68e..426d4f04 100644 --- a/tests/integration/test_pipeline.py +++ b/tests/integration/test_pipeline.py @@ -1,11 +1,10 @@ import os import shutil import tempfile -from unittest.mock import MagicMock, patch, call -from dataclasses import asdict -import json +from unittest.mock import MagicMock, patch import pytest + from datasets import Dataset @@ -86,7 +85,7 @@ def mock_config(temp_dir): def test_ingestion_stage(mock_config, temp_dir, mock_no_docs): """ Test the ingestion stage of the YourBench pipeline. - + Verifies that the ingestion stage correctly processes source documents. """ # Create test document structure @@ -94,28 +93,28 @@ def test_ingestion_stage(mock_config, temp_dir, mock_no_docs): output_dir = mock_config["pipeline"]["ingestion"]["output_dir"] os.makedirs(raw_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True) - + # Create a test document only if not testing the no-docs case if not mock_no_docs: with open(os.path.join(raw_dir, "test_doc.txt"), "w") as f: f.write("This is a test document for ingestion.") - + # Mock the core functionality instead of just the MarkItDown class with ( patch("yourbench.pipeline.ingestion.MarkItDown") as mock_markitdown, - patch("yourbench.pipeline.ingestion._convert_document_to_markdown") as mock_convert + patch("yourbench.pipeline.ingestion._convert_document_to_markdown") as mock_convert, ): # Configure mocks mock_markitdown_instance = MagicMock() mock_markitdown.return_value = mock_markitdown_instance mock_convert.return_value = True - + # Import the run function after mocking from yourbench.pipeline.ingestion import run - + # Run the ingestion stage run(mock_config) - + # Verify behavior if mock_no_docs: mock_convert.assert_not_called() @@ -127,33 +126,42 @@ def test_ingestion_stage(mock_config, temp_dir, mock_no_docs): def test_summarization_stage(mock_config): """ Test the summarization stage of the YourBench pipeline. - + Verifies that summarization correctly calls inference and processes the results. """ # Mock Dataset loading and saving mock_dataset = Dataset.from_dict({ "document_id": ["doc1", "doc2"], "document_text": ["This is document 1", "This is document 2"], - "document_filename": ["doc1.md", "doc2.md"] + "document_filename": ["doc1.md", "doc2.md"], }) - + # Setup mocks with ( patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, patch("yourbench.pipeline.summarization.run_inference") as mock_run_inference, - patch("yourbench.pipeline.summarization.extract_content_from_xml_tags") as mock_extract + patch("yourbench.pipeline.summarization.extract_content_from_xml_tags") as mock_extract, ): # Configure mocks - mock_run_inference.return_value = {"fake_model": ["Summary for doc1", "Summary for doc2"]} - mock_extract.side_effect = lambda text, tag: f"Summary for doc{text.split('doc')[1].split('<')[0]}" if tag == "final_summary" else None - + mock_run_inference.return_value = { + "fake_model": [ + "Summary for doc1", + "Summary for doc2", + ] + } + mock_extract.side_effect = ( + lambda text, tag: f"Summary for doc{text.split('doc')[1].split('<')[0]}" + if tag == "final_summary" + else None + ) + # Import the summarization run function from yourbench.pipeline.summarization import run - + # Run the summarization stage run(mock_config) - + # Verify the summarization stage ran as expected mock_load.assert_called_once() assert mock_run_inference.call_count == 1 @@ -164,33 +172,36 @@ def test_summarization_stage(mock_config): def test_chunking_stage(mock_config): """ Test the chunking stage of the YourBench pipeline. - + Verifies that documents are properly chunked according to the configuration. """ # Mock Dataset loading and saving mock_dataset = Dataset.from_dict({ "document_id": ["doc1", "doc2"], - "document_text": ["This is document 1 with enough text to be chunked properly", "This is document 2 which also has sufficient text for chunking"], - "document_summary": ["Summary 1", "Summary 2"] + "document_text": [ + "This is document 1 with enough text to be chunked properly", + "This is document 2 which also has sufficient text for chunking", + ], + "document_summary": ["Summary 1", "Summary 2"], }) - + # Mock functions and dependencies with ( patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, patch("yourbench.pipeline.chunking._compute_info_density_metrics") as mock_metrics, - patch("yourbench.pipeline.chunking.split_into_token_chunks") as mock_split + patch("yourbench.pipeline.chunking.split_into_token_chunks") as mock_split, ): # Configure mock returns mock_split.return_value = ["Chunk 1", "Chunk 2"] mock_metrics.return_value = [] - + # Import the chunking run function from yourbench.pipeline.chunking import run - + # Run the chunking stage run(mock_config) - + # Verify the chunking stage behavior mock_load.assert_called_once() assert mock_split.call_count > 0 @@ -201,7 +212,7 @@ def test_chunking_stage(mock_config): def test_single_shot_question_generation_stage(mock_config): """ Test the single-shot question generation stage of the YourBench pipeline. - + Verifies that questions are generated for single chunks of text. """ # Mock dataset with chunks @@ -210,15 +221,15 @@ def test_single_shot_question_generation_stage(mock_config): "document_id": ["doc1"], "document_summary": ["Document 1 summary"], "document_filename": ["doc1.md"], - "chunks": [chunks] + "chunks": [chunks], }) - + # Setup mocks with ( patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, patch("yourbench.utils.inference_engine.run_inference") as mock_run_inference, - patch("yourbench.pipeline.single_shot_question_generation.parse_qa_pairs_from_response") as mock_parse + patch("yourbench.pipeline.single_shot_question_generation.parse_qa_pairs_from_response") as mock_parse, ): # Configure mocks mock_run_inference.return_value = {"fake_model": ["Question generation response"]} @@ -229,16 +240,16 @@ def test_single_shot_question_generation_stage(mock_config): "estimated_difficulty": 5, "question_type": "factual", "thought_process": "Reasoning", - "citations": ["citation"] + "citations": ["citation"], } ] - + # Import run function from yourbench.pipeline.single_shot_question_generation import run - + # Run the stage run(mock_config) - + # Verify behavior mock_load.assert_called_once() mock_run_inference.assert_called_once() @@ -250,26 +261,28 @@ def test_single_shot_question_generation_stage(mock_config): def test_multi_hop_question_generation_stage(mock_config): """ Test the multi-hop question generation stage of the YourBench pipeline. - + Verifies that questions are generated requiring reasoning across multiple chunks. """ # Mock dataset with chunks and valid multihop_chunks format chunks = [{"chunk_id": "chunk1", "chunk_text": "This is chunk 1"}] # Correct format for multihop_chunks - multihop_chunks = [{ - "multihop_id": "mh1", - "source_chunks": [ - {"chunk_id": "chunk1", "chunk_text": "Text 1"}, - {"chunk_id": "chunk2", "chunk_text": "Text 2"} - ] - }] + multihop_chunks = [ + { + "multihop_id": "mh1", + "source_chunks": [ + {"chunk_id": "chunk1", "chunk_text": "Text 1"}, + {"chunk_id": "chunk2", "chunk_text": "Text 2"}, + ], + } + ] mock_dataset = Dataset.from_dict({ "document_id": ["doc1"], "document_summary": ["Document 1 summary"], "chunks": [chunks], - "multihop_chunks": [multihop_chunks] + "multihop_chunks": [multihop_chunks], }) - + # Setup mocks with ( patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, @@ -277,7 +290,7 @@ def test_multi_hop_question_generation_stage(mock_config): patch("yourbench.utils.inference_engine.run_inference") as mock_run_inference, patch("yourbench.pipeline.multi_hop_question_generation.parse_qa_pairs_from_response") as mock_parse, # Mock the chunk sampling function to bypass the empty multihop check - patch("yourbench.pipeline.multi_hop_question_generation._multihop_chunk_sampling_and_calls") as mock_sampling + patch("yourbench.pipeline.multi_hop_question_generation._multihop_chunk_sampling_and_calls") as mock_sampling, ): # Configure mocks mock_run_inference.return_value = {"fake_model": ["Multi-hop question generation response"]} @@ -288,20 +301,20 @@ def test_multi_hop_question_generation_stage(mock_config): "estimated_difficulty": 7, "question_type": "reasoning", "thought_process": "Complex reasoning", - "citations": ["citation1", "citation2"] + "citations": ["citation1", "citation2"], } ] mock_sampling.return_value = ( [MagicMock()], # Mock inference calls list - [(0, "doc1", ["chunk1", "chunk2"])] # Mock call index mapping + [(0, "doc1", ["chunk1", "chunk2"])], # Mock call index mapping ) - + # Import run function from yourbench.pipeline.multi_hop_question_generation import run - + # Run the stage run(mock_config) - + # Verify behavior mock_load.assert_called_once() mock_run_inference.assert_called_once() @@ -312,7 +325,7 @@ def test_multi_hop_question_generation_stage(mock_config): def test_lighteval_stage(mock_config): """ Test the lighteval stage of the YourBench pipeline. - + Verifies that the stage combines questions into a unified dataset for evaluation. """ # Mock single-shot and multi-hop datasets @@ -324,9 +337,9 @@ def test_lighteval_stage(mock_config): "estimated_difficulty": [5], "self_assessed_question_type": ["factual"], "generating_model": ["fake_model"], - "additional_instructions": ["Generate questions"] + "additional_instructions": ["Generate questions"], }) - + multi_hop_ds = Dataset.from_dict({ "document_id": ["doc1"], "source_chunk_ids": [["chunk1", "chunk2"]], @@ -335,24 +348,21 @@ def test_lighteval_stage(mock_config): "estimated_difficulty": [7], "self_assessed_question_type": ["reasoning"], "generating_model": ["fake_model"], - "additional_instructions": ["Generate questions"] + "additional_instructions": ["Generate questions"], }) - + chunked_ds = Dataset.from_dict({ "document_id": ["doc1"], "document_text": ["Full document text"], - "chunks": [[{"chunk_id": "chunk1", "chunk_text": "Chunk 1 text"}]] - }) - - summarized_ds = Dataset.from_dict({ - "document_id": ["doc1"], - "document_summary": ["Document 1 summary"] + "chunks": [[{"chunk_id": "chunk1", "chunk_text": "Chunk 1 text"}]], }) - + + summarized_ds = Dataset.from_dict({"document_id": ["doc1"], "document_summary": ["Document 1 summary"]}) + # Setup mocks with ( patch("yourbench.utils.dataset_engine.custom_load_dataset") as mock_load, - patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save + patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, ): # Configure mock to return different datasets based on the subset parameter def load_dataset_side_effect(config, subset): @@ -365,15 +375,15 @@ def load_dataset_side_effect(config, subset): elif subset == "summarized": return summarized_ds return Dataset.from_dict({}) - + mock_load.side_effect = load_dataset_side_effect - + # Import run function from yourbench.pipeline.lighteval import run - + # Run the stage run(mock_config) - + # Verify behavior assert mock_load.call_count == 4 mock_save.assert_called_once() From d394a8d75e4aa911740d70a6a8cf9907a9804349 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 14 May 2025 20:20:31 -0500 Subject: [PATCH 08/69] feat: add cost tracking to inference engine --- yourbench/utils/inference_engine.py | 185 +++++++++++++++++++++++++--- 1 file changed, 170 insertions(+), 15 deletions(-) diff --git a/yourbench/utils/inference_engine.py b/yourbench/utils/inference_engine.py index d624b640..874c03ec 100644 --- a/yourbench/utils/inference_engine.py +++ b/yourbench/utils/inference_engine.py @@ -1,14 +1,20 @@ """ -Inference Engine For Yourbench - Now with true concurrency throttling. +Inference Engine For Yourbench - Now with true concurrency throttling and cost tracking. +Inference Engine For Yourbench - Now with true concurrency throttling and cost tracking. """ import os +import csv import time import uuid +import atexit import asyncio +import datetime +import collections from typing import Any, Dict, List, Optional from dataclasses import field, dataclass +import tiktoken # Added for token counting from dotenv import load_dotenv from loguru import logger from tqdm.asyncio import tqdm_asyncio @@ -20,6 +26,12 @@ GLOBAL_TIMEOUT = 300 +# Using defaultdict for easier accumulation +_cost_data = collections.defaultdict(lambda: {"input_tokens": 0, "output_tokens": 0, "calls": 0}) +_individual_log_file = os.path.join("logs", "inference_cost_log_individual.csv") +_aggregate_log_file = os.path.join("logs", "inference_cost_log_aggregate.csv") +_individual_header_written = False + @dataclass class Model: @@ -29,8 +41,8 @@ class Model: base_url: str | None = None api_key: str | None = field(default=None, repr=False) bill_to: str | None = None - max_concurrent_requests: int = 16 + encoding_name: str = "cl100k_base" def __post_init__(self): if self.api_key is None: @@ -46,14 +58,14 @@ class InferenceCall: messages: List of message dictionaries in the format expected by the LLM API. temperature: Optional sampling temperature for controlling randomness in generation. tags: List of string tags that can be set to any values by the user. Used internally - for logging and cost tracking purposes. + for logging and cost tracking purposes (e.g., pipeline stage). max_retries: Maximum number of retry attempts for failed inference calls. seed: Optional random seed for reproducible outputs. """ messages: List[Dict[str, str]] temperature: Optional[float] = None - tags: List[str] = field(default_factory=lambda: ["dev"]) + tags: List[str] = field(default_factory=lambda: ["dev"]) # Tags will identify the 'stage' max_retries: int = 8 seed: Optional[int] = None @@ -63,15 +75,116 @@ class InferenceJob: inference_calls: List[InferenceCall] +def _ensure_logs_dir(): + """Ensures the logs directory exists.""" + os.makedirs("logs", exist_ok=True) + + +def _get_encoding(encoding_name: str = "cl100k_base") -> tiktoken.Encoding: + """Gets a tiktoken encoding, defaulting to cl100k_base with fallback.""" + try: + return tiktoken.get_encoding(encoding_name) + except Exception as e: + logger.warning(f"Failed to get encoding '{encoding_name}'. Falling back to 'cl100k_base'. Error: {e}") + return tiktoken.get_encoding("cl100k_base") + + +def _count_tokens(text: str, encoding: tiktoken.Encoding) -> int: + """Counts tokens in a single string.""" + if not text: + return 0 + try: + return len(encoding.encode(text)) + except Exception as e: + logger.error(f"Error counting tokens: {e}") + return 0 + + +def _count_message_tokens(messages: List[Dict[str, str]], encoding: tiktoken.Encoding) -> int: + """Counts tokens in a list of messages, approximating OpenAI's format.""" + num_tokens = 0 + # Approximation based on OpenAI's cookbook: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb + # This might not be perfectly accurate for all models/providers but is a reasonable estimate. + tokens_per_message = 3 + tokens_per_name = 1 + + for message in messages: + num_tokens += tokens_per_message + for key, value in message.items(): + if value: + num_tokens += _count_tokens(str(value), encoding) + if key == "name": + num_tokens += tokens_per_name + num_tokens += 3 + return num_tokens + + +def _log_individual_call(model_name: str, input_tokens: int, output_tokens: int, tags: List[str], encoding_name: str): + """Logs a single inference call's cost details.""" + global _individual_header_written + try: + _ensure_logs_dir() + is_new_file = not os.path.exists(_individual_log_file) + mode = "a" if not is_new_file else "w" + + with open(_individual_log_file, mode, newline="", encoding="utf-8") as f: + writer = csv.writer(f) + # Write header only if the file is new or header wasn't written yet in this run + if is_new_file or not _individual_header_written: + writer.writerow(["timestamp", "model_name", "stage", "input_tokens", "output_tokens", "encoding_used"]) + _individual_header_written = True + + stage = ";".join(tags) if tags else "unknown" + timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat() + writer.writerow([timestamp, model_name, stage, input_tokens, output_tokens, encoding_name]) + except Exception as e: + logger.error(f"Failed to write to individual cost log: {e}") + + +def _update_aggregate_cost(model_name: str, input_tokens: int, output_tokens: int): + """Updates the global dictionary for aggregate costs.""" + try: + _cost_data[model_name]["input_tokens"] += input_tokens + _cost_data[model_name]["output_tokens"] += output_tokens + _cost_data[model_name]["calls"] += 1 + except Exception as e: + logger.error(f"Failed to update aggregate cost data: {e}") + + +def _write_aggregate_log(): + """Writes the aggregated cost data to a file at program exit.""" + try: + if not _cost_data: + logger.info("No cost data collected, skipping aggregate log.") + return + + _ensure_logs_dir() + logger.info(f"Writing aggregate cost log to {_aggregate_log_file}") + with open(_aggregate_log_file, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(["model_name", "total_input_tokens", "total_output_tokens", "total_calls"]) + for model_name, data in sorted(_cost_data.items()): + writer.writerow([model_name, data["input_tokens"], data["output_tokens"], data["calls"]]) + logger.success(f"Aggregate cost log successfully written to {_aggregate_log_file}") + except Exception as e: + # Use print here as logger might be shutting down during atexit + print(f"ERROR: Failed to write aggregate cost log: {e}", flush=True) + + +# Register the aggregate log function to run at exit +atexit.register(_write_aggregate_log) + + async def _get_response(model: Model, inference_call: InferenceCall) -> str: """ Send one inference call to the model endpoint within a global timeout context. - Logs start/end times for better concurrency tracing. + Logs start/end times for better concurrency tracing and tracks token costs. """ start_time = time.time() logger.debug( - "START _get_response: model='{}' (timestamp={:.4f})", + "START _get_response: model='{}' (encoding='{}') (timestamp={:.4f})", model.model_name, + model.encoding_name, start_time, ) @@ -92,6 +205,9 @@ async def _get_response(model: Model, inference_call: InferenceCall) -> str: model=model.model_name, messages=inference_call.messages, temperature=inference_call.temperature, + # Note: seed is not directly supported by chat_completion in huggingface_hub client API as of recent versions + # It might need to be passed via extra_body if the provider supports it. + # seed=inference_call.seed, # This might cause an error if not supported ) # Safe-guarding in case the response is missing .choices @@ -99,9 +215,22 @@ async def _get_response(model: Model, inference_call: InferenceCall) -> str: logger.error("Empty response or missing .choices from model {}", model.model_name) raise Exception("Failed Inference") + output_content = response.choices[0].message.content + + try: + encoding = _get_encoding(model.encoding_name) + input_tokens = _count_message_tokens(inference_call.messages, encoding) + output_tokens = _count_tokens(output_content, encoding) + + _log_individual_call(model.model_name, input_tokens, output_tokens, inference_call.tags, model.encoding_name) + _update_aggregate_cost(model.model_name, input_tokens, output_tokens) + logger.debug(f"Cost tracked: Model={model.model_name}, Input={input_tokens}, Output={output_tokens}") + except Exception as cost_e: + logger.error(f"Error during cost tracking for model {model.model_name}: {cost_e}") + finish_time = time.time() logger.debug( - "END _get_response: model='{}' (timestamp={:.4f}, duration={:.2f}s)", + "END _get_response: model='{}' (timestamp={:.4f}, duration={:.2f}s)", model.model_name, finish_time, (finish_time - start_time), @@ -109,9 +238,9 @@ async def _get_response(model: Model, inference_call: InferenceCall) -> str: logger.debug( "Response content from model {} = {}", model.model_name, - response.choices[0].message.content, + output_content, ) - return response.choices[0].message.content + return output_content async def _retry_with_backoff(model: Model, inference_call: InferenceCall, semaphore: asyncio.Semaphore) -> str: @@ -136,13 +265,13 @@ async def _retry_with_backoff(model: Model, inference_call: InferenceCall, semap attempt + 1, model.max_concurrent_requests, ) - return await _get_response(model, inference_call) + return await _get_response(model, inference_call) # Cost tracking happens inside _get_response except Exception as e: logger.error("Error invoking model {}: {}", model.model_name, e) # Only sleep if not on the last attempt if attempt < inference_call.max_retries - 1: - backoff_secs = 2 ** (attempt + 2) + backoff_secs = 2 ** (attempt + 2) # Exponential backoff (4, 8, 16, ...) logger.debug("Backing off for {} seconds before next attempt...", backoff_secs) await asyncio.sleep(backoff_secs) @@ -151,6 +280,16 @@ async def _retry_with_backoff(model: Model, inference_call: InferenceCall, semap model.model_name, inference_call.max_retries, ) + + try: + encoding = _get_encoding(model.encoding_name) + input_tokens = _count_message_tokens(inference_call.messages, encoding) + _log_individual_call(model.model_name, input_tokens, 0, ["FAILED"] + inference_call.tags, model.encoding_name) + _update_aggregate_cost(model.model_name, input_tokens, 0) + logger.warning(f"Logged failed call for {model.model_name} with input tokens {input_tokens}, output 0.") + except Exception as cost_e: + logger.error(f"Error during cost tracking for *failed* call {model.model_name}: {cost_e}") + return "" @@ -229,15 +368,23 @@ def _load_models(base_config: Dict[str, Any], step_name: str) -> List[Model]: # If no role models are defined for this step, use the first model from model_list if not role_models and all_configured_models: + first_model_config = all_configured_models[0] logger.info( "No models defined in model_roles for step '{}'. Using the first model from model_list: {}", step_name, - all_configured_models[0]["model_name"], + first_model_config["model_name"], ) - return [Model(**all_configured_models[0])] + return [ + Model(**{**first_model_config, "encoding_name": first_model_config.get("encoding_name", "cl100k_base")}) + ] # Filter out only those with a matching 'model_name' - matched = [Model(**m) for m in all_configured_models if m["model_name"] in role_models] + matched = [] + for m_config in all_configured_models: + if m_config["model_name"] in role_models: + model_instance = Model(**{**m_config, "encoding_name": m_config.get("encoding_name", "cl100k_base")}) + matched.append(model_instance) + logger.info( "Found {} models in config for step '{}': {}", len(matched), @@ -266,9 +413,17 @@ def run_inference( logger.warning("No models found for step '{}'. Returning empty dictionary.", step_name) return {} + # Assign the step_name as a tag if not already present (for cost tracking) + for call in inference_calls: + if step_name not in call.tags: + call.tags.append(step_name) + # 2. Run the concurrency-enabled async helper try: return asyncio.run(_run_inference_async_helper(models, inference_calls)) except Exception as e: logger.critical("Error running inference for step '{}': {}", step_name, e) - return {} + # Ensure aggregate log is attempted even on critical error during run + # Note: atexit should handle this, but adding a safeguard doesn't hurt + # _write_aggregate_log() # Redundant due to atexit + return {} # Return empty on failure From 8652c4e3814f21ff210e49cdcc412049b68bb76a Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Wed, 14 May 2025 21:42:20 -0500 Subject: [PATCH 09/69] Update inference_engine.py --- yourbench/utils/inference_engine.py | 1 - 1 file changed, 1 deletion(-) diff --git a/yourbench/utils/inference_engine.py b/yourbench/utils/inference_engine.py index 874c03ec..c86576b0 100644 --- a/yourbench/utils/inference_engine.py +++ b/yourbench/utils/inference_engine.py @@ -1,6 +1,5 @@ """ Inference Engine For Yourbench - Now with true concurrency throttling and cost tracking. -Inference Engine For Yourbench - Now with true concurrency throttling and cost tracking. """ import os From a8202094dfae514048822eef9caee641f562143c Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Wed, 14 May 2025 22:33:42 -0500 Subject: [PATCH 10/69] fix push to hub --- yourbench/utils/dataset_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yourbench/utils/dataset_engine.py b/yourbench/utils/dataset_engine.py index 3e8c1f84..029fccf3 100644 --- a/yourbench/utils/dataset_engine.py +++ b/yourbench/utils/dataset_engine.py @@ -186,7 +186,7 @@ def custom_save_dataset( config: Dict[str, Any], subset: Optional[str] = None, save_local: bool = True, - push_to_hub: bool = False, + push_to_hub: bool = True, ) -> None: """ Save a dataset subset locally and push it to Hugging Face Hub. From 6ec0b1ecc60a8dc93d4f0aaacb3204c026f138ed Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Fri, 16 May 2025 06:20:34 -0500 Subject: [PATCH 11/69] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 54ddf83f..6409f801 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "yourbench" -version = "0.3.0" +version = "0.3.1" authors = [ { name = "Sumuk Shashidhar", email = "sumuks2@illinois.edu" }, { name = "Alina Lozovskaia", email = "alina.lozovskaia@huggingface.co" }, From c35cae3d8b25064a77a09a85879d64c891377779 Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Fri, 16 May 2025 06:28:49 -0500 Subject: [PATCH 12/69] Update inference_engine.py --- yourbench/utils/inference_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yourbench/utils/inference_engine.py b/yourbench/utils/inference_engine.py index c86576b0..6f9bb23b 100644 --- a/yourbench/utils/inference_engine.py +++ b/yourbench/utils/inference_engine.py @@ -13,7 +13,7 @@ from typing import Any, Dict, List, Optional from dataclasses import field, dataclass -import tiktoken # Added for token counting +import tiktoken from dotenv import load_dotenv from loguru import logger from tqdm.asyncio import tqdm_asyncio From 80f01df32e0405fef7ab39a46245e820ab567d76 Mon Sep 17 00:00:00 2001 From: m-peko Date: Fri, 16 May 2025 13:57:46 +0200 Subject: [PATCH 13/69] Fix Dockerfile --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index caa83c91..0a728339 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,10 +36,10 @@ ENV AWS_DEFAULT_REGION="us-east-1" ENV WORKDIR="/app" # Create a startup script to run the processing workflow -RUN echo '#!/bin/bash\n\ -echo "Running yourbench workflow..."\n\ -exec python run_yourbench.py' > /app/entrypoint.sh \ - && chmod +x /app/entrypoint.sh +RUN printf '#!/bin/bash\n\ + echo "Running yourbench workflow..."\n\ + exec python run_yourbench.py\n' > /app/entrypoint.sh && \ + chmod +x /app/entrypoint.sh # Use the startup script as entry point ENTRYPOINT ["/app/entrypoint.sh"] From feb3948928b8d98f53e0a8aacf1ed025c92f4293 Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Fri, 16 May 2025 14:04:00 +0200 Subject: [PATCH 14/69] Stability fixes [WIP] --- yourbench/pipeline/lighteval.py | 45 +++++++++++++------ .../pipeline/multi_hop_question_generation.py | 18 +++----- .../single_shot_question_generation.py | 13 +----- yourbench/pipeline/summarization.py | 36 +++++++-------- yourbench/utils/chunking_utils.py | 2 +- yourbench/utils/inference_engine.py | 6 +-- yourbench/utils/parsing_engine.py | 23 ++++++++++ 7 files changed, 82 insertions(+), 61 deletions(-) diff --git a/yourbench/pipeline/lighteval.py b/yourbench/pipeline/lighteval.py index e99336f2..8b3662ce 100644 --- a/yourbench/pipeline/lighteval.py +++ b/yourbench/pipeline/lighteval.py @@ -173,9 +173,18 @@ def make_single_shot_record(row: Dict[str, Any]) -> Dict[str, Any]: # if multiple choice question convert to number gold = row.get("self_answer", "") - if row.get("choices"): - gold = [ord(gold) - ord("A")] - + if not gold: + logger.warning("Row has empty answer line") + + stage_cfg = config.get("pipeline", {}).get("single_shot_question_generation", {}) + if stage_cfg.get("question_type") == "multi-choice": + if not gold: + gold = [0] + else: + gold = [ord(gold) - ord("A")] + else: + gold = [gold] + return { "question": row.get("question", ""), "additional_instructions": row.get("additional_instructions", ""), @@ -216,9 +225,17 @@ def make_multi_hop_record(row: Dict[str, Any]) -> Dict[str, Any]: # if multiple choice question convert to number gold = row.get("self_answer", "") - if row.get("choices"): - gold = [ord(gold) - ord("A")] - + if not gold: + logger.warning("Row has empty answer line") + + stage_cfg = config.get("pipeline", {}).get("single_shot_question_generation", {}) + if stage_cfg.get("question_type") == "multi-choice": + if not gold: + gold = [0] + else: + gold = [ord(gold) - ord("A")] + else: + gold = [gold] return { "question": row.get("question", ""), "additional_instructions": row.get("additional_instructions", ""), @@ -259,16 +276,16 @@ def make_multi_hop_record(row: Dict[str, Any]) -> Dict[str, Any]: # ---------------------------------------- logger.info(f"Assembling final dataset with {len(combined_records)} rows.") try: - # Convert to column-wise dict for HF Dataset - col_names = list(combined_records[0].keys()) - final_dict = {c: [] for c in col_names} - for rec in combined_records: - for c in col_names: - final_dict[c].append(rec[c]) - final_ds = Dataset.from_dict(final_dict) + # # Convert to column-wise dict for HF Dataset + # col_names = list(combined_records[0].keys()) + # final_dict = {c: [] for c in col_names} + # for rec in combined_records: + # for c in col_names: + # final_dict[c].append(rec[c]) + final_ds = Dataset.from_list(combined_records) except Exception as ds_error: - logger.error(f"Failed to create final dataset object: {ds_error}") + logger.exception("Failed to create final dataset object") return # ---------------------------------------- diff --git a/yourbench/pipeline/multi_hop_question_generation.py b/yourbench/pipeline/multi_hop_question_generation.py index 51f8b26b..404466f4 100644 --- a/yourbench/pipeline/multi_hop_question_generation.py +++ b/yourbench/pipeline/multi_hop_question_generation.py @@ -62,7 +62,7 @@ ) # Import the unified parsing function -from yourbench.utils.parsing_engine import shuffle_mcq, parse_qa_pairs_from_response +from yourbench.utils.parsing_engine import _force_int_in_range, shuffle_mcq, parse_qa_pairs_from_response, _validate_list from yourbench.utils.inference_engine import InferenceCall, run_inference @@ -87,11 +87,16 @@ def __post_init__(self) -> None: self.estimated_difficulty = _force_int_in_range(self.estimated_difficulty, 1, 10) self.question_type = str(self.question_type) self.thought_process = str(self.thought_process) + if not isinstance(self.citations, list): self.citations = [] + else: + self.citations = _validate_list(self.citations) if not isinstance(self.choices, list): self.choices = [] + else: + self.choices = _validate_list(self.choices) @dataclass @@ -362,14 +367,3 @@ def _parse_and_build_final( except Exception as ds_error: logger.error(f"Failed to create dataset from multi-hop question rows: {ds_error}") return None - - -def _force_int_in_range(value: Any, min_val: int, max_val: int) -> int: - """ - Convert a value to int and clamp it between min_val and max_val. - """ - try: - ivalue = int(value) - except (ValueError, TypeError): - ivalue = (min_val + max_val) // 2 - return max(min_val, min(ivalue, max_val)) diff --git a/yourbench/pipeline/single_shot_question_generation.py b/yourbench/pipeline/single_shot_question_generation.py index b1879fca..ef2709e4 100644 --- a/yourbench/pipeline/single_shot_question_generation.py +++ b/yourbench/pipeline/single_shot_question_generation.py @@ -46,7 +46,7 @@ ) # Import the unified parsing function -from yourbench.utils.parsing_engine import shuffle_mcq, parse_qa_pairs_from_response +from yourbench.utils.parsing_engine import _force_int_in_range, shuffle_mcq, parse_qa_pairs_from_response, _validate_list from yourbench.utils.inference_engine import InferenceCall, run_inference @@ -314,7 +314,7 @@ def _process_responses_and_build_dataset( # Safely extract data from pair question_text = str(pair.get("question", "")).strip() answer_text = str(pair.get("answer", "")).strip() - choices = pair.get("choices", []) + choices = _validate_list(pair.get("choices", [])) difficulty_val = _force_int_in_range(pair.get("estimated_difficulty", 5), 1, 10) question_type = str(pair.get("question_type", "unknown")) thought_process = str(pair.get("thought_process", "")) @@ -355,12 +355,3 @@ def _process_responses_and_build_dataset( return Dataset.from_dict(final_data) -def _force_int_in_range(value: Any, min_val: int, max_val: int) -> int: - """ - Convert a value to int and clamp it between min_val and max_val. - """ - try: - ivalue = int(value) - except (ValueError, TypeError): - ivalue = (min_val + max_val) // 2 - return max(min_val, min(ivalue, max_val)) diff --git a/yourbench/pipeline/summarization.py b/yourbench/pipeline/summarization.py index 52884d78..2f6f0c39 100644 --- a/yourbench/pipeline/summarization.py +++ b/yourbench/pipeline/summarization.py @@ -64,11 +64,6 @@ from yourbench.utils.inference_engine import InferenceCall, run_inference -############################ -# Internal helper functions # -############################ - - def _build_chunk_calls( dataset: Dataset, max_tokens: int, @@ -84,7 +79,6 @@ def _build_chunk_calls( calls: List[InferenceCall] = [] mapping: List[Tuple[int, int]] = [] # (doc_index, chunk_index) - # ─── NEW: robust encoding fetch with fallback ──────────────────────────── try: enc = tiktoken.get_encoding(encoding_name) except Exception as e: # KeyError on unknown name, ValueError on bad cache @@ -94,10 +88,10 @@ def _build_chunk_calls( str(e)[:60] + ("…" if len(str(e)) > 60 else ""), ) enc = tiktoken.get_encoding("cl100k_base") - # ──────────────────────────────────────────────────────────────────────── + for doc_idx, doc_text in enumerate(dataset["document_text"]): - token_len = len(enc.encode(doc_text)) + token_len = len(enc.encode(doc_text, disallowed_special=())) if token_len <= max_tokens: # treat as single chunk (chunk_idx = -1) prompt = CHUNK_SUMMARIZATION_USER_PROMPT.format(chunk=doc_text) calls.append(InferenceCall(messages=[{"role": "user", "content": prompt}], tags=["chunk_summary"])) @@ -127,10 +121,7 @@ def _collect_chunk_summaries( ) -> Tuple[str, List[List[str]], List[List[str]]]: """Re-orders raw model responses back into per-document lists. - Notes - ----- - `model_name` is always `str` (never None) because we early-return if - `response_dict` is empty. + model_name: str is guaranteed to be set, as we return early if response_dict is empty. """ if not response_dict: return "", [], [] @@ -195,11 +186,6 @@ def _merge_final_summaries( return final_summaries -################# -# Stage runner # -################# - - def run(config: dict[str, Any]) -> None: stage_cfg = config.get("pipeline", {}).get("summarization", {}) if not stage_cfg.get("run", False): @@ -226,6 +212,10 @@ def run(config: dict[str, Any]) -> None: # 3) Second pass – combine summaries where needed combine_calls, doc_indices = _build_combine_calls(clean_chunk_by_doc) + + if len(doc_indices) == 0: + logger.info("All documents were short enough for single-pass summarization.") + combine_summaries_raw: List[str] = [] if combine_calls: combine_resp = run_inference(config=config, step_name="summarization_combine", inference_calls=combine_calls) @@ -243,9 +233,15 @@ def run(config: dict[str, Any]) -> None: # 4) Add columns & persist dataset = dataset.add_column("raw_chunk_summaries", raw_chunk_by_doc) dataset = dataset.add_column("chunk_summaries", clean_chunk_by_doc) - dataset = dataset.add_column( - "raw_document_summary", combine_summaries_raw if combine_calls else [""] * len(dataset) - ) + + # Fill summaries only for combined docs; others stay empty for alignment. + raw_document_summary = [""] * len(dataset) + if combine_calls: + for idx, doc_idx in enumerate(doc_indices): + raw_document_summary[doc_idx] = combine_summaries_raw[idx] + + dataset = dataset.add_column("raw_document_summary", raw_document_summary) + dataset = dataset.add_column("document_summary", final_summaries) dataset = dataset.add_column("summarization_model", [model_name] * len(dataset)) diff --git a/yourbench/utils/chunking_utils.py b/yourbench/utils/chunking_utils.py index 20c0af3c..c774d5e3 100644 --- a/yourbench/utils/chunking_utils.py +++ b/yourbench/utils/chunking_utils.py @@ -27,6 +27,6 @@ def split_into_token_chunks( text = preprocess(text) enc = tiktoken.get_encoding(encoding_name) - tokens = enc.encode(text) + tokens = enc.encode(text, disallowed_special=()) stride = chunk_tokens - overlap return [enc.decode(tokens[i : i + chunk_tokens]) for i in range(0, len(tokens), stride)] diff --git a/yourbench/utils/inference_engine.py b/yourbench/utils/inference_engine.py index d624b640..02d6c6bf 100644 --- a/yourbench/utils/inference_engine.py +++ b/yourbench/utils/inference_engine.py @@ -54,7 +54,7 @@ class InferenceCall: messages: List[Dict[str, str]] temperature: Optional[float] = None tags: List[str] = field(default_factory=lambda: ["dev"]) - max_retries: int = 8 + max_retries: int = 12 seed: Optional[int] = None @@ -96,7 +96,7 @@ async def _get_response(model: Model, inference_call: InferenceCall) -> str: # Safe-guarding in case the response is missing .choices if not response or not response.choices: - logger.error("Empty response or missing .choices from model {}", model.model_name) + logger.warning("Empty response or missing .choices from model {}", model.model_name) raise Exception("Failed Inference") finish_time = time.time() @@ -138,7 +138,7 @@ async def _retry_with_backoff(model: Model, inference_call: InferenceCall, semap ) return await _get_response(model, inference_call) except Exception as e: - logger.error("Error invoking model {}: {}", model.model_name, e) + logger.warning("Error invoking model {}: {}", model.model_name, e) # Only sleep if not on the last attempt if attempt < inference_call.max_retries - 1: diff --git a/yourbench/utils/parsing_engine.py b/yourbench/utils/parsing_engine.py index 2586ce03..0d8da123 100644 --- a/yourbench/utils/parsing_engine.py +++ b/yourbench/utils/parsing_engine.py @@ -186,3 +186,26 @@ def shuffle_mcq(question_dict: dict) -> dict: question_dict["answer"] = new_answer_letter return question_dict + + +def _force_int_in_range(value: Any, min_val: int, max_val: int) -> int: + """ + Convert a value to int and clamp it between min_val and max_val. + """ + try: + ivalue = int(value) + except (ValueError, TypeError): + ivalue = (min_val + max_val) // 2 + return max(min_val, min(ivalue, max_val)) + +def _validate_list(some_list: list[str]) -> list[str]: + """ + Force possible list of strings to be a list of strings + """ + if not isinstance(some_list, list): + return [] + + try: + return [str(value) for value in some_list] + except Exception: + return [] From d26927ff4ea047023794c3a94d6fd1988be1a322 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 14 May 2025 19:29:55 -0500 Subject: [PATCH 15/69] add offline mode --- yourbench/utils/dataset_engine.py | 58 +++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/yourbench/utils/dataset_engine.py b/yourbench/utils/dataset_engine.py index 029fccf3..286fbfea 100644 --- a/yourbench/utils/dataset_engine.py +++ b/yourbench/utils/dataset_engine.py @@ -1,5 +1,5 @@ import os -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Union from loguru import logger @@ -14,7 +14,17 @@ class ConfigurationError(Exception): pass +def _is_offline_mode() -> bool: + """Check if offline mode is enabled via environment variable.""" + return os.environ.get("HF_HUB_OFFLINE", "0").lower() in ("1", "true", "yes") + + def _safe_get_organization(config: Dict, dataset_name: str, organization: str, token: str) -> str: + # In offline mode, don't try to fetch organization + if _is_offline_mode(): + logger.info("Offline mode detected. Skipping organization fetch.") + return organization + if not organization or (isinstance(organization, str) and organization.startswith("$")): if isinstance(organization, str) and organization.startswith("$"): # Log if it was explicitly set but unexpanded @@ -106,6 +116,11 @@ def _get_full_dataset_repo_name(config: Dict[str, Any]) -> str: if organization and "/" not in dataset_name: full_dataset_name = f"{organization}/{dataset_name}" + # Skip Hub validation in offline mode + if _is_offline_mode(): + logger.debug(f"Offline mode detected. Skipping Hub validation for repo ID '{full_dataset_name}'") + return full_dataset_name + # Use HfApi for robust validation api = HfApi() try: @@ -147,11 +162,14 @@ def _get_full_dataset_repo_name(config: Dict[str, Any]) -> str: def custom_load_dataset(config: Dict[str, Any], subset: Optional[str] = None) -> Dataset: """ Load a dataset subset from a local directory if specified, otherwise from Hugging Face. + In offline mode, only load from local directory. """ local_dataset_dir = config.get("local_dataset_dir", None) + if local_dataset_dir is None and "hf_configuration" in config and "local_dataset_dir" in config["hf_configuration"]: + local_dataset_dir = config["hf_configuration"].get("local_dataset_dir") + + # First try loading from local path if local_dataset_dir: - import os - if os.path.exists(local_dataset_dir): logger.info(f"Loading dataset locally from '{local_dataset_dir}'") dataset = load_from_disk(local_dataset_dir) @@ -164,11 +182,22 @@ def custom_load_dataset(config: Dict[str, Any], subset: Optional[str] = None) -> return Dataset.from_dict({}) return dataset else: - logger.warning( - f"local_dataset_dir '{local_dataset_dir}' does not exist. Falling back to Hugging Face Hub." - ) + logger.warning(f"local_dataset_dir '{local_dataset_dir}' does not exist.") + if _is_offline_mode(): + logger.error("Offline mode is enabled but local dataset not found. Returning empty dataset.") + return Dataset.from_dict({}) + else: + logger.warning("Falling back to Hugging Face Hub.") + + # If we're in offline mode and made it here, the local dataset doesn't exist + if _is_offline_mode(): + logger.warning("Offline mode enabled but no local dataset found. Returning empty dataset.") + return Dataset.from_dict({}) + + # If we're here, try to get from Hub dataset_repo_name = _get_full_dataset_repo_name(config) - logger.info(f"Loading dataset HuggingFace Hub with repo_id='{dataset_repo_name}'") + logger.info(f"Loading dataset from HuggingFace Hub with repo_id='{dataset_repo_name}'") + # If subset name does NOT exist, return an empty dataset to avoid the crash: try: return load_dataset(dataset_repo_name, name=subset, split="train") @@ -196,10 +225,19 @@ def custom_save_dataset( create a new DatasetDict containing that subset. - All subsets are saved to the same local_dataset_dir. """ + # In offline mode, force save local and disable push to hub + if _is_offline_mode(): + save_local = True + if push_to_hub: + logger.warning("Offline mode enabled. Disabling push_to_hub operation.") + push_to_hub = False dataset_repo_name = _get_full_dataset_repo_name(config) local_dataset_dir = config.get("local_dataset_dir", None) + if local_dataset_dir is None and "hf_configuration" in config and "local_dataset_dir" in config["hf_configuration"]: + local_dataset_dir = config["hf_configuration"].get("local_dataset_dir") + if local_dataset_dir and save_local: logger.info(f"Saving dataset locally to: '{local_dataset_dir}'") @@ -250,7 +288,7 @@ def custom_save_dataset( local_dataset = dataset # Create the directory if it doesn't exist - os.makedirs(local_dataset_dir, exist_ok=True) + os.makedirs(os.path.dirname(local_dataset_dir), exist_ok=True) try: # Save the dataset to disk @@ -281,7 +319,7 @@ def custom_save_dataset( # Re-raise if it's a different permission error raise - if config["hf_configuration"].get("concat_if_exist", False): + if config["hf_configuration"].get("concat_if_exist", False) and not _is_offline_mode(): existing_dataset = custom_load_dataset(config=config, subset=subset) dataset = concatenate_datasets([existing_dataset, dataset]) logger.info("Concatenated dataset with an existing one") @@ -291,7 +329,7 @@ def custom_save_dataset( else: config_name = "default" - if push_to_hub: + if push_to_hub and not _is_offline_mode(): logger.info(f"Pushing dataset to HuggingFace Hub with repo_id='{dataset_repo_name}'") dataset.push_to_hub( repo_id=dataset_repo_name, From 40f06e54e7f0585e80a496bbb76469883a991f44 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 14 May 2025 19:58:44 -0500 Subject: [PATCH 16/69] add cq --- yourbench/utils/dataset_engine.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/yourbench/utils/dataset_engine.py b/yourbench/utils/dataset_engine.py index 286fbfea..a9f8c2ad 100644 --- a/yourbench/utils/dataset_engine.py +++ b/yourbench/utils/dataset_engine.py @@ -1,5 +1,5 @@ import os -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional from loguru import logger @@ -165,9 +165,13 @@ def custom_load_dataset(config: Dict[str, Any], subset: Optional[str] = None) -> In offline mode, only load from local directory. """ local_dataset_dir = config.get("local_dataset_dir", None) - if local_dataset_dir is None and "hf_configuration" in config and "local_dataset_dir" in config["hf_configuration"]: + if ( + local_dataset_dir is None + and "hf_configuration" in config + and "local_dataset_dir" in config["hf_configuration"] + ): local_dataset_dir = config["hf_configuration"].get("local_dataset_dir") - + # First try loading from local path if local_dataset_dir: if os.path.exists(local_dataset_dir): @@ -193,11 +197,11 @@ def custom_load_dataset(config: Dict[str, Any], subset: Optional[str] = None) -> if _is_offline_mode(): logger.warning("Offline mode enabled but no local dataset found. Returning empty dataset.") return Dataset.from_dict({}) - + # If we're here, try to get from Hub dataset_repo_name = _get_full_dataset_repo_name(config) logger.info(f"Loading dataset from HuggingFace Hub with repo_id='{dataset_repo_name}'") - + # If subset name does NOT exist, return an empty dataset to avoid the crash: try: return load_dataset(dataset_repo_name, name=subset, split="train") @@ -235,7 +239,11 @@ def custom_save_dataset( dataset_repo_name = _get_full_dataset_repo_name(config) local_dataset_dir = config.get("local_dataset_dir", None) - if local_dataset_dir is None and "hf_configuration" in config and "local_dataset_dir" in config["hf_configuration"]: + if ( + local_dataset_dir is None + and "hf_configuration" in config + and "local_dataset_dir" in config["hf_configuration"] + ): local_dataset_dir = config["hf_configuration"].get("local_dataset_dir") if local_dataset_dir and save_local: From ca7a3b071ef24c5de905511fef0b4bc310dadbd1 Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Fri, 16 May 2025 06:35:08 -0500 Subject: [PATCH 17/69] Update dataset_engine.py --- yourbench/utils/dataset_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yourbench/utils/dataset_engine.py b/yourbench/utils/dataset_engine.py index a9f8c2ad..454fca9d 100644 --- a/yourbench/utils/dataset_engine.py +++ b/yourbench/utils/dataset_engine.py @@ -189,7 +189,7 @@ def custom_load_dataset(config: Dict[str, Any], subset: Optional[str] = None) -> logger.warning(f"local_dataset_dir '{local_dataset_dir}' does not exist.") if _is_offline_mode(): logger.error("Offline mode is enabled but local dataset not found. Returning empty dataset.") - return Dataset.from_dict({}) + raise else: logger.warning("Falling back to Hugging Face Hub.") From 141a696bdf42042ad38a02940b45cee788d01f93 Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Fri, 16 May 2025 06:37:33 -0500 Subject: [PATCH 18/69] Update dataset_engine.py --- yourbench/utils/dataset_engine.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/yourbench/utils/dataset_engine.py b/yourbench/utils/dataset_engine.py index 454fca9d..1191a80c 100644 --- a/yourbench/utils/dataset_engine.py +++ b/yourbench/utils/dataset_engine.py @@ -188,8 +188,7 @@ def custom_load_dataset(config: Dict[str, Any], subset: Optional[str] = None) -> else: logger.warning(f"local_dataset_dir '{local_dataset_dir}' does not exist.") if _is_offline_mode(): - logger.error("Offline mode is enabled but local dataset not found. Returning empty dataset.") - raise + raise ValueError("Offline mode is enabled but local dataset not found") else: logger.warning("Falling back to Hugging Face Hub.") From 942de83a1b858bd3a4560c0e69f20dbfd02738e9 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Tue, 20 May 2025 06:53:35 -0500 Subject: [PATCH 19/69] add new readme, remove legacy figure --- README.md | 226 +++++++++++++---------------- docs/assets/process-figure.png | Bin 163188 -> 0 bytes docs/assets/yourbench_pipeline.png | Bin 0 -> 253238 bytes 3 files changed, 100 insertions(+), 126 deletions(-) delete mode 100644 docs/assets/process-figure.png create mode 100644 docs/assets/yourbench_pipeline.png diff --git a/README.md b/README.md index 1f961225..7a0dd62f 100644 --- a/README.md +++ b/README.md @@ -1,198 +1,172 @@ - -

- - - YourBench Logo + YourBench Logo

YourBench: A Dynamic Benchmark Generation Framework

+ + GitHub Repo stars + +

- [GitHub] - · - [Dataset] - · - [Documentation] - · + [GitHub] · + [Dataset] · + [Documentation] · [Paper]

- - - GitHub Repo stars + + YourBench Demo Video +
+ Watch Demo on YouTube +
+ Watch our 3-minute demo of the YourBench pipeline
-

- - YourBench Demo Video -
- Watch Demo on YouTube -
- Watch our 3-minute demo of the YourBench pipeline -
-

-
--- -> **YourBench** is an open-source framework for generating domain-specific benchmarks in a zero-shot manner. It aims to keep your large language models on their toes—even as new data sources, domains, and knowledge demands evolve. +> **YourBench** is an open-source framework for generating domain-specific benchmarks in a zero-shot manner. It aims to keep your large language models on their toes – even as new data sources, domains, and knowledge demands evolve. **Highlights**: -- **Dynamic Benchmark Generation**: Produce diverse, up-to-date questions from real-world source documents (PDF, Word, HTML, even multimedia). -- **Scalable & Structured**: Seamlessly handles ingestion, summarization, and multi-hop chunking for large or specialized datasets. -- **Zero-Shot Focus**: Emulates real-world usage scenarios by creating fresh tasks that guard against memorized knowledge. -- **Extensible**: Out-of-the-box pipeline stages (ingestion, summarization, question generation), plus an easy plugin mechanism to accommodate custom models or domain constraints. - ---- -## Quick Start (Alpha) +* **Dynamic Benchmark Generation** – Produce diverse, up-to-date question-answer pairs derived from real-world source documents (PDF, Word, HTML, even multimedia). +* **Scalable & Structured** – Seamlessly handle ingestion, summarization, and multi-hop chunking for large or specialized datasets. +* **Extensible Pipeline** – Use out-of-the-box stages (ingestion, summarization, question generation) or plug in custom models and logic to accommodate domain-specific needs. +* **Robust Configuration** – Control the entire pipeline via a single YAML config (model choices, data paths, chunking parameters, generation prompts, deduplication thresholds, etc.). +* **Multi-Model Support** – Assign different LLMs for each stage (ingestion, summarization, QG, answering), fostering broader coverage and question-style diversity. +* **Deduplication & Quality Filtering** – Automatically group near-duplicates to prune questions and retain a curated set of high-quality queries. +* **Logging & Analysis** – Built-in metrics evaluate dataset coverage, question distribution, difficulty, and more. +* **Flexible Output** – Save generated benchmarks locally or push them to the Hugging Face Hub for sharing or public leaderboards. -```bash -# 1. Clone the repo -git clone https://github.com/huggingface/yourbench.git -cd yourbench - -# Use uv to install the dependencies -# pip install uv # if you do not have uv already -uv venv -source .venv/bin/activate -uv sync -uv pip install -e . - -# 3. Get a key from https://openrouter.ai/ and add it to the .env file (or make your own config with a different model!) -touch .env -echo "HF_TOKEN=" >> .env -echo "HF_ORGANIZATION=" >> .env - -# 4. Run the pipeline with an example config -yourbench run --config example/configs/example.yaml -``` - -**Note**: The above instructions are a work-in-progress, and more comprehensive usage info will be provided soon. - - -# Process Flow +YourBench tackles a critical evaluation gap for LLMs. Traditional static benchmarks are quickly **saturated** or contaminated by training data, making it hard to assess models on new knowledge. Domain-specific or up-to-date evaluation is often costly and slow with human annotation. **YourBench addresses this by enabling dynamic, automated generation of reliable, domain-tailored benchmarks directly from your data, without manual labeling**. In a recent study, YourBench replicated several subsets of a popular benchmark (MMLU) using minimal source text for **under \$15** in total cost, while preserving the original ranking of model performance (Spearman ρ = 1). By grounding questions in user-provided documents, YourBench ensures evaluations stay relevant and **truly test a model’s knowledge on content it hasn’t seen before**. -![Process Flow](docs/assets/process-figure.png) +## Installation +YourBench is available on PyPI and requires **Python 3.12+**. You can install it as follows: -## Key Features +* **Install via PyPI (stable release):** -- **Automated Benchmark Generation** - Generate question-answer pairs that test LLMs on specific domains or knowledge slices, derived directly from your raw documents. + ```bash + # uv (recommended; get it here: https://docs.astral.sh/uv/getting-started/installation/) + uv pip install yourbench -- **Flexible Pipeline** - Each stage (ingestion, summarization, chunking, multi-/single-hop QG, deduplication) can be enabled or disabled via YAML config. Fine-grained control allows minimal or comprehensive runs. + # pip (standard support) + pip install yourbench + ``` -- **Robust Config System** - A single YAML config controls model roles, data paths, chunking parameters, question generation instructions, deduplication thresholds, etc. + This will install the latest published version (e.g. `0.3.1`). -- **Multi-Model Ensemble Support** - Use different LLMs for ingestion, summarization, question generation, or answering. This fosters broader coverage and question style diversity. +* **Install from source (development version):** -- **Deduplication & Quality Filtering** - Automatic grouping of near-duplicate questions to prune and keep a curated set. + ```bash + git clone https://github.com/huggingface/yourbench.git + cd yourbench + + # uv, recommended + uv venv + source .venv/bin/activate + uv pip install -e . -- **Extensive Logging & Analysis** - Built-in modules measure dataset coverage, question distribution, difficulty metrics, and more. + # pip + pip install -e . + ``` -- **Public or Private** - Optionally push ingested or generated data to the Hugging Face Hub or keep it local. + Installing from source is recommended if you want the latest updates or to run the included example configuration. -- **Extensible** - Each pipeline step is modular. Easily add custom question-generation prompts, chunking logic, or domain-specific expansions. +> **Note:** If you plan to use models that require API access (e.g. OpenAI GPT-4o or Hugging Face Inference API), make sure to have the appropriate credentials. You’ll also need a Hugging Face token (to optionally to upload results). See below for how to configure these before running YourBench. ---- +## Quickstart Usage -## Core Concepts & Workflow +Once installed, YourBench can be run from the command line to generate a custom evaluation set. Here’s a quick example: -YourBench follows a multi-stage approach: +```bash +# 1. (Optional) If not done already, install YourBench +pip install yourbench -1. **Document Ingestion** - Convert PDFs, HTML, Word, or text into a standardized Markdown format. +# 2. Prepare your API credentials (for model inference and Hub access) +# For example, create a .env file with required keys: +# echo "OPENROUTER_API_KEY=" >> .env # Example +echo "HF_TOKEN=" >> .env # Hugging Face token (for Hub datasets & inference) +echo "HF_ORGANIZATION=" >> .env # (Optional) Organization name for dataset pushing -2. **Summarization** - Generate a concise "global summary" for each document, using a designated summarization LLM. +# 3. Run the pipeline on the provided example config (uses sample docs and models) +yourbench run --config example/configs/example.yaml -3. **Chunking** - Split or chunk documents (and optionally combine multiple smaller segments) based on text similarity or length constraints. +# 4. (Optional) Run the pipeline on your own documents: +yourbench run --config my_custom_config.yaml +``` -4. **Question Generation** - - **Single-Shot**: Create straightforward, single-chunk questions. - - **Multi-Hop**: Combine multiple chunks to produce more complex, integrative questions. +The **example configuration** `example/configs/example.yaml` (included in the repository) demonstrates a basic setup. It specifies sample documents and default models for each stage of the pipeline. In step 3 above, YourBench will automatically ingest the example documents, generate a set of Q\&A pairs, and output a Hugging Face Dataset containing the evaluation questions and answers. -5. **Deduplication** - Remove or group near-duplicate questions across your dataset using embedding-based similarity. +For your own data, you can create a YAML config pointing to your documents and preferred models. For instance, you might specify a folder of PDFs or text files under a `documents` field, and choose which LLM to use for question generation. **YourBench is fully configurable** – you can easily **toggle stages** on or off and swap in different models. *For example: you could disable the summarization stage for very short texts, or use a powerful, large, API model for question generation while using a faster local model for summarization.* The possibilities are endless! Simply adjust the YAML, and the pipeline will accommodate it. (See the [usage example](https://github.com/huggingface/yourbench/blob/main/example/configs/advanced_example.yaml) for all available options!) -6. **Analysis** - Evaluate question distribution, difficulty, coverage, or run custom analyses. +## Process Flow -7. **Export** - The resulting question sets can be stored locally or uploaded as a new dataset on the Hugging Face Hub. +![YourBench pipeline process flow diagram – from document ingestion to evaluation](docs/assets/yourbench_pipeline.png) ---- +Under the hood, YourBench follows a multi-stage pipeline to turn raw documents into a ready-to-use benchmark dataset: -## 🧰 Development +1. **Document Ingestion** – Convert PDFs, HTML, Word docs, or raw text files into a standardized format (Markdown) for downstream processing. +2. **Summarization** – Generate a concise *global summary* of each document using a designated summarization model. This helps distill key points and limit the scope for question generation. +3. **Chunking** – Split documents into smaller chunks (and optionally merge small pieces) based on semantic similarity or length constraints. This ensures long or complex documents are broken into manageable sections for Q\&A generation. +4. **Question Generation** – For each chunk (or combination of chunks), generate questions: -We use: -- [Ruff](https://github.com/astral-sh/ruff) for code formatting and linting -- [pytest](https://docs.pytest.org/) for testing + * *Single-Hop:* Create straightforward questions answerable from a single chunk. + * *Multi-Hop:* Combine multiple chunks to produce more complex questions that require integrating information from different parts of the content. +5. **Deduplication** – Remove or group together near-duplicate questions using embedding-based similarity, to avoid redundant entries in your benchmark. +6. **Analysis** – Evaluate the question set for coverage and difficulty. YourBench provides logging and analysis tools to measure how well the questions cover the source content, the distribution of topics, estimated difficulty levels, etc., and can run custom analysis modules. +7. **Export** – Finally, output the generated Q\&A benchmark. The results can be saved as a local dataset (using the Hugging Face `datasets` format) or even uploaded to the Hugging Face Hub for sharing. This makes it easy to evaluate models on the new benchmark or even set up a public leaderboard. +Throughout this process, **YourBench ensures the questions are grounded in your provided documents**, rather than what an LLM might already know. By using documents (and even an optional fresh document dataset like *Tempora-0325* for time-sensitive topics), the pipeline minimizes reliance on a model’s parametric memory, yielding more truthful and up-to-date evaluation queries. -## 🚀 Try YourBench on Hugging Face +## Try it Online (Hugging Face Spaces) -To test YourBench on your own documents: +You can **try YourBench right away in your browser** – no installation needed: -- Use the [Demo Space](https://huggingface.co/spaces/yourbench/demo) to generate a dataset and leaderboard in one click – entirely free -- Use the [Advanced Space](https://huggingface.co/spaces/yourbench/advanced) for full control over the pipeline, with custom configs and your own inference +* **[YourBench Demo Space](https://huggingface.co/spaces/yourbench/demo)** – Use our ready-to-go web demo to upload a document (or paste text) and generate a custom evaluation set with **one click**, complete with an instant model leaderboard. **(This free demo will use a default set of models to answer the questions and show how different models perform.)** +* **[YourBench Advanced Space](https://huggingface.co/spaces/yourbench/advanced)** – For power users, the advanced demo lets you provide a custom YAML config and plug in your own models or API endpoints. This gives you full control over the pipeline (choose specific models, adjust chunking parameters, etc.) via a convenient UI, right from the browser. +👉 Both hosted apps are available on Hugging Face Spaces under the **[yourbench](https://huggingface.co/yourbench)** organization. Give them a try to see how YourBench can generate benchmarks tailored to your use-case in minutes. -## 🤝 Contributing +## Contributing -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/amazing-feature`) -3. Install development dependencies -4. Make your changes -5. Run tests and ensure code style compliance -6. Commit your changes (`git commit -m 'Add amazing feature'`) -7. Push to the branch (`git push origin feature/amazing-feature`) -8. Open a Pull Request +Contributions are welcome! If you’d like to improve YourBench or add new features, please follow these steps: -## 📄 License +1. **Fork** the repository (on GitHub). +2. **Create a branch** for your feature (`git checkout -b feature/amazing-feature`). +3. **Install dev dependencies** (e.g. `pip install -r requirements.txt` or use `poetry/uv` if available) and set up the project for development. +4. **Make your changes**, adding new tests if applicable. +5. **Run tests** (`pytest`) and ensure code style compliance with `make style` and `make quality` (we use [Ruff](https://github.com/charliermarsh/ruff) for linting). +6. **Commit** your changes (`git commit -m 'Add amazing feature'`). +7. **Push** to your branch (`git push origin your-amazing-feature`). +8. Open a **Pull Request** on the main repository. -This project is licensed under the Apache-2.0 License - see the [LICENSE](LICENSE) file for details. +We actively review PRs and welcome improvements or fixes from the community. For major changes, feel free to open an issue first to discuss the idea. -## 🙏 Acknowledgments +## License -- [Sentence Transformers](https://www.sbert.net/) for semantic embeddings -- [Hugging Face](https://huggingface.co/) for dataset infrastructure +This project is licensed under the Apache 2.0 License – see the [LICENSE](LICENSE) file for details. You are free to use, modify, and distribute YourBench in either commercial or academic projects under the terms of this license. ## Citation -If YourBench is helpful to you, please cite!: +If you use **YourBench** in your research or applications, please consider citing our paper: -``` +```bibtex @misc{shashidhar2025yourbencheasycustomevaluation, - title={YourBench: Easy Custom Evaluation Sets for Everyone}, + title={YourBench: Easy Custom Evaluation Sets for Everyone}, author={Sumuk Shashidhar and Clémentine Fourrier and Alina Lozovskia and Thomas Wolf and Gokhan Tur and Dilek Hakkani-Tür}, year={2025}, eprint={2504.01833}, archivePrefix={arXiv}, primaryClass={cs.CL}, - url={https://arxiv.org/abs/2504.01833}, + url={https://arxiv.org/abs/2504.01833} } ``` diff --git a/docs/assets/process-figure.png b/docs/assets/process-figure.png deleted file mode 100644 index 849fc798f1e46e6960b1820d60699c41b6af09c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163188 zcmeFZi8mDB`##(vlxzu+HHu_Ub|ZwwzD4$snC#j2WGl;%eIK%gkbPewrihVUma$Lv z7~9z9eWt#@&w0O}_b+(Q`JJQFF=os&_w(HMecjh}J>F?)D3f1dymIc`IdbUZM>^-u zk>#H|cafNs7(DX%Wv2r8@4Tyyvi!NSex?=h4UvtUy4<;Q6)|Kd7MH;Hmz^IQx}G~n z(MkAs9_jS)`MGoWeCQ)NJukEMQ?h~E?wJ^zO~ZOrl|^Le*RtGdo!37H+HaBm{`~BG zlL?XWv;3Q%eHlqjbDq|;lfC+x|CIlV*=yL+{I^^9R9JR?-6zLQjn|~=_dNBP-p=ZD zh>KR~K8>QvoNO--^ANoHL{vBrMd10;=Y^nPgWbU zL*qFA*ViDGSu)G*|Md`f24hN&+kZbcIB+#z^}k*n7xseN^uHey5m8wA@5l0T7*vw~ z_dCaNlJ{vf{`X@r+pxj^&vF#*C-DFGav0P8?-Kt;`Ts`oe-7LKU(aH-b-Jjk_5bU9 z=XqmKw*uR|cD{2(inXk$cfqALpECLV`Le=Hc-A0iRlZx%m!)KLGhXA`Po8zt9+R}n zEZ)yuuzg~M?7V)SQ!dIb-3&I@k2mVjQQp(m$)-KtEt`#N5eDY|5B5q!!`vUV4q;KU zvMrJgDQ2S$lAj8N1U}XMCuxnImNoD6SawCwcCm*u>14Q%8g>;GW*H@UM<4u6*m#h+ z!?W8+R?YkXQOBvLQ~n3jT~Mw}J>pvWN)lGT&sVcp{da$MJs305l|K5NyOMcVp!tuy zM0p`1z-PPPv$g8#AGQDHg~icbkpJx_d%B`IbkF#YQL_Lc9%&}Mq7F$tal+Mp8Uub$lTAGgUG^tz3T?

Mt+?g+w;XV81kL){&zJzcB>IRg|1IC4 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

C=USvV+#QR%K`TSxMK*hrw;spw3ip* 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%0DOky=KVVV%##)U*7*@gmjcd;|lP$`Vreu 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 z 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 literal 0 HcmV?d00001 From e0f335ee490715e42798feed3f0d2213477d6276 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Tue, 20 May 2025 07:42:34 -0500 Subject: [PATCH 20/69] move video and highlights to bottom --- README.md | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7a0dd62f..dda7de50 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,6 @@

- - YourBench Demo Video -
- Watch Demo on YouTube -
- Watch our 3-minute demo of the YourBench pipeline -
@@ -35,17 +28,6 @@ > **YourBench** is an open-source framework for generating domain-specific benchmarks in a zero-shot manner. It aims to keep your large language models on their toes – even as new data sources, domains, and knowledge demands evolve. -**Highlights**: - -* **Dynamic Benchmark Generation** – Produce diverse, up-to-date question-answer pairs derived from real-world source documents (PDF, Word, HTML, even multimedia). -* **Scalable & Structured** – Seamlessly handle ingestion, summarization, and multi-hop chunking for large or specialized datasets. -* **Extensible Pipeline** – Use out-of-the-box stages (ingestion, summarization, question generation) or plug in custom models and logic to accommodate domain-specific needs. -* **Robust Configuration** – Control the entire pipeline via a single YAML config (model choices, data paths, chunking parameters, generation prompts, deduplication thresholds, etc.). -* **Multi-Model Support** – Assign different LLMs for each stage (ingestion, summarization, QG, answering), fostering broader coverage and question-style diversity. -* **Deduplication & Quality Filtering** – Automatically group near-duplicates to prune questions and retain a curated set of high-quality queries. -* **Logging & Analysis** – Built-in metrics evaluate dataset coverage, question distribution, difficulty, and more. -* **Flexible Output** – Save generated benchmarks locally or push them to the Hugging Face Hub for sharing or public leaderboards. - YourBench tackles a critical evaluation gap for LLMs. Traditional static benchmarks are quickly **saturated** or contaminated by training data, making it hard to assess models on new knowledge. Domain-specific or up-to-date evaluation is often costly and slow with human annotation. **YourBench addresses this by enabling dynamic, automated generation of reliable, domain-tailored benchmarks directly from your data, without manual labeling**. In a recent study, YourBench replicated several subsets of a popular benchmark (MMLU) using minimal source text for **under \$15** in total cost, while preserving the original ranking of model performance (Spearman ρ = 1). By grounding questions in user-provided documents, YourBench ensures evaluations stay relevant and **truly test a model’s knowledge on content it hasn’t seen before**. ## Installation @@ -151,6 +133,29 @@ Contributions are welcome! If you’d like to improve YourBench or add new featu We actively review PRs and welcome improvements or fixes from the community. For major changes, feel free to open an issue first to discuss the idea. +## Highlights + + +* **Dynamic Benchmark Generation** – Produce diverse, up-to-date question-answer pairs derived from real-world source documents (PDF, Word, HTML, even multimedia). +* **Scalable & Structured** – Seamlessly handle ingestion, summarization, and multi-hop chunking for large or specialized datasets. +* **Extensible Pipeline** – Use out-of-the-box stages (ingestion, summarization, question generation) or plug in custom models and logic to accommodate domain-specific needs. +* **Robust Configuration** – Control the entire pipeline via a single YAML config (model choices, data paths, chunking parameters, generation prompts, deduplication thresholds, etc.). +* **Multi-Model Support** – Assign different LLMs for each stage (ingestion, summarization, QG, answering), fostering broader coverage and question-style diversity. +* **Deduplication & Quality Filtering** – Automatically group near-duplicates to prune questions and retain a curated set of high-quality queries. +* **Logging & Analysis** – Built-in metrics evaluate dataset coverage, question distribution, difficulty, and more. +* **Flexible Output** – Save generated benchmarks locally or push them to the Hugging Face Hub for sharing or public leaderboards. + + + ## License This project is licensed under the Apache 2.0 License – see the [LICENSE](LICENSE) file for details. You are free to use, modify, and distribute YourBench in either commercial or academic projects under the terms of this license. From 98d98e43b547a06ca29ef8275d43c886a9810ea1 Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Tue, 20 May 2025 12:33:20 -0500 Subject: [PATCH 21/69] Update README.md --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 38851ede..f9e614b8 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,6 @@

- - - GitHub Repo stars - - CI Status -

From 15727606b549f382703b042fbaa6c1dca1e4f2fc Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Tue, 20 May 2025 12:34:46 -0500 Subject: [PATCH 22/69] Update README.md --- README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/README.md b/README.md index f9e614b8..f38378cc 100644 --- a/README.md +++ b/README.md @@ -21,17 +21,6 @@

- -

- - YourBench Demo Video -
- Watch Demo on YouTube -
- Watch our 3-minute demo of the YourBench pipeline -
-

- --- From 28d27f2c3877f2dcff5365769a6ae13ff46ca30b Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Wed, 21 May 2025 10:59:33 -0500 Subject: [PATCH 23/69] Delete yourbench/utils/load_task_config.py --- yourbench/utils/load_task_config.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 yourbench/utils/load_task_config.py diff --git a/yourbench/utils/load_task_config.py b/yourbench/utils/load_task_config.py deleted file mode 100644 index e69de29b..00000000 From b0e473fe7747502d70eedb015c6d3b3d35a6edc9 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 21 May 2025 11:13:04 -0500 Subject: [PATCH 24/69] refactor loading engine --- yourbench/utils/loading_engine.py | 105 ++++++++++-------------------- 1 file changed, 36 insertions(+), 69 deletions(-) diff --git a/yourbench/utils/loading_engine.py b/yourbench/utils/loading_engine.py index 502084ba..5bb0d4cb 100644 --- a/yourbench/utils/loading_engine.py +++ b/yourbench/utils/loading_engine.py @@ -1,12 +1,9 @@ -""" -Loading Engine Module - -This module provides utility functions to load configuration files for tasks, -with support for environment variable substitution. -""" +"""Utilities for loading YAML configuration files with environment expansion.""" import os -from typing import Any, Dict +from typing import Any +from pathlib import Path +from dataclasses import dataclass import yaml from dotenv import load_dotenv @@ -14,73 +11,43 @@ def _expand_env_vars(obj: Any) -> Any: - """ - Recursively substitute environment variables in all string values within a data structure. - - Args: - obj (Any): The input data structure (dict, list, or primitive). - - Returns: - Any: The data structure with environment variables expanded in all string values. - - Example: - >>> os.environ['FOO'] = 'bar' - >>> _expand_env_vars({'a': '$FOO', 'b': ['${FOO}', 123]}) - {'a': 'bar', 'b': ['bar', 123]} - """ + """Recursively expand environment variables in nested structures.""" if isinstance(obj, dict): return {k: _expand_env_vars(v) for k, v in obj.items()} if isinstance(obj, list): - return [_expand_env_vars(item) for item in obj] + return [_expand_env_vars(i) for i in obj] if isinstance(obj, str): return os.path.expandvars(obj) return obj -def load_config(config_path: str) -> Dict[str, Any]: - """ - Load the task configuration from a YAML file, substituting environment variables. - - This function reads a YAML configuration file, expands any environment variables - present (using the '$VAR' syntax), and returns the configuration as a dictionary. - Environment variable substitution is performed recursively on all string values - in the resulting configuration dictionary. - - Parameters: - config_path (str): Path to the YAML configuration file. - - Returns: - Dict[str, Any]: The configuration loaded as a dictionary. - - Raises: - FileNotFoundError: If the configuration file could not be found at config_path. - yaml.YAMLError: If there was an error parsing the YAML content. - """ - # Load environment variables from .env files - load_dotenv() - - if not os.path.exists(config_path): - logger.error("Configuration file not found: {}", config_path) - raise FileNotFoundError(f"Configuration file not found: {config_path}") - - try: - # Read the raw configuration file - with open(config_path, "r") as file: - config_str = file.read() - logger.debug("Successfully read configuration file from {}", config_path) - - # Substitute environment variables in the configuration string - expanded_config_str = os.path.expandvars(config_str) - - # Parse the YAML configuration - config = yaml.safe_load(expanded_config_str) - - # Recursively expand environment variables in all string values - config = _expand_env_vars(config) - - logger.info("Configuration loaded successfully from {}", config_path) - return config - - except Exception as exc: - logger.exception("Failed to load configuration due to: {}", exc) - raise +def load_config(config_path: str | Path) -> dict[str, Any]: + """Convenience wrapper returning ``ConfigLoader(config_path).load()``.""" + return ConfigLoader(Path(config_path)).load() + + +@dataclass(slots=True) +class ConfigLoader: + path: Path + + def load(self) -> dict[str, Any]: + load_dotenv() + + if not self.path.is_file(): + logger.error(f"Configuration file not found: {self.path}") + raise FileNotFoundError(self.path) + + try: + text = self.path.read_text() + logger.debug(f"Read configuration from {self.path}") + expanded = os.path.expandvars(text) + config = yaml.safe_load(expanded) or {} + result = _expand_env_vars(config) + logger.debug(f"Configuration loaded successfully from {self.path}") + return result + except yaml.YAMLError as exc: + logger.error(f"Error parsing YAML {self.path}: {exc}") + raise + except Exception as exc: # noqa: BLE001 + logger.error(f"Failed to load configuration {self.path}: {exc}") + raise From 80f9f49b5c11dcfba74903c875a4f7a571e2f9ec Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 21 May 2025 12:55:55 -0500 Subject: [PATCH 25/69] fix summarization and refactor --- yourbench/pipeline/summarization.py | 241 ++++++++++++---------------- yourbench/utils/prompts.py | 16 +- 2 files changed, 103 insertions(+), 154 deletions(-) diff --git a/yourbench/pipeline/summarization.py b/yourbench/pipeline/summarization.py index 52884d78..356ea7a0 100644 --- a/yourbench/pipeline/summarization.py +++ b/yourbench/pipeline/summarization.py @@ -1,62 +1,12 @@ -# summarization.py -# ============================================================================= -# Author: @sumukshashidhar -# -# Module: Summarization Pipeline Stage -# ============================================================================= -""" -Summarization Stage -=================== - -This module handles the summarization stage of the YourBench pipeline. It takes -documents (with their raw text) and generates concise yet comprehensive summaries -for each document. - -Usage: ------- -1. Ensure the pipeline configuration has an entry for the `summarization` stage - with the desired settings. For example: - - summarization: - run: true - timeout_seconds: 300 - -2. When the pipeline runs, it loads the target dataset, calls the summarization - model(s) to produce summaries, logs intermediate steps, and saves the updated - dataset with new columns: - - raw_document_summary - - document_summary - - summarization_model - -Error Handling & Logging: -------------------------- -- All errors are logged using `loguru` to `logs/summarization.log`. -- The stage attempts to proceed with partial data even if some calls fail, never - abruptly terminating the pipeline. - -Important Notes: ----------------- -- This stage relies on the `run_inference` utility function from yourbench.utils.inference_engine - for concurrency, timeouts, and model management. -- Summaries are extracted from the model's output by parsing XML tags. -- If no valid summary is found, the pipeline substitutes a fallback string. - -See Also: ---------- -- yourbench.utils.inference_engine for concurrency logic -- yourbench.utils.dataset_engine for loading/saving dataset -""" - -from __future__ import annotations -from typing import Any, List, Tuple +from typing import Any import tiktoken from loguru import logger from datasets import Dataset from yourbench.utils.prompts import ( + SUMMARIZATION_USER_PROMPT, COMBINE_SUMMARIES_USER_PROMPT, - CHUNK_SUMMARIZATION_USER_PROMPT, ) from yourbench.utils.chunking_utils import split_into_token_chunks from yourbench.utils.dataset_engine import custom_load_dataset, custom_save_dataset @@ -74,37 +24,37 @@ def _build_chunk_calls( max_tokens: int, overlap: int, encoding_name: str, -) -> Tuple[List[InferenceCall], List[Tuple[int, int]]]: +) -> tuple[list[InferenceCall], list[tuple[int, int]]]: """Prepare inference calls for first-level chunk summaries. - Returns - ------- - (calls, mapping) where *mapping* aligns each call to (doc_idx, chunk_idx). + Returns: + A tuple containing: + - A list of inference calls. + - A list of mappings, where each mapping is a tuple (doc_idx, chunk_idx) + aligning each call to its document and chunk index. chunk_idx is -1 for + documents treated as a single chunk. """ - calls: List[InferenceCall] = [] - mapping: List[Tuple[int, int]] = [] # (doc_index, chunk_index) + calls: list[InferenceCall] = [] + mapping: list[tuple[int, int]] = [] # (doc_index, chunk_index) - # ─── NEW: robust encoding fetch with fallback ──────────────────────────── try: enc = tiktoken.get_encoding(encoding_name) - except Exception as e: # KeyError on unknown name, ValueError on bad cache + except Exception as e: + error_message = str(e) + truncated_error = error_message[:60] + ("…" if len(error_message) > 60 else "") logger.warning( - "Unknown / unavailable encoding '{}'. Falling back to 'cl100k_base' ({})", - encoding_name, - str(e)[:60] + ("…" if len(str(e)) > 60 else ""), + f"Unknown / unavailable encoding '{encoding_name}'. Falling back to 'cl100k_base' ({truncated_error})" ) enc = tiktoken.get_encoding("cl100k_base") - # ──────────────────────────────────────────────────────────────────────── for doc_idx, doc_text in enumerate(dataset["document_text"]): token_len = len(enc.encode(doc_text)) if token_len <= max_tokens: # treat as single chunk (chunk_idx = -1) - prompt = CHUNK_SUMMARIZATION_USER_PROMPT.format(chunk=doc_text) + prompt = SUMMARIZATION_USER_PROMPT.format(document=doc_text) calls.append(InferenceCall(messages=[{"role": "user", "content": prompt}], tags=["chunk_summary"])) mapping.append((doc_idx, -1)) continue - # Long doc ⇒ split & create a call per chunk chunks = split_into_token_chunks( doc_text, chunk_tokens=max_tokens, @@ -112,87 +62,86 @@ def _build_chunk_calls( encoding_name=encoding_name, ) for chunk_idx, chunk in enumerate(chunks): - prompt = CHUNK_SUMMARIZATION_USER_PROMPT.format(chunk=chunk) + prompt = SUMMARIZATION_USER_PROMPT.format(document=chunk) calls.append(InferenceCall(messages=[{"role": "user", "content": prompt}], tags=["chunk_summary"])) mapping.append((doc_idx, chunk_idx)) - logger.info("Prepared {} chunk-level inference calls.", len(calls)) + logger.info(f"Prepared {len(calls)} chunk-level inference calls.") return calls, mapping def _collect_chunk_summaries( - response_dict: dict[str, List[str]], - mapping: List[Tuple[int, int]], + response_dict: dict[str, list[str]], + mapping: list[tuple[int, int]], num_docs: int, -) -> Tuple[str, List[List[str]], List[List[str]]]: - """Re-orders raw model responses back into per-document lists. - - Notes - ----- - `model_name` is always `str` (never None) because we early-return if - `response_dict` is empty. - """ +) -> tuple[str, list[list[str]], list[list[str]]]: + """Re-orders raw model responses back into per-document lists of summaries.""" if not response_dict: return "", [], [] model_name = list(response_dict.keys())[0] responses = response_dict[model_name] - # Ensure response count matches call count if len(responses) != len(mapping): - logger.warning("Response count {} ≠ mapping count {} – truncating/min-padding.", len(responses), len(mapping)) - # pad / trim + logger.warning(f"Response count {len(responses)} ≠ mapping count {len(mapping)} – truncating/min-padding.") diff = len(mapping) - len(responses) if diff > 0: responses.extend([""] * diff) else: responses = responses[: len(mapping)] - # bucket by doc - raw_by_doc: List[List[str]] = [[] for _ in range(num_docs)] - cleaned_by_doc: List[List[str]] = [[] for _ in range(num_docs)] + raw_by_doc: list[list[str]] = [[] for _ in range(num_docs)] + cleaned_by_doc: list[list[str]] = [[] for _ in range(num_docs)] for resp, (doc_idx, _chunk_idx) in zip(responses, mapping): raw_by_doc[doc_idx].append(resp) - summary = extract_content_from_xml_tags(resp, "chunk_summary") or extract_content_from_xml_tags( + summary_content = extract_content_from_xml_tags(resp, "chunk_summary") or extract_content_from_xml_tags( resp, "final_summary" ) - cleaned_by_doc[doc_idx].append(summary.strip() if summary else "") + cleaned_by_doc[doc_idx].append(summary_content.strip() if summary_content else "") return model_name, raw_by_doc, cleaned_by_doc -def _build_combine_calls(summaries_by_doc: List[List[str]]) -> Tuple[List[InferenceCall], List[int]]: - """Prepare second-stage calls that merge chunk summaries into one summary.""" - calls: List[InferenceCall] = [] - doc_indices: List[int] = [] - skipped = 0 # MOD: track how many docs are trivially short +def _build_combine_calls(summaries_by_doc: list[list[str]]) -> tuple[list[InferenceCall], list[int]]: + """Prepare second-stage calls to merge multiple chunk summaries into a single summary.""" + calls: list[InferenceCall] = [] + doc_indices_for_combine: list[int] = [] + skipped_doc_count = 0 for doc_idx, chunk_summaries in enumerate(summaries_by_doc): - if len(chunk_summaries) <= 1: # already short ⇒ skip combine - skipped += 1 + if len(chunk_summaries) <= 1: # Already a single summary (or empty), skip combine + skipped_doc_count += 1 + continue + + valid_summaries = [s for s in chunk_summaries if s] + if not valid_summaries: + skipped_doc_count += 1 continue - bullet_list = "\n".join(f"- {s}" for s in chunk_summaries if s) + + bullet_list = "\\n".join(f"- {s}" for s in valid_summaries) prompt = COMBINE_SUMMARIES_USER_PROMPT.format(chunk_summaries=bullet_list) calls.append(InferenceCall(messages=[{"role": "user", "content": prompt}], tags=["merge_summary"])) - doc_indices.append(doc_idx) + doc_indices_for_combine.append(doc_idx) - logger.info("Prepared {} reducer calls ({} docs skipped – single / empty chunk).", len(calls), skipped) # NEW line - return calls, doc_indices + logger.info( + f"Prepared {len(calls)} combine-stage inference calls ({skipped_doc_count} docs skipped – single/empty chunk list)." + ) + return calls, doc_indices_for_combine def _merge_final_summaries( - existing_singletons: List[str], - combine_responses: List[str], - doc_indices: List[int], -) -> List[str]: - """Blend reducer results with already-final single-chunk docs.""" - final_summaries = existing_singletons.copy() + current_final_summaries: list[str], + combined_responses: list[str], + doc_indices_to_update: list[int], +) -> list[str]: + """Integrates combined summaries into the list of final summaries.""" + updated_final_summaries = current_final_summaries.copy() - for resp, doc_idx in zip(combine_responses, doc_indices): - parsed = extract_content_from_xml_tags(resp, "final_summary") - final_summaries[doc_idx] = parsed.strip() if parsed else "No summary available." - return final_summaries + for resp, doc_idx in zip(combined_responses, doc_indices_to_update): + parsed_summary = extract_content_from_xml_tags(resp, "final_summary") + updated_final_summaries[doc_idx] = parsed_summary.strip() if parsed_summary else "No summary available." + return updated_final_summaries ################# @@ -201,53 +150,65 @@ def _merge_final_summaries( def run(config: dict[str, Any]) -> None: + """Executes the hierarchical summarization pipeline.""" stage_cfg = config.get("pipeline", {}).get("summarization", {}) if not stage_cfg.get("run", False): logger.info("Summarization stage disabled – skipping.") return - max_tokens = stage_cfg.get("max_tokens", 16384) - overlap = stage_cfg.get("token_overlap", 128) - encoding_name = stage_cfg.get("encoding_name", "cl100k_base") + max_tokens: int = stage_cfg.get("max_tokens", 16384) + overlap: int = stage_cfg.get("token_overlap", 128) + encoding_name: str = stage_cfg.get("encoding_name", "cl100k_base") logger.info("=== Summarization v2 – map-reduce ===") - # 1) Load dataset produced by ingestion dataset = custom_load_dataset(config=config, subset="ingested") - if len(dataset) == 0: - logger.warning("Ingested dataset empty – nothing to summarise.") + if not dataset or len(dataset) == 0: + logger.warning("Ingested dataset is empty or None – nothing to summarise.") return - logger.info("Loaded {} documents for summarisation.", len(dataset)) + logger.info(f"Loaded {len(dataset)} documents for summarisation.") - # 2) First pass – chunk summaries chunk_calls, call_map = _build_chunk_calls(dataset, max_tokens, overlap, encoding_name) - chunk_resp = run_inference(config=config, step_name="summarization_chunk", inference_calls=chunk_calls) - model_name, raw_chunk_by_doc, clean_chunk_by_doc = _collect_chunk_summaries(chunk_resp, call_map, len(dataset)) + chunk_responses_dict = run_inference(config=config, step_name="summarization_chunk", inference_calls=chunk_calls) + model_name, raw_chunk_summaries_by_doc, cleaned_chunk_summaries_by_doc = _collect_chunk_summaries( + chunk_responses_dict, call_map, len(dataset) + ) - # 3) Second pass – combine summaries where needed - combine_calls, doc_indices = _build_combine_calls(clean_chunk_by_doc) - combine_summaries_raw: List[str] = [] - if combine_calls: - combine_resp = run_inference(config=config, step_name="summarization_combine", inference_calls=combine_calls) - combine_model = list(combine_resp.keys())[0] if combine_resp else model_name - if combine_model != model_name: - logger.warning("Different model used in reducer stage: {} vs {}", combine_model, model_name) - combine_summaries_raw = combine_resp.get(combine_model, []) if combine_resp else [] - - # produce final list matching dataset order - # Start with single-chunk docs: take their sole summary - final_summaries = [docs[0] if docs else "" for docs in clean_chunk_by_doc] + combine_calls, doc_indices_for_combine = _build_combine_calls(cleaned_chunk_summaries_by_doc) + + raw_combined_summaries: list[str] = [] if combine_calls: - final_summaries = _merge_final_summaries(final_summaries, combine_summaries_raw, doc_indices) + combine_responses_dict = run_inference( + config=config, step_name="summarization_combine", inference_calls=combine_calls + ) + if combine_responses_dict: + combine_model_name = list(combine_responses_dict.keys())[0] + if combine_model_name != model_name and model_name: + logger.warning(f"Different model used in combine stage: {combine_model_name} vs {model_name}") + raw_combined_summaries = combine_responses_dict.get(combine_model_name, []) + else: + raw_combined_summaries = [""] * len(doc_indices_for_combine) - # 4) Add columns & persist - dataset = dataset.add_column("raw_chunk_summaries", raw_chunk_by_doc) - dataset = dataset.add_column("chunk_summaries", clean_chunk_by_doc) - dataset = dataset.add_column( - "raw_document_summary", combine_summaries_raw if combine_calls else [""] * len(dataset) - ) - dataset = dataset.add_column("document_summary", final_summaries) - dataset = dataset.add_column("summarization_model", [model_name] * len(dataset)) + final_document_summaries: list[str] = [ + summaries[0] if summaries else "" for summaries in cleaned_chunk_summaries_by_doc + ] + + if combine_calls and raw_combined_summaries: + final_document_summaries = _merge_final_summaries( + final_document_summaries, raw_combined_summaries, doc_indices_for_combine + ) + + full_raw_combined_summaries = [""] * len(dataset) + for i, doc_idx in enumerate(doc_indices_for_combine): + if i < len(raw_combined_summaries): + full_raw_combined_summaries[doc_idx] = raw_combined_summaries[i] + + dataset = dataset.add_column("raw_chunk_summaries", raw_chunk_summaries_by_doc) + dataset = dataset.add_column("chunk_summaries", cleaned_chunk_summaries_by_doc) + dataset = dataset.add_column("raw_document_summary", full_raw_combined_summaries) + dataset = dataset.add_column("document_summary", final_document_summaries) + effective_model_name = model_name if model_name else "unknown" + dataset = dataset.add_column("summarization_model", [effective_model_name] * len(dataset)) custom_save_dataset(dataset=dataset, config=config, subset="summarized") - logger.success("Hierarchical summarisation completed ({} documents).", len(dataset)) + logger.success(f"Hierarchical summarisation completed ({len(dataset)} documents).") diff --git a/yourbench/utils/prompts.py b/yourbench/utils/prompts.py index bbfe87ea..0dff63fe 100644 --- a/yourbench/utils/prompts.py +++ b/yourbench/utils/prompts.py @@ -543,22 +543,10 @@ class MultipleChoiceQuestion(BaseModel): {answer_b} """ -CHUNK_SUMMARIZATION_USER_PROMPT = """\ -You are an expert note-taker. Summarise the following document *chunk* in \ -10-12 crisp sentences capturing only the information that will matter for a \ -later global summary. - - -{chunk} - - -Wrap your output inside tags.""" - COMBINE_SUMMARIES_USER_PROMPT = """\ -You will receive a bullet-list of chunk-level summaries from the *same* \ +You will receive a list of chunk-level summaries from the *same* \ document. Combine them into a single, well-structured paragraph that reads \ -naturally and eliminates redundancy. Keep the final answer under 1/3rd of the \ -original combined length. +naturally and eliminates redundancy. {chunk_summaries} From e67a11c7849668d7cb71b9b943e1a925f863ddcb Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Thu, 22 May 2025 05:38:14 -0500 Subject: [PATCH 26/69] add sample question viewer to analyze --- yourbench/analysis/view_sample_questions.py | 132 ++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 yourbench/analysis/view_sample_questions.py diff --git a/yourbench/analysis/view_sample_questions.py b/yourbench/analysis/view_sample_questions.py new file mode 100644 index 00000000..db439aa1 --- /dev/null +++ b/yourbench/analysis/view_sample_questions.py @@ -0,0 +1,132 @@ +import random +from typing import List, Literal +from dataclasses import dataclass + +from loguru import logger +from rich.table import Table +from rich.console import Console + +from yourbench.utils.dataset_engine import custom_load_dataset +from yourbench.utils.loading_engine import load_config + + +@dataclass +class Question: + question: str + answer: str + question_type: str + choices: List[str] + difficulty: str + index: int + + @classmethod + def from_dataset_row(cls, row: dict, index: int) -> "Question": + return cls( + question=row.get("question", ""), + answer=row.get("self_answer", ""), + question_type=row.get("self_assessed_question_type", "unknown"), + choices=row.get("choices", []) or [], + difficulty=str(row.get("estimated_difficulty", "")), + index=index, + ) + + @property + def choices_display(self) -> str: + return "\n".join(self.choices) if self.choices else "N/A" + + +class QuestionDisplay: + def __init__(self, console: Console): + self.console = console + + def create_table(self) -> Table: + table = Table(show_header=True, header_style="bold cyan", show_lines=True) + table.add_column("Q #", style="dim", width=5) + table.add_column("Q Type", style="white", width=16) + table.add_column("Question", style="white", no_wrap=False) + table.add_column("Answer", style="white", no_wrap=False) + table.add_column("Choices", style="white", no_wrap=False) + table.add_column("Difficulty", style="white", justify="center", width=10) + return table + + def display_questions(self, questions: List[Question], title: str, title_style: str) -> None: + if not questions: + self.console.print(f"[bold red]No {title.lower()} found or it's empty.[/bold red]") + return + + self.console.print(f"[{title_style}]=== {title} ===[/{title_style}]\n") + table = self.create_table() + + for idx, question in enumerate(questions, 1): + table.add_row( + str(idx), + question.question_type, + question.question, + question.answer, + question.choices_display, + question.difficulty, + ) + + self.console.print(table) + self.console.print() + + +class QuestionLoader: + def __init__(self, config: dict, sample_size: int): + self.config = config + self.sample_size = sample_size + + def load_questions(self, subset: Literal["single_shot_questions", "multi_hop_questions"]) -> List[Question]: + dataset = custom_load_dataset(config=self.config, subset=subset) + if not dataset: + return [] + + indices = random.sample(range(len(dataset)), min(self.sample_size, len(dataset))) + return [Question.from_dataset_row(dataset[i], i) for i in indices] + + +def run(*cli_args: List[str]) -> None: + """ + Usage: + yourbench analyze view_sample_questions path/to/config.yaml [sample_size] + + This command loads up to 'sample_size' questions from both + 'single_shot_questions' and 'multi_hop_questions' subsets, then prints + them in a Rich table showing relevant details: + - Question Type + - Actual Question + - Answer (or multiple-choice correct letter) + - Choices (if any) + - Difficulty + - Citations (if any) + + Args: + *cli_args: The CLI arguments passed after 'view_sample_questions' + e.g. ["my_config.yaml", "5"] + """ + if not cli_args: + logger.error("No arguments provided. Usage: yourbench analyze view_sample_questions CONFIG_PATH [SAMPLE_SIZE]") + return + + config_path = cli_args[0] + sample_size = int(cli_args[1]) if len(cli_args) > 1 and cli_args[1].isdigit() else 5 + + try: + config = load_config(config_path) + except FileNotFoundError: + logger.error(f"Configuration file not found at '{config_path}'. Aborting.") + return + except Exception as e: + logger.error(f"Failed to load config from '{config_path}': {e}") + return + + loader = QuestionLoader(config, sample_size) + display = QuestionDisplay(Console()) + + # Display single-shot questions + single_shot_questions = loader.load_questions("single_shot_questions") + display.display_questions(single_shot_questions, "Single-Shot Questions (Detailed)", "bold magenta") + + # Display multi-hop questions + multi_hop_questions = loader.load_questions("multi_hop_questions") + display.display_questions(multi_hop_questions, "Multi-Hop Questions (Detailed)", "bold green") From 1c6239f58a4b4f6db3102d8cc44b0e58af3510af Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Thu, 22 May 2025 05:42:19 -0500 Subject: [PATCH 27/69] add docs --- .../analysis/view_sample_questions.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/yourbench/analysis/view_sample_questions.md diff --git a/docs/yourbench/analysis/view_sample_questions.md b/docs/yourbench/analysis/view_sample_questions.md new file mode 100644 index 00000000..2ebe4e5e --- /dev/null +++ b/docs/yourbench/analysis/view_sample_questions.md @@ -0,0 +1,33 @@ +## Viewing Sample Questions + +Once you have run the pipeline and generated the `single_shot_questions` and +`multi_hop_questions` subsets, you can quickly preview a handful of them from +the command line. + +```bash +# syntax: yourbench analyze view_sample_questions CONFIG_PATH [SAMPLE_SIZE] + +yourbench analyze view_sample_questions example/configs/simple_example.yaml 5 +```` + +* **`CONFIG_PATH`** – path to the YAML/JSON config you used for the pipeline. +* **`SAMPLE_SIZE`** – *(optional, default = 5)* number of random questions to + display from each subset. + +The command prints two Rich tables: + +| Column | Description | +| -------------- | ----------------------------------------------------- | +| **Q #** | Running index of the sampled question | +| **Q Type** | Model-reported category (factual, conceptual, etc.) | +| **Question** | Full text of the question | +| **Answer** | Correct answer (or correct option letter) | +| **Choices** | Multiple-choice options if present; “N/A” otherwise | +| **Difficulty** | Estimated difficulty 1-10 (as generated by the model) | + +This quick preview is handy for: + +* sanity-checking that generation looks reasonable before large runs; +* spot-checking difficulty levels or citation formatting; +* debugging prompt or parsing issues without opening full datasets. + From 1acef89b2d9e237782e347d3c5aa3f2ccbaac3a1 Mon Sep 17 00:00:00 2001 From: m-peko Date: Fri, 23 May 2025 11:44:27 +0200 Subject: [PATCH 28/69] Change output format of generated benchmark --- .env.template | 12 +++++- run_docker.sh | 3 ++ run_yourbench.py | 108 ++++++++++++++++++++--------------------------- 3 files changed, 58 insertions(+), 65 deletions(-) diff --git a/.env.template b/.env.template index 0a54b30d..7d3af1dc 100644 --- a/.env.template +++ b/.env.template @@ -1,2 +1,10 @@ -HF_TOKEN= -HF_ORGANIZATION= \ No newline at end of file +OPENROUTER_API_KEY= + +BENCHMARK_NAME="test" +INPUT_S3_BUCKET="layerlens-private-test-organization" +INPUT_S3_KEY="benchmarks/test-project/benchmark-name/data.zip" +OUTPUT_S3_BUCKET="layerlens-private-test-organization" +OUTPUT_S3_KEY="benchmarks/test-project/benchmark-name/" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= \ No newline at end of file diff --git a/run_docker.sh b/run_docker.sh index 385d8cf8..198052ce 100755 --- a/run_docker.sh +++ b/run_docker.sh @@ -18,6 +18,7 @@ docker build -t yourbench-processor . if [ -z "$INPUT_S3_BUCKET" ] || [ -z "$INPUT_S3_KEY" ] || [ -z "$OUTPUT_S3_BUCKET" ] || [ -z "$OUTPUT_S3_KEY" ] || [ -z "$OPENROUTER_API_KEY" ]; then echo "Error: Required environment variables are not set." echo "Please set these variables before running:" + echo " - BENCHMARK_NAME: benchmark name" echo " - INPUT_S3_BUCKET: S3 bucket containing input data" echo " - INPUT_S3_KEY: S3 key for input data zip file" echo " - OUTPUT_S3_BUCKET: S3 bucket for output data" @@ -25,6 +26,7 @@ if [ -z "$INPUT_S3_BUCKET" ] || [ -z "$INPUT_S3_KEY" ] || [ -z "$OUTPUT_S3_BUCKE echo " - OPENROUTER_API_KEY: API key for OpenRouter" echo "" echo "Example:" + echo " export BENCHMARK_NAME=benchmark-name" echo " export INPUT_S3_BUCKET=my-input-bucket" echo " export INPUT_S3_KEY=input/data.zip" echo " export OUTPUT_S3_BUCKET=my-output-bucket" @@ -36,6 +38,7 @@ fi # Run the Docker container echo "Running yourbench processor Docker container..." docker run --rm \ + -e BENCHMARK_NAME="$BENCHMARK_NAME" \ -e INPUT_S3_BUCKET="$INPUT_S3_BUCKET" \ -e INPUT_S3_KEY="$INPUT_S3_KEY" \ -e OUTPUT_S3_BUCKET="$OUTPUT_S3_BUCKET" \ diff --git a/run_yourbench.py b/run_yourbench.py index ed1fbcb7..192ecd16 100644 --- a/run_yourbench.py +++ b/run_yourbench.py @@ -9,50 +9,38 @@ from yourbench.utils.convert_to_atlas_module import convert_dataset # Setup logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) logger = logging.getLogger(__name__) -def zip_multiple_directories(directories, base_dir, output_zip_path): - """ - Zip multiple directories into a single zip archive, preserving their structure relative to base_dir. - Args: - directories (list): List of Path or str directories to include. - base_dir (Path or str): Base directory for relative paths in zip. - output_zip_path (Path or str): Path to the output zip file. - """ - logger.info(f"Zipping directories {directories} into {output_zip_path}") - with zipfile.ZipFile(output_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: - for directory in directories: - for root, _, files in os.walk(directory): - for file in files: - file_path = os.path.join(root, file) - arcname = os.path.relpath(file_path, base_dir) - zipf.write(file_path, arcname) - logger.info("Zipping completed for: " + ", ".join([str(d) for d in directories])) def download_from_s3(bucket_name, object_key, local_path): """Download file from S3 bucket""" logger.info(f"Downloading {object_key} from bucket {bucket_name} to {local_path}") - s3_client = boto3.client('s3') + s3_client = boto3.client("s3") s3_client.download_file(bucket_name, object_key, local_path) logger.info("Download completed") + def unzip_file(zip_path, extract_dir): """Unzip file to specified directory""" logger.info(f"Extracting {zip_path} to {extract_dir}") os.makedirs(extract_dir, exist_ok=True) - with zipfile.ZipFile(zip_path, 'r') as zip_ref: + with zipfile.ZipFile(zip_path, "r") as zip_ref: zip_ref.extractall(extract_dir) logger.info("Extraction completed") + def create_config_file(config_content, config_path): """Create config.yaml file""" logger.info(f"Creating config file at {config_path}") os.makedirs(os.path.dirname(config_path), exist_ok=True) - with open(config_path, 'w') as f: + with open(config_path, "w") as f: yaml.dump(yaml.safe_load(config_content), f) logger.info("Config file created") + def run_yourbench(config_path): """Run yourbench with the provided config using direct Python API call.""" logger.info(f"Running yourbench with config {config_path}") @@ -66,7 +54,9 @@ def run_yourbench(config_path): try: yourbench_main() except SystemExit as e: - logger.info(f"yourbench exited with code {e.code} (caught SystemExit, continuing)") + logger.info( + f"yourbench exited with code {e.code} (caught SystemExit, continuing)" + ) finally: sys.argv = sys_argv_backup logger.info("yourbench execution completed successfully") @@ -75,53 +65,52 @@ def run_yourbench(config_path): logger.error(f"Error during yourbench execution: {str(e)}") raise -def zip_directory(dir_path, output_path): - """Zip directory to output path""" - logger.info(f"Zipping directory {dir_path} to {output_path}") - with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf: - for root, _, files in os.walk(dir_path): - for file in files: - file_path = os.path.join(root, file) - zipf.write(file_path, os.path.relpath(file_path, os.path.join(dir_path, '..'))) - logger.info("Zipping completed") def upload_to_s3(local_path, bucket_name, object_key): - """Upload file to S3 bucket""" logger.info(f"Uploading {local_path} to bucket {bucket_name} as {object_key}") - s3_client = boto3.client('s3') + s3_client = boto3.client("s3") s3_client.upload_file(local_path, bucket_name, object_key) logger.info("Upload completed") + +def upload_directory_to_s3(directory_path, bucket_name, s3_prefix=""): + for filename in os.listdir(directory_path): + local_path = os.path.join(directory_path, filename) + if os.path.isfile(local_path): + object_key = os.path.join(s3_prefix, filename) if s3_prefix else filename + upload_to_s3(local_path, bucket_name, object_key) + + def main(): # Get environment variables - input_bucket = os.environ.get('INPUT_S3_BUCKET') - input_key = os.environ.get('INPUT_S3_KEY') - output_bucket = os.environ.get('OUTPUT_S3_BUCKET') - output_key = os.environ.get('OUTPUT_S3_KEY') - + benchmark_name = os.environ.get("BENCHMARK_NAME") + input_bucket = os.environ.get("INPUT_S3_BUCKET") + input_key = os.environ.get("INPUT_S3_KEY") + output_bucket = os.environ.get("OUTPUT_S3_BUCKET") + output_key = os.environ.get("OUTPUT_S3_KEY") + if not all([input_bucket, input_key, output_bucket, output_key]): logger.error("Missing required environment variables") raise ValueError("Required environment variables are missing") - + # Define local paths - base_dir = Path(os.environ.get('WORKDIR', '/app')) + base_dir = Path(os.environ.get("WORKDIR", "/app")) download_path = base_dir / "input.zip" raw_data_dir = base_dir / "task/data/raw" dataset_dir = base_dir / "task/dataset" config_path = dataset_dir / "config.yaml" excel_dir = base_dir / "task/excel" - output_zip_path = base_dir / "output.zip" - + # Create required directories os.makedirs(raw_data_dir, exist_ok=True) os.makedirs(dataset_dir, exist_ok=True) - + # Step 1: Download file from S3 download_from_s3(input_bucket, input_key, download_path) - + # Step 2: Unzip file to raw data directory unzip_file(download_path, raw_data_dir) - + # Step 3: Create config.yaml config_content = """ hf_configuration: @@ -149,22 +138,21 @@ def main(): citation_score_filtering: """ create_config_file(config_content, config_path) - + # Step 4: Run yourbench run_yourbench(config_path) - + # Step 5: Convert datasets to Excel convert_datasets_to_excel(str(dataset_dir), str(excel_dir), logger=logger) # Step 6: Convert to Atlas format try: - atlas_dir = base_dir / "task/atlas_dataset" lighteval_path = dataset_dir / "lighteval" if lighteval_path.exists(): logger.info(f"Converting lighteval dataset to Atlas format") - atlas_output = convert_dataset( + convert_dataset( hf_path=str(lighteval_path), - name="atlas_dataset", + name=benchmark_name, system_prompt=( "You are an expert answering benchmark questions. " "Give a very concise answer." @@ -176,24 +164,18 @@ def main(): ) logger.info(f"Atlas conversion completed.") else: - logger.warning(f"Lighteval dataset not found at {lighteval_path}, skipping Atlas conversion") + logger.warning( + f"Lighteval dataset not found at {lighteval_path}, skipping Atlas conversion" + ) except Exception as e: logger.error(f"Atlas conversion failed: {e}") logger.warning("Continuing with the rest of the pipeline") - # Step 7: Zip the dataset, excel, and atlas directories into a single archive - directories_to_zip = [dataset_dir, excel_dir] - atlas_dataset_dir = base_dir / "task/atlas_dataset" - if atlas_dataset_dir.exists(): - logger.info(f"Including Atlas dataset directory in zip: {atlas_dataset_dir}") - directories_to_zip.append(atlas_dataset_dir) - zip_multiple_directories(directories_to_zip, base_dir, output_zip_path) - - # Step 8: Upload the combined zip to S3 - upload_to_s3(output_zip_path, output_bucket, output_key) - logger.info("Combined zip uploaded to S3 successfully") - + upload_directory_to_s3( + base_dir / "task" / benchmark_name, output_bucket, output_key + ) logger.info("All tasks completed successfully") + if __name__ == "__main__": main() From 47357ad1b808e5c2904cdc57dc8336114e82ea79 Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Fri, 23 May 2025 14:15:04 +0200 Subject: [PATCH 29/69] Improve lighteval.py for MCQ and long task --- yourbench/pipeline/lighteval.py | 56 +++++++-------------------------- 1 file changed, 12 insertions(+), 44 deletions(-) diff --git a/yourbench/pipeline/lighteval.py b/yourbench/pipeline/lighteval.py index 8b3662ce..a71039bf 100644 --- a/yourbench/pipeline/lighteval.py +++ b/yourbench/pipeline/lighteval.py @@ -87,9 +87,7 @@ def run(config: Dict[str, Any]) -> None: logger.info("Saving lighteval compatible dataset") - # ---------------------------------------- - # 2) Load datasets - # ---------------------------------------- + # Load datasets try: single_shot_ds = custom_load_dataset(config=config, subset="single_shot_questions") logger.info(f"Loaded single-shot Q subset single_shot_questions with {len(single_shot_ds)} rows.") @@ -123,15 +121,7 @@ def run(config: Dict[str, Any]) -> None: logger.error("No data in single-shot or multi-hop datasets. Exiting.") return - # ---------------------------------------- - # 3) Prepare lookups from chunked dataset - # ---------------------------------------- - # We'll store: doc_id -> (document_text, chunk_id -> chunk_text). - # chunked_ds typically has the following columns: - # - document_id (str) - # - document_text (str) - # - chunks (list of dicts with {chunk_id, chunk_text}) - # Possibly also "multihop_chunks" but we only need single-hop "chunks". + # Prepare lookups from chunked dataset doc_meta_map = {} for row in chunked_ds: doc_id = row.get("document_id", "") @@ -149,9 +139,7 @@ def run(config: Dict[str, Any]) -> None: if doc_id in doc_meta_map: doc_meta_map[doc_id].update({"document_summary": row.get("document_summary")}) - # ---------------------------------------- - # 4) Helper functions to transform a row - # ---------------------------------------- + # Helper functions to transform a row def make_single_shot_record(row: Dict[str, Any]) -> Dict[str, Any]: """ Transform a single-shot question row into a standardized dictionary @@ -159,9 +147,6 @@ def make_single_shot_record(row: Dict[str, Any]) -> Dict[str, Any]: """ doc_id: str = row.get("document_id", "") chunk_id: str = row.get("chunk_id", "") - # ground_truth is row["self_answer"] - # question_category is row["self_assessed_question_type"] - # question => row["question"], etc. # Grab doc meta doc_meta = doc_meta_map.get(doc_id, {}) @@ -184,7 +169,7 @@ def make_single_shot_record(row: Dict[str, Any]) -> Dict[str, Any]: gold = [ord(gold) - ord("A")] else: gold = [gold] - + return { "question": row.get("question", ""), "additional_instructions": row.get("additional_instructions", ""), @@ -227,7 +212,7 @@ def make_multi_hop_record(row: Dict[str, Any]) -> Dict[str, Any]: gold = row.get("self_answer", "") if not gold: logger.warning("Row has empty answer line") - + stage_cfg = config.get("pipeline", {}).get("single_shot_question_generation", {}) if stage_cfg.get("question_type") == "multi-choice": if not gold: @@ -254,42 +239,25 @@ def make_multi_hop_record(row: Dict[str, Any]) -> Dict[str, Any]: "document_summary": doc_summary, } - # ---------------------------------------- - # 5) Convert each dataset to final records - # ---------------------------------------- - combined_records = [] - - for row in single_shot_ds: - record = make_single_shot_record(row) - combined_records.append(record) - - for row in multi_hop_ds: - record = make_multi_hop_record(row) - combined_records.append(record) + # Convert each dataset to final records + combined_records = [ + *[make_single_shot_record(row) for row in single_shot_ds], + *[make_multi_hop_record(row) for row in multi_hop_ds], + ] if not combined_records: logger.warning("No final records to merge in lighteval. Exiting.") return - # ---------------------------------------- - # 6) Create a Hugging Face Dataset - # ---------------------------------------- + # Create a Hugging Face Dataset logger.info(f"Assembling final dataset with {len(combined_records)} rows.") try: - # # Convert to column-wise dict for HF Dataset - # col_names = list(combined_records[0].keys()) - # final_dict = {c: [] for c in col_names} - # for rec in combined_records: - # for c in col_names: - # final_dict[c].append(rec[c]) final_ds = Dataset.from_list(combined_records) except Exception as ds_error: logger.exception("Failed to create final dataset object") return - # ---------------------------------------- - # 7) Save dataset - # ---------------------------------------- + # Save dataset custom_save_dataset(dataset=final_ds, config=config, subset="lighteval") logger.success("Lighteval dataset saved successfully.") From 78c59a5660008a640f568226df702608b2e84e26 Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Fri, 23 May 2025 14:21:14 +0200 Subject: [PATCH 30/69] Apply Ruff --- yourbench/pipeline/multi_hop_question_generation.py | 7 ++++++- yourbench/pipeline/single_shot_question_generation.py | 9 ++++++--- yourbench/pipeline/summarization.py | 3 +-- yourbench/utils/parsing_engine.py | 3 ++- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/yourbench/pipeline/multi_hop_question_generation.py b/yourbench/pipeline/multi_hop_question_generation.py index 404466f4..b5657dad 100644 --- a/yourbench/pipeline/multi_hop_question_generation.py +++ b/yourbench/pipeline/multi_hop_question_generation.py @@ -62,7 +62,12 @@ ) # Import the unified parsing function -from yourbench.utils.parsing_engine import _force_int_in_range, shuffle_mcq, parse_qa_pairs_from_response, _validate_list +from yourbench.utils.parsing_engine import ( + shuffle_mcq, + _validate_list, + _force_int_in_range, + parse_qa_pairs_from_response, +) from yourbench.utils.inference_engine import InferenceCall, run_inference diff --git a/yourbench/pipeline/single_shot_question_generation.py b/yourbench/pipeline/single_shot_question_generation.py index ef2709e4..af83b67e 100644 --- a/yourbench/pipeline/single_shot_question_generation.py +++ b/yourbench/pipeline/single_shot_question_generation.py @@ -46,7 +46,12 @@ ) # Import the unified parsing function -from yourbench.utils.parsing_engine import _force_int_in_range, shuffle_mcq, parse_qa_pairs_from_response, _validate_list +from yourbench.utils.parsing_engine import ( + shuffle_mcq, + _validate_list, + _force_int_in_range, + parse_qa_pairs_from_response, +) from yourbench.utils.inference_engine import InferenceCall, run_inference @@ -353,5 +358,3 @@ def _process_responses_and_build_dataset( column_names = list(question_dataset_rows[0].keys()) final_data = {column: [row[column] for row in question_dataset_rows] for column in column_names} return Dataset.from_dict(final_data) - - diff --git a/yourbench/pipeline/summarization.py b/yourbench/pipeline/summarization.py index 2f6f0c39..93b14189 100644 --- a/yourbench/pipeline/summarization.py +++ b/yourbench/pipeline/summarization.py @@ -89,7 +89,6 @@ def _build_chunk_calls( ) enc = tiktoken.get_encoding("cl100k_base") - for doc_idx, doc_text in enumerate(dataset["document_text"]): token_len = len(enc.encode(doc_text, disallowed_special=())) if token_len <= max_tokens: # treat as single chunk (chunk_idx = -1) @@ -215,7 +214,7 @@ def run(config: dict[str, Any]) -> None: if len(doc_indices) == 0: logger.info("All documents were short enough for single-pass summarization.") - + combine_summaries_raw: List[str] = [] if combine_calls: combine_resp = run_inference(config=config, step_name="summarization_combine", inference_calls=combine_calls) diff --git a/yourbench/utils/parsing_engine.py b/yourbench/utils/parsing_engine.py index 0d8da123..a6714efc 100644 --- a/yourbench/utils/parsing_engine.py +++ b/yourbench/utils/parsing_engine.py @@ -198,13 +198,14 @@ def _force_int_in_range(value: Any, min_val: int, max_val: int) -> int: ivalue = (min_val + max_val) // 2 return max(min_val, min(ivalue, max_val)) + def _validate_list(some_list: list[str]) -> list[str]: """ Force possible list of strings to be a list of strings """ if not isinstance(some_list, list): return [] - + try: return [str(value) for value in some_list] except Exception: From 364d215e8ff5ef4f33a48ddb9ba94275fad7876f Mon Sep 17 00:00:00 2001 From: Patrick <64135237+patrickfleith@users.noreply.github.com> Date: Fri, 23 May 2025 13:42:27 +0100 Subject: [PATCH 31/69] Update quickstart with correct run command (#108) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f38378cc..696c8684 100644 --- a/README.md +++ b/README.md @@ -79,13 +79,13 @@ echo "HF_TOKEN=" >> .env # Hugging Face echo "HF_ORGANIZATION=" >> .env # (Optional) Organization name for dataset pushing # 3. Run the pipeline on the provided example config (uses sample docs and models) -yourbench run --config example/configs/example.yaml +yourbench run --config example/configs/simple_example.yaml # 4. (Optional) Run the pipeline on your own documents: yourbench run --config my_custom_config.yaml ``` -The **example configuration** `example/configs/example.yaml` (included in the repository) demonstrates a basic setup. It specifies sample documents and default models for each stage of the pipeline. In step 3 above, YourBench will automatically ingest the example documents, generate a set of Q\&A pairs, and output a Hugging Face Dataset containing the evaluation questions and answers. +The **example configuration** `example/configs/simple_example.yaml` (included in the repository) demonstrates a basic setup. It specifies sample documents and default models for each stage of the pipeline. In step 3 above, YourBench will automatically ingest the example documents, generate a set of Q\&A pairs, and output a Hugging Face Dataset containing the evaluation questions and answers. For your own data, you can create a YAML config pointing to your documents and preferred models. For instance, you might specify a folder of PDFs or text files under a `documents` field, and choose which LLM to use for question generation. **YourBench is fully configurable** – you can easily **toggle stages** on or off and swap in different models. *For example: you could disable the summarization stage for very short texts, or use a powerful, large, API model for question generation while using a faster local model for summarization.* The possibilities are endless! Simply adjust the YAML, and the pipeline will accommodate it. (See the [usage example](https://github.com/huggingface/yourbench/blob/main/example/configs/advanced_example.yaml) for all available options!) From a930329e96922b36d9de827f6540971249d47ec4 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Sat, 24 May 2025 14:27:25 -0500 Subject: [PATCH 32/69] remove main, unnecessary --- yourbench/pipeline/chunking.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/yourbench/pipeline/chunking.py b/yourbench/pipeline/chunking.py index 61326be3..08cc17f6 100644 --- a/yourbench/pipeline/chunking.py +++ b/yourbench/pipeline/chunking.py @@ -847,27 +847,3 @@ def _plot_aggregated_similarities(all_similarities: list[list[float]]) -> None: plt.savefig(plot_path, dpi=300, bbox_inches="tight") # Changed dpi to 300 plt.close() logger.info(f"Saved aggregated similarity plot at '{plot_path}'.") - - -# Make sure main guard exists if this file is runnable directly (optional but good practice) -if __name__ == "__main__": - # Example configuration for testing (replace with actual loading if needed) - test_config = { - "pipeline": { - "chunking": { - "run": True, - "chunking_configuration": { - "chunking_mode": "fast_chunking" # or "semantic_chunking" if deps installed - }, - # Add other necessary config keys like dataset paths etc. - } - }, - "settings": {"debug": True}, - # Add dataset config, model roles etc. - } - # Basic logger setup for standalone execution - logger.add("logs/chunking_standalone.log", rotation="10 MB") - logger.info("Running chunking module standalone (example)...") - # Note: You'd need a valid dataset configuration for run() to work fully. - # run(test_config) - logger.info("Standalone example finished.") From 93e5715ff59cb48605a51c2f0e43eb42d7ea3cff Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Sat, 24 May 2025 14:27:59 -0500 Subject: [PATCH 33/69] remove plotting code --- yourbench/pipeline/chunking.py | 66 ---------------------------------- 1 file changed, 66 deletions(-) diff --git a/yourbench/pipeline/chunking.py b/yourbench/pipeline/chunking.py index 08cc17f6..9ce62c3f 100644 --- a/yourbench/pipeline/chunking.py +++ b/yourbench/pipeline/chunking.py @@ -781,69 +781,3 @@ def _compute_info_density_metrics( return results - -def _plot_aggregated_similarities(all_similarities: list[list[float]]) -> None: - """ - Plots the average cosine similarity for each sentence-pair position across - all documents, with shaded regions representing one standard deviation. - - Args: - all_similarities (list[list[float]]): A list of lists, where each - sub-list is the array of consecutive sentence similarities for - a particular document. - """ - if all_similarities is None or len(all_similarities) == 0: - logger.debug("No similarities to plot. Skipping aggregated similarity plot.") - return - - # Check if matplotlib is available before trying to plot - try: - import matplotlib.pyplot as plt - except ImportError: - logger.warning("Matplotlib not found. Skipping similarity plot generation.") - return - - plt.figure(figsize=(10, 6)) - max_len = max(len(sims) for sims in all_similarities) - - avg_sim: list[float] = [] - std_sim: list[float] = [] - counts: list[int] = [] - - for position in range(max_len): - vals = [s[position] for s in all_similarities if position < len(s)] - if vals: - mean_val = sum(vals) / len(vals) - variance = sum((v - mean_val) ** 2 for v in vals) / len(vals) - stddev_val = variance**0.5 - - avg_sim.append(mean_val) - std_sim.append(stddev_val) - counts.append(len(vals)) - else: - break - - # X-axis positions - x_positions = list(range(len(avg_sim))) - plt.plot(x_positions, avg_sim, "b-", label="Avg Similarity") - - # Create confidence interval region - lower_bound = [max(0, a - s) for a, s in zip(avg_sim, std_sim)] - upper_bound = [min(1, a + s) for a, s in zip(avg_sim, std_sim)] - plt.fill_between(x_positions, lower_bound, upper_bound, alpha=0.3, color="blue") - - # Plot data points with size reflecting how many docs contributed - max_count = max(counts) if counts else 1 - sizes = [30.0 * (c / max_count) for c in counts] - plt.scatter(x_positions, avg_sim, s=sizes, alpha=0.5, color="navy") - - plt.title("Average Consecutive Sentence Similarity Across Documents") - plt.xlabel("Sentence Pair Index") - plt.ylabel("Cosine Similarity") - plt.grid(True) - plot_path: str = os.path.join("plots", "aggregated_similarities.png") - # Ensure plots directory exists - os.makedirs("plots", exist_ok=True) - plt.savefig(plot_path, dpi=300, bbox_inches="tight") # Changed dpi to 300 - plt.close() - logger.info(f"Saved aggregated similarity plot at '{plot_path}'.") From fe76858c2fd3ff9f48b8865a8c78c9220f957f6d Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Sat, 24 May 2025 14:28:23 -0500 Subject: [PATCH 34/69] remove info density metrics --- yourbench/pipeline/chunking.py | 86 ---------------------------------- 1 file changed, 86 deletions(-) diff --git a/yourbench/pipeline/chunking.py b/yourbench/pipeline/chunking.py index 9ce62c3f..ded2bfe9 100644 --- a/yourbench/pipeline/chunking.py +++ b/yourbench/pipeline/chunking.py @@ -695,89 +695,3 @@ def _multihop_chunking( return final_multihop_chunks -def _compute_info_density_metrics( - chunks: list[SingleHopChunk], - local_perplexity_metric: Optional[Any], - local_use_textstat: bool, -) -> list[ChunkInfoMetrics]: - """ - Computes optional statistics for each chunk, including token count, perplexity, - readability (flesch, gunning fog), and basic lexical diversity metrics. - - Args: - chunks (list[SingleHopChunk]): The list of single-hop chunk objects. - local_perplexity_metric (Optional[Any]): If provided, used to compute - perplexity (from evaluate.load("perplexity")). - local_use_textstat (bool): If True, compute text readability metrics using textstat. - - Returns: - list[ChunkInfoMetrics]: One object per chunk with fields like: - - token_count - - unique_token_ratio - - bigram_diversity - - perplexity - - avg_token_length - - flesch_reading_ease - - gunning_fog - """ - results: list[ChunkInfoMetrics] = [] - - for chunk in chunks: - chunk_text: str = chunk.chunk_text - tokens = chunk_text.strip().split() - token_count: int = len(tokens) - - # Compute metrics step by step - unique_token_ratio = 0.0 - if token_count > 0: - unique_toks = len({t.lower() for t in tokens}) - unique_token_ratio = float(unique_toks / token_count) - - # Bigram diversity - bigram_diversity = 0.0 - if token_count > 1: - bigrams = [] - for i in range(token_count - 1): - bigrams.append((tokens[i].lower(), tokens[i + 1].lower())) - unique_bigrams = len(set(bigrams)) - bigram_diversity = float(unique_bigrams / len(bigrams)) - - # Perplexity - ppl_score: float = 0.0 - if local_perplexity_metric is not None and token_count > 0: - try: - result = local_perplexity_metric.compute(data=[chunk_text], batch_size=1) - ppl_score = result.get("mean_perplexity", 0.0) - except Exception as e: - logger.warning(f"Could not compute perplexity for chunk. Error: {e}") - - # Average token length - avg_token_length = 0.0 - if token_count > 0: - avg_len = sum(len(t) for t in tokens) / token_count - avg_token_length = float(avg_len) - - # Readability - flesch_reading_ease = 0.0 - gunning_fog = 0.0 - if local_use_textstat is True and chunk_text.strip(): - try: - flesch_reading_ease = float(textstat.flesch_reading_ease(chunk_text)) - gunning_fog = float(textstat.gunning_fog(chunk_text)) - except Exception as e: - logger.warning(f"Textstat error: {e}") - - results.append( - ChunkInfoMetrics( - token_count=float(token_count), - unique_token_ratio=unique_token_ratio, - bigram_diversity=bigram_diversity, - perplexity=ppl_score, - avg_token_length=avg_token_length, - flesch_reading_ease=flesch_reading_ease, - gunning_fog=gunning_fog, - ) - ) - - return results - From 004fba73a28a32fcf2f5f9cad71de62c14a10b30 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Sat, 24 May 2025 15:05:58 -0500 Subject: [PATCH 35/69] refactor chunking and heavily reduce LoC --- yourbench/pipeline/chunking.py | 787 ++++++--------------------------- 1 file changed, 142 insertions(+), 645 deletions(-) diff --git a/yourbench/pipeline/chunking.py b/yourbench/pipeline/chunking.py index ded2bfe9..5fa53ab2 100644 --- a/yourbench/pipeline/chunking.py +++ b/yourbench/pipeline/chunking.py @@ -1,697 +1,194 @@ -# ============================================================================= -# chunking.py -# ============================================================================= -""" -@module chunking -@author @sumukshashidhar - -This module implements two modes of chunking for the YourBench pipeline: -1) "fast_chunking" (the default), which chunks by purely length-based rules. -2) "semantic_chunking" (requires explicit config), which uses sentence embeddings - and a similarity threshold to decide chunk boundaries. - -Usage: ------- -Typically, you do not call this module directly. Instead, the handler.py -automatically invokes run(config) if the corresponding pipeline setting -(pipeline.chunking.run) is enabled. - -The run(config) function: -1. Loads a dataset specified by the pipeline configuration. -2. Depending on the configured chunking mode: - - fast_chunking (default): Chunks text solely based on maximum token length, - ignoring sentence similarity. - - semantic_chunking (requires pipeline.chunking.chunking_configuration.chunking_mode="semantic_chunking"): - Splits each document into single-hop chunks, guided by user-defined token - length constraints (l_min_tokens, l_max_tokens) and a similarity threshold (tau_threshold). - Uses a transformer model specified in config['model_roles']['chunking'], or a default. -3. Creates multi-hop chunks by sampling subsets of single-hop chunks and concatenating them. -4. Computes optional readability and perplexity metrics for each chunk if debug mode is enabled - and required packages (textstat, evaluate) are available. -5. Saves the dataset containing new columns: - - "chunks" (list of single-hop segments) - - "multihop_chunks" (list of multi-hop segment groups) - - "chunk_info_metrics" (various statistics) - - "chunking_model" (the model used for embeddings; default string if fast_chunking) - -Error Handling and Logging: ---------------------------- -- All warnings, errors, and debugging information are logged to both the console - and a dedicated log file at logs/chunking.log. -- If any critical errors occur while loading or processing data, the process - logs the exception and attempts a graceful exit without crashing the entire - pipeline. - -Debug Visualization: --------------------- -- In semantic_chunking mode, if debug mode is on, the module will generate a plot - of average consecutive sentence similarities and save it to plots/aggregated_similarities.png. -""" - -import os -import re import time -from typing import Any, Dict, Optional -from dataclasses import asdict, dataclass +from dataclasses import dataclass, asdict +from typing import Any +from collections.abc import Sequence import numpy as np -from loguru import logger # type: ignore +from loguru import logger from tqdm.auto import tqdm from yourbench.utils.chunking_utils import split_into_token_chunks from yourbench.utils.dataset_engine import custom_load_dataset, custom_save_dataset -# Try importing torch-related libraries -_torch_available = False -try: - import torch - import torch.nn.functional as F - from torch.amp import autocast - - _torch_available = True - logger.info("PyTorch is available.") -except ImportError: - logger.info("PyTorch is not available. Semantic chunking features requiring torch will be disabled.") - - # Define dummy autocast if torch not found - class DummyAutocast: - def __enter__(self): - pass - - def __exit__(self, type, value, traceback): - pass - - def autocast(device_type): - return DummyAutocast() # type: ignore - - -# Try importing transformers -_transformers_available = False -try: - from transformers import AutoModel, AutoTokenizer - - _transformers_available = True - logger.info("Transformers library is available.") -except ImportError: - logger.info( - "Transformers library is not available. Semantic chunking features requiring transformers will be disabled." - ) - AutoModel = None # type: ignore - AutoTokenizer = None # type: ignore - - -try: - import evaluate - - # Attempt to load perplexity metric from evaluate - _perplexity_metric = evaluate.load("perplexity", module_type="metric", model_id="gpt2") - logger.info("Loaded 'perplexity' metric with model_id='gpt2'.") -except Exception as perplexity_load_error: - logger.info( - f"Could not load perplexity metric from 'evaluate'. Skipping perplexity. Error: {perplexity_load_error}" - ) - _perplexity_metric = None - -try: - # Attempt to import textstat for readability metrics - import textstat - - _use_textstat = True -except ImportError: - logger.info("Package 'textstat' not installed. Readability metrics will be skipped.") - _use_textstat = False - - -# ----------------------------------------------------------------------------- -# Dataclasses for cleaner configuration and result handling -# ----------------------------------------------------------------------------- -@dataclass -class ChunkingParameters: - l_min_tokens: int = 64 - l_max_tokens: int = 128 - tau_threshold: float = 0.3 +@dataclass(frozen=True) +class ChunkingConfig: + """Configuration for chunking parameters.""" + max_tokens: int = 256 h_min: int = 2 - h_max: int = 3 - num_multihops_factor: int = 2 - chunking_mode: str = "fast_chunking" # "fast_chunking" or "semantic_chunking" + h_max: int = 5 + num_multihops_factor: int = 1 -@dataclass +@dataclass(frozen=True) class SingleHopChunk: - chunk_id: Any + """A single text chunk with its identifier.""" + chunk_id: str chunk_text: str -@dataclass +@dataclass(frozen=True) class MultiHopChunk: + """A combination of multiple single-hop chunks.""" chunk_ids: list[str] chunks_text: list[str] -@dataclass -class ChunkInfoMetrics: - token_count: float - unique_token_ratio: float - bigram_diversity: float - perplexity: float - avg_token_length: float - flesch_reading_ease: float - gunning_fog: float - - -def _parse_chunking_parameters(config: Dict[str, Any]) -> ChunkingParameters: - """ - Extracts the chunking parameters from the config dictionary, falling back - to default values if keys are missing. The chunking_mode defaults to - "fast_chunking" unless explicitly set to "semantic_chunking." - """ +def extract_config(config: dict[str, Any]) -> ChunkingConfig: + """Extract chunking configuration from pipeline config.""" chunking_params = config.get("pipeline", {}).get("chunking", {}).get("chunking_configuration", {}) - return ChunkingParameters( - l_min_tokens=chunking_params.get("l_min_tokens", 128), - l_max_tokens=chunking_params.get("l_max_tokens", 256), - tau_threshold=chunking_params.get("tau_threshold", 0.7), + return ChunkingConfig( + max_tokens=chunking_params.get("l_max_tokens", 256), h_min=chunking_params.get("h_min", 2), h_max=chunking_params.get("h_max", 5), num_multihops_factor=chunking_params.get("num_multihops_factor", 1), - chunking_mode=chunking_params.get("chunking_mode", "fast_chunking"), - ) - - -def run(config: Dict[str, Any]) -> None: - """ - Main pipeline entry point for the chunking stage. - - Args: - config (Dict[str, Any]): The entire pipeline configuration dictionary. - - Returns: - None. This function saves the updated dataset containing chunked - documents to disk or the Hugging Face Hub, based on the config. - - Raises: - RuntimeError: If a critical error is encountered that prevents chunking. - The error is logged, and execution attempts a graceful exit. - """ - # Retrieve chunking configuration from config - chunking_config = config.get("pipeline", {}).get("chunking", {}) - if chunking_config is None or not chunking_config.get("run", False): - logger.info("Chunking stage is disabled. Skipping.") - return - - logger.info("Starting chunking stage...") - - # Attempt to load dataset - dataset = custom_load_dataset(config=config, subset="summarized") - logger.info(f"Loaded summarized subset with {len(dataset)} rows for chunking.") - - # Retrieve chunking parameters into a dataclass - params = _parse_chunking_parameters(config) - l_min_tokens = params.l_min_tokens - l_max_tokens = params.l_max_tokens - tau_threshold = params.tau_threshold - h_min = params.h_min - h_max = params.h_max - num_multihops_factor = params.num_multihops_factor - chunking_mode = params.chunking_mode.lower().strip() - - # Check debug setting - debug_mode: bool = config.get("settings", {}).get("debug", False) - if debug_mode is False: - # If not debug mode, skip perplexity and readability to save time - logger.debug("Skipping perplexity and readability metrics (debug mode off).") - local_perplexity_metric = None - local_use_textstat = False - else: - local_perplexity_metric = _perplexity_metric - local_use_textstat = _use_textstat - - # We'll only load the chunking model if in semantic_chunking mode - tokenizer = None - model = None - device = "cpu" - model_name = "no_model_for_fast_chunking" - - if chunking_mode == "semantic_chunking": - # Check if required libraries are installed - if not _torch_available or not _transformers_available: - logger.error( - "Semantic chunking requires 'torch' and 'transformers' libraries. " - "Please install them (e.g., pip install yourbench[semantic]) or use 'fast_chunking' mode." - ) - return # Exit if dependencies are missing for semantic chunking - - try: - # Extract model name from config if available - model_name_list = config.get("model_roles", {}).get("chunking", []) - if model_name_list is None or len(model_name_list) == 0: - logger.info( - "No chunking model specified in config['model_roles']['chunking']. " - "Using default 'intfloat/multilingual-e5-large-instruct'." - ) - model_name = "intfloat/multilingual-e5-large-instruct" - else: - model_name = model_name_list[0] - - logger.info(f"Using chunking model: '{model_name}'") - # Determine device only if torch is available - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - tokenizer = AutoTokenizer.from_pretrained(model_name) # type: ignore - model = AutoModel.from_pretrained(model_name).to(device).eval() # type: ignore - except Exception as model_error: - logger.error(f"Error loading tokenizer/model '{model_name}': {model_error}") - logger.warning("Chunking stage cannot proceed with semantic_chunking. Exiting.") - return - else: - logger.info("Using fast_chunking mode: purely length-based chunking with no embeddings.") - - # Prepare data structures - all_single_hop_chunks: list[list[SingleHopChunk]] = [] - all_multihop_chunks: list[list[MultiHopChunk]] = [] - all_chunk_info_metrics: list[list[ChunkInfoMetrics]] = [] - all_similarities: list[list[float]] = [] - - # Process each document in the dataset - start_time = time.time() - total_docs = len(dataset) - logger.info(f"Starting chunking process for {total_docs} documents") - - for idx, row in enumerate(tqdm(dataset, desc="Chunking documents", ncols=100)): - doc_start_time = time.time() - logger.info( - f"[{idx + 1}/{total_docs}] Processing document ID={row.get('document_id', f'doc_{idx}')} ({len(row.get('document_text', ''))} chars)" - ) - doc_text = row.get("document_text", "") - doc_id = row.get("document_id", f"doc_{idx}") - logger.info(f"[{idx}] doc_id={row.get('document_id')} | text_len={len(doc_text)} | preview={doc_text[:100]!r}") - - # If text is empty or missing - if doc_text is None or not doc_text.strip(): - logger.warning(f"Document at index {idx} has empty text. Storing empty chunks.") - doc_process_time = time.time() - doc_start_time - logger.info(f"Completed document {idx + 1}/{total_docs} in {doc_process_time:.2f}s") - all_single_hop_chunks.append([]) - all_multihop_chunks.append([]) - all_chunk_info_metrics.append([]) - continue - - if (idx + 1) % 1 == 0: - elapsed_time = time.time() - start_time - avg_time_per_doc = elapsed_time / (idx + 1) - remaining_docs = total_docs - (idx + 1) - estimated_remaining = avg_time_per_doc * remaining_docs - progress_pct = (idx + 1) / total_docs * 100 - - logger.info(f"Progress: {progress_pct:.1f}% | Completed {idx + 1}/{total_docs} documents") - logger.info( - f"Avg time per doc: {avg_time_per_doc:.2f}s | Est. remaining: {estimated_remaining / 60:.1f} minutes" - ) - - # Split the document into sentences - sentences = _split_into_sentences(doc_text) - - if sentences is None or len(sentences) == 0: - logger.warning(f"No valid sentences found for doc at index {idx}, doc_id={doc_id}.") - all_single_hop_chunks.append([]) - all_multihop_chunks.append([]) - all_chunk_info_metrics.append([]) - continue - - # Depending on the chunking mode: - if chunking_mode == "semantic_chunking": - # Debug log showing current dependency state - logger.debug( - f"Semantic chunking check: torch={_torch_available}, transformers={_transformers_available}, model_loaded={model is not None}, tokenizer_loaded={tokenizer is not None}" - ) - - # Ensure dependencies one last time before computation - if not _torch_available or not _transformers_available or model is None or tokenizer is None: - logger.error("Cannot perform semantic chunking due to missing dependencies or model loading issues.") - # Add empty lists and continue to avoid crashing the loop for this document - all_single_hop_chunks.append([]) - all_multihop_chunks.append([]) - all_chunk_info_metrics.append([]) - continue - - # 1) Compute embeddings for sentences - sentence_embeddings = _compute_embeddings(tokenizer, model, texts=sentences, device=device, max_len=512) - # 2) Compute consecutive sentence similarities - consecutive_sims: list[float] = [] - for sentence_index in range(len(sentences) - 1): - cos_sim = float( - F.cosine_similarity( - sentence_embeddings[sentence_index].unsqueeze(0), - sentence_embeddings[sentence_index + 1].unsqueeze(0), - dim=1, - )[0] - ) - consecutive_sims.append(cos_sim) - if consecutive_sims: - all_similarities.append(consecutive_sims) - - # 3) Create single-hop chunks with semantic logic - single_hop_chunks = _chunk_document_semantic( - sentences=sentences, - similarities=consecutive_sims, - l_min_tokens=l_min_tokens, - l_max_tokens=l_max_tokens, - tau=tau_threshold, - doc_id=doc_id, - ) - else: - # Debug line for fast chunking - logger.info( - f"[{doc_id}] Performing fast_chunking on {len(sentences)} sentences (l_max_tokens={l_max_tokens})" - ) - - # Fast chunking: purely length-based - single_hop_chunks = _chunk_document_fast( - sentences=sentences, - l_max_tokens=l_max_tokens, - doc_id=doc_id, - ) - - # Create multi-hop chunks - multihop = _multihop_chunking( - single_hop_chunks, - h_min=h_min, - h_max=h_max, - num_multihops_factor=num_multihops_factor, - ) - - # Compute metrics (token_count, perplexity, readability, etc.) - chunk_metrics = _compute_info_density_metrics(single_hop_chunks, local_perplexity_metric, local_use_textstat) - - # Accumulate - all_single_hop_chunks.append(single_hop_chunks) - all_multihop_chunks.append(multihop) - all_chunk_info_metrics.append(chunk_metrics) - - # Optional: Save aggregated similarity plot only if in semantic_chunking and debug - if chunking_mode == "semantic_chunking" and all_similarities and debug_mode: - _plot_aggregated_similarities(all_similarities) - - # Convert dataclasses back to dicts for safe addition to the dataset - dataset = dataset.add_column( - "chunks", - [[asdict(chunk) for chunk in chunk_list] for chunk_list in all_single_hop_chunks], ) - dataset = dataset.add_column( - "multihop_chunks", - [[asdict(mh) for mh in multihop_list] for multihop_list in all_multihop_chunks], - ) - dataset = dataset.add_column( - "chunk_info_metrics", - [[asdict(cm) for cm in metric_list] for metric_list in all_chunk_info_metrics], - ) - dataset = dataset.add_column("chunking_model", [model_name] * len(dataset)) - - # Save updated dataset - custom_save_dataset(dataset=dataset, config=config, subset="chunked") - logger.success("Chunking stage completed successfully.") -def _split_into_sentences(text: str) -> list[str]: +def chunk_document(text: str, doc_id: str, max_tokens: int) -> list[SingleHopChunk]: """ - Splits the input text into sentences using a simple rule-based approach - that looks for punctuation delimiters ('.', '!', '?'). - + Chunk a document into segments based on token count. + Args: - text (str): The full document text to be split. - + text: Document text to chunk + doc_id: Unique document identifier + max_tokens: Maximum tokens per chunk + Returns: - list[str]: A list of sentence strings. + List of single-hop chunks """ - # Replace newlines with spaces for consistency - normalized_text = text.replace("\n", " ").strip() - if normalized_text is None or normalized_text == "": + if not text or not text.strip(): return [] - - # Split using capturing parentheses to retain delimiters, then recombine. - segments = re.split(r"([.!?])", normalized_text) - sentences: list[str] = [] - for i in range(0, len(segments), 2): - if i + 1 < len(segments): - # Combine the text and delimiter - candidate = (segments[i] + segments[i + 1]).strip() - else: - # If no delimiter segment, use the text directly - candidate = segments[i].strip() - if candidate: - sentences.append(candidate) - return sentences - - -def _compute_embeddings( - tokenizer: AutoTokenizer, - model: AutoModel, - texts: list[str], - device: "torch.device", - max_len: int = 512, - batch_size: int = 16, -) -> "list[torch.Tensor]": - """ - Computes sentence embeddings by mean pooling the last hidden states, - normalized to unit length. - - Args: - tokenizer (AutoTokenizer): A Hugging Face tokenizer. - model (AutoModel): A pretrained transformer model to generate embeddings. - texts (list[str]): The list of sentence strings to be embedded. - device (torch.device): The device on which to run inference (CPU or GPU). - max_len (int): Max sequence length for tokenization. - batch_size (int): Batch size. - Returns: - list[torch.Tensor]: A list of PyTorch tensors (one per sentence). - """ - embeddings = [] - model.eval() - - # Determine autocast device type string - autocast_device_type = "cuda" if _torch_available and torch.cuda.is_available() else "cpu" - - for i in range(0, len(texts), batch_size): - batch_texts = texts[i : i + batch_size] - batch_dict = tokenizer(batch_texts, max_length=max_len, padding=True, truncation=True, return_tensors="pt").to( - device - ) - - with torch.no_grad(): - # Use autocast context manager - with autocast(autocast_device_type): - outputs = model(**batch_dict) - last_hidden_states = outputs.last_hidden_state - attention_mask = batch_dict["attention_mask"] - - # Zero out non-attended tokens - last_hidden_states = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0) - - # Mean pooling - sum_hidden = last_hidden_states.sum(dim=1) - valid_token_counts = attention_mask.sum(dim=1, keepdim=True) - batch_embeddings = sum_hidden / valid_token_counts.clamp(min=1e-9) - - # Normalize - batch_embeddings = F.normalize(batch_embeddings, p=2, dim=1) - - embeddings.extend(batch_embeddings.cpu()) - - return embeddings - - -def _chunk_document_semantic( - sentences: list[str], - similarities: list[float], - l_min_tokens: int, - l_max_tokens: int, - tau: float, - doc_id: str, -) -> list[SingleHopChunk]: - """ - Creates single-hop chunks from sentences using semantic guidance. Ensures each - chunk is at least l_min_tokens in length and at most l_max_tokens, introducing - a chunk boundary when consecutive sentence similarity is below threshold tau. - - Args: - sentences (list[str]): The list of sentences for a single document. - similarities (list[float]): Cosine similarities between consecutive sentences. - l_min_tokens (int): Minimum tokens per chunk. - l_max_tokens (int): Maximum tokens per chunk. - tau (float): Similarity threshold for introducing a chunk boundary. - doc_id (str): Unique identifier for the document. - - Returns: - list[SingleHopChunk]: A list of SingleHopChunk objects. - """ - chunks: list[SingleHopChunk] = [] - current_chunk: list[str] = [] - current_len: int = 0 - chunk_index: int = 0 - - for i, sentence in enumerate(sentences): - sentence_token_count = len(sentence.split()) - - # If one sentence alone exceeds l_max, finalize the current chunk if non-empty, - # then store this sentence as its own chunk. - if sentence_token_count >= l_max_tokens: - # Dump the current chunk - if len(current_chunk) > 0: - chunk_str = " ".join(current_chunk) - chunks.append(SingleHopChunk(chunk_id=f"{doc_id}_{chunk_index}", chunk_text=chunk_str)) - chunk_index += 1 - current_chunk = [] - current_len = 0 - # Store the sentence alone - chunks.append(SingleHopChunk(chunk_id=f"{doc_id}_{chunk_index}", chunk_text=sentence)) - chunk_index += 1 - continue - - # Otherwise, add this sentence to the current chunk - current_chunk.append(sentence) - current_len += sentence_token_count - - # If we exceed l_max, close the current chunk and start a new one - if current_len >= l_max_tokens: - chunk_str = " ".join(current_chunk) - chunks.append(SingleHopChunk(chunk_id=f"{doc_id}_{chunk_index}", chunk_text=chunk_str)) - chunk_index += 1 - current_chunk = [] - current_len = 0 - continue - - # If we have at least l_min tokens and the next sentence similarity is below threshold, break here - if (current_len >= l_min_tokens) and (i < len(sentences) - 1): - if similarities[i] < tau: - chunk_str = " ".join(current_chunk) - chunks.append(SingleHopChunk(chunk_id=f"{doc_id}_{chunk_index}", chunk_text=chunk_str)) - chunk_index += 1 - current_chunk = [] - current_len = 0 - - # Any leftover - if len(current_chunk) > 0: - chunk_str = " ".join(current_chunk) - chunks.append(SingleHopChunk(chunk_id=f"{doc_id}_{chunk_index}", chunk_text=chunk_str)) - - return chunks - - -def _chunk_document_fast( - sentences: list[str], - l_max_tokens: int, - doc_id: str, - show_progress: bool = True, -) -> list[SingleHopChunk]: - """ - Uses token-based chunking with optional overlap, based on tiktoken. - - Args: - sentences (list[str]): Sentences of the document. - l_max_tokens (int): Max tokens per chunk. - doc_id (str): Unique identifier for the document. - show_progress (bool): Show progress bar (ignored here, kept for API symmetry). - - Returns: - list[SingleHopChunk]: A list of token-based chunks. - """ - text = " ".join(sentences) - chunk_texts = split_into_token_chunks( - text, - chunk_tokens=l_max_tokens, - overlap=0, - ) - - return [SingleHopChunk(chunk_id=f"{doc_id}_{i}", chunk_text=chunk) for i, chunk in enumerate(chunk_texts)] + + chunk_texts = split_into_token_chunks(text, chunk_tokens=max_tokens, overlap=0) + return [ + SingleHopChunk(chunk_id=f"{doc_id}_{i}", chunk_text=chunk) + for i, chunk in enumerate(chunk_texts) + ] -def _multihop_chunking( - single_hop_chunks: list[SingleHopChunk], +def create_multihop_chunks( + chunks: Sequence[SingleHopChunk], h_min: int, h_max: int, - num_multihops_factor: int, + num_multihops_factor: int ) -> list[MultiHopChunk]: """ - Creates multi-hop chunks via numpy random sampling. - - Generates combinations of size effective_h_max, slices them to sizes - between h_min and effective_h_max, and collects unique combinations. - Target number = max(1, total_single_hops // num_multihops_factor). - Actual number may be less due to sampling/de-duplication. - + Create multi-hop chunks by randomly sampling combinations of single-hop chunks. + Args: - single_hop_chunks: List of single-hop chunks. - h_min: Min single-hops per multi-hop. - h_max: Max single-hops per multi-hop. - num_multihops_factor: Factor to determine target multi-hop count. - + chunks: List of single-hop chunks + h_min: Minimum chunks per multi-hop + h_max: Maximum chunks per multi-hop + num_multihops_factor: Factor to determine number of multi-hops + Returns: - List of unique MultiHopChunk objects. + List of multi-hop chunks """ - total_single_hops = len(single_hop_chunks) - logger.info(f"Starting multi-hop chunking, total single chunks: {total_single_hops}") - - if not single_hop_chunks: - logger.warning("Empty input 'single_hop_chunks'. Returning [].") + if not chunks or h_min > len(chunks) or h_min > h_max or h_min <= 0: return [] - if not (0 < h_min <= h_max): - logger.warning(f"Invalid hop range h_min={h_min}, h_max={h_max}. Returning [].") - return [] - - effective_h_max = min(h_max, total_single_hops) + + total_chunks = len(chunks) + effective_h_max = min(h_max, total_chunks) + if h_min > effective_h_max: - logger.warning(f"h_min ({h_min}) > effective_h_max ({effective_h_max}). Cannot form chunks. Returning [].") return [] - - if num_multihops_factor <= 0: - logger.info("num_multihops_factor <= 0. Targeting all single hops.") - num_multihops_target = total_single_hops - else: - num_multihops_target = max(1, total_single_hops // num_multihops_factor) - - if np.prod((num_multihops_target, effective_h_max)) > total_single_hops: - logger.warning( - f"Target {num_multihops_target} is too high for given sample size: {total_single_hops} and effective_h_max: {effective_h_max}" - ) - num_multihops_target = total_single_hops // effective_h_max - - logger.info( - f"Targeting ~{num_multihops_target} multi-hop chunks, effective h_max: {effective_h_max}, h_min: {h_min}" - ) - + + # Determine target number of multi-hop chunks + target_count = max(1, total_chunks // max(1, num_multihops_factor)) + + # Adjust if target is unrealistic + if target_count * effective_h_max > total_chunks: + target_count = total_chunks // effective_h_max + + if target_count == 0: + return [] + rng = np.random.default_rng() - - # Generate initial index combinations (size effective_h_max) - initial_indices = rng.choice( - total_single_hops, - size=(num_multihops_target, effective_h_max), - replace=False, # Unique indices per combination - ) - - # Generate random slice sizes - slice_sizes = rng.integers(low=h_min, high=effective_h_max, size=num_multihops_target, endpoint=True) - - # Slice, sort, tuple for hashing, and collect unique combinations - unique_combo_indices_set = { - tuple(np.sort(initial_indices[i][: slice_sizes[i]])) for i in range(num_multihops_target) + + # Generate random combinations + indices_array = rng.choice(total_chunks, size=(target_count, effective_h_max), replace=False) + sizes = rng.integers(low=h_min, high=effective_h_max + 1, size=target_count) + + # Create unique combinations + unique_combos = { + tuple(sorted(indices_array[i][:sizes[i]])) + for i in range(target_count) } - - logger.info(f"Generated {len(unique_combo_indices_set)} unique index combinations.") - - if not unique_combo_indices_set: - logger.warning("No unique combinations generated.") - return [] - - # --- Build MultiHopChunk Objects --- - final_multihop_chunks = [ + + # Build multi-hop chunks + return [ MultiHopChunk( - chunk_ids=[single_hop_chunks[idx].chunk_id for idx in combo_indices], - chunks_text=[single_hop_chunks[idx].chunk_text for idx in combo_indices], + chunk_ids=[chunks[idx].chunk_id for idx in combo], + chunks_text=[chunks[idx].chunk_text for idx in combo] ) - for combo_indices in unique_combo_indices_set - # combo_indices guaranteed non-empty by h_min >= 1 + for combo in unique_combos ] - logger.info(f"Created {len(final_multihop_chunks)} multi-hop chunks.") - return final_multihop_chunks - +def run(config: dict[str, Any]) -> None: + """ + Main entry point for the chunking pipeline stage. + + Args: + config: Pipeline configuration dictionary + """ + chunking_config = config.get("pipeline", {}).get("chunking", {}) + if not chunking_config.get("run", False): + logger.info("Chunking stage is disabled. Skipping.") + return + + logger.info("Starting chunking stage...") + + # Load dataset + dataset = custom_load_dataset(config=config, subset="summarized") + logger.info(f"Loaded {len(dataset)} documents for chunking") + + # Extract configuration + params = extract_config(config) + + # Process documents + all_single_chunks: list[list[SingleHopChunk]] = [] + all_multihop_chunks: list[list[MultiHopChunk]] = [] + + start_time = time.time() + + for idx, row in enumerate(tqdm(dataset, desc="Chunking documents")): + doc_text = row.get("document_text", "") + doc_id = row.get("document_id", f"doc_{idx}") + + # Create single-hop chunks + single_chunks = chunk_document(doc_text, doc_id, params.max_tokens) + + # Create multi-hop chunks + multihop_chunks = create_multihop_chunks( + single_chunks, + params.h_min, + params.h_max, + params.num_multihops_factor + ) + + all_single_chunks.append(single_chunks) + all_multihop_chunks.append(multihop_chunks) + + # Progress logging + if (idx + 1) % 100 == 0: + elapsed = time.time() - start_time + rate = (idx + 1) / elapsed + logger.info(f"Progress: {idx + 1}/{len(dataset)} docs ({rate:.1f} docs/sec)") + + # Add columns to dataset + dataset = dataset.add_column( + "chunks", + [[asdict(chunk) for chunk in chunks] for chunks in all_single_chunks] + ) + dataset = dataset.add_column( + "multihop_chunks", + [[asdict(mh) for mh in multihops] for multihops in all_multihop_chunks] + ) + + # Save dataset + custom_save_dataset(dataset=dataset, config=config, subset="chunked") + + elapsed_total = time.time() - start_time + logger.success(f"Chunking completed in {elapsed_total:.1f} seconds") \ No newline at end of file From 1971b2025e06ac6291f9049391b818368d356145 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Sat, 24 May 2025 15:06:16 -0500 Subject: [PATCH 36/69] fix cq --- yourbench/pipeline/chunking.py | 94 +++++++++++++++------------------- 1 file changed, 40 insertions(+), 54 deletions(-) diff --git a/yourbench/pipeline/chunking.py b/yourbench/pipeline/chunking.py index 5fa53ab2..aed035fd 100644 --- a/yourbench/pipeline/chunking.py +++ b/yourbench/pipeline/chunking.py @@ -1,6 +1,6 @@ import time -from dataclasses import dataclass, asdict from typing import Any +from dataclasses import asdict, dataclass from collections.abc import Sequence import numpy as np @@ -14,6 +14,7 @@ @dataclass(frozen=True) class ChunkingConfig: """Configuration for chunking parameters.""" + max_tokens: int = 256 h_min: int = 2 h_max: int = 5 @@ -23,6 +24,7 @@ class ChunkingConfig: @dataclass(frozen=True) class SingleHopChunk: """A single text chunk with its identifier.""" + chunk_id: str chunk_text: str @@ -30,6 +32,7 @@ class SingleHopChunk: @dataclass(frozen=True) class MultiHopChunk: """A combination of multiple single-hop chunks.""" + chunk_ids: list[str] chunks_text: list[str] @@ -48,79 +51,69 @@ def extract_config(config: dict[str, Any]) -> ChunkingConfig: def chunk_document(text: str, doc_id: str, max_tokens: int) -> list[SingleHopChunk]: """ Chunk a document into segments based on token count. - + Args: text: Document text to chunk doc_id: Unique document identifier max_tokens: Maximum tokens per chunk - + Returns: List of single-hop chunks """ if not text or not text.strip(): return [] - + chunk_texts = split_into_token_chunks(text, chunk_tokens=max_tokens, overlap=0) - return [ - SingleHopChunk(chunk_id=f"{doc_id}_{i}", chunk_text=chunk) - for i, chunk in enumerate(chunk_texts) - ] + return [SingleHopChunk(chunk_id=f"{doc_id}_{i}", chunk_text=chunk) for i, chunk in enumerate(chunk_texts)] def create_multihop_chunks( - chunks: Sequence[SingleHopChunk], - h_min: int, - h_max: int, - num_multihops_factor: int + chunks: Sequence[SingleHopChunk], h_min: int, h_max: int, num_multihops_factor: int ) -> list[MultiHopChunk]: """ Create multi-hop chunks by randomly sampling combinations of single-hop chunks. - + Args: chunks: List of single-hop chunks h_min: Minimum chunks per multi-hop h_max: Maximum chunks per multi-hop num_multihops_factor: Factor to determine number of multi-hops - + Returns: List of multi-hop chunks """ if not chunks or h_min > len(chunks) or h_min > h_max or h_min <= 0: return [] - + total_chunks = len(chunks) effective_h_max = min(h_max, total_chunks) - + if h_min > effective_h_max: return [] - + # Determine target number of multi-hop chunks target_count = max(1, total_chunks // max(1, num_multihops_factor)) - + # Adjust if target is unrealistic if target_count * effective_h_max > total_chunks: target_count = total_chunks // effective_h_max - + if target_count == 0: return [] - + rng = np.random.default_rng() - + # Generate random combinations indices_array = rng.choice(total_chunks, size=(target_count, effective_h_max), replace=False) sizes = rng.integers(low=h_min, high=effective_h_max + 1, size=target_count) - + # Create unique combinations - unique_combos = { - tuple(sorted(indices_array[i][:sizes[i]])) - for i in range(target_count) - } - + unique_combos = {tuple(sorted(indices_array[i][: sizes[i]])) for i in range(target_count)} + # Build multi-hop chunks return [ MultiHopChunk( - chunk_ids=[chunks[idx].chunk_id for idx in combo], - chunks_text=[chunks[idx].chunk_text for idx in combo] + chunk_ids=[chunks[idx].chunk_id for idx in combo], chunks_text=[chunks[idx].chunk_text for idx in combo] ) for combo in unique_combos ] @@ -129,7 +122,7 @@ def create_multihop_chunks( def run(config: dict[str, Any]) -> None: """ Main entry point for the chunking pipeline stage. - + Args: config: Pipeline configuration dictionary """ @@ -137,58 +130,51 @@ def run(config: dict[str, Any]) -> None: if not chunking_config.get("run", False): logger.info("Chunking stage is disabled. Skipping.") return - + logger.info("Starting chunking stage...") - + # Load dataset dataset = custom_load_dataset(config=config, subset="summarized") logger.info(f"Loaded {len(dataset)} documents for chunking") - + # Extract configuration params = extract_config(config) - + # Process documents all_single_chunks: list[list[SingleHopChunk]] = [] all_multihop_chunks: list[list[MultiHopChunk]] = [] - + start_time = time.time() - + for idx, row in enumerate(tqdm(dataset, desc="Chunking documents")): doc_text = row.get("document_text", "") doc_id = row.get("document_id", f"doc_{idx}") - + # Create single-hop chunks single_chunks = chunk_document(doc_text, doc_id, params.max_tokens) - + # Create multi-hop chunks multihop_chunks = create_multihop_chunks( - single_chunks, - params.h_min, - params.h_max, - params.num_multihops_factor + single_chunks, params.h_min, params.h_max, params.num_multihops_factor ) - + all_single_chunks.append(single_chunks) all_multihop_chunks.append(multihop_chunks) - + # Progress logging if (idx + 1) % 100 == 0: elapsed = time.time() - start_time rate = (idx + 1) / elapsed logger.info(f"Progress: {idx + 1}/{len(dataset)} docs ({rate:.1f} docs/sec)") - + # Add columns to dataset + dataset = dataset.add_column("chunks", [[asdict(chunk) for chunk in chunks] for chunks in all_single_chunks]) dataset = dataset.add_column( - "chunks", - [[asdict(chunk) for chunk in chunks] for chunks in all_single_chunks] + "multihop_chunks", [[asdict(mh) for mh in multihops] for multihops in all_multihop_chunks] ) - dataset = dataset.add_column( - "multihop_chunks", - [[asdict(mh) for mh in multihops] for multihops in all_multihop_chunks] - ) - + # Save dataset custom_save_dataset(dataset=dataset, config=config, subset="chunked") - + elapsed_total = time.time() - start_time - logger.success(f"Chunking completed in {elapsed_total:.1f} seconds") \ No newline at end of file + logger.success(f"Chunking completed in {elapsed_total:.1f} seconds") From f58c00df7417e2a4c6555e8891474e8db39a76db Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Sat, 24 May 2025 15:57:43 -0500 Subject: [PATCH 37/69] update testcase --- tests/integration/test_pipeline.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/tests/integration/test_pipeline.py b/tests/integration/test_pipeline.py index 426d4f04..7023e8be 100644 --- a/tests/integration/test_pipeline.py +++ b/tests/integration/test_pipeline.py @@ -56,13 +56,10 @@ def mock_config(temp_dir): "chunking": { "run": True, "chunking_configuration": { - "l_min_tokens": 64, - "l_max_tokens": 128, - "tau_threshold": 0.8, + "l_max_tokens": 128, # Only max_tokens is used now "h_min": 2, "h_max": 5, "num_multihops_factor": 2, - "chunking_mode": "fast_chunking", }, }, "single_shot_question_generation": { @@ -179,8 +176,8 @@ def test_chunking_stage(mock_config): mock_dataset = Dataset.from_dict({ "document_id": ["doc1", "doc2"], "document_text": [ - "This is document 1 with enough text to be chunked properly", - "This is document 2 which also has sufficient text for chunking", + "This is document 1 with enough text to be chunked properly. " * 10, + "This is document 2 which also has sufficient text for chunking. " * 10, ], "document_summary": ["Summary 1", "Summary 2"], }) @@ -189,12 +186,10 @@ def test_chunking_stage(mock_config): with ( patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, - patch("yourbench.pipeline.chunking._compute_info_density_metrics") as mock_metrics, - patch("yourbench.pipeline.chunking.split_into_token_chunks") as mock_split, + patch("yourbench.utils.chunking_utils.split_into_token_chunks") as mock_split, ): # Configure mock returns mock_split.return_value = ["Chunk 1", "Chunk 2"] - mock_metrics.return_value = [] # Import the chunking run function from yourbench.pipeline.chunking import run @@ -204,8 +199,12 @@ def test_chunking_stage(mock_config): # Verify the chunking stage behavior mock_load.assert_called_once() - assert mock_split.call_count > 0 + assert mock_split.call_count == 2 # Called once for each document mock_save.assert_called_once() + + # Verify that the dataset was saved with the right subset + saved_args = mock_save.call_args + assert saved_args[1]["subset"] == "chunked" # Test for single-shot question generation stage @@ -266,14 +265,11 @@ def test_multi_hop_question_generation_stage(mock_config): """ # Mock dataset with chunks and valid multihop_chunks format chunks = [{"chunk_id": "chunk1", "chunk_text": "This is chunk 1"}] - # Correct format for multihop_chunks + # Updated format for multihop_chunks based on the refactored code multihop_chunks = [ { - "multihop_id": "mh1", - "source_chunks": [ - {"chunk_id": "chunk1", "chunk_text": "Text 1"}, - {"chunk_id": "chunk2", "chunk_text": "Text 2"}, - ], + "chunk_ids": ["chunk1", "chunk2"], + "chunks_text": ["Text 1", "Text 2"], } ] mock_dataset = Dataset.from_dict({ @@ -386,4 +382,4 @@ def load_dataset_side_effect(config, subset): # Verify behavior assert mock_load.call_count == 4 - mock_save.assert_called_once() + mock_save.assert_called_once() \ No newline at end of file From 2801943520c4d6c57eebb305135cca6e24cd513c Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Sat, 24 May 2025 15:58:15 -0500 Subject: [PATCH 38/69] add cq for tests --- tests/integration/test_pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_pipeline.py b/tests/integration/test_pipeline.py index 7023e8be..4080b613 100644 --- a/tests/integration/test_pipeline.py +++ b/tests/integration/test_pipeline.py @@ -201,7 +201,7 @@ def test_chunking_stage(mock_config): mock_load.assert_called_once() assert mock_split.call_count == 2 # Called once for each document mock_save.assert_called_once() - + # Verify that the dataset was saved with the right subset saved_args = mock_save.call_args assert saved_args[1]["subset"] == "chunked" @@ -382,4 +382,4 @@ def load_dataset_side_effect(config, subset): # Verify behavior assert mock_load.call_count == 4 - mock_save.assert_called_once() \ No newline at end of file + mock_save.assert_called_once() From dc827a3b2d6fa854421f0b27ee56a782e17cca99 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Sat, 24 May 2025 16:23:44 -0500 Subject: [PATCH 39/69] remove unnecessary dependencies based on semantic deduplications --- pyproject.toml | 15 ------ uv.lock | 128 +++---------------------------------------------- 2 files changed, 6 insertions(+), 137 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6409f801..fbb31897 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,36 +12,21 @@ readme = "README.md" requires-python = ">=3.12, <3.13" dependencies = [ "asyncio>=3.4.3", - "black>=25.1.0", "click>=8.1.7", "datasets>=3.3.0", "hf-transfer>=0.1.9", "huggingface-hub[inference,hf_xet]>=0.30.2", "loguru>=0.7.3", "markitdown[all]>=0.0.2", - "matplotlib>=3.10.0", "python-dotenv>=1.0.1", "rich>=13.7.0", "ruff>=0.11.1", - "scikit-learn>=1.6.1", - "seaborn>=0.13.2", "thefuzz>=0.22.1", "tiktoken>=0.9.0", "tqdm>=4.67.1", "typer>=0.15.2", ] -[project.optional-dependencies] -semantic = [ - "torch>=2.6.0", - "transformers>=4.48.3", - "bert-score>=0.3.13", - "rouge-score>=0.1.2", -] -all = [ - "yourbench[semantic]", -] - [project.scripts] yourbench = "yourbench.main:main" diff --git a/uv.lock b/uv.lock index 34b10b24..15a1bce0 100644 --- a/uv.lock +++ b/uv.lock @@ -159,26 +159,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/8c/bc5457de4c004b1a623b31f7bc8d0375fb699b7d67df11879098b4b7b7c8/bert_score-0.3.13-py3-none-any.whl", hash = "sha256:bbbb4c7fcdaa46d7681aff49f37f96faa09ed74e1b150e659bdc6b58a66989b9", size = 61135, upload-time = "2023-02-20T21:07:27.226Z" }, ] -[[package]] -name = "black" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, - { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, - { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, - { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, -] - [[package]] name = "certifi" version = "2025.4.26" @@ -521,6 +501,9 @@ wheels = [ ] [package.optional-dependencies] +hf-xet = [ + { name = "hf-xet" }, +] inference = [ { name = "aiohttp" }, ] @@ -853,15 +836,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/d9/f7f9379981e39b8c2511c9e0326d212accacb82f12fbfdc1aa2ce2a7b2b6/multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3", size = 133351, upload-time = "2024-01-28T18:52:31.981Z" }, ] -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - [[package]] name = "networkx" version = "3.4.2" @@ -1107,15 +1081,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, ] -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, -] - [[package]] name = "pdfminer-six" version = "20250416" @@ -1148,15 +1113,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, ] -[[package]] -name = "platformdirs" -version = "4.3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, -] - [[package]] name = "propcache" version = "0.3.1" @@ -1467,59 +1423,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/e2/b011c38e5394c4c18fb5500778a55ec43ad6106126e74723ffaee246f56e/safetensors-0.5.3-cp38-abi3-win_amd64.whl", hash = "sha256:836cbbc320b47e80acd40e44c8682db0e8ad7123209f69b093def21ec7cafd11", size = 308878, upload-time = "2025-02-26T09:15:14.99Z" }, ] -[[package]] -name = "scikit-learn" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "joblib" }, - { name = "numpy" }, - { name = "scipy" }, - { name = "threadpoolctl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312, upload-time = "2025-01-10T08:07:55.348Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516, upload-time = "2025-01-10T08:06:40.009Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837, upload-time = "2025-01-10T08:06:43.305Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728, upload-time = "2025-01-10T08:06:47.618Z" }, - { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700, upload-time = "2025-01-10T08:06:50.888Z" }, - { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613, upload-time = "2025-01-10T08:06:54.115Z" }, -] - -[[package]] -name = "scipy" -version = "1.15.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/b9/31ba9cd990e626574baf93fbc1ac61cf9ed54faafd04c479117517661637/scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec", size = 59417316, upload-time = "2025-02-17T00:42:24.791Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/5d/3c78815cbab499610f26b5bae6aed33e227225a9fa5290008a733a64f6fc/scipy-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c4697a10da8f8765bb7c83e24a470da5797e37041edfd77fd95ba3811a47c4fd", size = 38756184, upload-time = "2025-02-17T00:31:50.623Z" }, - { url = "https://files.pythonhosted.org/packages/37/20/3d04eb066b471b6e171827548b9ddb3c21c6bbea72a4d84fc5989933910b/scipy-1.15.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:869269b767d5ee7ea6991ed7e22b3ca1f22de73ab9a49c44bad338b725603301", size = 30163558, upload-time = "2025-02-17T00:31:56.721Z" }, - { url = "https://files.pythonhosted.org/packages/a4/98/e5c964526c929ef1f795d4c343b2ff98634ad2051bd2bbadfef9e772e413/scipy-1.15.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bad78d580270a4d32470563ea86c6590b465cb98f83d760ff5b0990cb5518a93", size = 22437211, upload-time = "2025-02-17T00:32:03.042Z" }, - { url = "https://files.pythonhosted.org/packages/1d/cd/1dc7371e29195ecbf5222f9afeedb210e0a75057d8afbd942aa6cf8c8eca/scipy-1.15.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b09ae80010f52efddb15551025f9016c910296cf70adbf03ce2a8704f3a5ad20", size = 25232260, upload-time = "2025-02-17T00:32:07.847Z" }, - { url = "https://files.pythonhosted.org/packages/f0/24/1a181a9e5050090e0b5138c5f496fee33293c342b788d02586bc410c6477/scipy-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6fd6eac1ce74a9f77a7fc724080d507c5812d61e72bd5e4c489b042455865e", size = 35198095, upload-time = "2025-02-17T00:32:14.565Z" }, - { url = "https://files.pythonhosted.org/packages/c0/53/eaada1a414c026673eb983f8b4a55fe5eb172725d33d62c1b21f63ff6ca4/scipy-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b871df1fe1a3ba85d90e22742b93584f8d2b8e6124f8372ab15c71b73e428b8", size = 37297371, upload-time = "2025-02-17T00:32:21.411Z" }, - { url = "https://files.pythonhosted.org/packages/e9/06/0449b744892ed22b7e7b9a1994a866e64895363572677a316a9042af1fe5/scipy-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:03205d57a28e18dfd39f0377d5002725bf1f19a46f444108c29bdb246b6c8a11", size = 36872390, upload-time = "2025-02-17T00:32:29.421Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6f/a8ac3cfd9505ec695c1bc35edc034d13afbd2fc1882a7c6b473e280397bb/scipy-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:601881dfb761311045b03114c5fe718a12634e5608c3b403737ae463c9885d53", size = 39700276, upload-time = "2025-02-17T00:32:37.431Z" }, - { url = "https://files.pythonhosted.org/packages/f5/6f/e6e5aff77ea2a48dd96808bb51d7450875af154ee7cbe72188afb0b37929/scipy-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c68b6a43259ba0aab737237876e5c2c549a031ddb7abc28c7b47f22e202ded", size = 40942317, upload-time = "2025-02-17T00:32:45.47Z" }, -] - -[[package]] -name = "seaborn" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "matplotlib" }, - { name = "numpy" }, - { name = "pandas" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, -] - [[package]] name = "setuptools" version = "80.3.1" @@ -1592,15 +1495,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/4f/1695e70ceb3604f19eda9908e289c687ea81c4fecef4d90a9d1d0f2f7ae9/thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481", size = 8245, upload-time = "2024-01-19T19:18:20.362Z" }, ] -[[package]] -name = "threadpoolctl" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, -] - [[package]] name = "tiktoken" version = "0.9.0" @@ -1848,24 +1742,19 @@ wheels = [ [[package]] name = "yourbench" -version = "0.3.0" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "asyncio" }, - { name = "black" }, { name = "click" }, { name = "datasets" }, { name = "hf-transfer" }, - { name = "hf-xet" }, - { name = "huggingface-hub", extra = ["inference"] }, + { name = "huggingface-hub", extra = ["hf-xet", "inference"] }, { name = "loguru" }, { name = "markitdown", extra = ["all"] }, - { name = "matplotlib" }, { name = "python-dotenv" }, { name = "rich" }, { name = "ruff" }, - { name = "scikit-learn" }, - { name = "seaborn" }, { name = "thefuzz" }, { name = "tiktoken" }, { name = "tqdm" }, @@ -1890,21 +1779,16 @@ semantic = [ requires-dist = [ { name = "asyncio", specifier = ">=3.4.3" }, { name = "bert-score", marker = "extra == 'semantic'", specifier = ">=0.3.13" }, - { name = "black", specifier = ">=25.1.0" }, { name = "click", specifier = ">=8.1.7" }, { name = "datasets", specifier = ">=3.3.0" }, { name = "hf-transfer", specifier = ">=0.1.9" }, - { name = "hf-xet", specifier = ">=1.1.0" }, - { name = "huggingface-hub", extras = ["inference"], specifier = ">=0.30.2" }, + { name = "huggingface-hub", extras = ["inference", "hf-xet"], specifier = ">=0.30.2" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "markitdown", extras = ["all"], specifier = ">=0.0.2" }, - { name = "matplotlib", specifier = ">=3.10.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "rich", specifier = ">=13.7.0" }, { name = "rouge-score", marker = "extra == 'semantic'", specifier = ">=0.1.2" }, { name = "ruff", specifier = ">=0.11.1" }, - { name = "scikit-learn", specifier = ">=1.6.1" }, - { name = "seaborn", specifier = ">=0.13.2" }, { name = "thefuzz", specifier = ">=0.22.1" }, { name = "tiktoken", specifier = ">=0.9.0" }, { name = "torch", marker = "extra == 'semantic'", specifier = ">=2.6.0" }, From 3910a6fabd67acfe5a1c2e2b437d2ba52955d21f Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Mon, 26 May 2025 10:53:11 +0200 Subject: [PATCH 40/69] Pull summarization.py from main --- yourbench/pipeline/summarization.py | 240 ++++++++++++++++------------ 1 file changed, 142 insertions(+), 98 deletions(-) diff --git a/yourbench/pipeline/summarization.py b/yourbench/pipeline/summarization.py index 10694350..52884d78 100644 --- a/yourbench/pipeline/summarization.py +++ b/yourbench/pipeline/summarization.py @@ -1,12 +1,62 @@ -from typing import Any +# summarization.py +# ============================================================================= +# Author: @sumukshashidhar +# +# Module: Summarization Pipeline Stage +# ============================================================================= +""" +Summarization Stage +=================== + +This module handles the summarization stage of the YourBench pipeline. It takes +documents (with their raw text) and generates concise yet comprehensive summaries +for each document. + +Usage: +------ +1. Ensure the pipeline configuration has an entry for the `summarization` stage + with the desired settings. For example: + + summarization: + run: true + timeout_seconds: 300 + +2. When the pipeline runs, it loads the target dataset, calls the summarization + model(s) to produce summaries, logs intermediate steps, and saves the updated + dataset with new columns: + - raw_document_summary + - document_summary + - summarization_model + +Error Handling & Logging: +------------------------- +- All errors are logged using `loguru` to `logs/summarization.log`. +- The stage attempts to proceed with partial data even if some calls fail, never + abruptly terminating the pipeline. + +Important Notes: +---------------- +- This stage relies on the `run_inference` utility function from yourbench.utils.inference_engine + for concurrency, timeouts, and model management. +- Summaries are extracted from the model's output by parsing XML tags. +- If no valid summary is found, the pipeline substitutes a fallback string. + +See Also: +--------- +- yourbench.utils.inference_engine for concurrency logic +- yourbench.utils.dataset_engine for loading/saving dataset +""" + +from __future__ import annotations +from typing import Any, List, Tuple import tiktoken from loguru import logger from datasets import Dataset from yourbench.utils.prompts import ( - SUMMARIZATION_USER_PROMPT, COMBINE_SUMMARIES_USER_PROMPT, + CHUNK_SUMMARIZATION_USER_PROMPT, ) from yourbench.utils.chunking_utils import split_into_token_chunks from yourbench.utils.dataset_engine import custom_load_dataset, custom_save_dataset @@ -14,42 +64,47 @@ from yourbench.utils.inference_engine import InferenceCall, run_inference +############################ +# Internal helper functions # +############################ + + def _build_chunk_calls( dataset: Dataset, max_tokens: int, overlap: int, encoding_name: str, -) -> tuple[list[InferenceCall], list[tuple[int, int]]]: +) -> Tuple[List[InferenceCall], List[Tuple[int, int]]]: """Prepare inference calls for first-level chunk summaries. - Returns: - A tuple containing: - - A list of inference calls. - - A list of mappings, where each mapping is a tuple (doc_idx, chunk_idx) - aligning each call to its document and chunk index. chunk_idx is -1 for - documents treated as a single chunk. + Returns + ------- + (calls, mapping) where *mapping* aligns each call to (doc_idx, chunk_idx). """ - calls: list[InferenceCall] = [] - mapping: list[tuple[int, int]] = [] # (doc_index, chunk_index) + calls: List[InferenceCall] = [] + mapping: List[Tuple[int, int]] = [] # (doc_index, chunk_index) + # ─── NEW: robust encoding fetch with fallback ──────────────────────────── try: enc = tiktoken.get_encoding(encoding_name) - except Exception as e: - error_message = str(e) - truncated_error = error_message[:60] + ("…" if len(error_message) > 60 else "") + except Exception as e: # KeyError on unknown name, ValueError on bad cache logger.warning( - f"Unknown / unavailable encoding '{encoding_name}'. Falling back to 'cl100k_base' ({truncated_error})" + "Unknown / unavailable encoding '{}'. Falling back to 'cl100k_base' ({})", + encoding_name, + str(e)[:60] + ("…" if len(str(e)) > 60 else ""), ) enc = tiktoken.get_encoding("cl100k_base") + # ──────────────────────────────────────────────────────────────────────── for doc_idx, doc_text in enumerate(dataset["document_text"]): - token_len = len(enc.encode(doc_text, disallowed_special=())) + token_len = len(enc.encode(doc_text)) if token_len <= max_tokens: # treat as single chunk (chunk_idx = -1) - prompt = SUMMARIZATION_USER_PROMPT.format(document=doc_text) + prompt = CHUNK_SUMMARIZATION_USER_PROMPT.format(chunk=doc_text) calls.append(InferenceCall(messages=[{"role": "user", "content": prompt}], tags=["chunk_summary"])) mapping.append((doc_idx, -1)) continue + # Long doc ⇒ split & create a call per chunk chunks = split_into_token_chunks( doc_text, chunk_tokens=max_tokens, @@ -57,81 +112,82 @@ def _build_chunk_calls( encoding_name=encoding_name, ) for chunk_idx, chunk in enumerate(chunks): - prompt = SUMMARIZATION_USER_PROMPT.format(document=chunk) + prompt = CHUNK_SUMMARIZATION_USER_PROMPT.format(chunk=chunk) calls.append(InferenceCall(messages=[{"role": "user", "content": prompt}], tags=["chunk_summary"])) mapping.append((doc_idx, chunk_idx)) - logger.info(f"Prepared {len(calls)} chunk-level inference calls.") + logger.info("Prepared {} chunk-level inference calls.", len(calls)) return calls, mapping def _collect_chunk_summaries( - response_dict: dict[str, list[str]], - mapping: list[tuple[int, int]], + response_dict: dict[str, List[str]], + mapping: List[Tuple[int, int]], num_docs: int, -) -> tuple[str, list[list[str]], list[list[str]]]: - """Re-orders raw model responses back into per-document lists of summaries.""" +) -> Tuple[str, List[List[str]], List[List[str]]]: + """Re-orders raw model responses back into per-document lists. + + Notes + ----- + `model_name` is always `str` (never None) because we early-return if + `response_dict` is empty. + """ if not response_dict: return "", [], [] model_name = list(response_dict.keys())[0] responses = response_dict[model_name] + # Ensure response count matches call count if len(responses) != len(mapping): - logger.warning(f"Response count {len(responses)} ≠ mapping count {len(mapping)} – truncating/min-padding.") + logger.warning("Response count {} ≠ mapping count {} – truncating/min-padding.", len(responses), len(mapping)) + # pad / trim diff = len(mapping) - len(responses) if diff > 0: responses.extend([""] * diff) else: responses = responses[: len(mapping)] - raw_by_doc: list[list[str]] = [[] for _ in range(num_docs)] - cleaned_by_doc: list[list[str]] = [[] for _ in range(num_docs)] + # bucket by doc + raw_by_doc: List[List[str]] = [[] for _ in range(num_docs)] + cleaned_by_doc: List[List[str]] = [[] for _ in range(num_docs)] for resp, (doc_idx, _chunk_idx) in zip(responses, mapping): raw_by_doc[doc_idx].append(resp) - summary_content = extract_content_from_xml_tags(resp, "chunk_summary") or extract_content_from_xml_tags( + summary = extract_content_from_xml_tags(resp, "chunk_summary") or extract_content_from_xml_tags( resp, "final_summary" ) - cleaned_by_doc[doc_idx].append(summary_content.strip() if summary_content else "") + cleaned_by_doc[doc_idx].append(summary.strip() if summary else "") return model_name, raw_by_doc, cleaned_by_doc -def _build_combine_calls(summaries_by_doc: list[list[str]]) -> tuple[list[InferenceCall], list[int]]: - """Prepare second-stage calls to merge multiple chunk summaries into a single summary.""" - calls: list[InferenceCall] = [] - doc_indices_for_combine: list[int] = [] - skipped_doc_count = 0 +def _build_combine_calls(summaries_by_doc: List[List[str]]) -> Tuple[List[InferenceCall], List[int]]: + """Prepare second-stage calls that merge chunk summaries into one summary.""" + calls: List[InferenceCall] = [] + doc_indices: List[int] = [] + skipped = 0 # MOD: track how many docs are trivially short for doc_idx, chunk_summaries in enumerate(summaries_by_doc): - if len(chunk_summaries) <= 1: # Already a single summary (or empty), skip combine - skipped_doc_count += 1 + if len(chunk_summaries) <= 1: # already short ⇒ skip combine + skipped += 1 continue - - valid_summaries = [s for s in chunk_summaries if s] - if not valid_summaries: - skipped_doc_count += 1 - continue - - bullet_list = "\\n".join(f"- {s}" for s in valid_summaries) + bullet_list = "\n".join(f"- {s}" for s in chunk_summaries if s) prompt = COMBINE_SUMMARIES_USER_PROMPT.format(chunk_summaries=bullet_list) calls.append(InferenceCall(messages=[{"role": "user", "content": prompt}], tags=["merge_summary"])) - doc_indices_for_combine.append(doc_idx) + doc_indices.append(doc_idx) - logger.info( - f"Prepared {len(calls)} combine-stage inference calls ({skipped_doc_count} docs skipped – single/empty chunk list)." - ) - return calls, doc_indices_for_combine + logger.info("Prepared {} reducer calls ({} docs skipped – single / empty chunk).", len(calls), skipped) # NEW line + return calls, doc_indices def _merge_final_summaries( - current_final_summaries: list[str], - combined_responses: list[str], - doc_indices_to_update: list[int], -) -> list[str]: - """Integrates combined summaries into the list of final summaries.""" - updated_final_summaries = current_final_summaries.copy() + existing_singletons: List[str], + combine_responses: List[str], + doc_indices: List[int], +) -> List[str]: + """Blend reducer results with already-final single-chunk docs.""" + final_summaries = existing_singletons.copy() for resp, doc_idx in zip(combine_responses, doc_indices): parsed = extract_content_from_xml_tags(resp, "final_summary") @@ -145,65 +201,53 @@ def _merge_final_summaries( def run(config: dict[str, Any]) -> None: - """Executes the hierarchical summarization pipeline.""" stage_cfg = config.get("pipeline", {}).get("summarization", {}) if not stage_cfg.get("run", False): logger.info("Summarization stage disabled – skipping.") return - max_tokens: int = stage_cfg.get("max_tokens", 16384) - overlap: int = stage_cfg.get("token_overlap", 128) - encoding_name: str = stage_cfg.get("encoding_name", "cl100k_base") + max_tokens = stage_cfg.get("max_tokens", 16384) + overlap = stage_cfg.get("token_overlap", 128) + encoding_name = stage_cfg.get("encoding_name", "cl100k_base") logger.info("=== Summarization v2 – map-reduce ===") + # 1) Load dataset produced by ingestion dataset = custom_load_dataset(config=config, subset="ingested") - if not dataset or len(dataset) == 0: - logger.warning("Ingested dataset is empty or None – nothing to summarise.") + if len(dataset) == 0: + logger.warning("Ingested dataset empty – nothing to summarise.") return - logger.info(f"Loaded {len(dataset)} documents for summarisation.") + logger.info("Loaded {} documents for summarisation.", len(dataset)) + # 2) First pass – chunk summaries chunk_calls, call_map = _build_chunk_calls(dataset, max_tokens, overlap, encoding_name) - chunk_responses_dict = run_inference(config=config, step_name="summarization_chunk", inference_calls=chunk_calls) - model_name, raw_chunk_summaries_by_doc, cleaned_chunk_summaries_by_doc = _collect_chunk_summaries( - chunk_responses_dict, call_map, len(dataset) - ) + chunk_resp = run_inference(config=config, step_name="summarization_chunk", inference_calls=chunk_calls) + model_name, raw_chunk_by_doc, clean_chunk_by_doc = _collect_chunk_summaries(chunk_resp, call_map, len(dataset)) - combine_calls, doc_indices_for_combine = _build_combine_calls(cleaned_chunk_summaries_by_doc) - - raw_combined_summaries: list[str] = [] + # 3) Second pass – combine summaries where needed + combine_calls, doc_indices = _build_combine_calls(clean_chunk_by_doc) + combine_summaries_raw: List[str] = [] if combine_calls: - combine_responses_dict = run_inference( - config=config, step_name="summarization_combine", inference_calls=combine_calls - ) - if combine_responses_dict: - combine_model_name = list(combine_responses_dict.keys())[0] - if combine_model_name != model_name and model_name: - logger.warning(f"Different model used in combine stage: {combine_model_name} vs {model_name}") - raw_combined_summaries = combine_responses_dict.get(combine_model_name, []) - else: - raw_combined_summaries = [""] * len(doc_indices_for_combine) - - final_document_summaries: list[str] = [ - summaries[0] if summaries else "" for summaries in cleaned_chunk_summaries_by_doc - ] - - if combine_calls and raw_combined_summaries: - final_document_summaries = _merge_final_summaries( - final_document_summaries, raw_combined_summaries, doc_indices_for_combine - ) - - full_raw_combined_summaries = [""] * len(dataset) - for i, doc_idx in enumerate(doc_indices_for_combine): - if i < len(raw_combined_summaries): - full_raw_combined_summaries[doc_idx] = raw_combined_summaries[i] + combine_resp = run_inference(config=config, step_name="summarization_combine", inference_calls=combine_calls) + combine_model = list(combine_resp.keys())[0] if combine_resp else model_name + if combine_model != model_name: + logger.warning("Different model used in reducer stage: {} vs {}", combine_model, model_name) + combine_summaries_raw = combine_resp.get(combine_model, []) if combine_resp else [] + + # produce final list matching dataset order + # Start with single-chunk docs: take their sole summary + final_summaries = [docs[0] if docs else "" for docs in clean_chunk_by_doc] + if combine_calls: + final_summaries = _merge_final_summaries(final_summaries, combine_summaries_raw, doc_indices) - dataset = dataset.add_column("raw_chunk_summaries", raw_chunk_summaries_by_doc) - dataset = dataset.add_column("chunk_summaries", cleaned_chunk_summaries_by_doc) - dataset = dataset.add_column("raw_document_summary", full_raw_combined_summaries) - dataset = dataset.add_column("document_summary", final_document_summaries) - effective_model_name = model_name if model_name else "unknown" - dataset = dataset.add_column("summarization_model", [effective_model_name] * len(dataset)) + # 4) Add columns & persist + dataset = dataset.add_column("raw_chunk_summaries", raw_chunk_by_doc) + dataset = dataset.add_column("chunk_summaries", clean_chunk_by_doc) + dataset = dataset.add_column( + "raw_document_summary", combine_summaries_raw if combine_calls else [""] * len(dataset) + ) + dataset = dataset.add_column("document_summary", final_summaries) + dataset = dataset.add_column("summarization_model", [model_name] * len(dataset)) custom_save_dataset(dataset=dataset, config=config, subset="summarized") - logger.success(f"Hierarchical summarisation completed ({len(dataset)} documents).") + logger.success("Hierarchical summarisation completed ({} documents).", len(dataset)) From 3aaae2d1abccc7d89a6ef1daa8d893f37f48996b Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Tue, 27 May 2025 17:36:06 -0500 Subject: [PATCH 41/69] Update citation_score_filtering.py --- yourbench/pipeline/citation_score_filtering.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/yourbench/pipeline/citation_score_filtering.py b/yourbench/pipeline/citation_score_filtering.py index 2ebb8110..ee1c96a2 100644 --- a/yourbench/pipeline/citation_score_filtering.py +++ b/yourbench/pipeline/citation_score_filtering.py @@ -130,6 +130,13 @@ def run(config: Dict[str, Any]) -> None: all_final_scores.append(final_score) # 4) Add these new columns to the dataset + # First, remove columns if they already exist to prevent duplication errors + columns_to_add = ["answer_citation_score", "chunk_citation_score", "citation_score"] + existing_columns_to_remove = [col for col in columns_to_add if col in lighteval_ds.column_names] + if existing_columns_to_remove: + logger.info(f"Removing existing columns before adding new ones: {existing_columns_to_remove}") + lighteval_ds = lighteval_ds.remove_columns(existing_columns_to_remove) + lighteval_ds = lighteval_ds.add_column("answer_citation_score", all_answer_citation_scores) lighteval_ds = lighteval_ds.add_column("chunk_citation_score", all_chunk_citation_scores) lighteval_ds = lighteval_ds.add_column("citation_score", all_final_scores) From feb96ac1e34c9fefa545ccc1003f80dfabecb617 Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Tue, 27 May 2025 17:40:44 -0500 Subject: [PATCH 42/69] remove semantic chunking reference and add warning --- example/configs/advanced_example.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/configs/advanced_example.yaml b/example/configs/advanced_example.yaml index a79d89d6..ef44c6d6 100644 --- a/example/configs/advanced_example.yaml +++ b/example/configs/advanced_example.yaml @@ -180,7 +180,8 @@ pipeline: chunking: run: true chunking_configuration: - chunking_mode: fast_chunking # "fast_chunking" or "semantic_chunking" + # chunking_mode: fast_chunking # "fast_chunking" or "semantic_chunking" + # WARNING: SEMANTIC CHUNKING IS TEMPORARILY REMOVED AFTER v0.3.1. l_max_tokens: 128 # Each chunk’s maximum token length token_overlap: 0 # Overlap between chunks in token-based splitting encoding_name: cl100k_base # Tokenizer name for measuring token lengths From 0fbbab585f8b8d3176ea3c843593cd176c00a8d6 Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Tue, 27 May 2025 18:07:20 -0500 Subject: [PATCH 43/69] Update ingestion.py --- yourbench/pipeline/ingestion.py | 70 +++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/yourbench/pipeline/ingestion.py b/yourbench/pipeline/ingestion.py index 9e612065..80177b9d 100644 --- a/yourbench/pipeline/ingestion.py +++ b/yourbench/pipeline/ingestion.py @@ -44,6 +44,7 @@ from typing import Any, Optional from dataclasses import field, dataclass +import trafilatura from loguru import logger from markitdown import MarkItDown @@ -270,14 +271,47 @@ def _initialize_markdown_processor(config: dict[str, Any]) -> MarkItDown: return MarkItDown() +def _extract_markdown_from_html(file_path: str) -> str | None: + """Attempts to extract markdown content from an HTML file using Trafilatura.""" + logger.debug(f"Attempting to extract Markdown from HTML file: {file_path} using Trafilatura.") + try: + with open(file_path, "r", encoding="utf-8") as f: + html_content = f.read() + + # output_format='markdown' is key for direct Markdown conversion + extracted_markdown = trafilatura.extract( + html_content, + output_format="markdown", + include_comments=False, # Do not include HTML comments + include_tables=True, # Try to include table data + ) + + if extracted_markdown: + logger.info(f"Successfully extracted Markdown from '{file_path}' using Trafilatura.") + return extracted_markdown + + logger.warning(f"Trafilatura returned no content for HTML file '{file_path}'.") + return None + except ImportError: + logger.error( + "Trafilatura library is not installed. Please install it (e.g., `pip install trafilatura`) " + f"to process HTML files like '{file_path}' effectively. Skipping Trafilatura for this file." + ) + return None + except Exception as e: + logger.error(f"Error using Trafilatura for HTML file '{file_path}': {e}. Skipping Trafilatura for this file.") + return None + + def _convert_document_to_markdown(file_path: str, output_dir: str, markdown_processor: MarkItDown) -> None: """ - Convert a single source file into Markdown using MarkItDown and save the result. + Convert a single source file into Markdown and save the result. + Uses Trafilatura for HTML files to strip junk and extract main content as Markdown. Args: file_path (str): The path to the source document. output_dir (str): Directory where the converted .md file will be written. - markdown_processor (MarkItDown): Configured MarkItDown instance to handle the conversion. + markdown_processor (MarkItDown): Configured MarkItDown instance for non-HTML/non-MD conversions or fallback. Returns: None @@ -285,17 +319,37 @@ def _convert_document_to_markdown(file_path: str, output_dir: str, markdown_proc Logs: - Debug info about the file being processed. - Warning if conversion fails or the file is empty. + - Info/errors related to Trafilatura processing for HTML files. """ logger.debug("Converting file: {}", file_path) try: - # Skipping conversion if file already in markdown - if os.path.splitext(file_path)[1] == ".md": - with open(file_path, "r") as f: + file_ext = os.path.splitext(file_path)[1].lower() + content: str | None = None + + if file_ext == ".md": + # For existing Markdown files, just read the content, ensuring UTF-8 + with open(file_path, "r", encoding="utf-8") as f: content = f.read() - else: - # Perform the file-to-markdown conversion + logger.info(f"File '{file_path}' is already Markdown. Content read directly.") + + elif file_ext in [".html", ".htm"]: + logger.info(f"Processing HTML file: {file_path} with Trafilatura.") + content = _extract_markdown_from_html(file_path) + if content is None: # Fallback to MarkItDown if Trafilatura failed or returned nothing + logger.warning( + f"Trafilatura processing failed or yielded no content for HTML '{file_path}'. " + "Falling back to MarkItDown for this file." + ) + content = markdown_processor.convert(file_path).text_content + + else: # For other file types, use the MarkItDown processor + logger.info(f"Converting non-HTML/Markdown file '{file_path}' using MarkItDown.") content = markdown_processor.convert(file_path).text_content + if content is None: + logger.warning(f"No content could be generated for file '{file_path}' after processing. Skipping output.") + return + # Construct an output filename with .md extension base_name = os.path.basename(file_path) file_name_no_ext = os.path.splitext(base_name)[0] @@ -305,6 +359,6 @@ def _convert_document_to_markdown(file_path: str, output_dir: str, markdown_proc with open(output_file, "w", encoding="utf-8") as out_f: out_f.write(content) - logger.info(f"Successfully converted '{file_path}' -> '{output_file}'.") + logger.info(f"Successfully processed '{file_path}' and saved as '{output_file}'.") except Exception as exc: logger.error(f"Failed to convert '{file_path}'. Error details: {exc}") From 65a3cd5e6c712985596091907aa70bba2abfba07 Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Tue, 27 May 2025 18:09:16 -0500 Subject: [PATCH 44/69] Update pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6409f801..62e4b789 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "thefuzz>=0.22.1", "tiktoken>=0.9.0", "tqdm>=4.67.1", + "trafilatura>=2.0.0", "typer>=0.15.2", ] From 42cf7fe5963fb89fec2d0a644b005263ea66ba19 Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Wed, 28 May 2025 17:46:26 +0200 Subject: [PATCH 45/69] Restore summarization.py from main --- yourbench/pipeline/summarization.py | 241 ++++++++++++---------------- 1 file changed, 101 insertions(+), 140 deletions(-) diff --git a/yourbench/pipeline/summarization.py b/yourbench/pipeline/summarization.py index 52884d78..356ea7a0 100644 --- a/yourbench/pipeline/summarization.py +++ b/yourbench/pipeline/summarization.py @@ -1,62 +1,12 @@ -# summarization.py -# ============================================================================= -# Author: @sumukshashidhar -# -# Module: Summarization Pipeline Stage -# ============================================================================= -""" -Summarization Stage -=================== - -This module handles the summarization stage of the YourBench pipeline. It takes -documents (with their raw text) and generates concise yet comprehensive summaries -for each document. - -Usage: ------- -1. Ensure the pipeline configuration has an entry for the `summarization` stage - with the desired settings. For example: - - summarization: - run: true - timeout_seconds: 300 - -2. When the pipeline runs, it loads the target dataset, calls the summarization - model(s) to produce summaries, logs intermediate steps, and saves the updated - dataset with new columns: - - raw_document_summary - - document_summary - - summarization_model - -Error Handling & Logging: -------------------------- -- All errors are logged using `loguru` to `logs/summarization.log`. -- The stage attempts to proceed with partial data even if some calls fail, never - abruptly terminating the pipeline. - -Important Notes: ----------------- -- This stage relies on the `run_inference` utility function from yourbench.utils.inference_engine - for concurrency, timeouts, and model management. -- Summaries are extracted from the model's output by parsing XML tags. -- If no valid summary is found, the pipeline substitutes a fallback string. - -See Also: ---------- -- yourbench.utils.inference_engine for concurrency logic -- yourbench.utils.dataset_engine for loading/saving dataset -""" - -from __future__ import annotations -from typing import Any, List, Tuple +from typing import Any import tiktoken from loguru import logger from datasets import Dataset from yourbench.utils.prompts import ( + SUMMARIZATION_USER_PROMPT, COMBINE_SUMMARIES_USER_PROMPT, - CHUNK_SUMMARIZATION_USER_PROMPT, ) from yourbench.utils.chunking_utils import split_into_token_chunks from yourbench.utils.dataset_engine import custom_load_dataset, custom_save_dataset @@ -74,37 +24,37 @@ def _build_chunk_calls( max_tokens: int, overlap: int, encoding_name: str, -) -> Tuple[List[InferenceCall], List[Tuple[int, int]]]: +) -> tuple[list[InferenceCall], list[tuple[int, int]]]: """Prepare inference calls for first-level chunk summaries. - Returns - ------- - (calls, mapping) where *mapping* aligns each call to (doc_idx, chunk_idx). + Returns: + A tuple containing: + - A list of inference calls. + - A list of mappings, where each mapping is a tuple (doc_idx, chunk_idx) + aligning each call to its document and chunk index. chunk_idx is -1 for + documents treated as a single chunk. """ - calls: List[InferenceCall] = [] - mapping: List[Tuple[int, int]] = [] # (doc_index, chunk_index) + calls: list[InferenceCall] = [] + mapping: list[tuple[int, int]] = [] # (doc_index, chunk_index) - # ─── NEW: robust encoding fetch with fallback ──────────────────────────── try: enc = tiktoken.get_encoding(encoding_name) - except Exception as e: # KeyError on unknown name, ValueError on bad cache + except Exception as e: + error_message = str(e) + truncated_error = error_message[:60] + ("…" if len(error_message) > 60 else "") logger.warning( - "Unknown / unavailable encoding '{}'. Falling back to 'cl100k_base' ({})", - encoding_name, - str(e)[:60] + ("…" if len(str(e)) > 60 else ""), + f"Unknown / unavailable encoding '{encoding_name}'. Falling back to 'cl100k_base' ({truncated_error})" ) enc = tiktoken.get_encoding("cl100k_base") - # ──────────────────────────────────────────────────────────────────────── for doc_idx, doc_text in enumerate(dataset["document_text"]): token_len = len(enc.encode(doc_text)) if token_len <= max_tokens: # treat as single chunk (chunk_idx = -1) - prompt = CHUNK_SUMMARIZATION_USER_PROMPT.format(chunk=doc_text) + prompt = SUMMARIZATION_USER_PROMPT.format(document=doc_text) calls.append(InferenceCall(messages=[{"role": "user", "content": prompt}], tags=["chunk_summary"])) mapping.append((doc_idx, -1)) continue - # Long doc ⇒ split & create a call per chunk chunks = split_into_token_chunks( doc_text, chunk_tokens=max_tokens, @@ -112,87 +62,86 @@ def _build_chunk_calls( encoding_name=encoding_name, ) for chunk_idx, chunk in enumerate(chunks): - prompt = CHUNK_SUMMARIZATION_USER_PROMPT.format(chunk=chunk) + prompt = SUMMARIZATION_USER_PROMPT.format(document=chunk) calls.append(InferenceCall(messages=[{"role": "user", "content": prompt}], tags=["chunk_summary"])) mapping.append((doc_idx, chunk_idx)) - logger.info("Prepared {} chunk-level inference calls.", len(calls)) + logger.info(f"Prepared {len(calls)} chunk-level inference calls.") return calls, mapping def _collect_chunk_summaries( - response_dict: dict[str, List[str]], - mapping: List[Tuple[int, int]], + response_dict: dict[str, list[str]], + mapping: list[tuple[int, int]], num_docs: int, -) -> Tuple[str, List[List[str]], List[List[str]]]: - """Re-orders raw model responses back into per-document lists. - - Notes - ----- - `model_name` is always `str` (never None) because we early-return if - `response_dict` is empty. - """ +) -> tuple[str, list[list[str]], list[list[str]]]: + """Re-orders raw model responses back into per-document lists of summaries.""" if not response_dict: return "", [], [] model_name = list(response_dict.keys())[0] responses = response_dict[model_name] - # Ensure response count matches call count if len(responses) != len(mapping): - logger.warning("Response count {} ≠ mapping count {} – truncating/min-padding.", len(responses), len(mapping)) - # pad / trim + logger.warning(f"Response count {len(responses)} ≠ mapping count {len(mapping)} – truncating/min-padding.") diff = len(mapping) - len(responses) if diff > 0: responses.extend([""] * diff) else: responses = responses[: len(mapping)] - # bucket by doc - raw_by_doc: List[List[str]] = [[] for _ in range(num_docs)] - cleaned_by_doc: List[List[str]] = [[] for _ in range(num_docs)] + raw_by_doc: list[list[str]] = [[] for _ in range(num_docs)] + cleaned_by_doc: list[list[str]] = [[] for _ in range(num_docs)] for resp, (doc_idx, _chunk_idx) in zip(responses, mapping): raw_by_doc[doc_idx].append(resp) - summary = extract_content_from_xml_tags(resp, "chunk_summary") or extract_content_from_xml_tags( + summary_content = extract_content_from_xml_tags(resp, "chunk_summary") or extract_content_from_xml_tags( resp, "final_summary" ) - cleaned_by_doc[doc_idx].append(summary.strip() if summary else "") + cleaned_by_doc[doc_idx].append(summary_content.strip() if summary_content else "") return model_name, raw_by_doc, cleaned_by_doc -def _build_combine_calls(summaries_by_doc: List[List[str]]) -> Tuple[List[InferenceCall], List[int]]: - """Prepare second-stage calls that merge chunk summaries into one summary.""" - calls: List[InferenceCall] = [] - doc_indices: List[int] = [] - skipped = 0 # MOD: track how many docs are trivially short +def _build_combine_calls(summaries_by_doc: list[list[str]]) -> tuple[list[InferenceCall], list[int]]: + """Prepare second-stage calls to merge multiple chunk summaries into a single summary.""" + calls: list[InferenceCall] = [] + doc_indices_for_combine: list[int] = [] + skipped_doc_count = 0 for doc_idx, chunk_summaries in enumerate(summaries_by_doc): - if len(chunk_summaries) <= 1: # already short ⇒ skip combine - skipped += 1 + if len(chunk_summaries) <= 1: # Already a single summary (or empty), skip combine + skipped_doc_count += 1 + continue + + valid_summaries = [s for s in chunk_summaries if s] + if not valid_summaries: + skipped_doc_count += 1 continue - bullet_list = "\n".join(f"- {s}" for s in chunk_summaries if s) + + bullet_list = "\\n".join(f"- {s}" for s in valid_summaries) prompt = COMBINE_SUMMARIES_USER_PROMPT.format(chunk_summaries=bullet_list) calls.append(InferenceCall(messages=[{"role": "user", "content": prompt}], tags=["merge_summary"])) - doc_indices.append(doc_idx) + doc_indices_for_combine.append(doc_idx) - logger.info("Prepared {} reducer calls ({} docs skipped – single / empty chunk).", len(calls), skipped) # NEW line - return calls, doc_indices + logger.info( + f"Prepared {len(calls)} combine-stage inference calls ({skipped_doc_count} docs skipped – single/empty chunk list)." + ) + return calls, doc_indices_for_combine def _merge_final_summaries( - existing_singletons: List[str], - combine_responses: List[str], - doc_indices: List[int], -) -> List[str]: - """Blend reducer results with already-final single-chunk docs.""" - final_summaries = existing_singletons.copy() + current_final_summaries: list[str], + combined_responses: list[str], + doc_indices_to_update: list[int], +) -> list[str]: + """Integrates combined summaries into the list of final summaries.""" + updated_final_summaries = current_final_summaries.copy() - for resp, doc_idx in zip(combine_responses, doc_indices): - parsed = extract_content_from_xml_tags(resp, "final_summary") - final_summaries[doc_idx] = parsed.strip() if parsed else "No summary available." - return final_summaries + for resp, doc_idx in zip(combined_responses, doc_indices_to_update): + parsed_summary = extract_content_from_xml_tags(resp, "final_summary") + updated_final_summaries[doc_idx] = parsed_summary.strip() if parsed_summary else "No summary available." + return updated_final_summaries ################# @@ -201,53 +150,65 @@ def _merge_final_summaries( def run(config: dict[str, Any]) -> None: + """Executes the hierarchical summarization pipeline.""" stage_cfg = config.get("pipeline", {}).get("summarization", {}) if not stage_cfg.get("run", False): logger.info("Summarization stage disabled – skipping.") return - max_tokens = stage_cfg.get("max_tokens", 16384) - overlap = stage_cfg.get("token_overlap", 128) - encoding_name = stage_cfg.get("encoding_name", "cl100k_base") + max_tokens: int = stage_cfg.get("max_tokens", 16384) + overlap: int = stage_cfg.get("token_overlap", 128) + encoding_name: str = stage_cfg.get("encoding_name", "cl100k_base") logger.info("=== Summarization v2 – map-reduce ===") - # 1) Load dataset produced by ingestion dataset = custom_load_dataset(config=config, subset="ingested") - if len(dataset) == 0: - logger.warning("Ingested dataset empty – nothing to summarise.") + if not dataset or len(dataset) == 0: + logger.warning("Ingested dataset is empty or None – nothing to summarise.") return - logger.info("Loaded {} documents for summarisation.", len(dataset)) + logger.info(f"Loaded {len(dataset)} documents for summarisation.") - # 2) First pass – chunk summaries chunk_calls, call_map = _build_chunk_calls(dataset, max_tokens, overlap, encoding_name) - chunk_resp = run_inference(config=config, step_name="summarization_chunk", inference_calls=chunk_calls) - model_name, raw_chunk_by_doc, clean_chunk_by_doc = _collect_chunk_summaries(chunk_resp, call_map, len(dataset)) + chunk_responses_dict = run_inference(config=config, step_name="summarization_chunk", inference_calls=chunk_calls) + model_name, raw_chunk_summaries_by_doc, cleaned_chunk_summaries_by_doc = _collect_chunk_summaries( + chunk_responses_dict, call_map, len(dataset) + ) - # 3) Second pass – combine summaries where needed - combine_calls, doc_indices = _build_combine_calls(clean_chunk_by_doc) - combine_summaries_raw: List[str] = [] - if combine_calls: - combine_resp = run_inference(config=config, step_name="summarization_combine", inference_calls=combine_calls) - combine_model = list(combine_resp.keys())[0] if combine_resp else model_name - if combine_model != model_name: - logger.warning("Different model used in reducer stage: {} vs {}", combine_model, model_name) - combine_summaries_raw = combine_resp.get(combine_model, []) if combine_resp else [] - - # produce final list matching dataset order - # Start with single-chunk docs: take their sole summary - final_summaries = [docs[0] if docs else "" for docs in clean_chunk_by_doc] + combine_calls, doc_indices_for_combine = _build_combine_calls(cleaned_chunk_summaries_by_doc) + + raw_combined_summaries: list[str] = [] if combine_calls: - final_summaries = _merge_final_summaries(final_summaries, combine_summaries_raw, doc_indices) + combine_responses_dict = run_inference( + config=config, step_name="summarization_combine", inference_calls=combine_calls + ) + if combine_responses_dict: + combine_model_name = list(combine_responses_dict.keys())[0] + if combine_model_name != model_name and model_name: + logger.warning(f"Different model used in combine stage: {combine_model_name} vs {model_name}") + raw_combined_summaries = combine_responses_dict.get(combine_model_name, []) + else: + raw_combined_summaries = [""] * len(doc_indices_for_combine) - # 4) Add columns & persist - dataset = dataset.add_column("raw_chunk_summaries", raw_chunk_by_doc) - dataset = dataset.add_column("chunk_summaries", clean_chunk_by_doc) - dataset = dataset.add_column( - "raw_document_summary", combine_summaries_raw if combine_calls else [""] * len(dataset) - ) - dataset = dataset.add_column("document_summary", final_summaries) - dataset = dataset.add_column("summarization_model", [model_name] * len(dataset)) + final_document_summaries: list[str] = [ + summaries[0] if summaries else "" for summaries in cleaned_chunk_summaries_by_doc + ] + + if combine_calls and raw_combined_summaries: + final_document_summaries = _merge_final_summaries( + final_document_summaries, raw_combined_summaries, doc_indices_for_combine + ) + + full_raw_combined_summaries = [""] * len(dataset) + for i, doc_idx in enumerate(doc_indices_for_combine): + if i < len(raw_combined_summaries): + full_raw_combined_summaries[doc_idx] = raw_combined_summaries[i] + + dataset = dataset.add_column("raw_chunk_summaries", raw_chunk_summaries_by_doc) + dataset = dataset.add_column("chunk_summaries", cleaned_chunk_summaries_by_doc) + dataset = dataset.add_column("raw_document_summary", full_raw_combined_summaries) + dataset = dataset.add_column("document_summary", final_document_summaries) + effective_model_name = model_name if model_name else "unknown" + dataset = dataset.add_column("summarization_model", [effective_model_name] * len(dataset)) custom_save_dataset(dataset=dataset, config=config, subset="summarized") - logger.success("Hierarchical summarisation completed ({} documents).", len(dataset)) + logger.success(f"Hierarchical summarisation completed ({len(dataset)} documents).") From 4057d64cd84ca5e6e1ddedac9df405cce6f91e6a Mon Sep 17 00:00:00 2001 From: m-peko Date: Wed, 28 May 2025 18:38:33 +0200 Subject: [PATCH 46/69] Introduce BENCHMARK_SYSTEM_PROMPT environment variable --- .env.template | 1 + Dockerfile | 2 ++ run_docker.sh | 3 +++ run_yourbench.py | 6 ++---- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.env.template b/.env.template index 7d3af1dc..9df3c4ae 100644 --- a/.env.template +++ b/.env.template @@ -1,6 +1,7 @@ OPENROUTER_API_KEY= BENCHMARK_NAME="test" +BENCHMARK_SYSTEM_PROMPT="test prompt" INPUT_S3_BUCKET="layerlens-private-test-organization" INPUT_S3_KEY="benchmarks/test-project/benchmark-name/data.zip" OUTPUT_S3_BUCKET="layerlens-private-test-organization" diff --git a/Dockerfile b/Dockerfile index 0a728339..a0e557f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,8 @@ RUN pip install --upgrade pip && \ RUN yourbench --version || echo "Yourbench installation verification failed but continuing build" # Environment variables (will be overridden at runtime) +ENV BENCHMARK_NAME="" +ENV BENCHMARK_SYSTEM_PROMPT="" ENV INPUT_S3_BUCKET="" ENV INPUT_S3_KEY="" ENV OUTPUT_S3_BUCKET="" diff --git a/run_docker.sh b/run_docker.sh index 198052ce..e2e4306f 100755 --- a/run_docker.sh +++ b/run_docker.sh @@ -19,6 +19,7 @@ if [ -z "$INPUT_S3_BUCKET" ] || [ -z "$INPUT_S3_KEY" ] || [ -z "$OUTPUT_S3_BUCKE echo "Error: Required environment variables are not set." echo "Please set these variables before running:" echo " - BENCHMARK_NAME: benchmark name" + echo " - BENCHMARK_SYSTEM_PROMPT: benchmark system prompt" echo " - INPUT_S3_BUCKET: S3 bucket containing input data" echo " - INPUT_S3_KEY: S3 key for input data zip file" echo " - OUTPUT_S3_BUCKET: S3 bucket for output data" @@ -27,6 +28,7 @@ if [ -z "$INPUT_S3_BUCKET" ] || [ -z "$INPUT_S3_KEY" ] || [ -z "$OUTPUT_S3_BUCKE echo "" echo "Example:" echo " export BENCHMARK_NAME=benchmark-name" + echo " export BENCHMARK_SYSTEM_PROMPT=benchmark-system-prompt" echo " export INPUT_S3_BUCKET=my-input-bucket" echo " export INPUT_S3_KEY=input/data.zip" echo " export OUTPUT_S3_BUCKET=my-output-bucket" @@ -39,6 +41,7 @@ fi echo "Running yourbench processor Docker container..." docker run --rm \ -e BENCHMARK_NAME="$BENCHMARK_NAME" \ + -e BENCHMARK_SYSTEM_PROMPT="$BENCHMARK_SYSTEM_PROMPT" \ -e INPUT_S3_BUCKET="$INPUT_S3_BUCKET" \ -e INPUT_S3_KEY="$INPUT_S3_KEY" \ -e OUTPUT_S3_BUCKET="$OUTPUT_S3_BUCKET" \ diff --git a/run_yourbench.py b/run_yourbench.py index 192ecd16..600245fc 100644 --- a/run_yourbench.py +++ b/run_yourbench.py @@ -84,6 +84,7 @@ def upload_directory_to_s3(directory_path, bucket_name, s3_prefix=""): def main(): # Get environment variables benchmark_name = os.environ.get("BENCHMARK_NAME") + benchmark_system_prompt = os.environ.get("BENCHMARK_SYSTEM_PROMPT") input_bucket = os.environ.get("INPUT_S3_BUCKET") input_key = os.environ.get("INPUT_S3_KEY") output_bucket = os.environ.get("OUTPUT_S3_BUCKET") @@ -153,10 +154,7 @@ def main(): convert_dataset( hf_path=str(lighteval_path), name=benchmark_name, - system_prompt=( - "You are an expert answering benchmark questions. " - "Give a very concise answer." - ), + system_prompt=benchmark_system_prompt, full_description="Dataset for evaluating built-in knowledge", short_description="Fact-based knowledge", category="YourBench", From 951d257bb48e14d937280a1e1d7fb6803524e1e1 Mon Sep 17 00:00:00 2001 From: Robert Leonard Date: Sat, 31 May 2025 23:35:22 -0400 Subject: [PATCH 47/69] use latest gemini flash model --- yourbench/utils/convert_to_atlas_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yourbench/utils/convert_to_atlas_module.py b/yourbench/utils/convert_to_atlas_module.py index a57744b7..98bfdc91 100644 --- a/yourbench/utils/convert_to_atlas_module.py +++ b/yourbench/utils/convert_to_atlas_module.py @@ -13,7 +13,7 @@ def _scorer_yaml(name: str) -> str: type: llm_judge options: regex_pattern: '' - judge_model: google/gemini-flash-1.5-8b + judge_model: google/gemini-2.5-flash-preview-05-20 judge_prompt: |- Your job is to look at a question, a gold target, and a predicted answer, and then assign a grade of either ["CORRECT", "INCORRECT", "NOT_ATTEMPTED"]. First, I will give examples of each grade, and then you will grade a new example. From c8415066246ddf259423be2e2d73ab70f438505d Mon Sep 17 00:00:00 2001 From: Robert Leonard Date: Sat, 31 May 2025 23:50:24 -0400 Subject: [PATCH 48/69] use latest gemini flash model --- yourbench/utils/convert_to_atlas_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yourbench/utils/convert_to_atlas_module.py b/yourbench/utils/convert_to_atlas_module.py index 98bfdc91..ba237a49 100644 --- a/yourbench/utils/convert_to_atlas_module.py +++ b/yourbench/utils/convert_to_atlas_module.py @@ -13,7 +13,7 @@ def _scorer_yaml(name: str) -> str: type: llm_judge options: regex_pattern: '' - judge_model: google/gemini-2.5-flash-preview-05-20 + judge_model: gemini-2.5-flash-preview-05-20 judge_prompt: |- Your job is to look at a question, a gold target, and a predicted answer, and then assign a grade of either ["CORRECT", "INCORRECT", "NOT_ATTEMPTED"]. First, I will give examples of each grade, and then you will grade a new example. From 68cdb5a372bc56476a1ae632b3b7ba42e18bba29 Mon Sep 17 00:00:00 2001 From: Robert Leonard Date: Sun, 1 Jun 2025 01:11:47 -0400 Subject: [PATCH 49/69] use correct model format for private evaluation --- yourbench/utils/convert_to_atlas_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yourbench/utils/convert_to_atlas_module.py b/yourbench/utils/convert_to_atlas_module.py index ba237a49..8ee4be79 100644 --- a/yourbench/utils/convert_to_atlas_module.py +++ b/yourbench/utils/convert_to_atlas_module.py @@ -13,7 +13,7 @@ def _scorer_yaml(name: str) -> str: type: llm_judge options: regex_pattern: '' - judge_model: gemini-2.5-flash-preview-05-20 + judge_model: google_gemini-2.5-pro-preview-03-25 judge_prompt: |- Your job is to look at a question, a gold target, and a predicted answer, and then assign a grade of either ["CORRECT", "INCORRECT", "NOT_ATTEMPTED"]. First, I will give examples of each grade, and then you will grade a new example. From 52efc599d2a9619cd73a6b081d7dd800b04bc787 Mon Sep 17 00:00:00 2001 From: Robert Leonard Date: Sun, 1 Jun 2025 01:30:47 -0400 Subject: [PATCH 50/69] use gemini flash for llm as a judge --- yourbench/utils/convert_to_atlas_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yourbench/utils/convert_to_atlas_module.py b/yourbench/utils/convert_to_atlas_module.py index 8ee4be79..c32bda4a 100644 --- a/yourbench/utils/convert_to_atlas_module.py +++ b/yourbench/utils/convert_to_atlas_module.py @@ -13,7 +13,7 @@ def _scorer_yaml(name: str) -> str: type: llm_judge options: regex_pattern: '' - judge_model: google_gemini-2.5-pro-preview-03-25 + judge_model: google_gemini-2.5-flash-preview-05-20 judge_prompt: |- Your job is to look at a question, a gold target, and a predicted answer, and then assign a grade of either ["CORRECT", "INCORRECT", "NOT_ATTEMPTED"]. First, I will give examples of each grade, and then you will grade a new example. From 2c4de989e4b7b7c60af9577c9062344d3d4496ca Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Tue, 3 Jun 2025 15:20:59 +0200 Subject: [PATCH 51/69] Ensure summarization uses correct model by aligning step name --- yourbench/pipeline/summarization.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/yourbench/pipeline/summarization.py b/yourbench/pipeline/summarization.py index 356ea7a0..fb191938 100644 --- a/yourbench/pipeline/summarization.py +++ b/yourbench/pipeline/summarization.py @@ -166,10 +166,14 @@ def run(config: dict[str, Any]) -> None: if not dataset or len(dataset) == 0: logger.warning("Ingested dataset is empty or None – nothing to summarise.") return - logger.info(f"Loaded {len(dataset)} documents for summarisation.") + logger.info(f"Loaded {len(dataset)} documents for summarization.") chunk_calls, call_map = _build_chunk_calls(dataset, max_tokens, overlap, encoding_name) - chunk_responses_dict = run_inference(config=config, step_name="summarization_chunk", inference_calls=chunk_calls) + chunk_responses_dict = run_inference( + config=config, + step_name="summarization", + inference_calls=chunk_calls + ) model_name, raw_chunk_summaries_by_doc, cleaned_chunk_summaries_by_doc = _collect_chunk_summaries( chunk_responses_dict, call_map, len(dataset) ) @@ -179,7 +183,9 @@ def run(config: dict[str, Any]) -> None: raw_combined_summaries: list[str] = [] if combine_calls: combine_responses_dict = run_inference( - config=config, step_name="summarization_combine", inference_calls=combine_calls + config=config, + step_name="summarization", + inference_calls=combine_calls ) if combine_responses_dict: combine_model_name = list(combine_responses_dict.keys())[0] @@ -211,4 +217,4 @@ def run(config: dict[str, Any]) -> None: dataset = dataset.add_column("summarization_model", [effective_model_name] * len(dataset)) custom_save_dataset(dataset=dataset, config=config, subset="summarized") - logger.success(f"Hierarchical summarisation completed ({len(dataset)} documents).") + logger.success(f"Hierarchical summarization completed ({len(dataset)} documents).") From 162ac00579fb6047ff8bc0e6088228af36296010 Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Tue, 3 Jun 2025 15:27:18 +0200 Subject: [PATCH 52/69] Apply Ruff --- yourbench/pipeline/summarization.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/yourbench/pipeline/summarization.py b/yourbench/pipeline/summarization.py index fb191938..750289a7 100644 --- a/yourbench/pipeline/summarization.py +++ b/yourbench/pipeline/summarization.py @@ -169,11 +169,7 @@ def run(config: dict[str, Any]) -> None: logger.info(f"Loaded {len(dataset)} documents for summarization.") chunk_calls, call_map = _build_chunk_calls(dataset, max_tokens, overlap, encoding_name) - chunk_responses_dict = run_inference( - config=config, - step_name="summarization", - inference_calls=chunk_calls - ) + chunk_responses_dict = run_inference(config=config, step_name="summarization", inference_calls=chunk_calls) model_name, raw_chunk_summaries_by_doc, cleaned_chunk_summaries_by_doc = _collect_chunk_summaries( chunk_responses_dict, call_map, len(dataset) ) @@ -182,11 +178,7 @@ def run(config: dict[str, Any]) -> None: raw_combined_summaries: list[str] = [] if combine_calls: - combine_responses_dict = run_inference( - config=config, - step_name="summarization", - inference_calls=combine_calls - ) + combine_responses_dict = run_inference(config=config, step_name="summarization", inference_calls=combine_calls) if combine_responses_dict: combine_model_name = list(combine_responses_dict.keys())[0] if combine_model_name != model_name and model_name: From da48707c66f8d53e784d1e0ce4177de6fd022968 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 4 Jun 2025 05:53:27 -0500 Subject: [PATCH 53/69] remove import error and refactor block --- uv.lock | 135 ++++++++++++++++++++++++++++++-- yourbench/pipeline/ingestion.py | 73 +++++++++-------- 2 files changed, 172 insertions(+), 36 deletions(-) diff --git a/uv.lock b/uv.lock index 34b10b24..4d1d507a 100644 --- a/uv.lock +++ b/uv.lock @@ -127,6 +127,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/9f/1f9f3ef4f49729ee207a712a5971a9ca747f2ca47d9cbf13cf6953e3478a/azure_identity-1.21.0-py3-none-any.whl", hash = "sha256:258ea6325537352440f71b35c3dffe9d240eae4a5126c1b7ce5efd5766bd9fd9", size = 189190, upload-time = "2025-03-11T20:53:09.197Z" }, ] +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + [[package]] name = "beautifulsoup4" version = "4.13.4" @@ -295,6 +304,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, ] +[[package]] +name = "courlan" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "tld" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/54/6d6ceeff4bed42e7a10d6064d35ee43a810e7b3e8beb4abeae8cff4713ae/courlan-1.3.2.tar.gz", hash = "sha256:0b66f4db3a9c39a6e22dd247c72cfaa57d68ea660e94bb2c84ec7db8712af190", size = 206382, upload-time = "2024-10-29T16:40:20.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/ca/6a667ccbe649856dcd3458bab80b016681b274399d6211187c6ab969fc50/courlan-1.3.2-py3-none-any.whl", hash = "sha256:d0dab52cf5b5b1000ee2839fbc2837e93b2514d3cb5bb61ae158a55b7a04c6be", size = 33848, upload-time = "2024-10-29T16:40:18.325Z" }, +] + [[package]] name = "cryptography" version = "44.0.3" @@ -364,6 +387,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/f5/668b3444a2f487b0052b908af631fe39eeb2bdb2359d9bbc2c3b80b71119/datasets-3.5.1-py3-none-any.whl", hash = "sha256:4074dda8dd6e9ece242b1580a8ef3928777d59ae1db144d911229e443a093cbb", size = 491436, upload-time = "2025-04-28T14:01:40.953Z" }, ] +[[package]] +name = "dateparser" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "regex" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/3f/d3207a05f5b6a78c66d86631e60bfba5af163738a599a5b9aa2c2737a09e/dateparser-1.2.1.tar.gz", hash = "sha256:7e4919aeb48481dbfc01ac9683c8e20bfe95bb715a38c1e9f6af889f4f30ccc3", size = 309924, upload-time = "2025-02-05T12:34:55.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/0a/981c438c4cd84147c781e4e96c1d72df03775deb1bc76c5a6ee8afa89c62/dateparser-1.2.1-py3-none-any.whl", hash = "sha256:bdcac262a467e6260030040748ad7c10d6bacd4f3b9cdb4cfd2251939174508c", size = 295658, upload-time = "2025-02-05T12:34:53.1Z" }, +] + [[package]] name = "defusedxml" version = "0.7.1" @@ -502,6 +540,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/53/d6/cb32842cbf1cf5a154b41fa918a2fd86003af9bca227a2397cd7f312a8a6/hf_xet-1.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:73153eab9abf3d6973b21e94a67ccba5d595c3e12feb8c0bf50be02964e7f126", size = 4204376, upload-time = "2025-04-29T21:15:52.69Z" }, ] +[[package]] +name = "htmldate" +version = "1.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "dateparser" }, + { name = "lxml" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/26/aaae4cab984f0b7dd0f5f1b823fa2ed2fd4a2bb50acd5bd2f0d217562678/htmldate-1.9.3.tar.gz", hash = "sha256:ac0caf4628c3ded4042011e2d60dc68dfb314c77b106587dd307a80d77e708e9", size = 44913, upload-time = "2024-12-30T12:52:35.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/49/8872130016209c20436ce0c1067de1cf630755d0443d068a5bc17fa95015/htmldate-1.9.3-py3-none-any.whl", hash = "sha256:3fadc422cf3c10a5cdb5e1b914daf37ec7270400a80a1b37e2673ff84faaaff8", size = 31565, upload-time = "2024-12-30T12:52:32.145Z" }, +] + [[package]] name = "huggingface-hub" version = "0.30.2" @@ -521,6 +575,9 @@ wheels = [ ] [package.optional-dependencies] +hf-xet = [ + { name = "hf-xet" }, +] inference = [ { name = "aiohttp" }, ] @@ -576,6 +633,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/d3/13ee227a148af1c693654932b8b0b02ed64af5e1f7406d56b088b57574cd/joblib-1.5.0-py3-none-any.whl", hash = "sha256:206144b320246485b712fc8cc51f017de58225fa8b414a1fe1764a7231aca491", size = 307682, upload-time = "2025-05-03T21:09:37.892Z" }, ] +[[package]] +name = "justext" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml", extra = ["html-clean"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/f3/45890c1b314f0d04e19c1c83d534e611513150939a7cf039664d9ab1e649/justext-3.0.2.tar.gz", hash = "sha256:13496a450c44c4cd5b5a75a5efcd9996066d2a189794ea99a49949685a0beb05", size = 828521, upload-time = "2025-02-25T20:21:49.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/ac/52f4e86d1924a7fc05af3aeb34488570eccc39b4af90530dd6acecdf16b5/justext-3.0.2-py2.py3-none-any.whl", hash = "sha256:62b1c562b15c3c6265e121cc070874243a443bfd53060e869393f09d6b6cc9a7", size = 837940, upload-time = "2025-02-25T20:21:44.179Z" }, +] + [[package]] name = "kiwisolver" version = "1.4.8" @@ -637,6 +706,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" }, ] +[package.optional-dependencies] +html-clean = [ + { name = "lxml-html-clean" }, +] + +[[package]] +name = "lxml-html-clean" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/b6/466e71db127950fb8d172026a8f0a9f0dc6f64c8e78e2ca79f252e5790b8/lxml_html_clean-0.4.2.tar.gz", hash = "sha256:91291e7b5db95430abf461bc53440964d58e06cc468950f9e47db64976cebcb3", size = 21622, upload-time = "2025-04-09T11:33:59.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/0b/942cb7278d6caad79343ad2ddd636ed204a47909b969d19114a3097f5aa3/lxml_html_clean-0.4.2-py3-none-any.whl", hash = "sha256:74ccfba277adcfea87a1e9294f47dd86b05d65b4da7c5b07966e3d5f3be8a505", size = 14184, upload-time = "2025-04-09T11:33:57.988Z" }, +] + [[package]] name = "magika" version = "0.6.2" @@ -1619,6 +1705,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897, upload-time = "2025-02-14T06:02:36.265Z" }, ] +[[package]] +name = "tld" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/a1/5723b07a70c1841a80afc9ac572fdf53488306848d844cd70519391b0d26/tld-0.13.1.tar.gz", hash = "sha256:75ec00936cbcf564f67361c41713363440b6c4ef0f0c1592b5b0fbe72c17a350", size = 462000, upload-time = "2025-05-21T22:18:29.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/70/b2f38360c3fc4bc9b5e8ef429e1fde63749144ac583c2dbdf7e21e27a9ad/tld-0.13.1-py2.py3-none-any.whl", hash = "sha256:a2d35109433ac83486ddf87e3c4539ab2c5c2478230e5d9c060a18af4b03aa7c", size = 274718, upload-time = "2025-05-21T22:18:25.811Z" }, +] + [[package]] name = "tokenizers" version = "0.21.1" @@ -1691,6 +1786,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "trafilatura" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "courlan" }, + { name = "htmldate" }, + { name = "justext" }, + { name = "lxml" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/25/e3ebeefdebfdfae8c4a4396f5a6ea51fc6fa0831d63ce338e5090a8003dc/trafilatura-2.0.0.tar.gz", hash = "sha256:ceb7094a6ecc97e72fea73c7dba36714c5c5b577b6470e4520dca893706d6247", size = 253404, upload-time = "2024-12-03T15:23:24.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/b6/097367f180b6383a3581ca1b86fcae284e52075fa941d1232df35293363c/trafilatura-2.0.0-py3-none-any.whl", hash = "sha256:77eb5d1e993747f6f20938e1de2d840020719735690c840b9a1024803a4cd51d", size = 132557, upload-time = "2024-12-03T15:23:21.41Z" }, +] + [[package]] name = "transformers" version = "4.51.3" @@ -1756,6 +1869,18 @@ wheels = [ { 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" }, ] +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + [[package]] name = "urllib3" version = "2.4.0" @@ -1848,7 +1973,7 @@ wheels = [ [[package]] name = "yourbench" -version = "0.3.0" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "asyncio" }, @@ -1856,8 +1981,7 @@ dependencies = [ { name = "click" }, { name = "datasets" }, { name = "hf-transfer" }, - { name = "hf-xet" }, - { name = "huggingface-hub", extra = ["inference"] }, + { name = "huggingface-hub", extra = ["hf-xet", "inference"] }, { name = "loguru" }, { name = "markitdown", extra = ["all"] }, { name = "matplotlib" }, @@ -1869,6 +1993,7 @@ dependencies = [ { name = "thefuzz" }, { name = "tiktoken" }, { name = "tqdm" }, + { name = "trafilatura" }, { name = "typer" }, ] @@ -1894,8 +2019,7 @@ requires-dist = [ { name = "click", specifier = ">=8.1.7" }, { name = "datasets", specifier = ">=3.3.0" }, { name = "hf-transfer", specifier = ">=0.1.9" }, - { name = "hf-xet", specifier = ">=1.1.0" }, - { name = "huggingface-hub", extras = ["inference"], specifier = ">=0.30.2" }, + { name = "huggingface-hub", extras = ["inference", "hf-xet"], specifier = ">=0.30.2" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "markitdown", extras = ["all"], specifier = ">=0.0.2" }, { name = "matplotlib", specifier = ">=3.10.0" }, @@ -1909,6 +2033,7 @@ requires-dist = [ { name = "tiktoken", specifier = ">=0.9.0" }, { name = "torch", marker = "extra == 'semantic'", specifier = ">=2.6.0" }, { name = "tqdm", specifier = ">=4.67.1" }, + { name = "trafilatura", specifier = ">=2.0.0" }, { name = "transformers", marker = "extra == 'semantic'", specifier = ">=4.48.3" }, { name = "typer", specifier = ">=0.15.2" }, { name = "yourbench", extras = ["semantic"], marker = "extra == 'all'" }, diff --git a/yourbench/pipeline/ingestion.py b/yourbench/pipeline/ingestion.py index 80177b9d..c24f62c0 100644 --- a/yourbench/pipeline/ingestion.py +++ b/yourbench/pipeline/ingestion.py @@ -292,26 +292,59 @@ def _extract_markdown_from_html(file_path: str) -> str | None: logger.warning(f"Trafilatura returned no content for HTML file '{file_path}'.") return None - except ImportError: - logger.error( - "Trafilatura library is not installed. Please install it (e.g., `pip install trafilatura`) " - f"to process HTML files like '{file_path}' effectively. Skipping Trafilatura for this file." - ) - return None except Exception as e: logger.error(f"Error using Trafilatura for HTML file '{file_path}': {e}. Skipping Trafilatura for this file.") return None +def _get_markdown_content(file_path: str, markdown_processor: MarkItDown) -> str | None: + """ + Extract or convert file content to Markdown based on file type. + + Args: + file_path (str): The path to the source document. + markdown_processor (MarkItDown): Configured MarkItDown instance for conversions. + + Returns: + str | None: The Markdown content, or None if conversion failed. + + Logs: + - Info about the processing method used for each file type. + - Warnings for fallback scenarios or failed conversions. + """ + file_ext = os.path.splitext(file_path)[1].lower() + + if file_ext == ".md": + # For existing Markdown files, just read the content, ensuring UTF-8 + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + logger.info(f"File '{file_path}' is already Markdown. Content read directly.") + return content + + elif file_ext in [".html", ".htm"]: + logger.info(f"Processing HTML file: {file_path} with Trafilatura.") + content = _extract_markdown_from_html(file_path) + if content is None: # Fallback to MarkItDown if Trafilatura failed or returned nothing + logger.warning( + f"Trafilatura processing failed or yielded no content for HTML '{file_path}'. " + "Falling back to MarkItDown for this file." + ) + content = markdown_processor.convert(file_path).text_content + return content + + else: # For other file types, use the MarkItDown processor + logger.info(f"Converting non-HTML/Markdown file '{file_path}' using MarkItDown.") + return markdown_processor.convert(file_path).text_content + + def _convert_document_to_markdown(file_path: str, output_dir: str, markdown_processor: MarkItDown) -> None: """ Convert a single source file into Markdown and save the result. - Uses Trafilatura for HTML files to strip junk and extract main content as Markdown. Args: file_path (str): The path to the source document. output_dir (str): Directory where the converted .md file will be written. - markdown_processor (MarkItDown): Configured MarkItDown instance for non-HTML/non-MD conversions or fallback. + markdown_processor (MarkItDown): Configured MarkItDown instance for conversions. Returns: None @@ -319,32 +352,10 @@ def _convert_document_to_markdown(file_path: str, output_dir: str, markdown_proc Logs: - Debug info about the file being processed. - Warning if conversion fails or the file is empty. - - Info/errors related to Trafilatura processing for HTML files. """ logger.debug("Converting file: {}", file_path) try: - file_ext = os.path.splitext(file_path)[1].lower() - content: str | None = None - - if file_ext == ".md": - # For existing Markdown files, just read the content, ensuring UTF-8 - with open(file_path, "r", encoding="utf-8") as f: - content = f.read() - logger.info(f"File '{file_path}' is already Markdown. Content read directly.") - - elif file_ext in [".html", ".htm"]: - logger.info(f"Processing HTML file: {file_path} with Trafilatura.") - content = _extract_markdown_from_html(file_path) - if content is None: # Fallback to MarkItDown if Trafilatura failed or returned nothing - logger.warning( - f"Trafilatura processing failed or yielded no content for HTML '{file_path}'. " - "Falling back to MarkItDown for this file." - ) - content = markdown_processor.convert(file_path).text_content - - else: # For other file types, use the MarkItDown processor - logger.info(f"Converting non-HTML/Markdown file '{file_path}' using MarkItDown.") - content = markdown_processor.convert(file_path).text_content + content = _get_markdown_content(file_path, markdown_processor) if content is None: logger.warning(f"No content could be generated for file '{file_path}' after processing. Skipping output.") From 07497ebcccb6d3896b774aea03dfe5ce99788832 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 4 Jun 2025 06:01:27 -0500 Subject: [PATCH 54/69] add helper --- .../pipeline/citation_score_filtering.py | 22 ++++++------ yourbench/utils/dataset_engine.py | 35 +++++++++++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/yourbench/pipeline/citation_score_filtering.py b/yourbench/pipeline/citation_score_filtering.py index ee1c96a2..742e66e2 100644 --- a/yourbench/pipeline/citation_score_filtering.py +++ b/yourbench/pipeline/citation_score_filtering.py @@ -29,7 +29,7 @@ from loguru import logger from thefuzz import fuzz # pip install thefuzz -from yourbench.utils.dataset_engine import custom_load_dataset, custom_save_dataset +from yourbench.utils.dataset_engine import custom_load_dataset, custom_save_dataset, replace_dataset_columns def run(config: Dict[str, Any]) -> None: @@ -130,16 +130,16 @@ def run(config: Dict[str, Any]) -> None: all_final_scores.append(final_score) # 4) Add these new columns to the dataset - # First, remove columns if they already exist to prevent duplication errors - columns_to_add = ["answer_citation_score", "chunk_citation_score", "citation_score"] - existing_columns_to_remove = [col for col in columns_to_add if col in lighteval_ds.column_names] - if existing_columns_to_remove: - logger.info(f"Removing existing columns before adding new ones: {existing_columns_to_remove}") - lighteval_ds = lighteval_ds.remove_columns(existing_columns_to_remove) - - lighteval_ds = lighteval_ds.add_column("answer_citation_score", all_answer_citation_scores) - lighteval_ds = lighteval_ds.add_column("chunk_citation_score", all_chunk_citation_scores) - lighteval_ds = lighteval_ds.add_column("citation_score", all_final_scores) + # Use helper function to replace columns cleanly + # Note: This doesn't preserve original column metadata, but for computed float scores + # this is acceptable as type inference will correctly identify them as numeric + columns_data = { + "answer_citation_score": all_answer_citation_scores, + "chunk_citation_score": all_chunk_citation_scores, + "citation_score": all_final_scores + } + + lighteval_ds = replace_dataset_columns(lighteval_ds, columns_data) # 5) Save the updated dataset # We reuse the "lighteval" subset name, but you could save it elsewhere if you prefer. diff --git a/yourbench/utils/dataset_engine.py b/yourbench/utils/dataset_engine.py index 1191a80c..a3299473 100644 --- a/yourbench/utils/dataset_engine.py +++ b/yourbench/utils/dataset_engine.py @@ -344,3 +344,38 @@ def custom_save_dataset( config_name=config_name, ) logger.success(f"Dataset successfully pushed to HuggingFace Hub with repo_id='{dataset_repo_name}'") + + +def replace_dataset_columns(dataset: Dataset, columns_data: dict[str, list], preserve_metadata: bool = False) -> Dataset: + """ + Replace columns in a dataset by removing existing columns and adding new ones. + + This helper function handles the common pattern of: + 1. Removing existing columns (if they exist) + 2. Adding new columns with computed data + + Args: + dataset: The input dataset to modify + columns_data: Dictionary mapping column names to their data lists + preserve_metadata: If True, attempts to preserve column metadata (not implemented) + + Returns: + Updated dataset with replaced columns + + Note: + Column metadata (types, features) is not preserved in the current implementation. + New columns will have types inferred from the provided data. + """ + # Remove existing columns to prevent duplication errors + columns_to_replace = list(columns_data.keys()) + existing_columns_to_remove = [col for col in columns_to_replace if col in dataset.column_names] + + if existing_columns_to_remove: + logger.info(f"Removing existing columns before adding new ones: {existing_columns_to_remove}") + dataset = dataset.remove_columns(existing_columns_to_remove) + + # Add new columns + for column_name, column_data in columns_data.items(): + dataset = dataset.add_column(column_name, column_data) + + return dataset From 3801f6394e01e9a7d285ed303360937ffaab10f2 Mon Sep 17 00:00:00 2001 From: sumukshashidhar Date: Wed, 4 Jun 2025 06:01:59 -0500 Subject: [PATCH 55/69] fix cq --- yourbench/pipeline/citation_score_filtering.py | 4 ++-- yourbench/utils/dataset_engine.py | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/yourbench/pipeline/citation_score_filtering.py b/yourbench/pipeline/citation_score_filtering.py index 742e66e2..3f6e7120 100644 --- a/yourbench/pipeline/citation_score_filtering.py +++ b/yourbench/pipeline/citation_score_filtering.py @@ -136,9 +136,9 @@ def run(config: Dict[str, Any]) -> None: columns_data = { "answer_citation_score": all_answer_citation_scores, "chunk_citation_score": all_chunk_citation_scores, - "citation_score": all_final_scores + "citation_score": all_final_scores, } - + lighteval_ds = replace_dataset_columns(lighteval_ds, columns_data) # 5) Save the updated dataset diff --git a/yourbench/utils/dataset_engine.py b/yourbench/utils/dataset_engine.py index a3299473..fb848cf0 100644 --- a/yourbench/utils/dataset_engine.py +++ b/yourbench/utils/dataset_engine.py @@ -346,22 +346,24 @@ def custom_save_dataset( logger.success(f"Dataset successfully pushed to HuggingFace Hub with repo_id='{dataset_repo_name}'") -def replace_dataset_columns(dataset: Dataset, columns_data: dict[str, list], preserve_metadata: bool = False) -> Dataset: +def replace_dataset_columns( + dataset: Dataset, columns_data: dict[str, list], preserve_metadata: bool = False +) -> Dataset: """ Replace columns in a dataset by removing existing columns and adding new ones. - + This helper function handles the common pattern of: 1. Removing existing columns (if they exist) 2. Adding new columns with computed data - + Args: dataset: The input dataset to modify columns_data: Dictionary mapping column names to their data lists preserve_metadata: If True, attempts to preserve column metadata (not implemented) - + Returns: Updated dataset with replaced columns - + Note: Column metadata (types, features) is not preserved in the current implementation. New columns will have types inferred from the provided data. @@ -369,13 +371,13 @@ def replace_dataset_columns(dataset: Dataset, columns_data: dict[str, list], pre # Remove existing columns to prevent duplication errors columns_to_replace = list(columns_data.keys()) existing_columns_to_remove = [col for col in columns_to_replace if col in dataset.column_names] - + if existing_columns_to_remove: logger.info(f"Removing existing columns before adding new ones: {existing_columns_to_remove}") dataset = dataset.remove_columns(existing_columns_to_remove) - + # Add new columns for column_name, column_data in columns_data.items(): dataset = dataset.add_column(column_name, column_data) - + return dataset From 5f56c0563033e09d19c2790971c150504ca13bd3 Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Wed, 4 Jun 2025 23:34:04 +0200 Subject: [PATCH 56/69] Refactor CLI and pipeline init logic --- yourbench/main.py | 3 + yourbench/pipeline/handler.py | 152 ++++++++++------------------------ 2 files changed, 48 insertions(+), 107 deletions(-) diff --git a/yourbench/main.py b/yourbench/main.py index 722622ed..4cad44d8 100644 --- a/yourbench/main.py +++ b/yourbench/main.py @@ -4,12 +4,15 @@ from pathlib import Path import typer +from dotenv import load_dotenv from loguru import logger from yourbench.analysis import run_analysis from yourbench.pipeline.handler import run_pipeline +load_dotenv() + app = typer.Typer( name="yourbench", add_completion=True, diff --git a/yourbench/pipeline/handler.py b/yourbench/pipeline/handler.py index d5759311..ad8fcf0a 100644 --- a/yourbench/pipeline/handler.py +++ b/yourbench/pipeline/handler.py @@ -1,31 +1,28 @@ -# handler.py -# ============================================================================= -# Author: @sumukshashidhar -# -# This module orchestrates the YourBench pipeline stages in a specified order. -# It reads pipeline configuration from a config dictionary, runs each stage -# if enabled, times each stage's execution, logs errors to stage-specific -# log files, and finally generates an overall timing chart of all stages. -# -# Usage: -# from yourbench.pipeline.handler import run_pipeline -# run_pipeline("/path/to/config.yaml", debug=True) -# -# The module assumes the presence of pipeline stages named after their .py -# files (e.g., ingestion, summarization), each exposing a `run(config: dict)`. -# -# Stages are executed in a fixed default order but will skip any that -# do not appear in the config or are explicitly disabled. Unrecognized -# stages in the config are also noted (but not executed). -# -# Key Responsibilities: -# 1. Load the user's pipeline configuration. -# 2. Execute each stage in `DEFAULT_STAGE_ORDER` if `run` is True in the config. -# 3. Log all events, including errors, to a stage-specific file and the console. -# 4. Collect and display timing data for each stage. -# 5. Detect any extra pipeline stages in the config that do not appear in -# `DEFAULT_STAGE_ORDER` and log a warning about them. -# ============================================================================= +""" +This module orchestrates the Yourbench pipeline stages in a specified order. +It reads pipeline configuration from a config dictionary, runs each stage +if enabled, times each stage's execution, logs errors to stage-specific +log files, and finally generates an overall timing chart of all stages. + +The module assumes the presence of pipeline stages named after their .py +files (e.g., ingestion, summarization), each exposing a `run(config: dict)`. + +Some stages may use direct function overrides (e.g., for question generation), +bypassing dynamic import. These are defined in `STAGE_FUNCTION_OVERRIDES`. + +Stages are executed in a fixed default order but will skip any that +do not appear in the config or are explicitly disabled. Unrecognized +stages in the config are also noted (but not executed). + +Key Responsibilities: +1. Load the user's pipeline configuration. +2. Execute each stage in `DEFAULT_STAGE_ORDER` if `run` is True in the config. +3. Use function overrides for specific stages if defined. +4. Log all events, including errors, to a stage-specific file and the console. +5. Collect and display timing data for each stage. +6. Detect any extra pipeline stages in the config that do not appear in + `DEFAULT_STAGE_ORDER` and log a warning about them. +""" from __future__ import annotations import os @@ -36,6 +33,10 @@ from loguru import logger from yourbench.utils.loading_engine import load_config +from yourbench.pipeline.question_generation import ( + run_multi_hop, + run_single_shot, +) # === Pipeline Stage Order Definition === @@ -52,63 +53,41 @@ "citation_score_filtering", ] -# This global list tracks the timing for all executed stages in the pipeline. PIPELINE_STAGE_TIMINGS: List[Dict[str, float]] = [] +STAGE_FUNCTION_OVERRIDES = { + "single_shot_question_generation": run_single_shot, + "multi_hop_question_generation": run_multi_hop, +} + + def run_pipeline( config_file_path: str, debug: bool = False, plot_stage_timing: bool = False, ) -> None: - """ - Run the YourBench pipeline based on a provided YAML/JSON configuration file. - - Args: - config_file_path (str): - Path to the pipeline configuration file that describes which stages to run (YAML or JSON). - debug (bool): - Enables more verbose logging (debug-level). Defaults to False. - plot_stage_timing (bool): - If True, generate a bar chart showing the time spent in each stage. Requires matplotlib. - - Raises: - FileNotFoundError: - If the configuration file is not found at the specified path. - Exception: - If any stage raises an unexpected error during execution, it is re-raised after logging. - """ global PIPELINE_STAGE_TIMINGS PIPELINE_STAGE_TIMINGS = [] - # Log level adjustments logger.debug(f"Loading pipeline configuration from {config_file_path}") config: Dict[str, Any] = load_config(config_file_path) - - # Attach debug flag to config for use in other modules config["debug"] = debug logger.info(f"Debug mode set to {config['debug']}") - # Extract pipeline portion of the config pipeline_config: Dict[str, Any] = config.get("pipeline", {}) if not pipeline_config: logger.warning("No pipeline stages configured. Exiting pipeline execution.") return - # Ensure logs directory exists to store stage-specific logs os.makedirs("logs", exist_ok=True) - - # Record overall pipeline start pipeline_execution_start_time: float = time.time() - # === Execute pipeline stages in the fixed default order === for stage_name in DEFAULT_STAGE_ORDER: - # Check if the stage is mentioned in the pipeline config at all if stage_name not in pipeline_config: logger.debug(f"Stage '{stage_name}' is not mentioned in the config. Skipping.") continue - # Get the settings for the stage. It might be None or a dict. stage_settings = pipeline_config.get(stage_name) if not isinstance(stage_settings, dict): pipeline_config[stage_name] = {"run": True} @@ -119,24 +98,21 @@ def run_pipeline( logger.info(f"Skipping stage: '{stage_name}' (run set to False).") continue - # Setup a stage-specific error log file error_log_path = os.path.join("logs", f"pipeline_{stage_name}.log") log_id = logger.add(error_log_path, level="ERROR", backtrace=True, diagnose=True, mode="a") logger.info(f"Starting execution of stage: '{stage_name}'") stage_start_time: float = time.time() - # Ensure the specific stage config is at least an empty dict if it was None - if stage_name in config.get("pipeline", {}) and config["pipeline"][stage_name] is None: - config["pipeline"][stage_name] = {} - try: - # Dynamically import the stage module, e.g. yourbench.pipeline.ingestion - stage_module = importlib.import_module(f"yourbench.pipeline.{stage_name}") - stage_module.run(config) - except Exception as pipeline_error: - logger.error(f"Error executing pipeline stage '{stage_name}': {str(pipeline_error)}") - # Remove stage-specific log handler before re-raising + stage_func = STAGE_FUNCTION_OVERRIDES.get(stage_name) + if stage_func: + stage_func(config) + else: + stage_module = importlib.import_module(f"yourbench.pipeline.{stage_name}") + stage_module.run(config) + except Exception: + logger.exception(f"Error executing pipeline stage '{stage_name}'") _remove_log_handler_safely(log_id) raise finally: @@ -152,47 +128,20 @@ def run_pipeline( }) logger.success(f"Completed stage: '{stage_name}' in {elapsed_time:.3f}s") - # Record overall pipeline end pipeline_execution_end_time: float = time.time() - - # Check for unrecognized stages in config _check_for_unrecognized_stages(pipeline_config) - # Optionally plot pipeline stage timings if plot_stage_timing or debug: - _plot_pipeline_stage_timing( - pipeline_start=pipeline_execution_start_time, - pipeline_end=pipeline_execution_end_time, - ) + _plot_pipeline_stage_timing() def _check_for_unrecognized_stages(pipeline_config: Dict[str, Any]) -> None: - """ - Warn about pipeline stages that exist in the config but - are not in DEFAULT_STAGE_ORDER. - - Args: - pipeline_config (Dict[str, Any]): - The pipeline configuration dict (subset of the main config). - """ for stage in pipeline_config.keys(): if stage not in DEFAULT_STAGE_ORDER: logger.warning(f"Unrecognized stage '{stage}' is present in config but not in DEFAULT_STAGE_ORDER.") -def _plot_pipeline_stage_timing( - pipeline_start: float, - pipeline_end: float, -) -> None: - """ - Generate a bar chart illustrating the stage timings for the entire pipeline. - - Args: - pipeline_start (float): - Timestamp when the pipeline started. - pipeline_end (float): - Timestamp when the pipeline ended. - """ +def _plot_pipeline_stage_timing() -> None: logger.info("Generating pipeline stage timing chart.") try: import matplotlib.pyplot as plt @@ -200,18 +149,15 @@ def _plot_pipeline_stage_timing( logger.warning("Cannot generate timing chart: matplotlib is not installed.") return - # Gather data stages = [timing["stage_name"] for timing in PIPELINE_STAGE_TIMINGS] durations = [timing["elapsed"] for timing in PIPELINE_STAGE_TIMINGS] - # Minimalistic bar chart fig, ax = plt.subplots(figsize=(3, 3), dpi=300) ax.barh(stages, durations, color="skyblue", edgecolor="black") ax.set_xlabel("Duration (s)") ax.set_title("Pipeline Stage Timings") - # Annotate each bar with the stage's duration for i, duration in enumerate(durations): ax.text(duration + 0.01, i, f"{duration:.2f}s", va="center", fontsize=6) @@ -222,14 +168,6 @@ def _plot_pipeline_stage_timing( def _remove_log_handler_safely(log_id: int) -> None: - """ - Remove a log handler (by log_id) from loguru, swallowing any ValueError - if the handler is already removed or doesn't exist. - - Args: - log_id (int): - The handler ID returned by logger.add(). - """ try: logger.remove(log_id) except ValueError: From 1fed2fa6351dad1fa93785ec9ad2b4465e91677a Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Wed, 4 Jun 2025 23:34:22 +0200 Subject: [PATCH 57/69] Refactor ingestion and QA stages --- yourbench/pipeline/ingestion.py | 2 +- yourbench/pipeline/lighteval.py | 4 +- .../pipeline/multi_hop_question_generation.py | 374 ------------------ yourbench/pipeline/question_generation.py | 133 +++++++ .../single_shot_question_generation.py | 360 ----------------- yourbench/pipeline/summarization.py | 12 +- 6 files changed, 137 insertions(+), 748 deletions(-) delete mode 100644 yourbench/pipeline/multi_hop_question_generation.py create mode 100644 yourbench/pipeline/question_generation.py delete mode 100644 yourbench/pipeline/single_shot_question_generation.py diff --git a/yourbench/pipeline/ingestion.py b/yourbench/pipeline/ingestion.py index 9e612065..7744a66c 100644 --- a/yourbench/pipeline/ingestion.py +++ b/yourbench/pipeline/ingestion.py @@ -48,7 +48,7 @@ from markitdown import MarkItDown from huggingface_hub import InferenceClient -from yourbench.utils.inference_engine import Model as ModelConfig +from yourbench.utils.inference.inference_core import Model as ModelConfig @dataclass diff --git a/yourbench/pipeline/lighteval.py b/yourbench/pipeline/lighteval.py index a71039bf..1ffae7e4 100644 --- a/yourbench/pipeline/lighteval.py +++ b/yourbench/pipeline/lighteval.py @@ -162,7 +162,7 @@ def make_single_shot_record(row: Dict[str, Any]) -> Dict[str, Any]: logger.warning("Row has empty answer line") stage_cfg = config.get("pipeline", {}).get("single_shot_question_generation", {}) - if stage_cfg.get("question_type") == "multi-choice": + if stage_cfg.get("question_mode") == "multi-choice": if not gold: gold = [0] else: @@ -214,7 +214,7 @@ def make_multi_hop_record(row: Dict[str, Any]) -> Dict[str, Any]: logger.warning("Row has empty answer line") stage_cfg = config.get("pipeline", {}).get("single_shot_question_generation", {}) - if stage_cfg.get("question_type") == "multi-choice": + if stage_cfg.get("question_mode") == "multi-choice": if not gold: gold = [0] else: diff --git a/yourbench/pipeline/multi_hop_question_generation.py b/yourbench/pipeline/multi_hop_question_generation.py deleted file mode 100644 index b5657dad..00000000 --- a/yourbench/pipeline/multi_hop_question_generation.py +++ /dev/null @@ -1,374 +0,0 @@ -# ============================================================ -# multi_hop_question_generation.py -# ============================================================ -""" -Author: @sumukshashidhar - -Module Name: ------------- -multi_hop_question_generation - -Purpose: --------- -This module implements the multi-hop question generation stage within the YourBench pipeline. -It processes a dataset of documents—each containing a list of multi-hop chunks—and generates -multi-hop questions requiring integrative reasoning across those chunks. It uses a Large -Language Model (LLM) to produce question-answer pairs in JSON format. - -Usage: ------- -This module is typically invoked as part of the overall YourBench pipeline. It expects: -1. A source dataset (e.g., documents with 'multihop_chunks' field). -2. Configuration for multi-hop question generation, such as sampling parameters and - additional instructions. -3. The pipeline orchestrator (in `handler.py`) calls `run(config)` if - `multi_hop_question_generation` is enabled in the YAML configuration. - -The module then: -1. Optionally samples multi-hop chunks from each document. -2. Prompts a Large Language Model (LLM) to generate multi-hop question-answer pairs. -3. Parses and saves the generated questions in a structured HuggingFace `Dataset`. - -Error Handling and Logging: ---------------------------- -- Comprehensive logging is performed using `loguru` at various levels to trace execution. -- Exceptions are caught and logged as errors, with the module attempting to continue - where practical. -- Critical issues produce warnings or errors and gracefully terminate the stage. - -Module-Level Dependencies: --------------------------- -- Requires Python 3.9+ for modern type annotations (`list[...]`, `dict[...]`). -- Relies on the shared pipeline utilities (e.g., `yourbench.utils.dataset_engine`, - `yourbench.utils.inference_engine`, `yourbench.utils.prompts`). -- Preserves the existing signature and functionality for downstream consistency. -""" - -import random -from typing import Any, Dict -from dataclasses import field, dataclass - -from loguru import logger - -from datasets import Dataset -from yourbench.utils.prompts import ( - MULTI_HOP_QUESTION_GENERATION_USER_PROMPT, - MULTI_HOP_QUESTION_GENERATION_SYSTEM_PROMPT, - MULTI_HOP_QUESTION_GENERATION_SYSTEM_PROMPT_MULTI, -) -from yourbench.utils.dataset_engine import ( - custom_load_dataset, - custom_save_dataset, -) - -# Import the unified parsing function -from yourbench.utils.parsing_engine import ( - shuffle_mcq, - _validate_list, - _force_int_in_range, - parse_qa_pairs_from_response, -) -from yourbench.utils.inference_engine import InferenceCall, run_inference - - -@dataclass -class QuestionAnswerPair: - """ - Data structure to represent a question-answer pair returned by the model. - """ - - question: str - answer: str - choices: list[str] - estimated_difficulty: int = 5 - question_type: str = "unknown" - thought_process: str = "" - citations: list[str] = field(default_factory=list) - - def __post_init__(self) -> None: - # Normalize fields - self.question = str(self.question).strip() - self.answer = str(self.answer).strip() - self.estimated_difficulty = _force_int_in_range(self.estimated_difficulty, 1, 10) - self.question_type = str(self.question_type) - self.thought_process = str(self.thought_process) - - if not isinstance(self.citations, list): - self.citations = [] - else: - self.citations = _validate_list(self.citations) - - if not isinstance(self.choices, list): - self.choices = [] - else: - self.choices = _validate_list(self.choices) - - -@dataclass -class MultiHopQuestionRow: - """ - Data structure to represent a single multi-hop question row. - """ - - document_id: str - source_chunk_ids: list[str] - additional_instructions: str - question: str - self_answer: str - choices: list[str] - estimated_difficulty: int - self_assessed_question_type: str - generating_model: str - thought_process: str - citations: list[str] = field(default_factory=list) - raw_response: str = field(default="") - - @classmethod - def from_qa_pair( - cls, - qa_pair: QuestionAnswerPair, - document_id: str, - source_chunk_ids: list[str], - generating_model: str, - raw_response: str = "", - additional_instructions: str = "", - ) -> "MultiHopQuestionRow": - return cls( - document_id=document_id, - source_chunk_ids=source_chunk_ids, - additional_instructions=additional_instructions, - question=qa_pair.question, - self_answer=qa_pair.answer, - choices=qa_pair.choices, - estimated_difficulty=qa_pair.estimated_difficulty, - self_assessed_question_type=qa_pair.question_type, - generating_model=generating_model, - thought_process=qa_pair.thought_process, - citations=qa_pair.citations, - raw_response=raw_response, - ) - - -def run(config: Dict[str, Any]) -> None: - """ - Execute the multi-hop question generation stage. - """ - stage_cfg = config.get("pipeline", {}).get("multi_hop_question_generation", {}) - if not stage_cfg.get("run", False): - logger.info("multi_hop_question_generation stage is disabled. Skipping.") - return - - # 1) Dataset Loading - dataset = custom_load_dataset(config=config, subset="chunked") - logger.info(f"Loaded chunked subset with {len(dataset)} rows for Multi-hop question generation.") - - # 2) Build Inference Calls (including sampling) - inference_calls, call_index_map = _multihop_chunk_sampling_and_calls(dataset, stage_cfg) - - # 3) Run Inference - if not inference_calls: - logger.warning("No multi-hop inference calls were created. Exiting stage.") - return - responses_dict = _multihop_qa_generation(config, inference_calls) - - # 4) Parse and Build Final Dataset - final_dataset = _parse_and_build_final(config, responses_dict, call_index_map, stage_cfg) - if final_dataset is None or len(final_dataset) == 0: - logger.warning("No valid multi-hop question rows produced. Exiting stage.") - return - - # 5) Save the result - custom_save_dataset(dataset=final_dataset, config=config, subset="multi_hop_questions") - logger.success("Multi-hop question generation completed successfully.") - - -def _multihop_chunk_sampling_and_calls(dataset, stage_cfg: Dict[str, Any]): - """ - Sample multi-hop chunks and build InferenceCalls. - Returns: - - inference_calls: list of InferenceCall - - call_index_map: parallel list of (row_idx, doc_id, source_chunk_ids) - """ - - if stage_cfg.get("question_type") == "multi-choice": - system_prompt = MULTI_HOP_QUESTION_GENERATION_SYSTEM_PROMPT_MULTI - else: - system_prompt = MULTI_HOP_QUESTION_GENERATION_SYSTEM_PROMPT - system_msg = { - "role": "system", - "content": system_prompt, - } - - all_inference_calls = [] - call_index_map = [] - - for row_idx, row in enumerate(dataset): - doc_summary = row.get("document_summary", "No summary provided.") - title = row.get("document_filename", f"Document_{row_idx}") - doc_id = row.get("document_id", f"doc_{row_idx}") - - multi_hop_chunks = row.get("multihop_chunks", []) - if not isinstance(multi_hop_chunks, list) or not multi_hop_chunks: - logger.warning(f"No multi-hop chunks found in row index={row_idx}, doc_id={doc_id}. Skipping row.") - continue - - chosen_multi_hops = _sample_multi_hop_chunks(multi_hop_chunks, stage_cfg.get("chunk_sampling", {})) - if not chosen_multi_hops: - logger.warning(f"Row idx={row_idx} doc_id={doc_id} had multi-hop chunks but none after sampling.") - continue - - additional_instructions = stage_cfg.get("additional_instructions", "undergraduate") - - for mh_idx, mh_dict in enumerate(chosen_multi_hops): - if not isinstance(mh_dict, dict): - continue - - subchunk_ids = mh_dict.get("chunk_ids", []) - subchunk_texts = mh_dict.get("chunks_text", []) - if not subchunk_texts: - logger.debug(f"Empty multi-hop chunk at row_idx={row_idx}, doc_id={doc_id}. Skipping.") - continue - - # Build user prompt by enumerating each subchunk - text_chunks_aggregated = "" - for i, sc_text in enumerate(subchunk_texts): - text_chunks_aggregated += f"{sc_text}\n" - - user_prompt_str = MULTI_HOP_QUESTION_GENERATION_USER_PROMPT.format( - title=title, - document_summary=doc_summary, - chunks=text_chunks_aggregated, - additional_instructions=additional_instructions, - ) - user_msg = {"role": "user", "content": user_prompt_str} - - inference_call = InferenceCall(messages=[system_msg, user_msg], tags=["multi_hop_qa"]) - all_inference_calls.append(inference_call) - call_index_map.append((row_idx, doc_id, subchunk_ids)) - - return all_inference_calls, call_index_map - - -def _sample_multi_hop_chunks( - mh_chunks: list[Dict[str, Any]], chunk_sampling_cfg: Dict[str, Any] -) -> list[Dict[str, Any]]: - """ - Sample multi-hop chunks based on the stage configuration. - """ - if len(chunk_sampling_cfg) == 0: - # If there's no config, return all - return mh_chunks - - mode = chunk_sampling_cfg.get("mode", "all").lower() - value = chunk_sampling_cfg.get("value", 1.0) - rand_seed = chunk_sampling_cfg.get("random_seed", 42) - random.seed(rand_seed) - - total_multi_hops = len(mh_chunks) - if total_multi_hops < 2: # if 0 or 1 chunk - return mh_chunks - - if mode == "percentage": - k = int(total_multi_hops * float(value)) - k = max(0, min(k, total_multi_hops)) - if k < total_multi_hops: - return random.sample(mh_chunks, k) - return mh_chunks - - elif mode == "count": - k = min(int(value), total_multi_hops) - if k < total_multi_hops: - return random.sample(mh_chunks, k) - return mh_chunks - - # Otherwise return all - return mh_chunks - - -def _multihop_qa_generation(config: Dict[str, Any], inference_calls: list[InferenceCall]): - """ - Call the inference engine to get multi-hop Q&A responses. - """ - logger.info(f"Sending {len(inference_calls)} multi-hop calls to inference...") - return run_inference( - config=config, - step_name="multi_hop_question_generation", - inference_calls=inference_calls, - ) - - -def _parse_and_build_final( - config: Dict[str, Any], - responses_dict: Dict[str, list[str]], - call_index_map: list[tuple], - stage_config: Dict[str, Any], -) -> Dataset: - """ - Parse each model's responses into MultiHopQuestionRow items, then build a final dataset. - """ - final_multi_hop_questions = [] - - for model_name, model_responses in responses_dict.items(): - logger.info(f"Processing {len(model_responses)} responses for model: {model_name}") - if len(model_responses) != len(call_index_map): - logger.error( - f"Model '{model_name}' returned {len(model_responses)} responses; expected {len(call_index_map)}. Mismatch." - ) - - for idx, raw_resp in enumerate(model_responses): - if idx >= len(call_index_map): - break - - row_idx, doc_id, source_chunk_ids = call_index_map[idx] - qa_pairs = parse_qa_pairs_from_response(raw_resp) - - if not qa_pairs: - logger.warning(f"No parseable JSON for row={row_idx}, doc_id={doc_id} (model={model_name}).") - continue - - # Otherwise, process each QA pair - for qap_dict in qa_pairs: - try: - # Shuffle before wrapping into dataclass - qap_dict = shuffle_mcq(qap_dict) - # Convert dictionary -> QuestionAnswerPair - pair_obj = QuestionAnswerPair( - question=qap_dict.get("question", ""), - answer=qap_dict.get("answer", ""), - choices=qap_dict.get("choices", []), - estimated_difficulty=qap_dict.get("estimated_difficulty", 5), - question_type=qap_dict.get("question_type", "unknown"), - thought_process=qap_dict.get("thought_process", ""), - citations=qap_dict.get("citations", []), - ) - if not pair_obj.question: - logger.debug(f"Empty question found for row={row_idx}, doc_id={doc_id}, skipping pair.") - continue - - row_obj = MultiHopQuestionRow.from_qa_pair( - qa_pair=pair_obj, - document_id=doc_id, - source_chunk_ids=source_chunk_ids, - generating_model=model_name, - raw_response=raw_resp, - additional_instructions=stage_config.get( - "additional_instructions", "Generate questions to test a curious adult" - ), - ) - final_multi_hop_questions.append(row_obj.__dict__) - - except Exception as pair_error: - logger.warning(f"Error processing QA pair for doc_id={doc_id}, skipping pair: {pair_error}") - continue - - if not final_multi_hop_questions: - return None - - logger.info(f"Constructing multi-hop question dataset with {len(final_multi_hop_questions)} rows...") - try: - col_keys = list(final_multi_hop_questions[0].keys()) - dataset_dict = {k: [row[k] for row in final_multi_hop_questions] for k in col_keys} - return Dataset.from_dict(dataset_dict) - except Exception as ds_error: - logger.error(f"Failed to create dataset from multi-hop question rows: {ds_error}") - return None diff --git a/yourbench/pipeline/question_generation.py b/yourbench/pipeline/question_generation.py new file mode 100644 index 00000000..736a22a2 --- /dev/null +++ b/yourbench/pipeline/question_generation.py @@ -0,0 +1,133 @@ +""" +Question Generation Pipeline (Single-Hop & Multi-Hop) + +This module defines a pipeline for generating question-answer pairs using either +single document chunks (single-hop) or multiple chunks (multi-hop). It supports +prompt-based inference via a language model, parses responses, and saves the output. + +Features: +- Configurable chunk sampling (by count or percentage) +- Prompt formatting for single-hop and multi-hop generation +- Response parsing and validation +- Integration with HuggingFace Datasets and custom I/O + +Main Functions: +- run_single_shot(): Generates single-hop questions. +- run_multi_hop(): Generates multi-hop questions. +""" + +from __future__ import annotations +from typing import Any + +from loguru import logger + +from datasets import Dataset +from yourbench.utils.prompts import ( + QUESTION_GENERATION_SYSTEM_PROMPT, + QUESTION_GENERATION_SYSTEM_PROMPT_MULTI, + MULTI_HOP_QUESTION_GENERATION_SYSTEM_PROMPT, + MULTI_HOP_QUESTION_GENERATION_SYSTEM_PROMPT_MULTI, +) +from yourbench.utils.chunking_utils import get_sampling_cfg +from yourbench.utils.dataset_engine import custom_load_dataset, custom_save_dataset +from yourbench.utils.parsing_engine import ( + parse_multi_hop_responses, + parse_single_shot_responses, +) +from yourbench.utils.inference.inference_core import run_inference +from yourbench.utils.inference.inference_builders import ( + build_multi_hop_inference_calls, + build_single_shot_inference_calls, +) + + +SINGLE_SHOT_KEY = "single_shot_question_generation" +MULTI_HOP_KEY = "multi_hop_question_generation" + + +def run_single_shot(config: dict[str, Any]) -> None: + """ + Orchestrates the single-hop question generation pipeline. + """ + stage_cfg = config.get("pipeline", {}).get(SINGLE_SHOT_KEY, {}) + if not stage_cfg.get("run", False): + logger.info("single_shot_question_generation stage is disabled.") + return + + question_mode = stage_cfg.get("question_mode", "open-ended") + allowed_types = {"open-ended", "multi-choice"} + if question_mode not in allowed_types: + logger.warning(f"Invalid question_mode '{question_mode}', defaulting to 'open-ended'") + question_mode = "open-ended" + + logger.info(f"Single-shot question_mode: {question_mode}") + + if question_mode == "multi-choice": + system_prompt = QUESTION_GENERATION_SYSTEM_PROMPT_MULTI + logger.debug("Using MULTI-CHOICE prompt for single-shot generation.") + else: + system_prompt = QUESTION_GENERATION_SYSTEM_PROMPT + logger.debug("Using OPEN-ENDED prompt for single-shot generation.") + + system_msg = {"role": "system", "content": system_prompt} + + dataset = custom_load_dataset(config=config, subset="chunked") + logger.info(f"Loaded {len(dataset)} chunks for single-shot.") + + sampling_cfg = get_sampling_cfg(stage_cfg) + + inference_calls, inference_index_map = build_single_shot_inference_calls( + dataset, system_msg, stage_cfg, sampling_cfg + ) + if not inference_calls: + logger.warning("No valid inference calls for single-shot.") + return + + responses = run_inference(config=config, step_name=SINGLE_SHOT_KEY, inference_calls=inference_calls) + final_rows = parse_single_shot_responses(responses, inference_index_map, stage_cfg) + + if final_rows: + logger.info(f"Saving {len(final_rows)} single-shot questions.") + custom_save_dataset(Dataset.from_list(final_rows), config=config, subset="single_shot_questions") + + +def run_multi_hop(config: dict[str, Any]) -> None: + """ + Orchestrates the multi-hop question generation pipeline. + """ + stage_cfg = config.get("pipeline", {}).get(MULTI_HOP_KEY, {}) + if not stage_cfg.get("run", False): + logger.info("multi_hop_question_generation stage is disabled.") + return + + question_mode = stage_cfg.get("question_mode", "open-ended") + allowed_types = {"open-ended", "multi-choice"} + if question_mode not in allowed_types: + logger.warning(f"Invalid question_mode '{question_mode}', defaulting to 'open-ended'") + question_mode = "open-ended" + + logger.info(f"Multi-hop question_mode: {question_mode}") + + if question_mode == "multi-choice": + system_prompt = MULTI_HOP_QUESTION_GENERATION_SYSTEM_PROMPT_MULTI + logger.debug("Using MULTI-CHOICE prompt for multi-hop generation.") + else: + system_prompt = MULTI_HOP_QUESTION_GENERATION_SYSTEM_PROMPT + logger.debug("Using OPEN-ENDED prompt for multi-hop generation.") + + system_msg = {"role": "system", "content": system_prompt} + + dataset = custom_load_dataset(config=config, subset="chunked") + logger.info(f"Loaded {len(dataset)} documents for multi-hop.") + + inference_calls, inference_index_map = build_multi_hop_inference_calls(dataset, system_msg, stage_cfg) + if not inference_calls: + logger.warning("No valid multi-hop chunks found for inference.") + return + + responses = run_inference(config=config, step_name=MULTI_HOP_KEY, inference_calls=inference_calls) + final_rows = parse_multi_hop_responses(responses, inference_index_map, stage_cfg) + + if final_rows: + logger.info(f"Saving {len(final_rows)} multi-hop questions.") + custom_save_dataset(Dataset.from_list(final_rows), config=config, subset="multi_hop_questions") diff --git a/yourbench/pipeline/single_shot_question_generation.py b/yourbench/pipeline/single_shot_question_generation.py deleted file mode 100644 index af83b67e..00000000 --- a/yourbench/pipeline/single_shot_question_generation.py +++ /dev/null @@ -1,360 +0,0 @@ -# ============================================================ -# single_shot_question_generation.py -# ============================================================ -""" -Author: @sumukshashidhar - -This module implements the Single-Shot Question Generation stage of the YourBench pipeline. - -Overview: - - Given a dataset containing document summaries and their associated single-hop chunks, - this stage generates question-answer pairs for each chunk using one or more LLMs. - - The generated questions are intended to be standalone, moderately challenging, - and reflect a deep understanding of the provided text chunk. - -Usage: - 1) The pipeline will call the `run()` function from this module if the user configures - `pipeline.single_shot_question_generation.run = True`. - 2) This function loads the required dataset (specified in the pipeline configuration), - samples chunks if necessary, and calls an LLM to generate questions. - 3) The output is stored in a new dataset containing each generated question, - an estimated difficulty rating, and the model's self-provided reasoning. - -Stage-Specific Logging: - - All errors and relevant log messages are recorded in `logs/single_shot_question_generation.log`. - -Google-Style Docstrings: - - This codebase uses Python type hints and Google-style docstrings for clarity, - maintainability, and consistency. -""" - -import random -from typing import Any -from dataclasses import field, dataclass - -from loguru import logger - -from datasets import Dataset -from yourbench.utils.prompts import ( - QUESTION_GENERATION_USER_PROMPT, - QUESTION_GENERATION_SYSTEM_PROMPT, - QUESTION_GENERATION_SYSTEM_PROMPT_MULTI, -) -from yourbench.utils.dataset_engine import ( - custom_load_dataset, - custom_save_dataset, -) - -# Import the unified parsing function -from yourbench.utils.parsing_engine import ( - shuffle_mcq, - _validate_list, - _force_int_in_range, - parse_qa_pairs_from_response, -) -from yourbench.utils.inference_engine import InferenceCall, run_inference - - -@dataclass -class SingleHopQuestionRow: - """ - Represents a single-hop question row derived from a single chunk of text. - - Attributes: - chunk_id: A string identifier for the chunk from which this question was generated. - document_id: Identifier for the parent document. - question: The generated question text. - self_answer: The LLM-produced short answer or reasoning. - estimated_difficulty: An integer from 1-10 indicating the estimated difficulty. - self_assessed_question_type: A descriptor for the type or style of question. - generating_model: The model used to generate this question. - thought_process: Free-form text describing how the question was derived. - raw_response: The full, unedited response from the model. - citations: A list of references or quotes extracted from the chunk. - """ - - chunk_id: str - document_id: str - additional_instructions: str - question: str - self_answer: str - choices: list[str] - estimated_difficulty: int - self_assessed_question_type: str - generating_model: str - thought_process: str - raw_response: str - citations: list[str] - - -@dataclass -class ChunkSamplingConfig: - mode: str = "all" - value: float = 1.0 - random_seed: int = 42 - - -@dataclass -class SingleShotQuestionGenerationConfig: - run: bool = False - source_subset: str = "" - output_subset: str = "" - additional_instructions: str = "Generate questions to test an undergraduate student" - chunk_sampling: ChunkSamplingConfig = field(default_factory=ChunkSamplingConfig) - question_type: str = "open-ended" - - -@dataclass -class DocumentRow: - document_summary: str = "No summary available." - document_filename: str = "" - document_id: str = "" - chunks: list[dict[str, Any]] = field(default_factory=list) - - -def run(config: dict[str, Any]) -> None: - """ - Executes the Single-Shot Question Generation stage of the pipeline. - """ - stage_config = _load_stage_config(config) - if not stage_config.run: - logger.info("single_shot_question_generation stage is disabled. Skipping.") - return - - dataset = custom_load_dataset(config=config, subset="chunked") - logger.info(f"Loaded chunked subset with {len(dataset)} rows for Single-shot question generation.") - - inference_calls, call_index_mapping = _build_inference_calls(dataset, stage_config) - if not inference_calls: - logger.warning("No inference calls were created for single_shot_question_generation.") - return - - responses_dict = _execute_inference(inference_calls, config) - if not responses_dict: - return - - question_dataset = _process_responses_and_build_dataset(responses_dict, call_index_mapping, stage_config) - if question_dataset is None or len(question_dataset) == 0: - logger.warning("No valid questions produced in single_shot_question_generation.") - return - - custom_save_dataset(dataset=question_dataset, config=config, subset="single_shot_questions") - logger.success("Single-shot question generation completed successfully.") - - -def _load_stage_config(config: dict[str, Any]) -> SingleShotQuestionGenerationConfig: - """ - Extract the stage-specific configuration from the pipeline config. - """ - pipeline_config = config.get("pipeline", {}) - stage_config_dict = pipeline_config.get("single_shot_question_generation", {}) - chunk_sampling_cfg = stage_config_dict.get("chunk_sampling", {}) - - # For readability: if len(chunk_sampling_cfg) == 0 - if len(chunk_sampling_cfg) == 0: - chunk_sampling = ChunkSamplingConfig() - else: - chunk_sampling = ChunkSamplingConfig( - mode=chunk_sampling_cfg.get("mode", "all"), - value=chunk_sampling_cfg.get("value", 1.0), - random_seed=chunk_sampling_cfg.get("random_seed", 42), - ) - - return SingleShotQuestionGenerationConfig( - run=stage_config_dict.get("run", False), - source_subset=stage_config_dict.get("source_subset", ""), - output_subset=stage_config_dict.get("output_subset", ""), - additional_instructions=stage_config_dict.get("additional_instructions", "undergraduate"), - chunk_sampling=chunk_sampling, - question_type=stage_config_dict.get("question_type", "open-ended"), - ) - - -def _sample_chunks_if_needed( - chunks_list: list[dict[str, Any]], chunk_sampling: ChunkSamplingConfig -) -> list[dict[str, Any]]: - """ - Samples chunks according to user configuration, either by percentage or count. - Returns all chunks if no sampling configuration is provided or invalid. - """ - if not chunks_list: - return chunks_list - - mode = chunk_sampling.mode.lower() - value = chunk_sampling.value - random_seed = chunk_sampling.random_seed - random.seed(random_seed) - - total_chunks = len(chunks_list) - if total_chunks == 0: - return chunks_list - - if mode == "percentage": - # e.g., value = 0.5 => sample 50% of the chunks - num_selected = int(total_chunks * float(value)) - num_selected = max(0, min(num_selected, total_chunks)) - if num_selected < total_chunks: - return random.sample(chunks_list, num_selected) - return chunks_list - - elif mode == "count": - # e.g., value = 10 => sample 10 chunks - num_selected = min(int(value), total_chunks) - if num_selected < total_chunks: - return random.sample(chunks_list, num_selected) - return chunks_list - - # "all" or unrecognized mode => return all - return chunks_list - - -def _build_inference_calls(dataset, stage_config: SingleShotQuestionGenerationConfig): - """ - Create the InferenceCall objects needed for single-shot question generation. - Returns the list of calls and a parallel mapping of (row_index, doc_id, chunk_id). - """ - - if stage_config.question_type == "multi-choice": - system_prompt = QUESTION_GENERATION_SYSTEM_PROMPT_MULTI - else: - system_prompt = QUESTION_GENERATION_SYSTEM_PROMPT - - system_message = {"role": "system", "content": system_prompt} - inference_calls = [] - call_index_mapping = [] - - for row_index, row in enumerate(dataset): - doc_row = DocumentRow( - document_summary=row.get("document_summary", "No summary available."), - document_filename=row.get("document_filename", f"Document_{row_index}"), - document_id=row.get("document_id", f"doc_{row_index}"), - chunks=row.get("chunks", []), - ) - - single_hop_chunks = doc_row.chunks - if not isinstance(single_hop_chunks, list) or not single_hop_chunks: - logger.debug(f"No chunks found in row index={row_index} for doc_id={doc_row.document_id}. Skipping row.") - continue - - chosen_chunks = _sample_chunks_if_needed(single_hop_chunks, stage_config.chunk_sampling) - additional_instructions = stage_config.additional_instructions - - # Build user messages for each chunk - for chunk_index, chunk_info in enumerate(chosen_chunks): - if not isinstance(chunk_info, dict): - chunk_text = str(chunk_info) - chunk_id = f"{doc_row.document_id}_{chunk_index}" - else: - chunk_text = chunk_info.get("chunk_text", "") - chunk_id = chunk_info.get("chunk_id", f"{doc_row.document_id}_{chunk_index}") - - user_prompt_str = QUESTION_GENERATION_USER_PROMPT.format( - title=doc_row.document_filename, - document_summary=doc_row.document_summary, - text_chunk=chunk_text, - additional_instructions=additional_instructions, - ) - user_message = {"role": "user", "content": user_prompt_str} - - inference_call = InferenceCall(messages=[system_message, user_message], tags=["single_shot_qa"]) - inference_calls.append(inference_call) - call_index_mapping.append((row_index, doc_row.document_id, chunk_id)) - - return inference_calls, call_index_mapping - - -def _execute_inference(inference_calls, config: dict[str, Any]): - """ - Sends the prepared inference calls to the LLM(s). Returns a dict of responses. - """ - logger.info(f"Sending {len(inference_calls)} calls to inference for single-shot question generation.") - try: - return run_inference( - config=config, - step_name="single_shot_question_generation", - inference_calls=inference_calls, - ) - except Exception as err: - logger.error(f"Inference failed for single_shot_question_generation: {err}") - return {} - - -def _process_responses_and_build_dataset( - responses_dict: dict[str, list[str]], - call_index_mapping: list[tuple], - stage_config: SingleShotQuestionGenerationConfig, -) -> Dataset: - """ - Take the LLM responses, parse them, and build a Hugging Face Dataset - of single-shot question rows. - """ - question_dataset_rows = [] - - for model_name, model_responses in responses_dict.items(): - logger.info(f"Processing {len(model_responses)} responses from model: {model_name}") - if len(model_responses) != len(call_index_mapping): - logger.error( - f"Model '{model_name}' returned {len(model_responses)} responses but expected {len(call_index_mapping)}. Mismatch." - ) - - for idx, raw_response in enumerate(model_responses): - if idx >= len(call_index_mapping): - break - - row_index, doc_id, chunk_id = call_index_mapping[idx] - qa_pairs = parse_qa_pairs_from_response(raw_response) - - # If parsing fails or returns nothing, still create a fallback row - if not qa_pairs: - logger.warning( - f"No parseable JSON found (or empty list) for row_index={row_index}, chunk_id={chunk_id}, model={model_name}. Creating fallback row." - ) - continue - - # Otherwise, process each QA pair - for pair in qa_pairs: - try: - # Shuffle MCQ before extracting fields - pair = shuffle_mcq(pair) - # Safely extract data from pair - question_text = str(pair.get("question", "")).strip() - answer_text = str(pair.get("answer", "")).strip() - choices = _validate_list(pair.get("choices", [])) - difficulty_val = _force_int_in_range(pair.get("estimated_difficulty", 5), 1, 10) - question_type = str(pair.get("question_type", "unknown")) - thought_process = str(pair.get("thought_process", "")) - citations = pair.get("citations", []) - if not isinstance(citations, list): - citations = [] - - if not question_text: - logger.debug(f"Empty question found; skipping this QA pair (row_index={row_index}).") - continue - - # Build final row - question_row = SingleHopQuestionRow( - chunk_id=chunk_id, - document_id=doc_id, - additional_instructions=stage_config.additional_instructions, - question=question_text, - self_answer=answer_text, - choices=choices, - estimated_difficulty=difficulty_val, - self_assessed_question_type=question_type, - generating_model=model_name, - thought_process=thought_process, - raw_response=raw_response, - citations=citations, - ) - question_dataset_rows.append(question_row.__dict__) - except Exception as e: - logger.error(f"Error processing QA pair for row_index={row_index}, chunk_id={chunk_id}: {e}") - continue - - if not question_dataset_rows: - return None - - logger.info(f"Constructing final dataset with {len(question_dataset_rows)} single-hop questions.") - column_names = list(question_dataset_rows[0].keys()) - final_data = {column: [row[column] for row in question_dataset_rows] for column in column_names} - return Dataset.from_dict(final_data) diff --git a/yourbench/pipeline/summarization.py b/yourbench/pipeline/summarization.py index 750289a7..15ef74b0 100644 --- a/yourbench/pipeline/summarization.py +++ b/yourbench/pipeline/summarization.py @@ -11,12 +11,7 @@ from yourbench.utils.chunking_utils import split_into_token_chunks from yourbench.utils.dataset_engine import custom_load_dataset, custom_save_dataset from yourbench.utils.parsing_engine import extract_content_from_xml_tags -from yourbench.utils.inference_engine import InferenceCall, run_inference - - -############################ -# Internal helper functions # -############################ +from yourbench.utils.inference.inference_core import InferenceCall, run_inference def _build_chunk_calls( @@ -144,11 +139,6 @@ def _merge_final_summaries( return updated_final_summaries -################# -# Stage runner # -################# - - def run(config: dict[str, Any]) -> None: """Executes the hierarchical summarization pipeline.""" stage_cfg = config.get("pipeline", {}).get("summarization", {}) From 0e8d38921fc15da9fa19b4b49700be2eed4e8544 Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Wed, 4 Jun 2025 23:34:38 +0200 Subject: [PATCH 58/69] Split inference logic into modular files --- .../utils/inference/inference_builders.py | 71 +++++++ .../inference_core.py} | 192 ++++-------------- .../utils/inference/inference_tracking.py | 116 +++++++++++ 3 files changed, 225 insertions(+), 154 deletions(-) create mode 100644 yourbench/utils/inference/inference_builders.py rename yourbench/utils/{inference_engine.py => inference/inference_core.py} (70%) create mode 100644 yourbench/utils/inference/inference_tracking.py diff --git a/yourbench/utils/inference/inference_builders.py b/yourbench/utils/inference/inference_builders.py new file mode 100644 index 00000000..2a266cc2 --- /dev/null +++ b/yourbench/utils/inference/inference_builders.py @@ -0,0 +1,71 @@ +from typing import List +from dataclasses import dataclass + +from loguru import logger + +from yourbench.utils.prompts import QUESTION_GENERATION_USER_PROMPT, MULTI_HOP_QUESTION_GENERATION_USER_PROMPT +from yourbench.utils.chunking_utils import sample_multihop_groups, sample_single_hop_chunks +from yourbench.utils.inference.inference_core import InferenceCall + + +@dataclass +class InferenceJob: + inference_calls: List[InferenceCall] + + +def build_single_shot_inference_calls(dataset, system_msg, stage_cfg, sampling_cfg): + calls = [] + index_map = [] + + for idx, row in enumerate(dataset): + document_chunks = row.get("chunks") or [] + selected_chunks = sample_single_hop_chunks(document_chunks, sampling_cfg) + + for ch_idx, chunk in enumerate(selected_chunks): + chunk_id = chunk.get("chunk_id", f"{idx}_{ch_idx}") + chunk_text = chunk.get("chunk_text", "") + user_msg = { + "role": "user", + "content": QUESTION_GENERATION_USER_PROMPT.format( + title=row.get("document_filename", f"doc_{idx}"), + document_summary=row.get("document_summary", ""), + text_chunk=chunk_text, + additional_instructions=stage_cfg.get("additional_instructions", ""), + ), + } + calls.append(InferenceCall(messages=[system_msg, user_msg], tags=["single_shot_qa"])) + index_map.append((idx, row.get("document_id", f"doc_{idx}"), chunk_id)) + + return calls, index_map + + +def build_multi_hop_inference_calls(dataset, system_msg, stage_cfg): + calls = [] + index_map = [] + + for idx, row in enumerate(dataset): + groups = sample_multihop_groups(row.get("multihop_chunks") or [], stage_cfg.get("chunk_sampling", {})) + for group in groups: + # TODO how it's possible here? + if not isinstance(group, dict): + logger.warning("Multihop groups are not a dict, skipping") + continue + chunk_ids = group.get("chunk_ids", []) + texts = group.get("chunks_text", []) + if not texts: + logger.warning("Chunks texts are empty, skipping") + continue + full_text = "".join([f"{t}\n" for i, t in enumerate(texts)]) + user_msg = { + "role": "user", + "content": MULTI_HOP_QUESTION_GENERATION_USER_PROMPT.format( + title=row.get("document_filename", ""), + document_summary=row.get("document_summary", ""), + chunks=full_text, + additional_instructions=stage_cfg.get("additional_instructions", ""), + ), + } + calls.append(InferenceCall(messages=[system_msg, user_msg], tags=["multi_hop_qa"])) + index_map.append((idx, row.get("document_id", f"doc_{idx}"), chunk_ids)) + + return calls, index_map diff --git a/yourbench/utils/inference_engine.py b/yourbench/utils/inference/inference_core.py similarity index 70% rename from yourbench/utils/inference_engine.py rename to yourbench/utils/inference/inference_core.py index a9a4c98b..e313ce72 100644 --- a/yourbench/utils/inference_engine.py +++ b/yourbench/utils/inference/inference_core.py @@ -1,36 +1,25 @@ -""" -Inference Engine For Yourbench - Now with true concurrency throttling and cost tracking. -""" - import os -import csv import time import uuid -import atexit import asyncio -import datetime -import collections from typing import Any, Dict, List, Optional from dataclasses import field, dataclass -import tiktoken -from dotenv import load_dotenv from loguru import logger from tqdm.asyncio import tqdm_asyncio from huggingface_hub import AsyncInferenceClient +from yourbench.utils.inference.inference_tracking import ( + _count_tokens, + _get_encoding, + _log_individual_call, + _count_message_tokens, + _update_aggregate_cost, +) -load_dotenv() - GLOBAL_TIMEOUT = 300 -# Using defaultdict for easier accumulation -_cost_data = collections.defaultdict(lambda: {"input_tokens": 0, "output_tokens": 0, "calls": 0}) -_individual_log_file = os.path.join("logs", "inference_cost_log_individual.csv") -_aggregate_log_file = os.path.join("logs", "inference_cost_log_aggregate.csv") -_individual_header_written = False - @dataclass class Model: @@ -69,109 +58,40 @@ class InferenceCall: seed: Optional[int] = None -@dataclass -class InferenceJob: - inference_calls: List[InferenceCall] - - -def _ensure_logs_dir(): - """Ensures the logs directory exists.""" - os.makedirs("logs", exist_ok=True) - - -def _get_encoding(encoding_name: str = "cl100k_base") -> tiktoken.Encoding: - """Gets a tiktoken encoding, defaulting to cl100k_base with fallback.""" - try: - return tiktoken.get_encoding(encoding_name) - except Exception as e: - logger.warning(f"Failed to get encoding '{encoding_name}'. Falling back to 'cl100k_base'. Error: {e}") - return tiktoken.get_encoding("cl100k_base") - - -def _count_tokens(text: str, encoding: tiktoken.Encoding) -> int: - """Counts tokens in a single string.""" - if not text: - return 0 - try: - return len(encoding.encode(text)) - except Exception as e: - logger.error(f"Error counting tokens: {e}") - return 0 - - -def _count_message_tokens(messages: List[Dict[str, str]], encoding: tiktoken.Encoding) -> int: - """Counts tokens in a list of messages, approximating OpenAI's format.""" - num_tokens = 0 - # Approximation based on OpenAI's cookbook: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb - # This might not be perfectly accurate for all models/providers but is a reasonable estimate. - tokens_per_message = 3 - tokens_per_name = 1 - - for message in messages: - num_tokens += tokens_per_message - for key, value in message.items(): - if value: - num_tokens += _count_tokens(str(value), encoding) - if key == "name": - num_tokens += tokens_per_name - num_tokens += 3 - return num_tokens - - -def _log_individual_call(model_name: str, input_tokens: int, output_tokens: int, tags: List[str], encoding_name: str): - """Logs a single inference call's cost details.""" - global _individual_header_written - try: - _ensure_logs_dir() - is_new_file = not os.path.exists(_individual_log_file) - mode = "a" if not is_new_file else "w" - - with open(_individual_log_file, mode, newline="", encoding="utf-8") as f: - writer = csv.writer(f) - # Write header only if the file is new or header wasn't written yet in this run - if is_new_file or not _individual_header_written: - writer.writerow(["timestamp", "model_name", "stage", "input_tokens", "output_tokens", "encoding_used"]) - _individual_header_written = True - - stage = ";".join(tags) if tags else "unknown" - timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat() - writer.writerow([timestamp, model_name, stage, input_tokens, output_tokens, encoding_name]) - except Exception as e: - logger.error(f"Failed to write to individual cost log: {e}") - - -def _update_aggregate_cost(model_name: str, input_tokens: int, output_tokens: int): - """Updates the global dictionary for aggregate costs.""" - try: - _cost_data[model_name]["input_tokens"] += input_tokens - _cost_data[model_name]["output_tokens"] += output_tokens - _cost_data[model_name]["calls"] += 1 - except Exception as e: - logger.error(f"Failed to update aggregate cost data: {e}") - +def _load_models(base_config: Dict[str, Any], step_name: str) -> List[Model]: + """ + Load only the models assigned to this step from the config's 'model_list' and 'model_roles'. + If no model role is defined for the step, use the first model from model_list. + """ + all_configured_models = base_config.get("model_list", []) + role_models = base_config.get("model_roles", {}).get(step_name, []) -def _write_aggregate_log(): - """Writes the aggregated cost data to a file at program exit.""" - try: - if not _cost_data: - logger.info("No cost data collected, skipping aggregate log.") - return - - _ensure_logs_dir() - logger.info(f"Writing aggregate cost log to {_aggregate_log_file}") - with open(_aggregate_log_file, "w", newline="", encoding="utf-8") as f: - writer = csv.writer(f) - writer.writerow(["model_name", "total_input_tokens", "total_output_tokens", "total_calls"]) - for model_name, data in sorted(_cost_data.items()): - writer.writerow([model_name, data["input_tokens"], data["output_tokens"], data["calls"]]) - logger.success(f"Aggregate cost log successfully written to {_aggregate_log_file}") - except Exception as e: - # Use print here as logger might be shutting down during atexit - print(f"ERROR: Failed to write aggregate cost log: {e}", flush=True) + # If no role models are defined for this step, use the first model from model_list + if not role_models and all_configured_models: + first_model_config = all_configured_models[0] + logger.info( + "No models defined in model_roles for step '{}'. Using the first model from model_list: {}", + step_name, + first_model_config["model_name"], + ) + return [ + Model(**{**first_model_config, "encoding_name": first_model_config.get("encoding_name", "cl100k_base")}) + ] + # Filter out only those with a matching 'model_name' + matched = [] + for m_config in all_configured_models: + if m_config["model_name"] in role_models: + model_instance = Model(**{**m_config, "encoding_name": m_config.get("encoding_name", "cl100k_base")}) + matched.append(model_instance) -# Register the aggregate log function to run at exit -atexit.register(_write_aggregate_log) + logger.info( + "Found {} models in config for step '{}': {}", + len(matched), + step_name, + [m.model_name for m in matched], + ) + return matched async def _get_response(model: Model, inference_call: InferenceCall) -> str: @@ -357,42 +277,6 @@ async def _run_inference_async_helper( return responses -def _load_models(base_config: Dict[str, Any], step_name: str) -> List[Model]: - """ - Load only the models assigned to this step from the config's 'model_list' and 'model_roles'. - If no model role is defined for the step, use the first model from model_list. - """ - all_configured_models = base_config.get("model_list", []) - role_models = base_config.get("model_roles", {}).get(step_name, []) - - # If no role models are defined for this step, use the first model from model_list - if not role_models and all_configured_models: - first_model_config = all_configured_models[0] - logger.info( - "No models defined in model_roles for step '{}'. Using the first model from model_list: {}", - step_name, - first_model_config["model_name"], - ) - return [ - Model(**{**first_model_config, "encoding_name": first_model_config.get("encoding_name", "cl100k_base")}) - ] - - # Filter out only those with a matching 'model_name' - matched = [] - for m_config in all_configured_models: - if m_config["model_name"] in role_models: - model_instance = Model(**{**m_config, "encoding_name": m_config.get("encoding_name", "cl100k_base")}) - matched.append(model_instance) - - logger.info( - "Found {} models in config for step '{}': {}", - len(matched), - step_name, - [m.model_name for m in matched], - ) - return matched - - def run_inference( config: Dict[str, Any], step_name: str, inference_calls: List[InferenceCall] ) -> Dict[str, List[str]]: diff --git a/yourbench/utils/inference/inference_tracking.py b/yourbench/utils/inference/inference_tracking.py new file mode 100644 index 00000000..a3bcb7b9 --- /dev/null +++ b/yourbench/utils/inference/inference_tracking.py @@ -0,0 +1,116 @@ +import os +import csv +import atexit +import datetime +import collections +from typing import Dict, List + +import tiktoken +from loguru import logger + + +# Using defaultdict for easier accumulation +_cost_data = collections.defaultdict(lambda: {"input_tokens": 0, "output_tokens": 0, "calls": 0}) +_individual_log_file = os.path.join("logs", "inference_cost_log_individual.csv") +_aggregate_log_file = os.path.join("logs", "inference_cost_log_aggregate.csv") +_individual_header_written = False + + +def _get_encoding(encoding_name: str = "cl100k_base") -> tiktoken.Encoding: + """Gets a tiktoken encoding, defaulting to cl100k_base with fallback.""" + try: + return tiktoken.get_encoding(encoding_name) + except Exception as e: + logger.warning(f"Failed to get encoding '{encoding_name}'. Falling back to 'cl100k_base'. Error: {e}") + return tiktoken.get_encoding("cl100k_base") + + +def _ensure_logs_dir(): + """Ensures the logs directory exists.""" + os.makedirs("logs", exist_ok=True) + + +def _count_tokens(text: str, encoding: tiktoken.Encoding) -> int: + """Counts tokens in a single string.""" + if not text: + return 0 + try: + return len(encoding.encode(text)) + except Exception as e: + logger.error(f"Error counting tokens: {e}") + return 0 + + +def _count_message_tokens(messages: List[Dict[str, str]], encoding: tiktoken.Encoding) -> int: + """Counts tokens in a list of messages, approximating OpenAI's format.""" + num_tokens = 0 + # Approximation based on OpenAI's cookbook: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb + # This might not be perfectly accurate for all models/providers but is a reasonable estimate. + tokens_per_message = 3 + tokens_per_name = 1 + + for message in messages: + num_tokens += tokens_per_message + for key, value in message.items(): + if value: + num_tokens += _count_tokens(str(value), encoding) + if key == "name": + num_tokens += tokens_per_name + num_tokens += 3 + return num_tokens + + +def _log_individual_call(model_name: str, input_tokens: int, output_tokens: int, tags: List[str], encoding_name: str): + """Logs a single inference call's cost details.""" + global _individual_header_written + try: + _ensure_logs_dir() + is_new_file = not os.path.exists(_individual_log_file) + mode = "a" if not is_new_file else "w" + + with open(_individual_log_file, mode, newline="", encoding="utf-8") as f: + writer = csv.writer(f) + # Write header only if the file is new or header wasn't written yet in this run + if is_new_file or not _individual_header_written: + writer.writerow(["timestamp", "model_name", "stage", "input_tokens", "output_tokens", "encoding_used"]) + _individual_header_written = True + + stage = ";".join(tags) if tags else "unknown" + timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat() + writer.writerow([timestamp, model_name, stage, input_tokens, output_tokens, encoding_name]) + except Exception as e: + logger.error(f"Failed to write to individual cost log: {e}") + + +def _update_aggregate_cost(model_name: str, input_tokens: int, output_tokens: int): + """Updates the global dictionary for aggregate costs.""" + try: + _cost_data[model_name]["input_tokens"] += input_tokens + _cost_data[model_name]["output_tokens"] += output_tokens + _cost_data[model_name]["calls"] += 1 + except Exception as e: + logger.error(f"Failed to update aggregate cost data: {e}") + + +def _write_aggregate_log(): + """Writes the aggregated cost data to a file at program exit.""" + try: + if not _cost_data: + logger.info("No cost data collected, skipping aggregate log.") + return + + _ensure_logs_dir() + logger.info(f"Writing aggregate cost log to {_aggregate_log_file}") + with open(_aggregate_log_file, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(["model_name", "total_input_tokens", "total_output_tokens", "total_calls"]) + for model_name, data in sorted(_cost_data.items()): + writer.writerow([model_name, data["input_tokens"], data["output_tokens"], data["calls"]]) + logger.success(f"Aggregate cost log successfully written to {_aggregate_log_file}") + except Exception as e: + # Use print here as logger might be shutting down during atexit + print(f"ERROR: Failed to write aggregate cost log: {e}", flush=True) + + +# Register the aggregate log function to run at exit +atexit.register(_write_aggregate_log) From c1f144657a3c1838d1089b543f931948d14959c3 Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Wed, 4 Jun 2025 23:34:46 +0200 Subject: [PATCH 59/69] Update parsing and QA model logic --- yourbench/utils/parsing_engine.py | 327 ++++++++++++++++++++++------- yourbench/utils/question_models.py | 160 ++++++++++++++ 2 files changed, 411 insertions(+), 76 deletions(-) create mode 100644 yourbench/utils/question_models.py diff --git a/yourbench/utils/parsing_engine.py b/yourbench/utils/parsing_engine.py index a6714efc..5c420a52 100644 --- a/yourbench/utils/parsing_engine.py +++ b/yourbench/utils/parsing_engine.py @@ -2,10 +2,79 @@ import json import random import hashlib -from typing import Any +from typing import Any, Optional from loguru import logger +from yourbench.utils.question_models import QuestionRow, validate_list, force_int_in_range + + +# JSON parsing functions + + +def _attempt_json_parse(json_str: str) -> Any: + """ + Attempt to parse a JSON string. Return parsed object if success, + or None if parsing fails. + """ + try: + return json.loads(json_str) + except Exception: + return None + + +def _maybe_strip_triple_backticks(text_in: str) -> str: + """ + Removes triple backticks (``` or ```json) from the beginning + and end of a string, if present. + """ + if not text_in or not isinstance(text_in, str): + return "" + try: + pattern = r"^\s*```(?:json)?\s*([\s\S]*?)\s*```$" + match = re.match(pattern, text_in) + if match: + return match.group(1) + except Exception as e: + logger.debug(f"Error stripping backticks: {e}") + return text_in + + +def _best_effort_json_extract(full_text: str) -> list[str]: + """ + Collect bracket-delimited substrings that might be valid JSON. + Returns a list of candidates (which may be empty). + """ + if not full_text or not isinstance(full_text, str): + return [] + candidates = [] + try: + pattern = r"([\[{].*?[\]}])" + matches = re.findall(pattern, full_text, flags=re.DOTALL) + for match_text in matches: + if (match_text.startswith("[") and match_text.endswith("]")) or ( + match_text.startswith("{") and match_text.endswith("}") + ): + candidates.append(match_text.strip()) + except Exception as e: + logger.debug(f"Error in best-effort JSON extraction: {e}") + return candidates + + +def _extract_tag_content(text: str, tag: str) -> str: + """ + Extract text enclosed in ... from the given string. + Returns an empty string if the tag is not found. + """ + try: + pattern = rf"<{tag}\s*>([\s\S]*?)" + match = re.search(pattern, text) + if match: + return match.group(1).strip() + except Exception as e: + logger.debug(f"Error extracting tag content for '{tag}': {e}") + return "" + def extract_content_from_xml_tags(full_content, xml_tag): # This function extracts the content between the XML tags @@ -87,69 +156,199 @@ def parse_qa_pairs_from_response(raw_response: str) -> list[dict[str, Any]]: return [] -def _extract_tag_content(text: str, tag: str) -> str: +# QA response parsing utils + +OPEN_ENDED_TYPES = { + "analytical", + "application-based", + "clarification", + "counterfactual", + "conceptual", + "true-false", + "factual", + "open-ended", + "false-premise", + "edge-case", +} + +MULTI_CHOICE_TYPES = { + "analytical", + "application-based", + "clarification", + "counterfactual", + "conceptual", + "true-false", + "factual", + "false-premise", + "edge-case", +} + + +def normalize_open_ended(pair: dict[str, Any]) -> Optional[dict[str, Any]]: """ - Extract text enclosed in ... from the given string. - Returns an empty string if the tag is not found. + Ensures open-ended questions are valid. + Returns None if the entry should be skipped. """ - try: - pattern = rf"<{tag}\s*>([\s\S]*?)" - match = re.search(pattern, text) - if match: - return match.group(1).strip() - except Exception as e: - logger.debug(f"Error extracting tag content for '{tag}': {e}") - return "" + pair = dict(pair) # defensive copy + mode = pair.get("question_mode", "").strip().lower() + q_type = pair.get("question_type", "").strip().lower() + if mode != "open-ended": + return pair -def _maybe_strip_triple_backticks(text_in: str) -> str: - """ - Removes triple backticks (``` or ```json) from the beginning - and end of a string, if present. - """ - if not text_in or not isinstance(text_in, str): - return "" - try: - pattern = r"^\s*```(?:json)?\s*([\s\S]*?)\s*```$" - match = re.match(pattern, text_in) - if match: - return match.group(1) - except Exception as e: - logger.debug(f"Error stripping backticks: {e}") - return text_in + if q_type not in OPEN_ENDED_TYPES: + logger.warning(f"Inconsistent open-ended question_type: '{q_type}'") + return None + # No choices for open-ended + pair["choices"] = [] -def _best_effort_json_extract(full_text: str) -> list[str]: - """ - Collect bracket-delimited substrings that might be valid JSON. - Returns a list of candidates (which may be empty). - """ - if not full_text or not isinstance(full_text, str): - return [] - candidates = [] - try: - pattern = r"([\[{].*?[\]}])" - matches = re.findall(pattern, full_text, flags=re.DOTALL) - for match_text in matches: - if (match_text.startswith("[") and match_text.endswith("]")) or ( - match_text.startswith("{") and match_text.endswith("}") - ): - candidates.append(match_text.strip()) - except Exception as e: - logger.debug(f"Error in best-effort JSON extraction: {e}") - return candidates + answer = pair.get("answer", "").strip() + if len(answer) == 1 and answer.upper() in {"A", "B", "C", "D"}: + # Misclassified multiple choice + return None + return pair -def _attempt_json_parse(json_str: str) -> Any: + +def normalize_multi_choice(pair: dict[str, Any]) -> Optional[dict[str, Any]]: """ - Attempt to parse a JSON string. Return parsed object if success, - or None if parsing fails. + Ensures multiple-choice questions are valid. + Returns None if the entry should be skipped. """ - try: - return json.loads(json_str) - except Exception: + pair = dict(pair) + mode = pair.get("question_mode", "").strip().lower() + q_type = pair.get("question_type", "").strip().lower() + + if mode != "multi-choice": + return pair + + if q_type not in MULTI_CHOICE_TYPES: + logger.warning(f"Inconsistent multiple-choice question_type: '{q_type}'") return None + choices = validate_list(pair.get("choices", [])) + if len(choices) != 4: + logger.warning("MCQ must have exactly 4 choices.") + return None + + pair["choices"] = choices + return pair + + +def parse_single_shot_responses(responses, index_map, stage_cfg): + rows = [] + question_mode = str(stage_cfg.get("question_mode", "open-ended")).strip().lower() + + for model, replies in responses.items(): + if len(replies) != len(index_map): + logger.error(f"Mismatch: model '{model}' replies={len(replies)}, expected={len(index_map)}") + continue + + for i, reply in enumerate(replies): + parsed_qa_pairs = parse_qa_pairs_from_response(reply) + if not parsed_qa_pairs: + logger.warning(f"No parseable QA pairs at index {i}.") + continue + + for pair in parsed_qa_pairs: + try: + pair = shuffle_mcq(pair) + pair["question_mode"] = question_mode + + if question_mode == "open-ended": + pair = normalize_open_ended(pair) + if pair is None: + continue + choices = [] + elif question_mode == "multi-choice": + pair = normalize_multi_choice(pair) + if pair is None: + continue + choices = pair["choices"] + else: + logger.warning(f"Unsupported question_mode: {question_mode}") + continue + + citations = validate_list(pair.get("citations", [])) + + rows.append( + QuestionRow( + chunk_id=index_map[i][2], + source_chunk_ids=None, + document_id=index_map[i][1], + additional_instructions=stage_cfg.get("additional_instructions", ""), + question=str(pair.get("question", "")).strip(), + self_answer=str(pair.get("answer", "")).strip(), + choices=choices, + estimated_difficulty=force_int_in_range(pair.get("estimated_difficulty", 5), 1, 10), + self_assessed_question_type=str(pair.get("question_type", "")).strip(), + question_mode=pair["question_mode"], + generating_model=model, + thought_process=str(pair.get("thought_process", "")), + raw_response=reply, + citations=citations, + ).to_dict(format="single-hop") + ) + except Exception as e: + logger.error(f"Error parsing QA pair at index {i}: {e}") + continue + + return rows + + +def parse_multi_hop_responses(responses, index_map, stage_cfg): + rows = [] + question_mode = str(stage_cfg.get("question_mode", "open-ended")).strip().lower() + + for model, replies in responses.items(): + for i, raw in enumerate(replies): + parsed = parse_qa_pairs_from_response(raw) + for pair in parsed: + try: + pair = shuffle_mcq(pair) + pair["question_mode"] = question_mode + + if question_mode == "open-ended": + pair = normalize_open_ended(pair) + if pair is None: + continue + choices = [] + elif question_mode == "multi-choice": + pair = normalize_multi_choice(pair) + if pair is None: + continue + choices = pair["choices"] + else: + logger.warning(f"Unsupported question_mode: {question_mode}") + continue + + citations = validate_list(pair.get("citations", [])) + + rows.append( + QuestionRow( + chunk_id=None, + source_chunk_ids=index_map[i][2], + document_id=index_map[i][1], + additional_instructions=stage_cfg.get("additional_instructions", ""), + question=str(pair.get("question", "")).strip(), + self_answer=str(pair.get("answer", "")).strip(), + choices=choices, + estimated_difficulty=force_int_in_range(pair.get("estimated_difficulty", 5), 1, 10), + self_assessed_question_type=str(pair.get("question_type", "")).strip(), + question_mode=pair["question_mode"], + generating_model=model, + thought_process=str(pair.get("thought_process", "")), + raw_response=raw, + citations=citations, + ).to_dict(format="multi-hop") + ) + except Exception as e: + logger.warning(f"Parse error in multi-hop QA for doc {index_map[i][1]}: {e}") + continue + + return rows + def shuffle_mcq(question_dict: dict) -> dict: """ @@ -186,27 +385,3 @@ def shuffle_mcq(question_dict: dict) -> dict: question_dict["answer"] = new_answer_letter return question_dict - - -def _force_int_in_range(value: Any, min_val: int, max_val: int) -> int: - """ - Convert a value to int and clamp it between min_val and max_val. - """ - try: - ivalue = int(value) - except (ValueError, TypeError): - ivalue = (min_val + max_val) // 2 - return max(min_val, min(ivalue, max_val)) - - -def _validate_list(some_list: list[str]) -> list[str]: - """ - Force possible list of strings to be a list of strings - """ - if not isinstance(some_list, list): - return [] - - try: - return [str(value) for value in some_list] - except Exception: - return [] diff --git a/yourbench/utils/question_models.py b/yourbench/utils/question_models.py new file mode 100644 index 00000000..a475b256 --- /dev/null +++ b/yourbench/utils/question_models.py @@ -0,0 +1,160 @@ +from __future__ import annotations +from typing import Any, Dict, List, Optional +from dataclasses import field, dataclass + + +def force_int_in_range(value: Any, min_val: int, max_val: int) -> int: + try: + ivalue = int(value) + except (ValueError, TypeError): + ivalue = (min_val + max_val) // 2 + return max(min_val, min(ivalue, max_val)) + + +def validate_list(some_list: list[str]) -> list[str]: + if not isinstance(some_list, list): + return [] + try: + return [str(value) for value in some_list] + except Exception: + return [] + + +@dataclass +class QuestionRow: + document_id: str + additional_instructions: str + question: str + self_answer: str + estimated_difficulty: int + self_assessed_question_type: str + question_mode: str + generating_model: str + thought_process: str + raw_response: str + + citations: List[str] = field(default_factory=list) + choices: Optional[List[str]] = field(default_factory=list) + + chunk_id: Optional[str] = None + source_chunk_ids: Optional[List[str]] = None + + def __post_init__(self) -> None: + self.question = str(self.question).strip() + self.self_answer = str(self.self_answer).strip() + self.estimated_difficulty = force_int_in_range(self.estimated_difficulty, 1, 10) + self.self_assessed_question_type = str(self.self_assessed_question_type).strip() + self.thought_process = str(self.thought_process) + self.citations = validate_list(self.citations) + self.question_mode = str(self.question_mode).strip().lower() + + if self.question_mode == "multi-choice": + self.choices = validate_list(self.choices) + if len(self.choices) != 4: + raise ValueError("Multi-choice questions must have exactly 4 choices.") + else: + self.choices = [] + + if self.chunk_id and self.source_chunk_ids: + raise ValueError("Cannot have both chunk_id and source_chunk_ids.") + if not self.chunk_id and not self.source_chunk_ids: + raise ValueError("Must have either chunk_id or source_chunk_ids.") + + @property + def answer(self) -> str: + return self.self_answer + + @property + def question_type(self) -> str: + return self.self_assessed_question_type + + def is_multi_hop(self) -> bool: + return self.source_chunk_ids is not None + + def is_single_hop(self) -> bool: + return self.chunk_id is not None + + @classmethod + def from_single_hop( + cls, + pair: Dict[str, Any], + chunk_id: str, + document_id: str, + model: str, + raw_response: str, + additional_instructions: str = "", + ) -> QuestionRow: + return cls( + chunk_id=chunk_id, + source_chunk_ids=None, + document_id=document_id, + additional_instructions=additional_instructions, + question=str(pair.get("question", "")).strip(), + self_answer=str(pair.get("answer", "")).strip(), + choices=pair.get("choices"), + estimated_difficulty=force_int_in_range(pair.get("estimated_difficulty", 5), 1, 10), + self_assessed_question_type=str(pair.get("question_type", "")).strip(), + question_mode=str(pair.get("question_mode", "")).strip().lower(), + generating_model=model, + thought_process=str(pair.get("thought_process", "")), + raw_response=raw_response, + citations=validate_list(pair.get("citations", [])), + ) + + @classmethod + def from_multi_hop( + cls, + pair: Dict[str, Any], + source_chunk_ids: List[str], + document_id: str, + model: str, + raw_response: str, + additional_instructions: str = "", + ) -> QuestionRow: + return cls( + chunk_id=None, + source_chunk_ids=source_chunk_ids, + document_id=document_id, + additional_instructions=additional_instructions, + question=str(pair.get("question", "")).strip(), + self_answer=str(pair.get("answer", "")).strip(), + choices=pair.get("choices"), + estimated_difficulty=force_int_in_range(pair.get("estimated_difficulty", 5), 1, 10), + self_assessed_question_type=str(pair.get("question_type", "")).strip(), + question_mode=str(pair.get("question_mode", "")).strip().lower(), + generating_model=model, + thought_process=str(pair.get("thought_process", "")), + raw_response=raw_response, + citations=validate_list(pair.get("citations", [])), + ) + + def to_dict(self, format: str = "unified") -> Dict[str, Any]: + base = { + "document_id": self.document_id, + "additional_instructions": self.additional_instructions, + "question": self.question, + "self_answer": self.self_answer, + "estimated_difficulty": self.estimated_difficulty, + "self_assessed_question_type": self.self_assessed_question_type, + "generating_model": self.generating_model, + "thought_process": self.thought_process, + "raw_response": self.raw_response, + "citations": self.citations, + } + + if self.question_mode == "multi-choice": + base["choices"] = self.choices + + if format == "multi-hop": + return { + **base, + "source_chunk_ids": self.source_chunk_ids, + } + + if format == "single-hop": + return { + **base, + "chunk_id": self.chunk_id, + } + + return {**base, "chunk_id": self.chunk_id, "source_chunk_ids": self.source_chunk_ids} From fb87276cce65491246e8cc3b95e5a5bbb81199a9 Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Wed, 4 Jun 2025 23:34:53 +0200 Subject: [PATCH 60/69] Refine question generation prompts --- yourbench/utils/prompts.py | 85 ++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/yourbench/utils/prompts.py b/yourbench/utils/prompts.py index 0dff63fe..7cb4510e 100644 --- a/yourbench/utils/prompts.py +++ b/yourbench/utils/prompts.py @@ -137,66 +137,97 @@ QUESTION_GENERATION_SYSTEM_PROMPT_OUTPUT = """## Output Structure -Present your final output as JSON objects strictly adhering to this Pydantic model within `` XML tags: +This prompt is used exclusively for generating **open-ended** questions. + +Present your final output as a list of JSON objects strictly adhering to this Pydantic model, wrapped within `` XML tags: ```python -class QuestionAnswerPair(BaseModel): +class QuestionRow(BaseModel): thought_process: str # Clear, detailed rationale for selecting question and analysis approach question_type: Literal["analytical", "application-based", "clarification", "counterfactual", "conceptual", "true-false", "factual", "open-ended", "false-premise", "edge-case"] - question: str - answer: str - estimated_difficulty: int # 1-10, calibrated according to additional instructions + question: str # The generated question + answer: str # Full answer to the question + estimated_difficulty: int # Difficulty level from 1 (easy) to 10 (very difficult), calibrated according to additional instructions citations: List[str] # Direct quotes from the text_chunk supporting the answer ``` ## Output Format -Begin by thoughtfully analyzing the provided text_chunk within `` XML tags. Then present the resulting JSON-formatted QuestionAnswerPairs clearly within `` XML tags.""" +Begin by thoughtfully analyzing the provided text_chunk within XML tags. +Then present the resulting list of QuestionRow objects in proper JSON format inside XML tags. + +## Example: + + +Key concept: Semantic chunking and its effect on information retrieval +Facts: Semantic chunking groups semantically similar sentences within token limits +Reasoning cues: Relevance of chunk boundaries for downstream QA tasks + + + +[ + { + "thought_process": "The question evaluates whether the model understands how semantic chunking contributes to retrieval quality. It encourages reflection on how coherence impacts model outputs.", + "question_type": "open-ended", + "question": "How does semantic chunking improve information retrieval performance in large document processing?", + "answer": "Semantic chunking improves retrieval by preserving contextual coherence, allowing models to access more relevant and interpretable chunks during downstream tasks like question answering.", + "estimated_difficulty": 6, + "citations": [ + "Semantic chunking groups related sentences within token boundaries.", + "Coherent chunks help downstream tasks focus on relevant context." + ], + }, + ... +] + +""" QUESTION_GENERATION_SYSTEM_PROMPT_OUTPUT_MULTI = """## Output Structure -Present your final output as JSON objects strictly adhering to this Pydantic model within `` XML tags: +Present your final output as JSON objects strictly adhering to this schema, enclosed within `` XML tags. This structure supports both open-ended and multiple-choice questions. ```python -class MultipleChoiceQuestion(BaseModel): - thought_process: str # Rationale for the question and distractors - question_type: Literal["analytical", "application-based", "clarification", +class QuestionRow(BaseModel): + thought_process: str # Explanation for why this question was generated, including reasoning or distractor logic + question_type: Literal["analytical", "application-based", "clarification", "counterfactual", "conceptual", "true-false", "factual", "false-premise", "edge-case"] - question: str - answer: str # One of "A", "B", "C", or "D" - choices: List[str] # Must contain exactly 4 items - estimated_difficulty: int # 1-10 - citations: List[str] # Direct support from the text_chunk + question: str # The question text + answer: str # One of "A", "B", "C", or "D" + choices: List[str] # Must contain exactly 4 items + estimated_difficulty: int # Integer between 1 (easy) and 10 (difficult) + citations: List[str] # Supporting quotes or phrases from the text ``` ## Output Format -Begin by thoughtfully analyzing the provided within XML tags. Your analysis should identify the key concepts, technical details, and reasoning opportunities found in the text. +Start with a thoughtful analysis of the wrapped inside tags. Identify key concepts, reasoning paths, and challenging content. + +Then output a list of well-structured questions in valid JSON syntax inside tags. -Then present the resulting multiple-choice questions as valid JSON objects within tags, strictly following this structure: +## Example: -- Key concept: ... -- Important facts: ... -- Reasoning opportunities: ... +Key concept: Semantic chunking and its role in preprocessing +Facts: Chunking maintains coherence based on token and semantic similarity +Reasoning cues: Trade-offs in chunk size and overlap [ { - "thought_process": "This question targets understanding of how the chunk explains the purpose of semantic chunking in document processing. Distractors are phrased using near-synonyms or subtle distortions of the true concept.", + "thought_process": "This question targets a conceptual understanding of why semantic chunking is needed. Distractors reflect common misconceptions.", "question_type": "conceptual", - "question": "What is the primary reason for using semantic chunking in document preprocessing?", + "question": "What is the primary benefit of using semantic chunking in document processing?", + "answer": "B", "choices": [ - "(A) To compress the document into fewer tokens.", - "(B) To group content based on semantic similarity and token limits.", - "(C) To translate the text into multiple languages.", - "(D) To strip metadata and formatting from the input file." + "(A) It compresses documents by removing white space.", + "(B) It groups related content within token constraints for coherence.", + "(C) It translates the document into a semantic graph.", + "(D) It removes all non-ASCII characters for parsing." ], - "answer": "B", "estimated_difficulty": 6, "citations": ["Semantic chunking partitions documents into coherent segments based on semantic similarity and token length constraints."] }, From 074c0bbffaf42f03fc2269d0c00717cda19a1fca Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Wed, 4 Jun 2025 23:34:59 +0200 Subject: [PATCH 61/69] Add chunk sampling logic --- yourbench/utils/chunking_utils.py | 67 ++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/yourbench/utils/chunking_utils.py b/yourbench/utils/chunking_utils.py index c774d5e3..999cb407 100644 --- a/yourbench/utils/chunking_utils.py +++ b/yourbench/utils/chunking_utils.py @@ -1,8 +1,22 @@ -from typing import Callable, Optional +import random +from typing import Any, Callable, Optional +from dataclasses import dataclass import tiktoken +CHUNK_MODE_PERCENT = "percentage" +CHUNK_MODE_COUNT = "count" +CHUNK_MODE_ALL = "all" + + +@dataclass +class ChunkSamplingConfig: + mode: str = CHUNK_MODE_ALL + value: float = 1.0 + random_seed: int = 42 + + def split_into_token_chunks( text: str, chunk_tokens: int = 1024, @@ -30,3 +44,54 @@ def split_into_token_chunks( tokens = enc.encode(text, disallowed_special=()) stride = chunk_tokens - overlap return [enc.decode(tokens[i : i + chunk_tokens]) for i in range(0, len(tokens), stride)] + + +def get_sampling_cfg(cfg: dict[str, Any]) -> ChunkSamplingConfig: + """Extract and return the chunk sampling config as a ChunkSamplingConfig dataclass""" + return ChunkSamplingConfig(**cfg.get("chunk_sampling", {})) + + +def safe_sample(lst: list[Any], k: int) -> list[Any]: + """Sample k elements from lst, or return lst if k >= len(lst)""" + return random.sample(lst, k) if k < len(lst) else lst + + +def sample_single_hop_chunks( + chunks_list: list[dict[str, Any]], chunk_sampling: ChunkSamplingConfig +) -> list[dict[str, Any]]: + if not chunks_list: + return [] + + random.seed(chunk_sampling.random_seed) + mode = chunk_sampling.mode.lower() + value = chunk_sampling.value + total = len(chunks_list) + + if mode == CHUNK_MODE_PERCENT: + k = int(total * value) + return safe_sample(chunks_list, k) + elif mode == CHUNK_MODE_COUNT: + k = min(int(value), total) + return safe_sample(chunks_list, k) + else: + return chunks_list + + +def sample_multihop_groups( + mh_chunks: list[dict[str, Any]], chunk_sampling_cfg: dict[str, Any] +) -> list[dict[str, Any]]: + if not chunk_sampling_cfg: + return mh_chunks + mode = chunk_sampling_cfg.get("mode", CHUNK_MODE_ALL).lower() + value = chunk_sampling_cfg.get("value", 1.0) + random.seed(chunk_sampling_cfg.get("random_seed", 42)) + total = len(mh_chunks) + if total < 2: + return mh_chunks + if mode == CHUNK_MODE_PERCENT: + k = int(total * value) + return safe_sample(mh_chunks, k) + elif mode == CHUNK_MODE_COUNT: + k = min(int(value), total) + return safe_sample(mh_chunks, k) + return mh_chunks From c98cb0565638f1100ccc8a828d99192246718b7d Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Wed, 4 Jun 2025 23:35:08 +0200 Subject: [PATCH 62/69] Update config and integration test for QA pipeline --- example/configs/advanced_example.yaml | 4 ++-- tests/integration/test_pipeline.py | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/example/configs/advanced_example.yaml b/example/configs/advanced_example.yaml index ef44c6d6..e18a007e 100644 --- a/example/configs/advanced_example.yaml +++ b/example/configs/advanced_example.yaml @@ -209,7 +209,7 @@ pipeline: # additional_instructions: "Generate factual, short-answer questions at a college level." # Control the format of generated questions: - # question_type: "open-ended" # "open-ended" (default): model generates the answer to the question + # question_mode: "open-ended" # "open-ended" (default): model generates the answer to the question # # "multi-choice": model creates options (A), (B), (C), (D) and selects the correct one # If your documents are large, you can randomly sample chunks to reduce cost: @@ -223,7 +223,7 @@ pipeline: # additional_instructions: "Try to integrate multiple pieces of evidence across chunks to create deeper questions." # Control the format of generated questions: - # question_type: "open-ended" # "open-ended" (default): model generates the answer to the question + # question_mode: "open-ended" # "open-ended" (default): model generates the answer to the question # # "multi-choice": model creates options (A), (B), (C), (D) and selects the correct one # Similarly, you can sample the multi-hop chunks to cut down on inference: diff --git a/tests/integration/test_pipeline.py b/tests/integration/test_pipeline.py index 4080b613..86a628b1 100644 --- a/tests/integration/test_pipeline.py +++ b/tests/integration/test_pipeline.py @@ -228,7 +228,7 @@ def test_single_shot_question_generation_stage(mock_config): patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, patch("yourbench.utils.inference_engine.run_inference") as mock_run_inference, - patch("yourbench.pipeline.single_shot_question_generation.parse_qa_pairs_from_response") as mock_parse, + patch("yourbench.pipeline.question_generation.parse_qa_pairs_from_response") as mock_parse, ): # Configure mocks mock_run_inference.return_value = {"fake_model": ["Question generation response"]} @@ -244,7 +244,7 @@ def test_single_shot_question_generation_stage(mock_config): ] # Import run function - from yourbench.pipeline.single_shot_question_generation import run + from yourbench.pipeline.question_generation import run_single_shot as run # Run the stage run(mock_config) @@ -284,9 +284,8 @@ def test_multi_hop_question_generation_stage(mock_config): patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, patch("yourbench.utils.inference_engine.run_inference") as mock_run_inference, - patch("yourbench.pipeline.multi_hop_question_generation.parse_qa_pairs_from_response") as mock_parse, - # Mock the chunk sampling function to bypass the empty multihop check - patch("yourbench.pipeline.multi_hop_question_generation._multihop_chunk_sampling_and_calls") as mock_sampling, + patch("yourbench.pipeline.question_generation.parse_qa_pairs_from_response") as mock_parse, + patch("yourbench.pipeline.question_generation.sample_multihop_chunks") as mock_sampling, ): # Configure mocks mock_run_inference.return_value = {"fake_model": ["Multi-hop question generation response"]} @@ -306,7 +305,7 @@ def test_multi_hop_question_generation_stage(mock_config): ) # Import run function - from yourbench.pipeline.multi_hop_question_generation import run + from yourbench.pipeline.question_generation import run_multi_hop as run # Run the stage run(mock_config) From 783a5533b37d91ba42803921b28832d7b75c1859 Mon Sep 17 00:00:00 2001 From: Alina Lozovskaya Date: Thu, 5 Jun 2025 12:14:15 +0200 Subject: [PATCH 63/69] Refactor test pipeline to support unified question_generation --- tests/integration/test_pipeline.py | 120 ++++++++++++++++++++++------- 1 file changed, 91 insertions(+), 29 deletions(-) diff --git a/tests/integration/test_pipeline.py b/tests/integration/test_pipeline.py index 86a628b1..d4c47df0 100644 --- a/tests/integration/test_pipeline.py +++ b/tests/integration/test_pipeline.py @@ -51,7 +51,10 @@ def mock_config(temp_dir): "source_documents_dir": os.path.join(temp_dir, "raw"), "output_dir": os.path.join(temp_dir, "processed"), }, - "upload_ingest_to_hub": {"run": False, "source_documents_dir": os.path.join(temp_dir, "processed")}, + "upload_ingest_to_hub": { + "run": False, + "source_documents_dir": os.path.join(temp_dir, "processed"), + }, "summarization": {"run": True}, "chunking": { "run": True, @@ -64,13 +67,23 @@ def mock_config(temp_dir): }, "single_shot_question_generation": { "run": True, + "question_mode": "open-ended", "additional_instructions": "Generate questions to test a curious adult", - "chunk_sampling": {"mode": "count", "value": 1, "random_seed": 123}, + "chunk_sampling": { + "mode": "count", + "value": 1, + "random_seed": 123, + }, }, "multi_hop_question_generation": { "run": True, - "additional_instructions": "Generate questions to test a curious adult", - "chunk_sampling": {"mode": "count", "value": 1, "random_seed": 42}, + "question_mode": "multi-choice", + "additional_instructions": "Generate multi-choice questions to test a curious adult", + "chunk_sampling": { + "mode": "count", + "value": 1, + "random_seed": 42, + }, }, "lighteval": {"run": True}, }, @@ -135,8 +148,8 @@ def test_summarization_stage(mock_config): # Setup mocks with ( - patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, - patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, + patch("yourbench.pipeline.summarization.custom_load_dataset", return_value=mock_dataset) as mock_load, + patch("yourbench.pipeline.summarization.custom_save_dataset") as mock_save, patch("yourbench.pipeline.summarization.run_inference") as mock_run_inference, patch("yourbench.pipeline.summarization.extract_content_from_xml_tags") as mock_extract, ): @@ -227,8 +240,8 @@ def test_single_shot_question_generation_stage(mock_config): with ( patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, - patch("yourbench.utils.inference_engine.run_inference") as mock_run_inference, - patch("yourbench.pipeline.question_generation.parse_qa_pairs_from_response") as mock_parse, + patch("yourbench.utils.inference.inference_core.run_inference") as mock_run_inference, + patch("yourbench.utils.parsing_engine.parse_qa_pairs_from_response") as mock_parse, ): # Configure mocks mock_run_inference.return_value = {"fake_model": ["Question generation response"]} @@ -238,6 +251,7 @@ def test_single_shot_question_generation_stage(mock_config): "answer": "Test answer", "estimated_difficulty": 5, "question_type": "factual", + "question_mode": "open-ended", "thought_process": "Reasoning", "citations": ["citation"], } @@ -263,29 +277,34 @@ def test_multi_hop_question_generation_stage(mock_config): Verifies that questions are generated requiring reasoning across multiple chunks. """ - # Mock dataset with chunks and valid multihop_chunks format - chunks = [{"chunk_id": "chunk1", "chunk_text": "This is chunk 1"}] - # Updated format for multihop_chunks based on the refactored code - multihop_chunks = [ + from datasets import Dataset + from yourbench.utils.inference.inference_core import InferenceCall + + # Mock dataset with valid multihop_chunks and corresponding chunks + mock_dataset = Dataset.from_list([ { - "chunk_ids": ["chunk1", "chunk2"], - "chunks_text": ["Text 1", "Text 2"], + "document_id": "doc1", + "document_summary": "Document 1 summary", + "chunks": [ + {"chunk_id": "chunk1", "chunk_text": "This is chunk 1"}, + {"chunk_id": "chunk2", "chunk_text": "This is chunk 2"}, + ], + "multihop_chunks": [ + { + "chunk_ids": ["chunk1", "chunk2"], + "chunks_text": ["This is chunk 1", "This is chunk 2"], + } + ], } - ] - mock_dataset = Dataset.from_dict({ - "document_id": ["doc1"], - "document_summary": ["Document 1 summary"], - "chunks": [chunks], - "multihop_chunks": [multihop_chunks], - }) + ]) # Setup mocks with ( - patch("yourbench.utils.dataset_engine.custom_load_dataset", return_value=mock_dataset) as mock_load, - patch("yourbench.utils.dataset_engine.custom_save_dataset") as mock_save, - patch("yourbench.utils.inference_engine.run_inference") as mock_run_inference, - patch("yourbench.pipeline.question_generation.parse_qa_pairs_from_response") as mock_parse, - patch("yourbench.pipeline.question_generation.sample_multihop_chunks") as mock_sampling, + patch("yourbench.pipeline.question_generation.custom_load_dataset", return_value=mock_dataset) as mock_load, + patch("yourbench.pipeline.question_generation.custom_save_dataset") as mock_save, + patch("yourbench.pipeline.question_generation.run_inference") as mock_run_inference, + patch("yourbench.pipeline.question_generation.parse_multi_hop_responses") as mock_parse, + patch("yourbench.pipeline.question_generation.build_multi_hop_inference_calls") as mock_builder, ): # Configure mocks mock_run_inference.return_value = {"fake_model": ["Multi-hop question generation response"]} @@ -299,9 +318,9 @@ def test_multi_hop_question_generation_stage(mock_config): "citations": ["citation1", "citation2"], } ] - mock_sampling.return_value = ( - [MagicMock()], # Mock inference calls list - [(0, "doc1", ["chunk1", "chunk2"])], # Mock call index mapping + mock_builder.return_value = ( + [InferenceCall(messages=[{"role": "user", "content": "Explain chunk1 and chunk2"}])], + [(0, "doc1", ["chunk1", "chunk2"])], ) # Import run function @@ -313,6 +332,7 @@ def test_multi_hop_question_generation_stage(mock_config): # Verify behavior mock_load.assert_called_once() mock_run_inference.assert_called_once() + mock_parse.assert_called_once() mock_save.assert_called_once() @@ -331,6 +351,7 @@ def test_lighteval_stage(mock_config): "self_answer": ["Single-shot answer"], "estimated_difficulty": [5], "self_assessed_question_type": ["factual"], + "question_mode": ["open-ended"], "generating_model": ["fake_model"], "additional_instructions": ["Generate questions"], }) @@ -342,6 +363,7 @@ def test_lighteval_stage(mock_config): "self_answer": ["Multi-hop answer"], "estimated_difficulty": [7], "self_assessed_question_type": ["reasoning"], + "question_mode": ["multi-choice"], "generating_model": ["fake_model"], "additional_instructions": ["Generate questions"], }) @@ -382,3 +404,43 @@ def load_dataset_side_effect(config, subset): # Verify behavior assert mock_load.call_count == 4 mock_save.assert_called_once() + + +def test_stage_function_overrides(monkeypatch, tmp_path): + """ + Test that STAGE_FUNCTION_OVERRIDES are honored and used instead of dynamic imports + """ + from yourbench.pipeline import handler + + # Track calls to override functions + called_stages = [] + + def mock_run_single_shot(config): + called_stages.append("single_shot_question_generation") + + def mock_run_multi_hop(config): + called_stages.append("multi_hop_question_generation") + + # Patch the override map to use mocks + monkeypatch.setitem(handler.STAGE_FUNCTION_OVERRIDES, "single_shot_question_generation", mock_run_single_shot) + monkeypatch.setitem(handler.STAGE_FUNCTION_OVERRIDES, "multi_hop_question_generation", mock_run_multi_hop) + + # Patch load_config to avoid reading real file + def mock_load_config(path): + return { + "pipeline": { + "single_shot_question_generation": {"run": True}, + "multi_hop_question_generation": {"run": True}, + } + } + + monkeypatch.setattr(handler, "load_config", mock_load_config) + + # Run pipeline + config_path = tmp_path / "fake_config.yaml" + config_path.write_text("fake: config") + handler.run_pipeline(str(config_path)) + + # Assert overrides were called + assert "single_shot_question_generation" in called_stages + assert "multi_hop_question_generation" in called_stages From b4883f8dc788d87db9f52a92d50daaf429ee10f9 Mon Sep 17 00:00:00 2001 From: Sumuk Shashidhar Date: Fri, 6 Jun 2025 08:03:06 -0500 Subject: [PATCH 64/69] Potential fix for code scanning alert no. 1: Workflow does not contain permissions (#101) Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/quality.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index 747e15bf..dca9d628 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -1,5 +1,8 @@ name: Quality +permissions: + contents: read + on: push: branches: From f5035f03a56107daf8570f4c1abe21e5da956503 Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Thu, 12 Jun 2025 22:04:51 -0300 Subject: [PATCH 65/69] Added `include_docment_text` option to `lighteval` step to skip adding the full document text into each row (useful when your docs are large) --- yourbench/pipeline/lighteval.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/yourbench/pipeline/lighteval.py b/yourbench/pipeline/lighteval.py index 1ffae7e4..44a6034a 100644 --- a/yourbench/pipeline/lighteval.py +++ b/yourbench/pipeline/lighteval.py @@ -18,7 +18,7 @@ 8) chunk_ids (List[str]) - The chunk ID(s) used in forming the question. 9) question_generating_model (str) - The HF model ID that generated this question. 10) chunks (List[str]) - The actual chunk text(s) the question came from. -11) document (str) - The entire document text. +11) document (str) - The entire document text (can be excluded with include_document_text=false). Configuration Example: ---------------------- @@ -29,13 +29,14 @@ multi_hop_subset: multi_hop_questions_deduplicated chunked_subset: chunked_documents output_subset: lighteval + include_document_text: false # Optional: Set to false to exclude full document text (saves memory) Usage: ------ 1. Load single-shot and multi-hop question subsets. 2. Merge them into a single dataset, marking 'kind' as "single_shot" or "multi_hop." 3. For each question row, look up the relevant chunks in the chunked dataset to - populate 'chunks' and the full 'document' text. + populate 'chunks' and the full 'document' text (if include_document_text is true). 4. Save final dataset to HF or local path as configured. """ @@ -76,6 +77,7 @@ def run(config: Dict[str, Any]) -> None: - config["pipeline"]["lighteval"]["multi_hop_subset"] (str): Subset containing multi-hop questions. - config["pipeline"]["lighteval"]["chunked_subset"] (str): Subset containing chunked documents. - config["pipeline"]["lighteval"]["output_subset"] (str): Subset name for saving final dataset. + - config["pipeline"]["lighteval"]["include_document_text"] (bool, optional): Whether to include full document text in the final dataset. Default is True. Returns: None. The merged dataset is saved to disk or HF Hub as configured. @@ -139,6 +141,11 @@ def run(config: Dict[str, Any]) -> None: if doc_id in doc_meta_map: doc_meta_map[doc_id].update({"document_summary": row.get("document_summary")}) + # Check if we should include document text + include_document_text = stage_cfg.get("include_document_text", True) + if not include_document_text: + logger.info("Document text will be excluded from the final dataset (include_document_text=False)") + # Helper functions to transform a row def make_single_shot_record(row: Dict[str, Any]) -> Dict[str, Any]: """ @@ -150,7 +157,7 @@ def make_single_shot_record(row: Dict[str, Any]) -> Dict[str, Any]: # Grab doc meta doc_meta = doc_meta_map.get(doc_id, {}) - doc_text = doc_meta.get("document_text", "") + doc_text = doc_meta.get("document_text", "") if include_document_text else "" doc_summary = doc_meta.get("document_summary", "") chunk_text_map = doc_meta.get("chunks_map", {}) # chunk text is chunk_text_map[chunk_id] if it exists @@ -197,7 +204,7 @@ def make_multi_hop_record(row: Dict[str, Any]) -> Dict[str, Any]: # e.g. row["source_chunk_ids"]: List[str] chunk_ids: List[str] = row.get("source_chunk_ids", []) doc_meta = doc_meta_map.get(doc_id, {}) - doc_text = doc_meta.get("document_text", "") + doc_text = doc_meta.get("document_text", "") if include_document_text else "" doc_summary = doc_meta.get("document_summary", "") chunk_text_map = doc_meta.get("chunks_map", {}) From 50f8c237b0a581d6fe80e2f27c6fdf55cc60b428 Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Thu, 12 Jun 2025 22:06:49 -0300 Subject: [PATCH 66/69] Turned off inclusion of doc contents in `lighteval` step Changed model to gpt-4o (from gpt-4.1). --- .dockerignore | 4 + Dockerfile | 4 - example/configs/advanced_example.yaml | 1 + run_yourbench.py | 4 +- uv.lock | 1460 +++++++++---------------- 5 files changed, 523 insertions(+), 950 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c893d390 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.venv +.git +__pycache__/ +datasets/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a0e557f7..dd39ca16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,10 +12,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # Copy all yourbench files COPY . . -# Install python3-pip -RUN apt-get update && apt-get install -y --no-install-recommends python3-pip && \ - apt-get clean && rm -rf /var/lib/apt/lists/* - # Install dependencies and yourbench in editable mode RUN pip install --upgrade pip && \ pip install boto3 pyyaml awscli && \ diff --git a/example/configs/advanced_example.yaml b/example/configs/advanced_example.yaml index e18a007e..88e4802a 100644 --- a/example/configs/advanced_example.yaml +++ b/example/configs/advanced_example.yaml @@ -238,6 +238,7 @@ pipeline: # quick or “lightweight” downstream evaluations. lighteval: run: true + include_document_text: false # Set to false to exclude full document text from the dataset (saves memory) # 8) CITATION_SCORE_FILTERING # Finally, runs fuzzy-match scoring on each question’s “citations” vs. the diff --git a/run_yourbench.py b/run_yourbench.py index 600245fc..6e77775e 100644 --- a/run_yourbench.py +++ b/run_yourbench.py @@ -120,7 +120,7 @@ def main(): local_dataset_dir: task/dataset model_list: - - model_name: openai/gpt-4.1 + - model_name: openai/gpt-4o provider: null base_url: "https://openrouter.ai/api/v1" api_key: $OPENROUTER_API_KEY @@ -136,6 +136,8 @@ def main(): single_shot_question_generation: multi_hop_question_generation: lighteval: + run: true + include_document_text: false # Set to false to exclude full document text from the dataset (saves memory) citation_score_filtering: """ create_config_file(config_content, config_path) diff --git a/uv.lock b/uv.lock index b7fd6dbe..61f4ea9d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,23 +1,14 @@ version = 1 -revision = 2 +revision = 1 requires-python = "==3.12.*" -[[package]] -name = "absl-py" -version = "2.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/f0/e6342091061ed3a46aadc116b13edd7bb5249c3ab1b3ef07f24b0c248fc3/absl_py-2.2.2.tar.gz", hash = "sha256:bf25b2c2eed013ca456918c453d687eab4e8309fba81ee2f4c1a6aa2494175eb", size = 119982, upload-time = "2025-04-03T12:41:04.55Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/d4/349f7f4bd5ea92dab34f5bb0fe31775ef6c311427a14d5a5b31ecb442341/absl_py-2.2.2-py3-none-any.whl", hash = "sha256:e5797bc6abe45f64fd95dc06394ca3f2bedf3b5d895e9da691c9ee3397d70092", size = 135565, upload-time = "2025-04-03T12:41:03.172Z" }, -] - [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } 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" }, + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, ] [[package]] @@ -33,24 +24,24 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/e7/fa1a8c00e2c54b05dc8cb5d1439f627f7c267874e3f7bb047146116020f9/aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a", size = 7678653, upload-time = "2025-04-21T09:43:09.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/e7/fa1a8c00e2c54b05dc8cb5d1439f627f7c267874e3f7bb047146116020f9/aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a", size = 7678653 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/d2/5bc436f42bf4745c55f33e1e6a2d69e77075d3e768e3d1a34f96ee5298aa/aiohttp-3.11.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2", size = 706671, upload-time = "2025-04-21T09:41:28.021Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d0/2dbabecc4e078c0474abb40536bbde717fb2e39962f41c5fc7a216b18ea7/aiohttp-3.11.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508", size = 466169, upload-time = "2025-04-21T09:41:29.783Z" }, - { url = "https://files.pythonhosted.org/packages/70/84/19edcf0b22933932faa6e0be0d933a27bd173da02dc125b7354dff4d8da4/aiohttp-3.11.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e", size = 457554, upload-time = "2025-04-21T09:41:31.327Z" }, - { url = "https://files.pythonhosted.org/packages/32/d0/e8d1f034ae5624a0f21e4fb3feff79342ce631f3a4d26bd3e58b31ef033b/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f", size = 1690154, upload-time = "2025-04-21T09:41:33.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/de/2f9dbe2ac6f38f8495562077131888e0d2897e3798a0ff3adda766b04a34/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f", size = 1733402, upload-time = "2025-04-21T09:41:35.634Z" }, - { url = "https://files.pythonhosted.org/packages/e0/04/bd2870e1e9aef990d14b6df2a695f17807baf5c85a4c187a492bda569571/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec", size = 1783958, upload-time = "2025-04-21T09:41:37.456Z" }, - { url = "https://files.pythonhosted.org/packages/23/06/4203ffa2beb5bedb07f0da0f79b7d9039d1c33f522e0d1a2d5b6218e6f2e/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6", size = 1695288, upload-time = "2025-04-21T09:41:39.756Z" }, - { url = "https://files.pythonhosted.org/packages/30/b2/e2285dda065d9f29ab4b23d8bcc81eb881db512afb38a3f5247b191be36c/aiohttp-3.11.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009", size = 1618871, upload-time = "2025-04-21T09:41:41.972Z" }, - { url = "https://files.pythonhosted.org/packages/57/e0/88f2987885d4b646de2036f7296ebea9268fdbf27476da551c1a7c158bc0/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4", size = 1646262, upload-time = "2025-04-21T09:41:44.192Z" }, - { url = "https://files.pythonhosted.org/packages/e0/19/4d2da508b4c587e7472a032290b2981f7caeca82b4354e19ab3df2f51d56/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9", size = 1677431, upload-time = "2025-04-21T09:41:46.049Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/047473ea50150a41440f3265f53db1738870b5a1e5406ece561ca61a3bf4/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb", size = 1637430, upload-time = "2025-04-21T09:41:47.973Z" }, - { url = "https://files.pythonhosted.org/packages/11/32/c6d1e3748077ce7ee13745fae33e5cb1dac3e3b8f8787bf738a93c94a7d2/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda", size = 1703342, upload-time = "2025-04-21T09:41:50.323Z" }, - { url = "https://files.pythonhosted.org/packages/c5/1d/a3b57bfdbe285f0d45572d6d8f534fd58761da3e9cbc3098372565005606/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1", size = 1740600, upload-time = "2025-04-21T09:41:52.111Z" }, - { url = "https://files.pythonhosted.org/packages/a5/71/f9cd2fed33fa2b7ce4d412fb7876547abb821d5b5520787d159d0748321d/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea", size = 1695131, upload-time = "2025-04-21T09:41:53.94Z" }, - { url = "https://files.pythonhosted.org/packages/97/97/d1248cd6d02b9de6aa514793d0dcb20099f0ec47ae71a933290116c070c5/aiohttp-3.11.18-cp312-cp312-win32.whl", hash = "sha256:12a62691eb5aac58d65200c7ae94d73e8a65c331c3a86a2e9670927e94339ee8", size = 412442, upload-time = "2025-04-21T09:41:55.689Z" }, - { url = "https://files.pythonhosted.org/packages/33/9a/e34e65506e06427b111e19218a99abf627638a9703f4b8bcc3e3021277ed/aiohttp-3.11.18-cp312-cp312-win_amd64.whl", hash = "sha256:364329f319c499128fd5cd2d1c31c44f234c58f9b96cc57f743d16ec4f3238c8", size = 439444, upload-time = "2025-04-21T09:41:57.977Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d2/5bc436f42bf4745c55f33e1e6a2d69e77075d3e768e3d1a34f96ee5298aa/aiohttp-3.11.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2", size = 706671 }, + { url = "https://files.pythonhosted.org/packages/fe/d0/2dbabecc4e078c0474abb40536bbde717fb2e39962f41c5fc7a216b18ea7/aiohttp-3.11.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508", size = 466169 }, + { url = "https://files.pythonhosted.org/packages/70/84/19edcf0b22933932faa6e0be0d933a27bd173da02dc125b7354dff4d8da4/aiohttp-3.11.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e", size = 457554 }, + { url = "https://files.pythonhosted.org/packages/32/d0/e8d1f034ae5624a0f21e4fb3feff79342ce631f3a4d26bd3e58b31ef033b/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f", size = 1690154 }, + { url = "https://files.pythonhosted.org/packages/16/de/2f9dbe2ac6f38f8495562077131888e0d2897e3798a0ff3adda766b04a34/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f", size = 1733402 }, + { url = "https://files.pythonhosted.org/packages/e0/04/bd2870e1e9aef990d14b6df2a695f17807baf5c85a4c187a492bda569571/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec", size = 1783958 }, + { url = "https://files.pythonhosted.org/packages/23/06/4203ffa2beb5bedb07f0da0f79b7d9039d1c33f522e0d1a2d5b6218e6f2e/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6", size = 1695288 }, + { url = "https://files.pythonhosted.org/packages/30/b2/e2285dda065d9f29ab4b23d8bcc81eb881db512afb38a3f5247b191be36c/aiohttp-3.11.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009", size = 1618871 }, + { url = "https://files.pythonhosted.org/packages/57/e0/88f2987885d4b646de2036f7296ebea9268fdbf27476da551c1a7c158bc0/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4", size = 1646262 }, + { url = "https://files.pythonhosted.org/packages/e0/19/4d2da508b4c587e7472a032290b2981f7caeca82b4354e19ab3df2f51d56/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9", size = 1677431 }, + { url = "https://files.pythonhosted.org/packages/eb/ae/047473ea50150a41440f3265f53db1738870b5a1e5406ece561ca61a3bf4/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb", size = 1637430 }, + { url = "https://files.pythonhosted.org/packages/11/32/c6d1e3748077ce7ee13745fae33e5cb1dac3e3b8f8787bf738a93c94a7d2/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda", size = 1703342 }, + { url = "https://files.pythonhosted.org/packages/c5/1d/a3b57bfdbe285f0d45572d6d8f534fd58761da3e9cbc3098372565005606/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1", size = 1740600 }, + { url = "https://files.pythonhosted.org/packages/a5/71/f9cd2fed33fa2b7ce4d412fb7876547abb821d5b5520787d159d0748321d/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea", size = 1695131 }, + { url = "https://files.pythonhosted.org/packages/97/97/d1248cd6d02b9de6aa514793d0dcb20099f0ec47ae71a933290116c070c5/aiohttp-3.11.18-cp312-cp312-win32.whl", hash = "sha256:12a62691eb5aac58d65200c7ae94d73e8a65c331c3a86a2e9670927e94339ee8", size = 412442 }, + { url = "https://files.pythonhosted.org/packages/33/9a/e34e65506e06427b111e19218a99abf627638a9703f4b8bcc3e3021277ed/aiohttp-3.11.18-cp312-cp312-win_amd64.whl", hash = "sha256:364329f319c499128fd5cd2d1c31c44f234c58f9b96cc57f743d16ec4f3238c8", size = 439444 }, ] [[package]] @@ -60,27 +51,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424, upload-time = "2024-12-13T17:10:40.86Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, ] [[package]] name = "asyncio" version = "3.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/54/054bafaf2c0fb8473d423743e191fcdf49b2c1fd5e9af3524efbe097bafd/asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", size = 204411, upload-time = "2015-03-10T14:11:26.494Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/54/054bafaf2c0fb8473d423743e191fcdf49b2c1fd5e9af3524efbe097bafd/asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", size = 204411 } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/74/07679c5b9f98a7cb0fc147b1ef1cc1853bc07a4eb9cb5731e24732c5f773/asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d", size = 101767, upload-time = "2015-03-10T14:05:10.959Z" }, + { url = "https://files.pythonhosted.org/packages/22/74/07679c5b9f98a7cb0fc147b1ef1cc1853bc07a4eb9cb5731e24732c5f773/asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d", size = 101767 }, ] [[package]] name = "attrs" version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, ] [[package]] @@ -92,9 +83,9 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940, upload-time = "2025-03-27T02:46:20.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005, upload-time = "2025-03-27T02:46:22.356Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005 }, ] [[package]] @@ -106,9 +97,9 @@ dependencies = [ { name = "six" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/29/ff7a519a315e41c85bab92a7478c6acd1cf0b14353139a08caee4c691f77/azure_core-1.34.0.tar.gz", hash = "sha256:bdb544989f246a0ad1c85d72eeb45f2f835afdcbc5b45e43f0dbde7461c81ece", size = 297999, upload-time = "2025-05-01T23:17:27.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/29/ff7a519a315e41c85bab92a7478c6acd1cf0b14353139a08caee4c691f77/azure_core-1.34.0.tar.gz", hash = "sha256:bdb544989f246a0ad1c85d72eeb45f2f835afdcbc5b45e43f0dbde7461c81ece", size = 297999 } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/9e/5c87b49f65bb16571599bc789857d0ded2f53014d3392bc88a5d1f3ad779/azure_core-1.34.0-py3-none-any.whl", hash = "sha256:0615d3b756beccdb6624d1c0ae97284f38b78fb59a2a9839bf927c66fbbdddd6", size = 207409, upload-time = "2025-05-01T23:17:29.818Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/5c87b49f65bb16571599bc789857d0ded2f53014d3392bc88a5d1f3ad779/azure_core-1.34.0-py3-none-any.whl", hash = "sha256:0615d3b756beccdb6624d1c0ae97284f38b78fb59a2a9839bf927c66fbbdddd6", size = 207409 }, ] [[package]] @@ -122,18 +113,18 @@ dependencies = [ { name = "msal-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/a1/f1a683672e7a88ea0e3119f57b6c7843ed52650fdcac8bfa66ed84e86e40/azure_identity-1.21.0.tar.gz", hash = "sha256:ea22ce6e6b0f429bc1b8d9212d5b9f9877bd4c82f1724bfa910760612c07a9a6", size = 266445, upload-time = "2025-03-11T20:53:07.463Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/a1/f1a683672e7a88ea0e3119f57b6c7843ed52650fdcac8bfa66ed84e86e40/azure_identity-1.21.0.tar.gz", hash = "sha256:ea22ce6e6b0f429bc1b8d9212d5b9f9877bd4c82f1724bfa910760612c07a9a6", size = 266445 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/9f/1f9f3ef4f49729ee207a712a5971a9ca747f2ca47d9cbf13cf6953e3478a/azure_identity-1.21.0-py3-none-any.whl", hash = "sha256:258ea6325537352440f71b35c3dffe9d240eae4a5126c1b7ce5efd5766bd9fd9", size = 189190, upload-time = "2025-03-11T20:53:09.197Z" }, + { url = "https://files.pythonhosted.org/packages/3d/9f/1f9f3ef4f49729ee207a712a5971a9ca747f2ca47d9cbf13cf6953e3478a/azure_identity-1.21.0-py3-none-any.whl", hash = "sha256:258ea6325537352440f71b35c3dffe9d240eae4a5126c1b7ce5efd5766bd9fd9", size = 189190 }, ] [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, ] [[package]] @@ -144,37 +135,46 @@ dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, ] [[package]] -name = "bert-score" -version = "0.3.13" +name = "boto3" +version = "1.38.32" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "matplotlib" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pandas" }, - { name = "requests" }, - { name = "torch" }, - { name = "tqdm" }, - { name = "transformers" }, + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/93/2c97a85cbb66a8a256a13176e11c9c4508074e2341299fe75ee955c81eff/bert_score-0.3.13.tar.gz", hash = "sha256:8ffe5838eac8cdd988b8b1a896af7f49071188c8c011a1ed160d71a9899a2ba4", size = 48621, upload-time = "2023-02-20T21:07:29.477Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/85/abba429fe0fc0b87db20b7b311deec062c613c5e74cfcaab2ad34e864bbf/boto3-1.38.32.tar.gz", hash = "sha256:3faa2c328a61745f3215a63039606a6fcf55d9afe1cc76e3a5e27b9db58cdbf6", size = 111874 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/8c/bc5457de4c004b1a623b31f7bc8d0375fb699b7d67df11879098b4b7b7c8/bert_score-0.3.13-py3-none-any.whl", hash = "sha256:bbbb4c7fcdaa46d7681aff49f37f96faa09ed74e1b150e659bdc6b58a66989b9", size = 61135, upload-time = "2023-02-20T21:07:27.226Z" }, + { url = "https://files.pythonhosted.org/packages/59/1a/2be51f4ac8592c2ccf699a17be7bb92c0aff8ce89fe2ffd657948b32bfeb/boto3-1.38.32-py3-none-any.whl", hash = "sha256:b998edac72f6740bd5d9d585cf3880f2dfeb4842e626b34430fd0e9623378011", size = 139940 }, +] + +[[package]] +name = "botocore" +version = "1.38.32" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/c2/c4c23c7ad746aba6edfa93ec9e6e14195efcf786425486f6a1b442734a8d/botocore-1.38.32.tar.gz", hash = "sha256:0899a090e352cb5eeaae2c7bb52a987b469d23912c7ece86664dfb5c2e074978", size = 13948764 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/6e/104f66963c9d2dc8d5ba4675535aca5ba5277eb3535047e004275329fc87/botocore-1.38.32-py3-none-any.whl", hash = "sha256:64ab919a5d8b74dd73eaac1f978d0e674d11ff3bbe8815c3d2982477be9a082c", size = 13608384 }, ] [[package]] name = "certifi" version = "2025.4.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, ] [[package]] @@ -184,41 +184,41 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, ] [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, ] [[package]] @@ -228,27 +228,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, ] [[package]] name = "cobble" version = "0.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/7a/a507c709be2c96e1bb6102eb7b7f4026c5e5e223ef7d745a17d239e9d844/cobble-0.1.4.tar.gz", hash = "sha256:de38be1539992c8a06e569630717c485a5f91be2192c461ea2b220607dfa78aa", size = 3805, upload-time = "2024-06-01T18:11:09.528Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/7a/a507c709be2c96e1bb6102eb7b7f4026c5e5e223ef7d745a17d239e9d844/cobble-0.1.4.tar.gz", hash = "sha256:de38be1539992c8a06e569630717c485a5f91be2192c461ea2b220607dfa78aa", size = 3805 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/e1/3714a2f371985215c219c2a70953d38e3eed81ef165aed061d21de0e998b/cobble-0.1.4-py3-none-any.whl", hash = "sha256:36c91b1655e599fd428e2b95fdd5f0da1ca2e9f1abb0bc871dec21a0e78a2b44", size = 3984, upload-time = "2024-06-01T18:11:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/3714a2f371985215c219c2a70953d38e3eed81ef165aed061d21de0e998b/cobble-0.1.4-py3-none-any.whl", hash = "sha256:36c91b1655e599fd428e2b95fdd5f0da1ca2e9f1abb0bc871dec21a0e78a2b44", size = 3984 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] @@ -258,30 +258,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "humanfriendly" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, -] - -[[package]] -name = "contourpy" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520 } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, - { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, - { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, - { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, - { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 }, ] [[package]] @@ -293,9 +272,9 @@ dependencies = [ { name = "tld" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/54/6d6ceeff4bed42e7a10d6064d35ee43a810e7b3e8beb4abeae8cff4713ae/courlan-1.3.2.tar.gz", hash = "sha256:0b66f4db3a9c39a6e22dd247c72cfaa57d68ea660e94bb2c84ec7db8712af190", size = 206382, upload-time = "2024-10-29T16:40:20.994Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/54/6d6ceeff4bed42e7a10d6064d35ee43a810e7b3e8beb4abeae8cff4713ae/courlan-1.3.2.tar.gz", hash = "sha256:0b66f4db3a9c39a6e22dd247c72cfaa57d68ea660e94bb2c84ec7db8712af190", size = 206382 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/ca/6a667ccbe649856dcd3458bab80b016681b274399d6211187c6ab969fc50/courlan-1.3.2-py3-none-any.whl", hash = "sha256:d0dab52cf5b5b1000ee2839fbc2837e93b2514d3cb5bb61ae158a55b7a04c6be", size = 33848, upload-time = "2024-10-29T16:40:18.325Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ca/6a667ccbe649856dcd3458bab80b016681b274399d6211187c6ab969fc50/courlan-1.3.2-py3-none-any.whl", hash = "sha256:d0dab52cf5b5b1000ee2839fbc2837e93b2514d3cb5bb61ae158a55b7a04c6be", size = 33848 }, ] [[package]] @@ -305,41 +284,32 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/d6/1411ab4d6108ab167d06254c5be517681f1e331f90edf1379895bcb87020/cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053", size = 711096, upload-time = "2025-05-02T19:36:04.667Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/53/c776d80e9d26441bb3868457909b4e74dd9ccabd182e10b2b0ae7a07e265/cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88", size = 6670281, upload-time = "2025-05-02T19:34:50.665Z" }, - { url = "https://files.pythonhosted.org/packages/6a/06/af2cf8d56ef87c77319e9086601bef621bedf40f6f59069e1b6d1ec498c5/cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137", size = 3959305, upload-time = "2025-05-02T19:34:53.042Z" }, - { url = "https://files.pythonhosted.org/packages/ae/01/80de3bec64627207d030f47bf3536889efee8913cd363e78ca9a09b13c8e/cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c", size = 4171040, upload-time = "2025-05-02T19:34:54.675Z" }, - { url = "https://files.pythonhosted.org/packages/bd/48/bb16b7541d207a19d9ae8b541c70037a05e473ddc72ccb1386524d4f023c/cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76", size = 3963411, upload-time = "2025-05-02T19:34:56.61Z" }, - { url = "https://files.pythonhosted.org/packages/42/b2/7d31f2af5591d217d71d37d044ef5412945a8a8e98d5a2a8ae4fd9cd4489/cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359", size = 3689263, upload-time = "2025-05-02T19:34:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/25/50/c0dfb9d87ae88ccc01aad8eb93e23cfbcea6a6a106a9b63a7b14c1f93c75/cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43", size = 4196198, upload-time = "2025-05-02T19:35:00.988Z" }, - { url = "https://files.pythonhosted.org/packages/66/c9/55c6b8794a74da652690c898cb43906310a3e4e4f6ee0b5f8b3b3e70c441/cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01", size = 3966502, upload-time = "2025-05-02T19:35:03.091Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f7/7cb5488c682ca59a02a32ec5f975074084db4c983f849d47b7b67cc8697a/cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d", size = 4196173, upload-time = "2025-05-02T19:35:05.018Z" }, - { url = "https://files.pythonhosted.org/packages/d2/0b/2f789a8403ae089b0b121f8f54f4a3e5228df756e2146efdf4a09a3d5083/cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904", size = 4087713, upload-time = "2025-05-02T19:35:07.187Z" }, - { url = "https://files.pythonhosted.org/packages/1d/aa/330c13655f1af398fc154089295cf259252f0ba5df93b4bc9d9c7d7f843e/cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44", size = 4299064, upload-time = "2025-05-02T19:35:08.879Z" }, - { url = "https://files.pythonhosted.org/packages/10/a8/8c540a421b44fd267a7d58a1fd5f072a552d72204a3f08194f98889de76d/cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d", size = 2773887, upload-time = "2025-05-02T19:35:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0d/c4b1657c39ead18d76bbd122da86bd95bdc4095413460d09544000a17d56/cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d", size = 3209737, upload-time = "2025-05-02T19:35:12.12Z" }, - { url = "https://files.pythonhosted.org/packages/34/a3/ad08e0bcc34ad436013458d7528e83ac29910943cea42ad7dd4141a27bbb/cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f", size = 6673501, upload-time = "2025-05-02T19:35:13.775Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f0/7491d44bba8d28b464a5bc8cc709f25a51e3eac54c0a4444cf2473a57c37/cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759", size = 3960307, upload-time = "2025-05-02T19:35:15.917Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c8/e5c5d0e1364d3346a5747cdcd7ecbb23ca87e6dea4f942a44e88be349f06/cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645", size = 4170876, upload-time = "2025-05-02T19:35:18.138Z" }, - { url = "https://files.pythonhosted.org/packages/73/96/025cb26fc351d8c7d3a1c44e20cf9a01e9f7cf740353c9c7a17072e4b264/cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2", size = 3964127, upload-time = "2025-05-02T19:35:19.864Z" }, - { url = "https://files.pythonhosted.org/packages/01/44/eb6522db7d9f84e8833ba3bf63313f8e257729cf3a8917379473fcfd6601/cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54", size = 3689164, upload-time = "2025-05-02T19:35:21.449Z" }, - { url = "https://files.pythonhosted.org/packages/68/fb/d61a4defd0d6cee20b1b8a1ea8f5e25007e26aeb413ca53835f0cae2bcd1/cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93", size = 4198081, upload-time = "2025-05-02T19:35:23.187Z" }, - { url = "https://files.pythonhosted.org/packages/1b/50/457f6911d36432a8811c3ab8bd5a6090e8d18ce655c22820994913dd06ea/cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c", size = 3967716, upload-time = "2025-05-02T19:35:25.426Z" }, - { url = "https://files.pythonhosted.org/packages/35/6e/dca39d553075980ccb631955c47b93d87d27f3596da8d48b1ae81463d915/cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f", size = 4197398, upload-time = "2025-05-02T19:35:27.678Z" }, - { url = "https://files.pythonhosted.org/packages/9b/9d/d1f2fe681eabc682067c66a74addd46c887ebacf39038ba01f8860338d3d/cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5", size = 4087900, upload-time = "2025-05-02T19:35:29.312Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f5/3599e48c5464580b73b236aafb20973b953cd2e7b44c7c2533de1d888446/cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b", size = 4301067, upload-time = "2025-05-02T19:35:31.547Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6c/d2c48c8137eb39d0c193274db5c04a75dab20d2f7c3f81a7dcc3a8897701/cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028", size = 2775467, upload-time = "2025-05-02T19:35:33.805Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ad/51f212198681ea7b0deaaf8846ee10af99fba4e894f67b353524eab2bbe5/cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334", size = 3210375, upload-time = "2025-05-02T19:35:35.369Z" }, -] - -[[package]] -name = "cycler" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/53/d6/1411ab4d6108ab167d06254c5be517681f1e331f90edf1379895bcb87020/cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053", size = 711096 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/53/c776d80e9d26441bb3868457909b4e74dd9ccabd182e10b2b0ae7a07e265/cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88", size = 6670281 }, + { url = "https://files.pythonhosted.org/packages/6a/06/af2cf8d56ef87c77319e9086601bef621bedf40f6f59069e1b6d1ec498c5/cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137", size = 3959305 }, + { url = "https://files.pythonhosted.org/packages/ae/01/80de3bec64627207d030f47bf3536889efee8913cd363e78ca9a09b13c8e/cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c", size = 4171040 }, + { url = "https://files.pythonhosted.org/packages/bd/48/bb16b7541d207a19d9ae8b541c70037a05e473ddc72ccb1386524d4f023c/cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76", size = 3963411 }, + { url = "https://files.pythonhosted.org/packages/42/b2/7d31f2af5591d217d71d37d044ef5412945a8a8e98d5a2a8ae4fd9cd4489/cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359", size = 3689263 }, + { url = "https://files.pythonhosted.org/packages/25/50/c0dfb9d87ae88ccc01aad8eb93e23cfbcea6a6a106a9b63a7b14c1f93c75/cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43", size = 4196198 }, + { url = "https://files.pythonhosted.org/packages/66/c9/55c6b8794a74da652690c898cb43906310a3e4e4f6ee0b5f8b3b3e70c441/cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01", size = 3966502 }, + { url = "https://files.pythonhosted.org/packages/b6/f7/7cb5488c682ca59a02a32ec5f975074084db4c983f849d47b7b67cc8697a/cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d", size = 4196173 }, + { url = "https://files.pythonhosted.org/packages/d2/0b/2f789a8403ae089b0b121f8f54f4a3e5228df756e2146efdf4a09a3d5083/cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904", size = 4087713 }, + { url = "https://files.pythonhosted.org/packages/1d/aa/330c13655f1af398fc154089295cf259252f0ba5df93b4bc9d9c7d7f843e/cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44", size = 4299064 }, + { url = "https://files.pythonhosted.org/packages/10/a8/8c540a421b44fd267a7d58a1fd5f072a552d72204a3f08194f98889de76d/cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d", size = 2773887 }, + { url = "https://files.pythonhosted.org/packages/b9/0d/c4b1657c39ead18d76bbd122da86bd95bdc4095413460d09544000a17d56/cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d", size = 3209737 }, + { url = "https://files.pythonhosted.org/packages/34/a3/ad08e0bcc34ad436013458d7528e83ac29910943cea42ad7dd4141a27bbb/cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f", size = 6673501 }, + { url = "https://files.pythonhosted.org/packages/b1/f0/7491d44bba8d28b464a5bc8cc709f25a51e3eac54c0a4444cf2473a57c37/cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759", size = 3960307 }, + { url = "https://files.pythonhosted.org/packages/f7/c8/e5c5d0e1364d3346a5747cdcd7ecbb23ca87e6dea4f942a44e88be349f06/cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645", size = 4170876 }, + { url = "https://files.pythonhosted.org/packages/73/96/025cb26fc351d8c7d3a1c44e20cf9a01e9f7cf740353c9c7a17072e4b264/cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2", size = 3964127 }, + { url = "https://files.pythonhosted.org/packages/01/44/eb6522db7d9f84e8833ba3bf63313f8e257729cf3a8917379473fcfd6601/cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54", size = 3689164 }, + { url = "https://files.pythonhosted.org/packages/68/fb/d61a4defd0d6cee20b1b8a1ea8f5e25007e26aeb413ca53835f0cae2bcd1/cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93", size = 4198081 }, + { url = "https://files.pythonhosted.org/packages/1b/50/457f6911d36432a8811c3ab8bd5a6090e8d18ce655c22820994913dd06ea/cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c", size = 3967716 }, + { url = "https://files.pythonhosted.org/packages/35/6e/dca39d553075980ccb631955c47b93d87d27f3596da8d48b1ae81463d915/cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f", size = 4197398 }, + { url = "https://files.pythonhosted.org/packages/9b/9d/d1f2fe681eabc682067c66a74addd46c887ebacf39038ba01f8860338d3d/cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5", size = 4087900 }, + { url = "https://files.pythonhosted.org/packages/c4/f5/3599e48c5464580b73b236aafb20973b953cd2e7b44c7c2533de1d888446/cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b", size = 4301067 }, + { url = "https://files.pythonhosted.org/packages/a7/6c/d2c48c8137eb39d0c193274db5c04a75dab20d2f7c3f81a7dcc3a8897701/cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028", size = 2775467 }, + { url = "https://files.pythonhosted.org/packages/c9/ad/51f212198681ea7b0deaaf8846ee10af99fba4e894f67b353524eab2bbe5/cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334", size = 3210375 }, ] [[package]] @@ -362,9 +332,9 @@ dependencies = [ { name = "tqdm" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/af/8c1d10daf37383c32ab0a7461eaa4d5c7a3c47808fe5a8563744de002df7/datasets-3.5.1.tar.gz", hash = "sha256:f835b45dbbd7065c1191734b6f7c8d96fdf8c5751feaa5aa52b2a0dc43eea58f", size = 568915, upload-time = "2025-04-28T14:01:42.974Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/af/8c1d10daf37383c32ab0a7461eaa4d5c7a3c47808fe5a8563744de002df7/datasets-3.5.1.tar.gz", hash = "sha256:f835b45dbbd7065c1191734b6f7c8d96fdf8c5751feaa5aa52b2a0dc43eea58f", size = 568915 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/f5/668b3444a2f487b0052b908af631fe39eeb2bdb2359d9bbc2c3b80b71119/datasets-3.5.1-py3-none-any.whl", hash = "sha256:4074dda8dd6e9ece242b1580a8ef3928777d59ae1db144d911229e443a093cbb", size = 491436, upload-time = "2025-04-28T14:01:40.953Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f5/668b3444a2f487b0052b908af631fe39eeb2bdb2359d9bbc2c3b80b71119/datasets-3.5.1-py3-none-any.whl", hash = "sha256:4074dda8dd6e9ece242b1580a8ef3928777d59ae1db144d911229e443a093cbb", size = 491436 }, ] [[package]] @@ -377,106 +347,89 @@ dependencies = [ { name = "regex" }, { name = "tzlocal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/3f/d3207a05f5b6a78c66d86631e60bfba5af163738a599a5b9aa2c2737a09e/dateparser-1.2.1.tar.gz", hash = "sha256:7e4919aeb48481dbfc01ac9683c8e20bfe95bb715a38c1e9f6af889f4f30ccc3", size = 309924, upload-time = "2025-02-05T12:34:55.593Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/3f/d3207a05f5b6a78c66d86631e60bfba5af163738a599a5b9aa2c2737a09e/dateparser-1.2.1.tar.gz", hash = "sha256:7e4919aeb48481dbfc01ac9683c8e20bfe95bb715a38c1e9f6af889f4f30ccc3", size = 309924 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/0a/981c438c4cd84147c781e4e96c1d72df03775deb1bc76c5a6ee8afa89c62/dateparser-1.2.1-py3-none-any.whl", hash = "sha256:bdcac262a467e6260030040748ad7c10d6bacd4f3b9cdb4cfd2251939174508c", size = 295658, upload-time = "2025-02-05T12:34:53.1Z" }, + { url = "https://files.pythonhosted.org/packages/cf/0a/981c438c4cd84147c781e4e96c1d72df03775deb1bc76c5a6ee8afa89c62/dateparser-1.2.1-py3-none-any.whl", hash = "sha256:bdcac262a467e6260030040748ad7c10d6bacd4f3b9cdb4cfd2251939174508c", size = 295658 }, ] [[package]] name = "defusedxml" version = "0.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, ] [[package]] name = "dill" version = "0.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/17/4d/ac7ffa80c69ea1df30a8aa11b3578692a5118e7cd1aa157e3ef73b092d15/dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", size = 184847, upload-time = "2024-01-27T23:42:16.145Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/4d/ac7ffa80c69ea1df30a8aa11b3578692a5118e7cd1aa157e3ef73b092d15/dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", size = 184847 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/7a/cef76fd8438a42f96db64ddaa85280485a9c395e7df3db8158cfec1eee34/dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7", size = 116252, upload-time = "2024-01-27T23:42:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7a/cef76fd8438a42f96db64ddaa85280485a9c395e7df3db8158cfec1eee34/dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7", size = 116252 }, ] [[package]] name = "et-xmlfile" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 }, ] [[package]] name = "filelock" version = "3.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, ] [[package]] name = "flatbuffers" version = "25.2.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/30/eb5dce7994fc71a2f685d98ec33cc660c0a5887db5610137e60d8cbc4489/flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e", size = 22170, upload-time = "2025-02-11T04:26:46.257Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953, upload-time = "2025-02-11T04:26:44.484Z" }, -] - -[[package]] -name = "fonttools" -version = "4.57.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/2d/a9a0b6e3a0cf6bd502e64fc16d894269011930cabfc89aee20d1635b1441/fonttools-4.57.0.tar.gz", hash = "sha256:727ece10e065be2f9dd239d15dd5d60a66e17eac11aea47d447f9f03fdbc42de", size = 3492448, upload-time = "2025-04-03T11:07:13.898Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/30/eb5dce7994fc71a2f685d98ec33cc660c0a5887db5610137e60d8cbc4489/flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e", size = 22170 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/98/d4bc42d43392982eecaaca117d79845734d675219680cd43070bb001bc1f/fonttools-4.57.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:889e45e976c74abc7256d3064aa7c1295aa283c6bb19810b9f8b604dfe5c7f31", size = 2751824, upload-time = "2025-04-03T11:06:03.782Z" }, - { url = "https://files.pythonhosted.org/packages/1a/62/7168030eeca3742fecf45f31e63b5ef48969fa230a672216b805f1d61548/fonttools-4.57.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0425c2e052a5f1516c94e5855dbda706ae5a768631e9fcc34e57d074d1b65b92", size = 2283072, upload-time = "2025-04-03T11:06:05.533Z" }, - { url = "https://files.pythonhosted.org/packages/5d/82/121a26d9646f0986ddb35fbbaf58ef791c25b59ecb63ffea2aab0099044f/fonttools-4.57.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44c26a311be2ac130f40a96769264809d3b0cb297518669db437d1cc82974888", size = 4788020, upload-time = "2025-04-03T11:06:07.249Z" }, - { url = "https://files.pythonhosted.org/packages/5b/26/e0f2fb662e022d565bbe280a3cfe6dafdaabf58889ff86fdef2d31ff1dde/fonttools-4.57.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c41ba992df5b8d680b89fd84c6a1f2aca2b9f1ae8a67400c8930cd4ea115f6", size = 4859096, upload-time = "2025-04-03T11:06:09.469Z" }, - { url = "https://files.pythonhosted.org/packages/9e/44/9075e323347b1891cdece4b3f10a3b84a8f4c42a7684077429d9ce842056/fonttools-4.57.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea1e9e43ca56b0c12440a7c689b1350066595bebcaa83baad05b8b2675129d98", size = 4964356, upload-time = "2025-04-03T11:06:11.294Z" }, - { url = "https://files.pythonhosted.org/packages/48/28/caa8df32743462fb966be6de6a79d7f30393859636d7732e82efa09fbbb4/fonttools-4.57.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84fd56c78d431606332a0627c16e2a63d243d0d8b05521257d77c6529abe14d8", size = 5226546, upload-time = "2025-04-03T11:06:13.6Z" }, - { url = "https://files.pythonhosted.org/packages/f6/46/95ab0f0d2e33c5b1a4fc1c0efe5e286ba9359602c0a9907adb1faca44175/fonttools-4.57.0-cp312-cp312-win32.whl", hash = "sha256:f4376819c1c778d59e0a31db5dc6ede854e9edf28bbfa5b756604727f7f800ac", size = 2146776, upload-time = "2025-04-03T11:06:15.643Z" }, - { url = "https://files.pythonhosted.org/packages/06/5d/1be5424bb305880e1113631f49a55ea7c7da3a5fe02608ca7c16a03a21da/fonttools-4.57.0-cp312-cp312-win_amd64.whl", hash = "sha256:57e30241524879ea10cdf79c737037221f77cc126a8cdc8ff2c94d4a522504b9", size = 2193956, upload-time = "2025-04-03T11:06:17.534Z" }, - { url = "https://files.pythonhosted.org/packages/90/27/45f8957c3132917f91aaa56b700bcfc2396be1253f685bd5c68529b6f610/fonttools-4.57.0-py3-none-any.whl", hash = "sha256:3122c604a675513c68bd24c6a8f9091f1c2376d18e8f5fe5a101746c81b3e98f", size = 1093605, upload-time = "2025-04-03T11:07:11.341Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953 }, ] [[package]] name = "frozenlist" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/f4/d744cba2da59b5c1d88823cf9e8a6c74e4659e2b27604ed973be2a0bf5ab/frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68", size = 42831, upload-time = "2025-04-17T22:38:53.099Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/8a/289b7d0de2fbac832ea80944d809759976f661557a38bb8e77db5d9f79b7/frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1", size = 160193, upload-time = "2025-04-17T22:36:47.382Z" }, - { url = "https://files.pythonhosted.org/packages/19/80/2fd17d322aec7f430549f0669f599997174f93ee17929ea5b92781ec902c/frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29", size = 123831, upload-time = "2025-04-17T22:36:49.401Z" }, - { url = "https://files.pythonhosted.org/packages/99/06/f5812da431273f78c6543e0b2f7de67dfd65eb0a433978b2c9c63d2205e4/frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25", size = 121862, upload-time = "2025-04-17T22:36:51.899Z" }, - { url = "https://files.pythonhosted.org/packages/d0/31/9e61c6b5fc493cf24d54881731204d27105234d09878be1a5983182cc4a5/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576", size = 316361, upload-time = "2025-04-17T22:36:53.402Z" }, - { url = "https://files.pythonhosted.org/packages/9d/55/22ca9362d4f0222324981470fd50192be200154d51509ee6eb9baa148e96/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8", size = 307115, upload-time = "2025-04-17T22:36:55.016Z" }, - { url = "https://files.pythonhosted.org/packages/ae/39/4fff42920a57794881e7bb3898dc7f5f539261711ea411b43bba3cde8b79/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9", size = 322505, upload-time = "2025-04-17T22:36:57.12Z" }, - { url = "https://files.pythonhosted.org/packages/55/f2/88c41f374c1e4cf0092a5459e5f3d6a1e17ed274c98087a76487783df90c/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e", size = 322666, upload-time = "2025-04-17T22:36:58.735Z" }, - { url = "https://files.pythonhosted.org/packages/75/51/034eeb75afdf3fd03997856195b500722c0b1a50716664cde64e28299c4b/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590", size = 302119, upload-time = "2025-04-17T22:37:00.512Z" }, - { url = "https://files.pythonhosted.org/packages/2b/a6/564ecde55ee633270a793999ef4fd1d2c2b32b5a7eec903b1012cb7c5143/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103", size = 316226, upload-time = "2025-04-17T22:37:02.102Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/6c0682c32377f402b8a6174fb16378b683cf6379ab4d2827c580892ab3c7/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c", size = 312788, upload-time = "2025-04-17T22:37:03.578Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b8/10fbec38f82c5d163ca1750bfff4ede69713badf236a016781cf1f10a0f0/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821", size = 325914, upload-time = "2025-04-17T22:37:05.213Z" }, - { url = "https://files.pythonhosted.org/packages/62/ca/2bf4f3a1bd40cdedd301e6ecfdbb291080d5afc5f9ce350c0739f773d6b9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70", size = 305283, upload-time = "2025-04-17T22:37:06.985Z" }, - { url = "https://files.pythonhosted.org/packages/09/64/20cc13ccf94abc2a1f482f74ad210703dc78a590d0b805af1c9aa67f76f9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f", size = 319264, upload-time = "2025-04-17T22:37:08.618Z" }, - { url = "https://files.pythonhosted.org/packages/20/ff/86c6a2bbe98cfc231519f5e6d712a0898488ceac804a917ce014f32e68f6/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046", size = 326482, upload-time = "2025-04-17T22:37:10.196Z" }, - { url = "https://files.pythonhosted.org/packages/2f/da/8e381f66367d79adca245d1d71527aac774e30e291d41ef161ce2d80c38e/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770", size = 318248, upload-time = "2025-04-17T22:37:12.284Z" }, - { url = "https://files.pythonhosted.org/packages/39/24/1a1976563fb476ab6f0fa9fefaac7616a4361dbe0461324f9fd7bf425dbe/frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc", size = 115161, upload-time = "2025-04-17T22:37:13.902Z" }, - { url = "https://files.pythonhosted.org/packages/80/2e/fb4ed62a65f8cd66044706b1013f0010930d8cbb0729a2219561ea075434/frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878", size = 120548, upload-time = "2025-04-17T22:37:15.326Z" }, - { url = "https://files.pythonhosted.org/packages/71/3e/b04a0adda73bd52b390d730071c0d577073d3d26740ee1bad25c3ad0f37b/frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", size = 12404, upload-time = "2025-04-17T22:38:51.668Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ee/f4/d744cba2da59b5c1d88823cf9e8a6c74e4659e2b27604ed973be2a0bf5ab/frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68", size = 42831 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/8a/289b7d0de2fbac832ea80944d809759976f661557a38bb8e77db5d9f79b7/frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1", size = 160193 }, + { url = "https://files.pythonhosted.org/packages/19/80/2fd17d322aec7f430549f0669f599997174f93ee17929ea5b92781ec902c/frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29", size = 123831 }, + { url = "https://files.pythonhosted.org/packages/99/06/f5812da431273f78c6543e0b2f7de67dfd65eb0a433978b2c9c63d2205e4/frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25", size = 121862 }, + { url = "https://files.pythonhosted.org/packages/d0/31/9e61c6b5fc493cf24d54881731204d27105234d09878be1a5983182cc4a5/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576", size = 316361 }, + { url = "https://files.pythonhosted.org/packages/9d/55/22ca9362d4f0222324981470fd50192be200154d51509ee6eb9baa148e96/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8", size = 307115 }, + { url = "https://files.pythonhosted.org/packages/ae/39/4fff42920a57794881e7bb3898dc7f5f539261711ea411b43bba3cde8b79/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9", size = 322505 }, + { url = "https://files.pythonhosted.org/packages/55/f2/88c41f374c1e4cf0092a5459e5f3d6a1e17ed274c98087a76487783df90c/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e", size = 322666 }, + { url = "https://files.pythonhosted.org/packages/75/51/034eeb75afdf3fd03997856195b500722c0b1a50716664cde64e28299c4b/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590", size = 302119 }, + { url = "https://files.pythonhosted.org/packages/2b/a6/564ecde55ee633270a793999ef4fd1d2c2b32b5a7eec903b1012cb7c5143/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103", size = 316226 }, + { url = "https://files.pythonhosted.org/packages/f1/c8/6c0682c32377f402b8a6174fb16378b683cf6379ab4d2827c580892ab3c7/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c", size = 312788 }, + { url = "https://files.pythonhosted.org/packages/b6/b8/10fbec38f82c5d163ca1750bfff4ede69713badf236a016781cf1f10a0f0/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821", size = 325914 }, + { url = "https://files.pythonhosted.org/packages/62/ca/2bf4f3a1bd40cdedd301e6ecfdbb291080d5afc5f9ce350c0739f773d6b9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70", size = 305283 }, + { url = "https://files.pythonhosted.org/packages/09/64/20cc13ccf94abc2a1f482f74ad210703dc78a590d0b805af1c9aa67f76f9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f", size = 319264 }, + { url = "https://files.pythonhosted.org/packages/20/ff/86c6a2bbe98cfc231519f5e6d712a0898488ceac804a917ce014f32e68f6/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046", size = 326482 }, + { url = "https://files.pythonhosted.org/packages/2f/da/8e381f66367d79adca245d1d71527aac774e30e291d41ef161ce2d80c38e/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770", size = 318248 }, + { url = "https://files.pythonhosted.org/packages/39/24/1a1976563fb476ab6f0fa9fefaac7616a4361dbe0461324f9fd7bf425dbe/frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc", size = 115161 }, + { url = "https://files.pythonhosted.org/packages/80/2e/fb4ed62a65f8cd66044706b1013f0010930d8cbb0729a2219561ea075434/frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878", size = 120548 }, + { url = "https://files.pythonhosted.org/packages/71/3e/b04a0adda73bd52b390d730071c0d577073d3d26740ee1bad25c3ad0f37b/frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", size = 12404 }, ] [[package]] name = "fsspec" version = "2025.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/f4/5721faf47b8c499e776bc34c6a8fc17efdf7fdef0b00f398128bc5dcb4ac/fsspec-2025.3.0.tar.gz", hash = "sha256:a935fd1ea872591f2b5148907d103488fc523295e6c64b835cfad8c3eca44972", size = 298491, upload-time = "2025-03-07T21:47:56.461Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/f4/5721faf47b8c499e776bc34c6a8fc17efdf7fdef0b00f398128bc5dcb4ac/fsspec-2025.3.0.tar.gz", hash = "sha256:a935fd1ea872591f2b5148907d103488fc523295e6c64b835cfad8c3eca44972", size = 298491 } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/53/eb690efa8513166adef3e0669afd31e95ffde69fb3c52ec2ac7223ed6018/fsspec-2025.3.0-py3-none-any.whl", hash = "sha256:efb87af3efa9103f94ca91a7f8cb7a4df91af9f74fc106c9c7ea0efd7277c1b3", size = 193615, upload-time = "2025-03-07T21:47:54.809Z" }, + { url = "https://files.pythonhosted.org/packages/56/53/eb690efa8513166adef3e0669afd31e95ffde69fb3c52ec2ac7223ed6018/fsspec-2025.3.0-py3-none-any.whl", hash = "sha256:efb87af3efa9103f94ca91a7f8cb7a4df91af9f74fc106c9c7ea0efd7277c1b3", size = 193615 }, ] [package.optional-dependencies] @@ -488,36 +441,36 @@ http = [ name = "hf-transfer" version = "0.1.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/eb/8fc64f40388c29ce8ce3b2b180a089d4d6b25b1d0d232d016704cb852104/hf_transfer-0.1.9.tar.gz", hash = "sha256:035572865dab29d17e783fbf1e84cf1cb24f3fcf8f1b17db1cfc7fdf139f02bf", size = 25201, upload-time = "2025-01-07T10:05:12.947Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/eb/8fc64f40388c29ce8ce3b2b180a089d4d6b25b1d0d232d016704cb852104/hf_transfer-0.1.9.tar.gz", hash = "sha256:035572865dab29d17e783fbf1e84cf1cb24f3fcf8f1b17db1cfc7fdf139f02bf", size = 25201 } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/f5/461d2e5f307e5048289b1168d5c642ae3bb2504e88dff1a38b92ed990a21/hf_transfer-0.1.9-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e66acf91df4a8b72f60223059df3003062a5ae111757187ed1a06750a30e911b", size = 1393046, upload-time = "2025-01-07T10:04:51.003Z" }, - { url = "https://files.pythonhosted.org/packages/41/ba/8d9fd9f1083525edfcb389c93738c802f3559cb749324090d7109c8bf4c2/hf_transfer-0.1.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:8669dbcc7a3e2e8d61d42cd24da9c50d57770bd74b445c65123291ca842a7e7a", size = 1348126, upload-time = "2025-01-07T10:04:45.712Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a2/cd7885bc9959421065a6fae0fe67b6c55becdeda4e69b873e52976f9a9f0/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fd0167c4407a3bc4cdd0307e65ada2294ec04f1813d8a69a5243e379b22e9d8", size = 3728604, upload-time = "2025-01-07T10:04:14.173Z" }, - { url = "https://files.pythonhosted.org/packages/f6/2e/a072cf196edfeda3310c9a5ade0a0fdd785e6154b3ce24fc738c818da2a7/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee8b10afedcb75f71091bcc197c526a6ebf5c58bbbadb34fdeee6160f55f619f", size = 3064995, upload-time = "2025-01-07T10:04:18.663Z" }, - { url = "https://files.pythonhosted.org/packages/c2/84/aec9ef4c0fab93c1ea2b1badff38c78b4b2f86f0555b26d2051dbc920cde/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5828057e313de59300dd1abb489444bc452efe3f479d3c55b31a8f680936ba42", size = 3580908, upload-time = "2025-01-07T10:04:32.834Z" }, - { url = "https://files.pythonhosted.org/packages/29/63/b560d39651a56603d64f1a0212d0472a44cbd965db2fa62b99d99cb981bf/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc6bd19e1cc177c66bdef15ef8636ad3bde79d5a4f608c158021153b4573509d", size = 3400839, upload-time = "2025-01-07T10:04:26.122Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d8/f87ea6f42456254b48915970ed98e993110521e9263472840174d32c880d/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdca9bfb89e6f8f281890cc61a8aff2d3cecaff7e1a4d275574d96ca70098557", size = 3552664, upload-time = "2025-01-07T10:04:40.123Z" }, - { url = "https://files.pythonhosted.org/packages/d6/56/1267c39b65fc8f4e2113b36297320f102718bf5799b544a6cbe22013aa1d/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89a23f58b7b7effbc047b8ca286f131b17728c99a9f972723323003ffd1bb916", size = 4073732, upload-time = "2025-01-07T10:04:55.624Z" }, - { url = "https://files.pythonhosted.org/packages/82/1a/9c748befbe3decf7cb415e34f8a0c3789a0a9c55910dea73d581e48c0ce5/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:dc7fff1345980d6c0ebb92c811d24afa4b98b3e07ed070c8e38cc91fd80478c5", size = 3390096, upload-time = "2025-01-07T10:04:59.98Z" }, - { url = "https://files.pythonhosted.org/packages/72/85/4c03da147b6b4b7cb12e074d3d44eee28604a387ed0eaf7eaaead5069c57/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1a6bd16c667ebe89a069ca163060127a794fa3a3525292c900b8c8cc47985b0d", size = 3664743, upload-time = "2025-01-07T10:05:05.416Z" }, - { url = "https://files.pythonhosted.org/packages/e7/6e/e597b04f753f1b09e6893075d53a82a30c13855cbaa791402695b01e369f/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d2fde99d502093ade3ab1b53f80da18480e9902aa960dab7f74fb1b9e5bc5746", size = 3695243, upload-time = "2025-01-07T10:05:11.411Z" }, - { url = "https://files.pythonhosted.org/packages/09/89/d4e234727a26b2546c8fb70a276cd924260d60135f2165bf8b9ed67bb9a4/hf_transfer-0.1.9-cp38-abi3-win32.whl", hash = "sha256:435cc3cdc8524ce57b074032b8fd76eed70a4224d2091232fa6a8cef8fd6803e", size = 1086605, upload-time = "2025-01-07T10:05:18.873Z" }, - { url = "https://files.pythonhosted.org/packages/a1/14/f1e15b851d1c2af5b0b1a82bf8eb10bda2da62d98180220ba6fd8879bb5b/hf_transfer-0.1.9-cp38-abi3-win_amd64.whl", hash = "sha256:16f208fc678911c37e11aa7b586bc66a37d02e636208f18b6bc53d29b5df40ad", size = 1160240, upload-time = "2025-01-07T10:05:14.324Z" }, + { url = "https://files.pythonhosted.org/packages/81/f5/461d2e5f307e5048289b1168d5c642ae3bb2504e88dff1a38b92ed990a21/hf_transfer-0.1.9-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e66acf91df4a8b72f60223059df3003062a5ae111757187ed1a06750a30e911b", size = 1393046 }, + { url = "https://files.pythonhosted.org/packages/41/ba/8d9fd9f1083525edfcb389c93738c802f3559cb749324090d7109c8bf4c2/hf_transfer-0.1.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:8669dbcc7a3e2e8d61d42cd24da9c50d57770bd74b445c65123291ca842a7e7a", size = 1348126 }, + { url = "https://files.pythonhosted.org/packages/8e/a2/cd7885bc9959421065a6fae0fe67b6c55becdeda4e69b873e52976f9a9f0/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fd0167c4407a3bc4cdd0307e65ada2294ec04f1813d8a69a5243e379b22e9d8", size = 3728604 }, + { url = "https://files.pythonhosted.org/packages/f6/2e/a072cf196edfeda3310c9a5ade0a0fdd785e6154b3ce24fc738c818da2a7/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee8b10afedcb75f71091bcc197c526a6ebf5c58bbbadb34fdeee6160f55f619f", size = 3064995 }, + { url = "https://files.pythonhosted.org/packages/c2/84/aec9ef4c0fab93c1ea2b1badff38c78b4b2f86f0555b26d2051dbc920cde/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5828057e313de59300dd1abb489444bc452efe3f479d3c55b31a8f680936ba42", size = 3580908 }, + { url = "https://files.pythonhosted.org/packages/29/63/b560d39651a56603d64f1a0212d0472a44cbd965db2fa62b99d99cb981bf/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc6bd19e1cc177c66bdef15ef8636ad3bde79d5a4f608c158021153b4573509d", size = 3400839 }, + { url = "https://files.pythonhosted.org/packages/d6/d8/f87ea6f42456254b48915970ed98e993110521e9263472840174d32c880d/hf_transfer-0.1.9-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdca9bfb89e6f8f281890cc61a8aff2d3cecaff7e1a4d275574d96ca70098557", size = 3552664 }, + { url = "https://files.pythonhosted.org/packages/d6/56/1267c39b65fc8f4e2113b36297320f102718bf5799b544a6cbe22013aa1d/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89a23f58b7b7effbc047b8ca286f131b17728c99a9f972723323003ffd1bb916", size = 4073732 }, + { url = "https://files.pythonhosted.org/packages/82/1a/9c748befbe3decf7cb415e34f8a0c3789a0a9c55910dea73d581e48c0ce5/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:dc7fff1345980d6c0ebb92c811d24afa4b98b3e07ed070c8e38cc91fd80478c5", size = 3390096 }, + { url = "https://files.pythonhosted.org/packages/72/85/4c03da147b6b4b7cb12e074d3d44eee28604a387ed0eaf7eaaead5069c57/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1a6bd16c667ebe89a069ca163060127a794fa3a3525292c900b8c8cc47985b0d", size = 3664743 }, + { url = "https://files.pythonhosted.org/packages/e7/6e/e597b04f753f1b09e6893075d53a82a30c13855cbaa791402695b01e369f/hf_transfer-0.1.9-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d2fde99d502093ade3ab1b53f80da18480e9902aa960dab7f74fb1b9e5bc5746", size = 3695243 }, + { url = "https://files.pythonhosted.org/packages/09/89/d4e234727a26b2546c8fb70a276cd924260d60135f2165bf8b9ed67bb9a4/hf_transfer-0.1.9-cp38-abi3-win32.whl", hash = "sha256:435cc3cdc8524ce57b074032b8fd76eed70a4224d2091232fa6a8cef8fd6803e", size = 1086605 }, + { url = "https://files.pythonhosted.org/packages/a1/14/f1e15b851d1c2af5b0b1a82bf8eb10bda2da62d98180220ba6fd8879bb5b/hf_transfer-0.1.9-cp38-abi3-win_amd64.whl", hash = "sha256:16f208fc678911c37e11aa7b586bc66a37d02e636208f18b6bc53d29b5df40ad", size = 1160240 }, ] [[package]] name = "hf-xet" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/2c/70009910fcbd204bde75842b60c1e47fe72edb0e978954cb8001735885c7/hf_xet-1.1.0.tar.gz", hash = "sha256:a7c2a4c2b6eee9ce0a1a367a82b60d95ba634420ef1c250addad7aa4af419cf4", size = 263996, upload-time = "2025-04-29T21:15:51.247Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2c/70009910fcbd204bde75842b60c1e47fe72edb0e978954cb8001735885c7/hf_xet-1.1.0.tar.gz", hash = "sha256:a7c2a4c2b6eee9ce0a1a367a82b60d95ba634420ef1c250addad7aa4af419cf4", size = 263996 } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/fd/0db331297e331f0f02005fd7ea666439bf15efd74f0dd62af02a43236a1b/hf_xet-1.1.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0322c42551e275fcb7949c083a54a81b2898e50787c9aa74284fcb8d2c58c12c", size = 5069444, upload-time = "2025-04-29T21:15:42.631Z" }, - { url = "https://files.pythonhosted.org/packages/b9/7d/4d7ae44219d3744ad55669cb90ef3d4ed9f5f8a4729fa635a6499491cb78/hf_xet-1.1.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:667153a0304ac2debf2af95a8ff7687186f885b493f4cd16344869af270cd110", size = 4881465, upload-time = "2025-04-29T21:15:40.799Z" }, - { url = "https://files.pythonhosted.org/packages/83/9a/d40d2a57b132d609d8a4ccc29e59ed69749021610616749cabcda2532158/hf_xet-1.1.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:995eeffb119636ea617b96c7d7bf3c3f5ea8727fa57974574e25d700b8532d48", size = 53584225, upload-time = "2025-04-29T21:15:37.754Z" }, - { url = "https://files.pythonhosted.org/packages/2e/01/d94553f91d85746e0862f24d239da88d10f5ce252b028565744e982432f4/hf_xet-1.1.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3aee847da362393331f515c4010d0aaa1c2669acfcca1f4b28946d6949cc0086", size = 52043680, upload-time = "2025-04-29T21:15:34.15Z" }, - { url = "https://files.pythonhosted.org/packages/29/89/1f31853bf378f0ceb3363c07fd8a12af9b904b1f8c21e65eb5c19397bc98/hf_xet-1.1.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68c5813a6074aa36e12ef5983230e3b03148cce61e0fcdd294096493795565b4", size = 53072672, upload-time = "2025-04-29T21:15:44.743Z" }, - { url = "https://files.pythonhosted.org/packages/b5/9f/5ecb92b18a4b2135a72a95dc08bcbeda9176f46642c745ee052420d2aea8/hf_xet-1.1.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ee9222bf9274b1c198b88a929de0b5a49349c4962d89c5b3b2f0f7f47d9761c", size = 53521053, upload-time = "2025-04-29T21:15:48.252Z" }, - { url = "https://files.pythonhosted.org/packages/53/d6/cb32842cbf1cf5a154b41fa918a2fd86003af9bca227a2397cd7f312a8a6/hf_xet-1.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:73153eab9abf3d6973b21e94a67ccba5d595c3e12feb8c0bf50be02964e7f126", size = 4204376, upload-time = "2025-04-29T21:15:52.69Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fd/0db331297e331f0f02005fd7ea666439bf15efd74f0dd62af02a43236a1b/hf_xet-1.1.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0322c42551e275fcb7949c083a54a81b2898e50787c9aa74284fcb8d2c58c12c", size = 5069444 }, + { url = "https://files.pythonhosted.org/packages/b9/7d/4d7ae44219d3744ad55669cb90ef3d4ed9f5f8a4729fa635a6499491cb78/hf_xet-1.1.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:667153a0304ac2debf2af95a8ff7687186f885b493f4cd16344869af270cd110", size = 4881465 }, + { url = "https://files.pythonhosted.org/packages/83/9a/d40d2a57b132d609d8a4ccc29e59ed69749021610616749cabcda2532158/hf_xet-1.1.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:995eeffb119636ea617b96c7d7bf3c3f5ea8727fa57974574e25d700b8532d48", size = 53584225 }, + { url = "https://files.pythonhosted.org/packages/2e/01/d94553f91d85746e0862f24d239da88d10f5ce252b028565744e982432f4/hf_xet-1.1.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3aee847da362393331f515c4010d0aaa1c2669acfcca1f4b28946d6949cc0086", size = 52043680 }, + { url = "https://files.pythonhosted.org/packages/29/89/1f31853bf378f0ceb3363c07fd8a12af9b904b1f8c21e65eb5c19397bc98/hf_xet-1.1.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68c5813a6074aa36e12ef5983230e3b03148cce61e0fcdd294096493795565b4", size = 53072672 }, + { url = "https://files.pythonhosted.org/packages/b5/9f/5ecb92b18a4b2135a72a95dc08bcbeda9176f46642c745ee052420d2aea8/hf_xet-1.1.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ee9222bf9274b1c198b88a929de0b5a49349c4962d89c5b3b2f0f7f47d9761c", size = 53521053 }, + { url = "https://files.pythonhosted.org/packages/53/d6/cb32842cbf1cf5a154b41fa918a2fd86003af9bca227a2397cd7f312a8a6/hf_xet-1.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:73153eab9abf3d6973b21e94a67ccba5d595c3e12feb8c0bf50be02964e7f126", size = 4204376 }, ] [[package]] @@ -531,9 +484,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/26/aaae4cab984f0b7dd0f5f1b823fa2ed2fd4a2bb50acd5bd2f0d217562678/htmldate-1.9.3.tar.gz", hash = "sha256:ac0caf4628c3ded4042011e2d60dc68dfb314c77b106587dd307a80d77e708e9", size = 44913, upload-time = "2024-12-30T12:52:35.206Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/26/aaae4cab984f0b7dd0f5f1b823fa2ed2fd4a2bb50acd5bd2f0d217562678/htmldate-1.9.3.tar.gz", hash = "sha256:ac0caf4628c3ded4042011e2d60dc68dfb314c77b106587dd307a80d77e708e9", size = 44913 } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/49/8872130016209c20436ce0c1067de1cf630755d0443d068a5bc17fa95015/htmldate-1.9.3-py3-none-any.whl", hash = "sha256:3fadc422cf3c10a5cdb5e1b914daf37ec7270400a80a1b37e2673ff84faaaff8", size = 31565, upload-time = "2024-12-30T12:52:32.145Z" }, + { url = "https://files.pythonhosted.org/packages/05/49/8872130016209c20436ce0c1067de1cf630755d0443d068a5bc17fa95015/htmldate-1.9.3-py3-none-any.whl", hash = "sha256:3fadc422cf3c10a5cdb5e1b914daf37ec7270400a80a1b37e2673ff84faaaff8", size = 31565 }, ] [[package]] @@ -549,9 +502,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/22/8eb91736b1dcb83d879bd49050a09df29a57cc5cd9f38e48a4b1c45ee890/huggingface_hub-0.30.2.tar.gz", hash = "sha256:9a7897c5b6fd9dad3168a794a8998d6378210f5b9688d0dfc180b1a228dc2466", size = 400868, upload-time = "2025-04-08T08:32:45.26Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/22/8eb91736b1dcb83d879bd49050a09df29a57cc5cd9f38e48a4b1c45ee890/huggingface_hub-0.30.2.tar.gz", hash = "sha256:9a7897c5b6fd9dad3168a794a8998d6378210f5b9688d0dfc180b1a228dc2466", size = 400868 } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/27/1fb384a841e9661faad1c31cbfa62864f59632e876df5d795234da51c395/huggingface_hub-0.30.2-py3-none-any.whl", hash = "sha256:68ff05969927058cfa41df4f2155d4bb48f5f54f719dd0390103eefa9b191e28", size = 481433, upload-time = "2025-04-08T08:32:43.305Z" }, + { url = "https://files.pythonhosted.org/packages/93/27/1fb384a841e9661faad1c31cbfa62864f59632e876df5d795234da51c395/huggingface_hub-0.30.2-py3-none-any.whl", hash = "sha256:68ff05969927058cfa41df4f2155d4bb48f5f54f719dd0390103eefa9b191e28", size = 481433 }, ] [package.optional-dependencies] @@ -569,48 +522,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyreadline3", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] name = "isodate" version = "0.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, -] - -[[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" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } 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" }, + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, ] [[package]] -name = "joblib" -version = "1.5.0" +name = "jmespath" +version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/08/8bd4a0250247861420a040b33ccf42f43c426ac91d99405374ef117e5872/joblib-1.5.0.tar.gz", hash = "sha256:d8757f955389a3dd7a23152e43bc297c2e0c2d3060056dad0feefc88a06939b5", size = 330234, upload-time = "2025-05-03T21:09:39.553Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/d3/13ee227a148af1c693654932b8b0b02ed64af5e1f7406d56b088b57574cd/joblib-1.5.0-py3-none-any.whl", hash = "sha256:206144b320246485b712fc8cc51f017de58225fa8b414a1fe1764a7231aca491", size = 307682, upload-time = "2025-05-03T21:09:37.892Z" }, + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, ] [[package]] @@ -620,32 +561,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lxml", extra = ["html-clean"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/f3/45890c1b314f0d04e19c1c83d534e611513150939a7cf039664d9ab1e649/justext-3.0.2.tar.gz", hash = "sha256:13496a450c44c4cd5b5a75a5efcd9996066d2a189794ea99a49949685a0beb05", size = 828521, upload-time = "2025-02-25T20:21:49.934Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/ac/52f4e86d1924a7fc05af3aeb34488570eccc39b4af90530dd6acecdf16b5/justext-3.0.2-py2.py3-none-any.whl", hash = "sha256:62b1c562b15c3c6265e121cc070874243a443bfd53060e869393f09d6b6cc9a7", size = 837940, upload-time = "2025-02-25T20:21:44.179Z" }, -] - -[[package]] -name = "kiwisolver" -version = "1.4.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/f3/45890c1b314f0d04e19c1c83d534e611513150939a7cf039664d9ab1e649/justext-3.0.2.tar.gz", hash = "sha256:13496a450c44c4cd5b5a75a5efcd9996066d2a189794ea99a49949685a0beb05", size = 828521 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" }, - { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" }, - { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" }, - { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" }, - { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" }, - { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" }, - { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" }, - { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" }, - { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" }, - { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, - { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, - { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ac/52f4e86d1924a7fc05af3aeb34488570eccc39b4af90530dd6acecdf16b5/justext-3.0.2-py2.py3-none-any.whl", hash = "sha256:62b1c562b15c3c6265e121cc070874243a443bfd53060e869393f09d6b6cc9a7", size = 837940 }, ] [[package]] @@ -656,34 +574,34 @@ dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "win32-setctime", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, ] [[package]] name = "lxml" version = "5.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" }, - { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" }, - { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" }, - { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" }, - { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" }, - { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" }, - { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" }, - { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" }, - { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" }, - { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" }, - { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" }, - { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" }, - { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392 }, + { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103 }, + { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224 }, + { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913 }, + { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441 }, + { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580 }, + { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493 }, + { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679 }, + { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691 }, + { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075 }, + { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680 }, + { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253 }, + { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651 }, + { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315 }, + { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149 }, + { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095 }, ] [package.optional-dependencies] @@ -698,9 +616,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lxml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/b6/466e71db127950fb8d172026a8f0a9f0dc6f64c8e78e2ca79f252e5790b8/lxml_html_clean-0.4.2.tar.gz", hash = "sha256:91291e7b5db95430abf461bc53440964d58e06cc468950f9e47db64976cebcb3", size = 21622, upload-time = "2025-04-09T11:33:59.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b6/466e71db127950fb8d172026a8f0a9f0dc6f64c8e78e2ca79f252e5790b8/lxml_html_clean-0.4.2.tar.gz", hash = "sha256:91291e7b5db95430abf461bc53440964d58e06cc468950f9e47db64976cebcb3", size = 21622 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/0b/942cb7278d6caad79343ad2ddd636ed204a47909b969d19114a3097f5aa3/lxml_html_clean-0.4.2-py3-none-any.whl", hash = "sha256:74ccfba277adcfea87a1e9294f47dd86b05d65b4da7c5b07966e3d5f3be8a505", size = 14184, upload-time = "2025-04-09T11:33:57.988Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0b/942cb7278d6caad79343ad2ddd636ed204a47909b969d19114a3097f5aa3/lxml_html_clean-0.4.2-py3-none-any.whl", hash = "sha256:74ccfba277adcfea87a1e9294f47dd86b05d65b4da7c5b07966e3d5f3be8a505", size = 14184 }, ] [[package]] @@ -713,12 +631,12 @@ dependencies = [ { name = "onnxruntime" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/8fdd991142ad3e037179a494b153f463024e5a211ef3ad948b955c26b4de/magika-0.6.2.tar.gz", hash = "sha256:37eb6ae8020f6e68f231bc06052c0a0cbe8e6fa27492db345e8dc867dbceb067", size = 3036634, upload-time = "2025-05-02T14:54:18.88Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/8fdd991142ad3e037179a494b153f463024e5a211ef3ad948b955c26b4de/magika-0.6.2.tar.gz", hash = "sha256:37eb6ae8020f6e68f231bc06052c0a0cbe8e6fa27492db345e8dc867dbceb067", size = 3036634 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/07/4f7748f34279f2852068256992377474f9700b6fbad6735d6be58605178f/magika-0.6.2-py3-none-any.whl", hash = "sha256:5ef72fbc07723029b3684ef81454bc224ac5f60986aa0fc5a28f4456eebcb5b2", size = 2967609, upload-time = "2025-05-02T14:54:09.696Z" }, - { url = "https://files.pythonhosted.org/packages/64/6d/0783af677e601d8a42258f0fbc47663abf435f927e58a8d2928296743099/magika-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9109309328a1553886c8ff36c2ee9a5e9cfd36893ad81b65bf61a57debdd9d0e", size = 12404787, upload-time = "2025-05-02T14:54:16.963Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ad/42e39748ddc4bbe55c2dc1093ce29079c04d096ac0d844f8ae66178bc3ed/magika-0.6.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:57cd1d64897634d15de552bd6b3ae9c6ff6ead9c60d384dc46497c08288e4559", size = 15091089, upload-time = "2025-05-02T14:54:11.59Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1f/28e412d0ccedc068fbccdae6a6233faaa97ec3e5e2ffd242e49655b10064/magika-0.6.2-py3-none-win_amd64.whl", hash = "sha256:711f427a633e0182737dcc2074748004842f870643585813503ff2553b973b9f", size = 12385740, upload-time = "2025-05-02T14:54:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/07/4f7748f34279f2852068256992377474f9700b6fbad6735d6be58605178f/magika-0.6.2-py3-none-any.whl", hash = "sha256:5ef72fbc07723029b3684ef81454bc224ac5f60986aa0fc5a28f4456eebcb5b2", size = 2967609 }, + { url = "https://files.pythonhosted.org/packages/64/6d/0783af677e601d8a42258f0fbc47663abf435f927e58a8d2928296743099/magika-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9109309328a1553886c8ff36c2ee9a5e9cfd36893ad81b65bf61a57debdd9d0e", size = 12404787 }, + { url = "https://files.pythonhosted.org/packages/8a/ad/42e39748ddc4bbe55c2dc1093ce29079c04d096ac0d844f8ae66178bc3ed/magika-0.6.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:57cd1d64897634d15de552bd6b3ae9c6ff6ead9c60d384dc46497c08288e4559", size = 15091089 }, + { url = "https://files.pythonhosted.org/packages/b0/1f/28e412d0ccedc068fbccdae6a6233faaa97ec3e5e2ffd242e49655b10064/magika-0.6.2-py3-none-win_amd64.whl", hash = "sha256:711f427a633e0182737dcc2074748004842f870643585813503ff2553b973b9f", size = 12385740 }, ] [[package]] @@ -728,9 +646,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cobble" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/a6/27a13ba068cf3ff764d631b8dd71dee1b33040aa8c143f66ce902b7d1da0/mammoth-1.9.0.tar.gz", hash = "sha256:74f5dae10ca240fd9b7a0e1a6deaebe0aad23bc590633ef6f5e868aa9b7042a6", size = 50906, upload-time = "2024-12-30T10:33:37.733Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/a6/27a13ba068cf3ff764d631b8dd71dee1b33040aa8c143f66ce902b7d1da0/mammoth-1.9.0.tar.gz", hash = "sha256:74f5dae10ca240fd9b7a0e1a6deaebe0aad23bc590633ef6f5e868aa9b7042a6", size = 50906 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/ab/f8e63fcabc127c6efd68b03633c189ee799a5304fa96c036a325a2894bcb/mammoth-1.9.0-py2.py3-none-any.whl", hash = "sha256:0eea277316586f0ca65d86834aec4de5a0572c83ec54b4991f9bb520a891150f", size = 52901, upload-time = "2024-12-30T10:33:34.879Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ab/f8e63fcabc127c6efd68b03633c189ee799a5304fa96c036a325a2894bcb/mammoth-1.9.0-py2.py3-none-any.whl", hash = "sha256:0eea277316586f0ca65d86834aec4de5a0572c83ec54b4991f9bb520a891150f", size = 52901 }, ] [[package]] @@ -740,9 +658,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, ] [[package]] @@ -753,9 +671,9 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/78/c48fed23c7aebc2c16049062e72de1da3220c274de59d28c942acdc9ffb2/markdownify-1.1.0.tar.gz", hash = "sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd", size = 17127, upload-time = "2025-03-05T11:54:40.574Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/78/c48fed23c7aebc2c16049062e72de1da3220c274de59d28c942acdc9ffb2/markdownify-1.1.0.tar.gz", hash = "sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd", size = 17127 } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/11/b751af7ad41b254a802cf52f7bc1fca7cabe2388132f2ce60a1a6b9b9622/markdownify-1.1.0-py3-none-any.whl", hash = "sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef", size = 13901, upload-time = "2025-03-05T11:54:39.454Z" }, + { url = "https://files.pythonhosted.org/packages/64/11/b751af7ad41b254a802cf52f7bc1fca7cabe2388132f2ce60a1a6b9b9622/markdownify-1.1.0-py3-none-any.whl", hash = "sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef", size = 13901 }, ] [[package]] @@ -769,9 +687,9 @@ dependencies = [ { name = "markdownify" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/e8/83669ba97718bbbccd4c432b763d22783df4c8218e770717151acf01e85b/markitdown-0.1.1.tar.gz", hash = "sha256:da97a55a45a3d775ea758e88a344d5cac94ee97115fb0293f99027d32c2fc3f6", size = 31475, upload-time = "2025-03-25T06:22:21.438Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/e8/83669ba97718bbbccd4c432b763d22783df4c8218e770717151acf01e85b/markitdown-0.1.1.tar.gz", hash = "sha256:da97a55a45a3d775ea758e88a344d5cac94ee97115fb0293f99027d32c2fc3f6", size = 31475 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/8a/c1f85ee609de5d45f80d0213bebf6664f76ab406e9d57709e684a4a436ba/markitdown-0.1.1-py3-none-any.whl", hash = "sha256:98ea8c009fe174b37ef933e00f4364214e8fed35691178b8521b13604d0c4a58", size = 48230, upload-time = "2025-03-25T06:22:19.773Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8a/c1f85ee609de5d45f80d0213bebf6664f76ab406e9d57709e684a4a436ba/markitdown-0.1.1-py3-none-any.whl", hash = "sha256:98ea8c009fe174b37ef933e00f4364214e8fed35691178b8521b13604d0c4a58", size = 48230 }, ] [package.optional-dependencies] @@ -790,65 +708,22 @@ all = [ { name = "youtube-transcript-api" }, ] -[[package]] -name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, -] - -[[package]] -name = "matplotlib" -version = "3.10.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "contourpy" }, - { name = "cycler" }, - { name = "fonttools" }, - { name = "kiwisolver" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "pyparsing" }, - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2f/08/b89867ecea2e305f408fbb417139a8dd941ecf7b23a2e02157c36da546f0/matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba", size = 36743335, upload-time = "2025-02-27T19:19:51.038Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/1d/5e0dc3b59c034e43de16f94deb68f4ad8a96b3ea00f4b37c160b7474928e/matplotlib-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:66e907a06e68cb6cfd652c193311d61a12b54f56809cafbed9736ce5ad92f107", size = 8175488, upload-time = "2025-02-27T19:18:51.436Z" }, - { url = "https://files.pythonhosted.org/packages/7a/81/dae7e14042e74da658c3336ab9799128e09a1ee03964f2d89630b5d12106/matplotlib-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b4bb156abb8fa5e5b2b460196f7db7264fc6d62678c03457979e7d5254b7be", size = 8046264, upload-time = "2025-02-27T19:18:54.344Z" }, - { url = "https://files.pythonhosted.org/packages/21/c4/22516775dcde10fc9c9571d155f90710761b028fc44f660508106c363c97/matplotlib-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1985ad3d97f51307a2cbfc801a930f120def19ba22864182dacef55277102ba6", size = 8452048, upload-time = "2025-02-27T19:18:56.536Z" }, - { url = "https://files.pythonhosted.org/packages/63/23/c0615001f67ce7c96b3051d856baedc0c818a2ed84570b9bf9bde200f85d/matplotlib-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96f2c2f825d1257e437a1482c5a2cf4fee15db4261bd6fc0750f81ba2b4ba3d", size = 8597111, upload-time = "2025-02-27T19:18:59.439Z" }, - { url = "https://files.pythonhosted.org/packages/ca/c0/a07939a82aed77770514348f4568177d7dadab9787ebc618a616fe3d665e/matplotlib-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35e87384ee9e488d8dd5a2dd7baf471178d38b90618d8ea147aced4ab59c9bea", size = 9402771, upload-time = "2025-02-27T19:19:01.944Z" }, - { url = "https://files.pythonhosted.org/packages/a6/b6/a9405484fb40746fdc6ae4502b16a9d6e53282ba5baaf9ebe2da579f68c4/matplotlib-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfd414bce89cc78a7e1d25202e979b3f1af799e416010a20ab2b5ebb3a02425c", size = 8063742, upload-time = "2025-02-27T19:19:04.632Z" }, -] - [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 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" }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } 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" }, + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, ] [[package]] @@ -860,9 +735,9 @@ dependencies = [ { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/90/81dcc50f0be11a8c4dcbae1a9f761a26e5f905231330a7cacc9f04ec4c61/msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35", size = 151449, upload-time = "2025-04-25T13:12:34.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/90/81dcc50f0be11a8c4dcbae1a9f761a26e5f905231330a7cacc9f04ec4c61/msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35", size = 151449 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/bf/81516b9aac7fd867709984d08eb4db1d2e3fe1df795c8e442cde9b568962/msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569", size = 115358, upload-time = "2025-04-25T13:12:33.034Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/81516b9aac7fd867709984d08eb4db1d2e3fe1df795c8e442cde9b568962/msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569", size = 115358 }, ] [[package]] @@ -872,35 +747,35 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "msal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 }, ] [[package]] name = "multidict" version = "6.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/2c/e367dfb4c6538614a0c9453e510d75d66099edf1c4e69da1b5ce691a1931/multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", size = 89372, upload-time = "2025-04-10T22:20:17.956Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/bb/3abdaf8fe40e9226ce8a2ba5ecf332461f7beec478a455d6587159f1bf92/multidict-6.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676", size = 64019, upload-time = "2025-04-10T22:18:23.174Z" }, - { url = "https://files.pythonhosted.org/packages/7e/b5/1b2e8de8217d2e89db156625aa0fe4a6faad98972bfe07a7b8c10ef5dd6b/multidict-6.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1", size = 37925, upload-time = "2025-04-10T22:18:24.834Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e2/3ca91c112644a395c8eae017144c907d173ea910c913ff8b62549dcf0bbf/multidict-6.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a", size = 37008, upload-time = "2025-04-10T22:18:26.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/23/79bc78146c7ac8d1ac766b2770ca2e07c2816058b8a3d5da6caed8148637/multidict-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054", size = 224374, upload-time = "2025-04-10T22:18:27.714Z" }, - { url = "https://files.pythonhosted.org/packages/86/35/77950ed9ebd09136003a85c1926ba42001ca5be14feb49710e4334ee199b/multidict-6.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc", size = 230869, upload-time = "2025-04-10T22:18:29.162Z" }, - { url = "https://files.pythonhosted.org/packages/49/97/2a33c6e7d90bc116c636c14b2abab93d6521c0c052d24bfcc231cbf7f0e7/multidict-6.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07", size = 231949, upload-time = "2025-04-10T22:18:30.679Z" }, - { url = "https://files.pythonhosted.org/packages/56/ce/e9b5d9fcf854f61d6686ada7ff64893a7a5523b2a07da6f1265eaaea5151/multidict-6.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde", size = 231032, upload-time = "2025-04-10T22:18:32.146Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ac/7ced59dcdfeddd03e601edb05adff0c66d81ed4a5160c443e44f2379eef0/multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c", size = 223517, upload-time = "2025-04-10T22:18:33.538Z" }, - { url = "https://files.pythonhosted.org/packages/db/e6/325ed9055ae4e085315193a1b58bdb4d7fc38ffcc1f4975cfca97d015e17/multidict-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae", size = 216291, upload-time = "2025-04-10T22:18:34.962Z" }, - { url = "https://files.pythonhosted.org/packages/fa/84/eeee6d477dd9dcb7691c3bb9d08df56017f5dd15c730bcc9383dcf201cf4/multidict-6.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3", size = 228982, upload-time = "2025-04-10T22:18:36.443Z" }, - { url = "https://files.pythonhosted.org/packages/82/94/4d1f3e74e7acf8b0c85db350e012dcc61701cd6668bc2440bb1ecb423c90/multidict-6.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507", size = 226823, upload-time = "2025-04-10T22:18:37.924Z" }, - { url = "https://files.pythonhosted.org/packages/09/f0/1e54b95bda7cd01080e5732f9abb7b76ab5cc795b66605877caeb2197476/multidict-6.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427", size = 222714, upload-time = "2025-04-10T22:18:39.807Z" }, - { url = "https://files.pythonhosted.org/packages/e7/a2/f6cbca875195bd65a3e53b37ab46486f3cc125bdeab20eefe5042afa31fb/multidict-6.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731", size = 233739, upload-time = "2025-04-10T22:18:41.341Z" }, - { url = "https://files.pythonhosted.org/packages/79/68/9891f4d2b8569554723ddd6154375295f789dc65809826c6fb96a06314fd/multidict-6.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713", size = 230809, upload-time = "2025-04-10T22:18:42.817Z" }, - { url = "https://files.pythonhosted.org/packages/e6/72/a7be29ba1e87e4fc5ceb44dabc7940b8005fd2436a332a23547709315f70/multidict-6.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a", size = 226934, upload-time = "2025-04-10T22:18:44.311Z" }, - { url = "https://files.pythonhosted.org/packages/12/c1/259386a9ad6840ff7afc686da96808b503d152ac4feb3a96c651dc4f5abf/multidict-6.4.3-cp312-cp312-win32.whl", hash = "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124", size = 35242, upload-time = "2025-04-10T22:18:46.193Z" }, - { url = "https://files.pythonhosted.org/packages/06/24/c8fdff4f924d37225dc0c56a28b1dca10728fc2233065fafeb27b4b125be/multidict-6.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db", size = 38635, upload-time = "2025-04-10T22:18:47.498Z" }, - { url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400, upload-time = "2025-04-10T22:20:16.445Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/da/2c/e367dfb4c6538614a0c9453e510d75d66099edf1c4e69da1b5ce691a1931/multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", size = 89372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/bb/3abdaf8fe40e9226ce8a2ba5ecf332461f7beec478a455d6587159f1bf92/multidict-6.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676", size = 64019 }, + { url = "https://files.pythonhosted.org/packages/7e/b5/1b2e8de8217d2e89db156625aa0fe4a6faad98972bfe07a7b8c10ef5dd6b/multidict-6.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1", size = 37925 }, + { url = "https://files.pythonhosted.org/packages/b4/e2/3ca91c112644a395c8eae017144c907d173ea910c913ff8b62549dcf0bbf/multidict-6.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a", size = 37008 }, + { url = "https://files.pythonhosted.org/packages/60/23/79bc78146c7ac8d1ac766b2770ca2e07c2816058b8a3d5da6caed8148637/multidict-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054", size = 224374 }, + { url = "https://files.pythonhosted.org/packages/86/35/77950ed9ebd09136003a85c1926ba42001ca5be14feb49710e4334ee199b/multidict-6.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc", size = 230869 }, + { url = "https://files.pythonhosted.org/packages/49/97/2a33c6e7d90bc116c636c14b2abab93d6521c0c052d24bfcc231cbf7f0e7/multidict-6.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07", size = 231949 }, + { url = "https://files.pythonhosted.org/packages/56/ce/e9b5d9fcf854f61d6686ada7ff64893a7a5523b2a07da6f1265eaaea5151/multidict-6.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde", size = 231032 }, + { url = "https://files.pythonhosted.org/packages/f0/ac/7ced59dcdfeddd03e601edb05adff0c66d81ed4a5160c443e44f2379eef0/multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c", size = 223517 }, + { url = "https://files.pythonhosted.org/packages/db/e6/325ed9055ae4e085315193a1b58bdb4d7fc38ffcc1f4975cfca97d015e17/multidict-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae", size = 216291 }, + { url = "https://files.pythonhosted.org/packages/fa/84/eeee6d477dd9dcb7691c3bb9d08df56017f5dd15c730bcc9383dcf201cf4/multidict-6.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3", size = 228982 }, + { url = "https://files.pythonhosted.org/packages/82/94/4d1f3e74e7acf8b0c85db350e012dcc61701cd6668bc2440bb1ecb423c90/multidict-6.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507", size = 226823 }, + { url = "https://files.pythonhosted.org/packages/09/f0/1e54b95bda7cd01080e5732f9abb7b76ab5cc795b66605877caeb2197476/multidict-6.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427", size = 222714 }, + { url = "https://files.pythonhosted.org/packages/e7/a2/f6cbca875195bd65a3e53b37ab46486f3cc125bdeab20eefe5042afa31fb/multidict-6.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731", size = 233739 }, + { url = "https://files.pythonhosted.org/packages/79/68/9891f4d2b8569554723ddd6154375295f789dc65809826c6fb96a06314fd/multidict-6.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713", size = 230809 }, + { url = "https://files.pythonhosted.org/packages/e6/72/a7be29ba1e87e4fc5ceb44dabc7940b8005fd2436a332a23547709315f70/multidict-6.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a", size = 226934 }, + { url = "https://files.pythonhosted.org/packages/12/c1/259386a9ad6840ff7afc686da96808b503d152ac4feb3a96c651dc4f5abf/multidict-6.4.3-cp312-cp312-win32.whl", hash = "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124", size = 35242 }, + { url = "https://files.pythonhosted.org/packages/06/24/c8fdff4f924d37225dc0c56a28b1dca10728fc2233065fafeb27b4b125be/multidict-6.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db", size = 38635 }, + { url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400 }, ] [[package]] @@ -910,197 +785,40 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dill" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/ae/04f39c5d0d0def03247c2893d6f2b83c136bf3320a2154d7b8858f2ba72d/multiprocess-0.70.16.tar.gz", hash = "sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1", size = 1772603, upload-time = "2024-01-28T18:52:34.85Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/ae/04f39c5d0d0def03247c2893d6f2b83c136bf3320a2154d7b8858f2ba72d/multiprocess-0.70.16.tar.gz", hash = "sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1", size = 1772603 } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02", size = 134824, upload-time = "2024-01-28T18:52:26.062Z" }, - { url = "https://files.pythonhosted.org/packages/50/15/b56e50e8debaf439f44befec5b2af11db85f6e0f344c3113ae0be0593a91/multiprocess-0.70.16-py311-none-any.whl", hash = "sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a", size = 143519, upload-time = "2024-01-28T18:52:28.115Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7d/a988f258104dcd2ccf1ed40fdc97e26c4ac351eeaf81d76e266c52d84e2f/multiprocess-0.70.16-py312-none-any.whl", hash = "sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e", size = 146741, upload-time = "2024-01-28T18:52:29.395Z" }, - { url = "https://files.pythonhosted.org/packages/ea/89/38df130f2c799090c978b366cfdf5b96d08de5b29a4a293df7f7429fa50b/multiprocess-0.70.16-py38-none-any.whl", hash = "sha256:a71d82033454891091a226dfc319d0cfa8019a4e888ef9ca910372a446de4435", size = 132628, upload-time = "2024-01-28T18:52:30.853Z" }, - { url = "https://files.pythonhosted.org/packages/da/d9/f7f9379981e39b8c2511c9e0326d212accacb82f12fbfdc1aa2ce2a7b2b6/multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3", size = 133351, upload-time = "2024-01-28T18:52:31.981Z" }, -] - -[[package]] -name = "networkx" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, -] - -[[package]] -name = "nltk" -version = "3.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "joblib" }, - { name = "regex" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691, upload-time = "2024-08-18T19:48:37.769Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442, upload-time = "2024-08-18T19:48:21.909Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f7/7ec7fddc92e50714ea3745631f79bd9c96424cb2702632521028e57d3a36/multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02", size = 134824 }, + { url = "https://files.pythonhosted.org/packages/50/15/b56e50e8debaf439f44befec5b2af11db85f6e0f344c3113ae0be0593a91/multiprocess-0.70.16-py311-none-any.whl", hash = "sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a", size = 143519 }, + { url = "https://files.pythonhosted.org/packages/0a/7d/a988f258104dcd2ccf1ed40fdc97e26c4ac351eeaf81d76e266c52d84e2f/multiprocess-0.70.16-py312-none-any.whl", hash = "sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e", size = 146741 }, + { url = "https://files.pythonhosted.org/packages/ea/89/38df130f2c799090c978b366cfdf5b96d08de5b29a4a293df7f7429fa50b/multiprocess-0.70.16-py38-none-any.whl", hash = "sha256:a71d82033454891091a226dfc319d0cfa8019a4e888ef9ca910372a446de4435", size = 132628 }, + { url = "https://files.pythonhosted.org/packages/da/d9/f7f9379981e39b8c2511c9e0326d212accacb82f12fbfdc1aa2ce2a7b2b6/multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3", size = 133351 }, ] [[package]] name = "numpy" version = "2.2.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/b2/ce4b867d8cd9c0ee84938ae1e6a6f7926ebf928c9090d036fc3c6a04f946/numpy-2.2.5.tar.gz", hash = "sha256:a9c0d994680cd991b1cb772e8b297340085466a6fe964bc9d4e80f5e2f43c291", size = 20273920, upload-time = "2025-04-19T23:27:42.561Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/f7/1fd4ff108cd9d7ef929b8882692e23665dc9c23feecafbb9c6b80f4ec583/numpy-2.2.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ee461a4eaab4f165b68780a6a1af95fb23a29932be7569b9fab666c407969051", size = 20948633, upload-time = "2025-04-19T22:37:52.4Z" }, - { url = "https://files.pythonhosted.org/packages/12/03/d443c278348371b20d830af155ff2079acad6a9e60279fac2b41dbbb73d8/numpy-2.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec31367fd6a255dc8de4772bd1658c3e926d8e860a0b6e922b615e532d320ddc", size = 14176123, upload-time = "2025-04-19T22:38:15.058Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0b/5ca264641d0e7b14393313304da48b225d15d471250376f3fbdb1a2be603/numpy-2.2.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:47834cde750d3c9f4e52c6ca28a7361859fcaf52695c7dc3cc1a720b8922683e", size = 5163817, upload-time = "2025-04-19T22:38:24.885Z" }, - { url = "https://files.pythonhosted.org/packages/04/b3/d522672b9e3d28e26e1613de7675b441bbd1eaca75db95680635dd158c67/numpy-2.2.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2c1a1c6ccce4022383583a6ded7bbcda22fc635eb4eb1e0a053336425ed36dfa", size = 6698066, upload-time = "2025-04-19T22:38:35.782Z" }, - { url = "https://files.pythonhosted.org/packages/a0/93/0f7a75c1ff02d4b76df35079676b3b2719fcdfb39abdf44c8b33f43ef37d/numpy-2.2.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d75f338f5f79ee23548b03d801d28a505198297534f62416391857ea0479571", size = 14087277, upload-time = "2025-04-19T22:38:57.697Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d9/7c338b923c53d431bc837b5b787052fef9ae68a56fe91e325aac0d48226e/numpy-2.2.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a801fef99668f309b88640e28d261991bfad9617c27beda4a3aec4f217ea073", size = 16135742, upload-time = "2025-04-19T22:39:22.689Z" }, - { url = "https://files.pythonhosted.org/packages/2d/10/4dec9184a5d74ba9867c6f7d1e9f2e0fb5fe96ff2bf50bb6f342d64f2003/numpy-2.2.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:abe38cd8381245a7f49967a6010e77dbf3680bd3627c0fe4362dd693b404c7f8", size = 15581825, upload-time = "2025-04-19T22:39:45.794Z" }, - { url = "https://files.pythonhosted.org/packages/80/1f/2b6fcd636e848053f5b57712a7d1880b1565eec35a637fdfd0a30d5e738d/numpy-2.2.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a0ac90e46fdb5649ab6369d1ab6104bfe5854ab19b645bf5cda0127a13034ae", size = 17899600, upload-time = "2025-04-19T22:40:13.427Z" }, - { url = "https://files.pythonhosted.org/packages/ec/87/36801f4dc2623d76a0a3835975524a84bd2b18fe0f8835d45c8eae2f9ff2/numpy-2.2.5-cp312-cp312-win32.whl", hash = "sha256:0cd48122a6b7eab8f06404805b1bd5856200e3ed6f8a1b9a194f9d9054631beb", size = 6312626, upload-time = "2025-04-19T22:40:25.223Z" }, - { url = "https://files.pythonhosted.org/packages/8b/09/4ffb4d6cfe7ca6707336187951992bd8a8b9142cf345d87ab858d2d7636a/numpy-2.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:ced69262a8278547e63409b2653b372bf4baff0870c57efa76c5703fd6543282", size = 12645715, upload-time = "2025-04-19T22:40:44.528Z" }, -] - -[[package]] -name = "nvidia-cublas-cu12" -version = "12.6.4.1" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/eb/ff4b8c503fa1f1796679dce648854d58751982426e4e4b37d6fce49d259c/nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb", size = 393138322, upload-time = "2024-11-20T17:40:25.65Z" }, -] - -[[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.6.80" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/60/7b6497946d74bcf1de852a21824d63baad12cd417db4195fc1bfe59db953/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6768bad6cab4f19e8292125e5f1ac8aa7d1718704012a0e3272a6f61c4bce132", size = 8917980, upload-time = "2024-11-20T17:36:04.019Z" }, - { url = "https://files.pythonhosted.org/packages/a5/24/120ee57b218d9952c379d1e026c4479c9ece9997a4fb46303611ee48f038/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a3eff6cdfcc6a4c35db968a06fcadb061cbc7d6dde548609a941ff8701b98b73", size = 8917972, upload-time = "2024-10-01T16:58:06.036Z" }, -] - -[[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.6.77" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/2e/46030320b5a80661e88039f59060d1790298b4718944a65a7f2aeda3d9e9/nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53", size = 23650380, upload-time = "2024-10-01T17:00:14.643Z" }, -] - -[[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.6.77" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/23/e717c5ac26d26cf39a27fbc076240fad2e3b817e5889d671b67f4f9f49c5/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba3b56a4f896141e25e19ab287cd71e52a6a0f4b29d0d31609f60e3b4d5219b7", size = 897690, upload-time = "2024-11-20T17:35:30.697Z" }, - { url = "https://files.pythonhosted.org/packages/f0/62/65c05e161eeddbafeca24dc461f47de550d9fa8a7e04eb213e32b55cfd99/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a84d15d5e1da416dd4774cb42edf5e954a3e60cc945698dc1d5be02321c44dc8", size = 897678, upload-time = "2024-10-01T16:57:33.821Z" }, -] - -[[package]] -name = "nvidia-cudnn-cu12" -version = "9.5.1.17" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-cublas-cu12" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/78/4535c9c7f859a64781e43c969a3a7e84c54634e319a996d43ef32ce46f83/nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2", size = 570988386, upload-time = "2024-10-25T19:54:26.39Z" }, -] - -[[package]] -name = "nvidia-cufft-cu12" -version = "11.3.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, -] +sdist = { url = "https://files.pythonhosted.org/packages/dc/b2/ce4b867d8cd9c0ee84938ae1e6a6f7926ebf928c9090d036fc3c6a04f946/numpy-2.2.5.tar.gz", hash = "sha256:a9c0d994680cd991b1cb772e8b297340085466a6fe964bc9d4e80f5e2f43c291", size = 20273920 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/16/73727675941ab8e6ffd86ca3a4b7b47065edcca7a997920b831f8147c99d/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5", size = 200221632, upload-time = "2024-11-20T17:41:32.357Z" }, - { url = "https://files.pythonhosted.org/packages/60/de/99ec247a07ea40c969d904fc14f3a356b3e2a704121675b75c366b694ee1/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.whl", hash = "sha256:768160ac89f6f7b459bee747e8d175dbf53619cfe74b2a5636264163138013ca", size = 200221622, upload-time = "2024-10-01T17:03:58.79Z" }, -] - -[[package]] -name = "nvidia-cufile-cu12" -version = "1.11.1.6" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/66/cc9876340ac68ae71b15c743ddb13f8b30d5244af344ec8322b449e35426/nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159", size = 1142103, upload-time = "2024-11-20T17:42:11.83Z" }, -] - -[[package]] -name = "nvidia-curand-cu12" -version = "10.3.7.77" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/1b/44a01c4e70933637c93e6e1a8063d1e998b50213a6b65ac5a9169c47e98e/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf", size = 56279010, upload-time = "2024-11-20T17:42:50.958Z" }, - { url = "https://files.pythonhosted.org/packages/4a/aa/2c7ff0b5ee02eaef890c0ce7d4f74bc30901871c5e45dee1ae6d0083cd80/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:99f1a32f1ac2bd134897fc7a203f779303261268a65762a623bf30cc9fe79117", size = 56279000, upload-time = "2024-10-01T17:04:45.274Z" }, -] - -[[package]] -name = "nvidia-cusolver-cu12" -version = "11.7.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-cublas-cu12" }, - { name = "nvidia-cusparse-cu12" }, - { name = "nvidia-nvjitlink-cu12" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/6e/c2cf12c9ff8b872e92b4a5740701e51ff17689c4d726fca91875b07f655d/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c", size = 158229790, upload-time = "2024-11-20T17:43:43.211Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/baba53585da791d043c10084cf9553e074548408e04ae884cfe9193bd484/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf28f17f64107a0c4d7802be5ff5537b2130bfc112f25d5a30df227058ca0e6", size = 158229780, upload-time = "2024-10-01T17:05:39.875Z" }, -] - -[[package]] -name = "nvidia-cusparse-cu12" -version = "12.5.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/1e/b8b7c2f4099a37b96af5c9bb158632ea9e5d9d27d7391d7eb8fc45236674/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73", size = 216561367, upload-time = "2024-11-20T17:44:54.824Z" }, - { url = "https://files.pythonhosted.org/packages/43/ac/64c4316ba163e8217a99680c7605f779accffc6a4bcd0c778c12948d3707/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:23749a6571191a215cb74d1cdbff4a86e7b19f1200c071b3fcf844a5bea23a2f", size = 216561357, upload-time = "2024-10-01T17:06:29.861Z" }, -] - -[[package]] -name = "nvidia-cusparselt-cu12" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/9a/72ef35b399b0e183bc2e8f6f558036922d453c4d8237dab26c666a04244b/nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46", size = 156785796, upload-time = "2024-10-15T21:29:17.709Z" }, -] - -[[package]] -name = "nvidia-nccl-cu12" -version = "2.26.2" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/ca/f42388aed0fddd64ade7493dbba36e1f534d4e6fdbdd355c6a90030ae028/nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6", size = 201319755, upload-time = "2025-03-13T00:29:55.296Z" }, -] - -[[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.6.85" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/d7/c5383e47c7e9bf1c99d5bd2a8c935af2b6d705ad831a7ec5c97db4d82f4f/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a", size = 19744971, upload-time = "2024-11-20T17:46:53.366Z" }, -] - -[[package]] -name = "nvidia-nvtx-cu12" -version = "12.6.77" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/9a/fff8376f8e3d084cd1530e1ef7b879bb7d6d265620c95c1b322725c694f4/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b90bed3df379fa79afbd21be8e04a0314336b8ae16768b58f2d34cb1d04cd7d2", size = 89276, upload-time = "2024-11-20T17:38:27.621Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4e/0d0c945463719429b7bd21dece907ad0bde437a2ff12b9b12fee94722ab0/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1", size = 89265, upload-time = "2024-10-01T17:00:38.172Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f7/1fd4ff108cd9d7ef929b8882692e23665dc9c23feecafbb9c6b80f4ec583/numpy-2.2.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ee461a4eaab4f165b68780a6a1af95fb23a29932be7569b9fab666c407969051", size = 20948633 }, + { url = "https://files.pythonhosted.org/packages/12/03/d443c278348371b20d830af155ff2079acad6a9e60279fac2b41dbbb73d8/numpy-2.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec31367fd6a255dc8de4772bd1658c3e926d8e860a0b6e922b615e532d320ddc", size = 14176123 }, + { url = "https://files.pythonhosted.org/packages/2b/0b/5ca264641d0e7b14393313304da48b225d15d471250376f3fbdb1a2be603/numpy-2.2.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:47834cde750d3c9f4e52c6ca28a7361859fcaf52695c7dc3cc1a720b8922683e", size = 5163817 }, + { url = "https://files.pythonhosted.org/packages/04/b3/d522672b9e3d28e26e1613de7675b441bbd1eaca75db95680635dd158c67/numpy-2.2.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2c1a1c6ccce4022383583a6ded7bbcda22fc635eb4eb1e0a053336425ed36dfa", size = 6698066 }, + { url = "https://files.pythonhosted.org/packages/a0/93/0f7a75c1ff02d4b76df35079676b3b2719fcdfb39abdf44c8b33f43ef37d/numpy-2.2.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d75f338f5f79ee23548b03d801d28a505198297534f62416391857ea0479571", size = 14087277 }, + { url = "https://files.pythonhosted.org/packages/b0/d9/7c338b923c53d431bc837b5b787052fef9ae68a56fe91e325aac0d48226e/numpy-2.2.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a801fef99668f309b88640e28d261991bfad9617c27beda4a3aec4f217ea073", size = 16135742 }, + { url = "https://files.pythonhosted.org/packages/2d/10/4dec9184a5d74ba9867c6f7d1e9f2e0fb5fe96ff2bf50bb6f342d64f2003/numpy-2.2.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:abe38cd8381245a7f49967a6010e77dbf3680bd3627c0fe4362dd693b404c7f8", size = 15581825 }, + { url = "https://files.pythonhosted.org/packages/80/1f/2b6fcd636e848053f5b57712a7d1880b1565eec35a637fdfd0a30d5e738d/numpy-2.2.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a0ac90e46fdb5649ab6369d1ab6104bfe5854ab19b645bf5cda0127a13034ae", size = 17899600 }, + { url = "https://files.pythonhosted.org/packages/ec/87/36801f4dc2623d76a0a3835975524a84bd2b18fe0f8835d45c8eae2f9ff2/numpy-2.2.5-cp312-cp312-win32.whl", hash = "sha256:0cd48122a6b7eab8f06404805b1bd5856200e3ed6f8a1b9a194f9d9054631beb", size = 6312626 }, + { url = "https://files.pythonhosted.org/packages/8b/09/4ffb4d6cfe7ca6707336187951992bd8a8b9142cf345d87ab858d2d7636a/numpy-2.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:ced69262a8278547e63409b2653b372bf4baff0870c57efa76c5703fd6543282", size = 12645715 }, ] [[package]] name = "olefile" version = "0.47" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240, upload-time = "2023-12-01T16:22:53.025Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240 } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565 }, ] [[package]] @@ -1116,10 +834,10 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/42/274438bbc259439fa1606d0d6d2eef4171cdbd2d7a1c3b249b4ba440424b/onnxruntime-1.21.1-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:f615c05869a523a94d0a4de1f0936d0199a473cf104d630fc26174bebd5759bd", size = 33658457, upload-time = "2025-04-18T12:01:22.937Z" }, - { url = "https://files.pythonhosted.org/packages/9c/93/76f629d4f22571b0b3a29a9d375204faae2bd2b07d557043b56df5848779/onnxruntime-1.21.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79dfb1f47386c4edd115b21015354b2f05f5566c40c98606251f15a64add3cbe", size = 14164881, upload-time = "2025-04-18T12:01:44.497Z" }, - { url = "https://files.pythonhosted.org/packages/1b/86/75cbaa4058758fa8ef912dfebba2d5a4e4fd6738615c15b6a2262d076198/onnxruntime-1.21.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2742935d6610fe0f58e1995018d9db7e8239d0201d9ebbdb7964a61386b5390a", size = 16019966, upload-time = "2025-04-18T12:01:47.366Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9d/fb8895b2cb38c9965d4b4e0a9aa1398f3e3f16c4acb75cf3b61689780a65/onnxruntime-1.21.1-cp312-cp312-win_amd64.whl", hash = "sha256:a7afdb3fcb162f5536225e13c2b245018068964b1d0eee05303ea6823ca6785e", size = 12302925, upload-time = "2025-04-18T12:01:26.147Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/274438bbc259439fa1606d0d6d2eef4171cdbd2d7a1c3b249b4ba440424b/onnxruntime-1.21.1-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:f615c05869a523a94d0a4de1f0936d0199a473cf104d630fc26174bebd5759bd", size = 33658457 }, + { url = "https://files.pythonhosted.org/packages/9c/93/76f629d4f22571b0b3a29a9d375204faae2bd2b07d557043b56df5848779/onnxruntime-1.21.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79dfb1f47386c4edd115b21015354b2f05f5566c40c98606251f15a64add3cbe", size = 14164881 }, + { url = "https://files.pythonhosted.org/packages/1b/86/75cbaa4058758fa8ef912dfebba2d5a4e4fd6738615c15b6a2262d076198/onnxruntime-1.21.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2742935d6610fe0f58e1995018d9db7e8239d0201d9ebbdb7964a61386b5390a", size = 16019966 }, + { url = "https://files.pythonhosted.org/packages/5f/9d/fb8895b2cb38c9965d4b4e0a9aa1398f3e3f16c4acb75cf3b61689780a65/onnxruntime-1.21.1-cp312-cp312-win_amd64.whl", hash = "sha256:a7afdb3fcb162f5536225e13c2b245018068964b1d0eee05303ea6823ca6785e", size = 12302925 }, ] [[package]] @@ -1129,18 +847,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "et-xmlfile" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] [[package]] @@ -1153,15 +871,15 @@ dependencies = [ { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, - { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, - { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, - { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, - { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, - { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, ] [[package]] @@ -1172,120 +890,120 @@ dependencies = [ { name = "charset-normalizer" }, { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/27/1a99ce4cfce829bb91040f82a53f33b33fec4e070d2b9c1b45f6796cd8dc/pdfminer_six-20250416.tar.gz", hash = "sha256:30956a85f9d0add806a4e460ed0d67c2b6a48b53323c7ac87de23174596d3acd", size = 7384630, upload-time = "2025-04-16T09:43:41.944Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/27/1a99ce4cfce829bb91040f82a53f33b33fec4e070d2b9c1b45f6796cd8dc/pdfminer_six-20250416.tar.gz", hash = "sha256:30956a85f9d0add806a4e460ed0d67c2b6a48b53323c7ac87de23174596d3acd", size = 7384630 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/32/89749ba23e5020e89fb584c1b39d7da6d7c56a9048307de8a88eec79e2d3/pdfminer_six-20250416-py3-none-any.whl", hash = "sha256:dd2a9ad7bc7dd6b62d009aaa9c101ac9d069a47937724569c375a6a9078da303", size = 5619271, upload-time = "2025-04-16T09:43:40.211Z" }, + { url = "https://files.pythonhosted.org/packages/77/32/89749ba23e5020e89fb584c1b39d7da6d7c56a9048307de8a88eec79e2d3/pdfminer_six-20250416-py3-none-any.whl", hash = "sha256:dd2a9ad7bc7dd6b62d009aaa9c101ac9d069a47937724569c375a6a9078da303", size = 5619271 }, ] [[package]] name = "pillow" version = "11.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload-time = "2025-04-12T17:48:00.417Z" }, - { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload-time = "2025-04-12T17:48:02.391Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload-time = "2025-04-12T17:48:04.554Z" }, - { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload-time = "2025-04-12T17:48:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload-time = "2025-04-12T17:48:09.229Z" }, - { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload-time = "2025-04-12T17:48:11.631Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload-time = "2025-04-12T17:48:13.592Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload-time = "2025-04-12T17:48:15.938Z" }, - { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" }, - { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" }, - { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, + { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185 }, + { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306 }, + { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121 }, + { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707 }, + { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921 }, + { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523 }, + { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836 }, + { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390 }, + { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309 }, + { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768 }, + { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087 }, ] [[package]] name = "propcache" version = "0.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/07/c8/fdc6686a986feae3541ea23dcaa661bd93972d3940460646c6bb96e21c40/propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", size = 43651, upload-time = "2025-03-26T03:06:12.05Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/c8/fdc6686a986feae3541ea23dcaa661bd93972d3940460646c6bb96e21c40/propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", size = 43651 } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/aa/ca78d9be314d1e15ff517b992bebbed3bdfef5b8919e85bf4940e57b6137/propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", size = 80430, upload-time = "2025-03-26T03:04:26.436Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d8/f0c17c44d1cda0ad1979af2e593ea290defdde9eaeb89b08abbe02a5e8e1/propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", size = 46637, upload-time = "2025-03-26T03:04:27.932Z" }, - { url = "https://files.pythonhosted.org/packages/ae/bd/c1e37265910752e6e5e8a4c1605d0129e5b7933c3dc3cf1b9b48ed83b364/propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", size = 46123, upload-time = "2025-03-26T03:04:30.659Z" }, - { url = "https://files.pythonhosted.org/packages/d4/b0/911eda0865f90c0c7e9f0415d40a5bf681204da5fd7ca089361a64c16b28/propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", size = 243031, upload-time = "2025-03-26T03:04:31.977Z" }, - { url = "https://files.pythonhosted.org/packages/0a/06/0da53397c76a74271621807265b6eb61fb011451b1ddebf43213df763669/propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", size = 249100, upload-time = "2025-03-26T03:04:33.45Z" }, - { url = "https://files.pythonhosted.org/packages/f1/eb/13090e05bf6b963fc1653cdc922133ced467cb4b8dab53158db5a37aa21e/propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", size = 250170, upload-time = "2025-03-26T03:04:35.542Z" }, - { url = "https://files.pythonhosted.org/packages/3b/4c/f72c9e1022b3b043ec7dc475a0f405d4c3e10b9b1d378a7330fecf0652da/propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", size = 245000, upload-time = "2025-03-26T03:04:37.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/fd/970ca0e22acc829f1adf5de3724085e778c1ad8a75bec010049502cb3a86/propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", size = 230262, upload-time = "2025-03-26T03:04:39.532Z" }, - { url = "https://files.pythonhosted.org/packages/c4/42/817289120c6b9194a44f6c3e6b2c3277c5b70bbad39e7df648f177cc3634/propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", size = 236772, upload-time = "2025-03-26T03:04:41.109Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9c/3b3942b302badd589ad6b672da3ca7b660a6c2f505cafd058133ddc73918/propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", size = 231133, upload-time = "2025-03-26T03:04:42.544Z" }, - { url = "https://files.pythonhosted.org/packages/98/a1/75f6355f9ad039108ff000dfc2e19962c8dea0430da9a1428e7975cf24b2/propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", size = 230741, upload-time = "2025-03-26T03:04:44.06Z" }, - { url = "https://files.pythonhosted.org/packages/67/0c/3e82563af77d1f8731132166da69fdfd95e71210e31f18edce08a1eb11ea/propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", size = 244047, upload-time = "2025-03-26T03:04:45.983Z" }, - { url = "https://files.pythonhosted.org/packages/f7/50/9fb7cca01532a08c4d5186d7bb2da6c4c587825c0ae134b89b47c7d62628/propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5", size = 246467, upload-time = "2025-03-26T03:04:47.699Z" }, - { url = "https://files.pythonhosted.org/packages/a9/02/ccbcf3e1c604c16cc525309161d57412c23cf2351523aedbb280eb7c9094/propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", size = 241022, upload-time = "2025-03-26T03:04:49.195Z" }, - { url = "https://files.pythonhosted.org/packages/db/19/e777227545e09ca1e77a6e21274ae9ec45de0f589f0ce3eca2a41f366220/propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", size = 40647, upload-time = "2025-03-26T03:04:50.595Z" }, - { url = "https://files.pythonhosted.org/packages/24/bb/3b1b01da5dd04c77a204c84e538ff11f624e31431cfde7201d9110b092b1/propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", size = 44784, upload-time = "2025-03-26T03:04:51.791Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload-time = "2025-03-26T03:06:10.5Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/ca78d9be314d1e15ff517b992bebbed3bdfef5b8919e85bf4940e57b6137/propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", size = 80430 }, + { url = "https://files.pythonhosted.org/packages/1a/d8/f0c17c44d1cda0ad1979af2e593ea290defdde9eaeb89b08abbe02a5e8e1/propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", size = 46637 }, + { url = "https://files.pythonhosted.org/packages/ae/bd/c1e37265910752e6e5e8a4c1605d0129e5b7933c3dc3cf1b9b48ed83b364/propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", size = 46123 }, + { url = "https://files.pythonhosted.org/packages/d4/b0/911eda0865f90c0c7e9f0415d40a5bf681204da5fd7ca089361a64c16b28/propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", size = 243031 }, + { url = "https://files.pythonhosted.org/packages/0a/06/0da53397c76a74271621807265b6eb61fb011451b1ddebf43213df763669/propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", size = 249100 }, + { url = "https://files.pythonhosted.org/packages/f1/eb/13090e05bf6b963fc1653cdc922133ced467cb4b8dab53158db5a37aa21e/propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", size = 250170 }, + { url = "https://files.pythonhosted.org/packages/3b/4c/f72c9e1022b3b043ec7dc475a0f405d4c3e10b9b1d378a7330fecf0652da/propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", size = 245000 }, + { url = "https://files.pythonhosted.org/packages/e8/fd/970ca0e22acc829f1adf5de3724085e778c1ad8a75bec010049502cb3a86/propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", size = 230262 }, + { url = "https://files.pythonhosted.org/packages/c4/42/817289120c6b9194a44f6c3e6b2c3277c5b70bbad39e7df648f177cc3634/propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", size = 236772 }, + { url = "https://files.pythonhosted.org/packages/7c/9c/3b3942b302badd589ad6b672da3ca7b660a6c2f505cafd058133ddc73918/propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", size = 231133 }, + { url = "https://files.pythonhosted.org/packages/98/a1/75f6355f9ad039108ff000dfc2e19962c8dea0430da9a1428e7975cf24b2/propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", size = 230741 }, + { url = "https://files.pythonhosted.org/packages/67/0c/3e82563af77d1f8731132166da69fdfd95e71210e31f18edce08a1eb11ea/propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", size = 244047 }, + { url = "https://files.pythonhosted.org/packages/f7/50/9fb7cca01532a08c4d5186d7bb2da6c4c587825c0ae134b89b47c7d62628/propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5", size = 246467 }, + { url = "https://files.pythonhosted.org/packages/a9/02/ccbcf3e1c604c16cc525309161d57412c23cf2351523aedbb280eb7c9094/propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", size = 241022 }, + { url = "https://files.pythonhosted.org/packages/db/19/e777227545e09ca1e77a6e21274ae9ec45de0f589f0ce3eca2a41f366220/propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", size = 40647 }, + { url = "https://files.pythonhosted.org/packages/24/bb/3b1b01da5dd04c77a204c84e538ff11f624e31431cfde7201d9110b092b1/propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", size = 44784 }, + { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376 }, ] [[package]] name = "protobuf" version = "6.30.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/8c/cf2ac658216eebe49eaedf1e06bc06cbf6a143469236294a1171a51357c3/protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048", size = 429315, upload-time = "2025-03-26T19:12:57.394Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/8c/cf2ac658216eebe49eaedf1e06bc06cbf6a143469236294a1171a51357c3/protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048", size = 429315 } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/85/cd53abe6a6cbf2e0029243d6ae5fb4335da2996f6c177bb2ce685068e43d/protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103", size = 419148, upload-time = "2025-03-26T19:12:41.359Z" }, - { url = "https://files.pythonhosted.org/packages/97/e9/7b9f1b259d509aef2b833c29a1f3c39185e2bf21c9c1be1cd11c22cb2149/protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9", size = 431003, upload-time = "2025-03-26T19:12:44.156Z" }, - { url = "https://files.pythonhosted.org/packages/8e/66/7f3b121f59097c93267e7f497f10e52ced7161b38295137a12a266b6c149/protobuf-6.30.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:0eb523c550a66a09a0c20f86dd554afbf4d32b02af34ae53d93268c1f73bc65b", size = 417579, upload-time = "2025-03-26T19:12:45.447Z" }, - { url = "https://files.pythonhosted.org/packages/d0/89/bbb1bff09600e662ad5b384420ad92de61cab2ed0f12ace1fd081fd4c295/protobuf-6.30.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:50f32cc9fd9cb09c783ebc275611b4f19dfdfb68d1ee55d2f0c7fa040df96815", size = 317319, upload-time = "2025-03-26T19:12:46.999Z" }, - { url = "https://files.pythonhosted.org/packages/28/50/1925de813499546bc8ab3ae857e3ec84efe7d2f19b34529d0c7c3d02d11d/protobuf-6.30.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4f6c687ae8efae6cf6093389a596548214467778146b7245e886f35e1485315d", size = 316212, upload-time = "2025-03-26T19:12:48.458Z" }, - { url = "https://files.pythonhosted.org/packages/e5/a1/93c2acf4ade3c5b557d02d500b06798f4ed2c176fa03e3c34973ca92df7f/protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51", size = 167062, upload-time = "2025-03-26T19:12:55.892Z" }, + { url = "https://files.pythonhosted.org/packages/be/85/cd53abe6a6cbf2e0029243d6ae5fb4335da2996f6c177bb2ce685068e43d/protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103", size = 419148 }, + { url = "https://files.pythonhosted.org/packages/97/e9/7b9f1b259d509aef2b833c29a1f3c39185e2bf21c9c1be1cd11c22cb2149/protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9", size = 431003 }, + { url = "https://files.pythonhosted.org/packages/8e/66/7f3b121f59097c93267e7f497f10e52ced7161b38295137a12a266b6c149/protobuf-6.30.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:0eb523c550a66a09a0c20f86dd554afbf4d32b02af34ae53d93268c1f73bc65b", size = 417579 }, + { url = "https://files.pythonhosted.org/packages/d0/89/bbb1bff09600e662ad5b384420ad92de61cab2ed0f12ace1fd081fd4c295/protobuf-6.30.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:50f32cc9fd9cb09c783ebc275611b4f19dfdfb68d1ee55d2f0c7fa040df96815", size = 317319 }, + { url = "https://files.pythonhosted.org/packages/28/50/1925de813499546bc8ab3ae857e3ec84efe7d2f19b34529d0c7c3d02d11d/protobuf-6.30.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4f6c687ae8efae6cf6093389a596548214467778146b7245e886f35e1485315d", size = 316212 }, + { url = "https://files.pythonhosted.org/packages/e5/a1/93c2acf4ade3c5b557d02d500b06798f4ed2c176fa03e3c34973ca92df7f/protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51", size = 167062 }, ] [[package]] name = "pyarrow" version = "20.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/ee/a7810cb9f3d6e9238e61d312076a9859bf3668fd21c69744de9532383912/pyarrow-20.0.0.tar.gz", hash = "sha256:febc4a913592573c8d5805091a6c2b5064c8bd6e002131f01061797d91c783c1", size = 1125187, upload-time = "2025-04-27T12:34:23.264Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/ee/a7810cb9f3d6e9238e61d312076a9859bf3668fd21c69744de9532383912/pyarrow-20.0.0.tar.gz", hash = "sha256:febc4a913592573c8d5805091a6c2b5064c8bd6e002131f01061797d91c783c1", size = 1125187 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/d6/0c10e0d54f6c13eb464ee9b67a68b8c71bcf2f67760ef5b6fbcddd2ab05f/pyarrow-20.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:75a51a5b0eef32727a247707d4755322cb970be7e935172b6a3a9f9ae98404ba", size = 30815067, upload-time = "2025-04-27T12:29:44.384Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e2/04e9874abe4094a06fd8b0cbb0f1312d8dd7d707f144c2ec1e5e8f452ffa/pyarrow-20.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:211d5e84cecc640c7a3ab900f930aaff5cd2702177e0d562d426fb7c4f737781", size = 32297128, upload-time = "2025-04-27T12:29:52.038Z" }, - { url = "https://files.pythonhosted.org/packages/31/fd/c565e5dcc906a3b471a83273039cb75cb79aad4a2d4a12f76cc5ae90a4b8/pyarrow-20.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ba3cf4182828be7a896cbd232aa8dd6a31bd1f9e32776cc3796c012855e1199", size = 41334890, upload-time = "2025-04-27T12:29:59.452Z" }, - { url = "https://files.pythonhosted.org/packages/af/a9/3bdd799e2c9b20c1ea6dc6fa8e83f29480a97711cf806e823f808c2316ac/pyarrow-20.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c3a01f313ffe27ac4126f4c2e5ea0f36a5fc6ab51f8726cf41fee4b256680bd", size = 42421775, upload-time = "2025-04-27T12:30:06.875Z" }, - { url = "https://files.pythonhosted.org/packages/10/f7/da98ccd86354c332f593218101ae56568d5dcedb460e342000bd89c49cc1/pyarrow-20.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a2791f69ad72addd33510fec7bb14ee06c2a448e06b649e264c094c5b5f7ce28", size = 40687231, upload-time = "2025-04-27T12:30:13.954Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1b/2168d6050e52ff1e6cefc61d600723870bf569cbf41d13db939c8cf97a16/pyarrow-20.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4250e28a22302ce8692d3a0e8ec9d9dde54ec00d237cff4dfa9c1fbf79e472a8", size = 42295639, upload-time = "2025-04-27T12:30:21.949Z" }, - { url = "https://files.pythonhosted.org/packages/b2/66/2d976c0c7158fd25591c8ca55aee026e6d5745a021915a1835578707feb3/pyarrow-20.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:89e030dc58fc760e4010148e6ff164d2f44441490280ef1e97a542375e41058e", size = 42908549, upload-time = "2025-04-27T12:30:29.551Z" }, - { url = "https://files.pythonhosted.org/packages/31/a9/dfb999c2fc6911201dcbf348247f9cc382a8990f9ab45c12eabfd7243a38/pyarrow-20.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6102b4864d77102dbbb72965618e204e550135a940c2534711d5ffa787df2a5a", size = 44557216, upload-time = "2025-04-27T12:30:36.977Z" }, - { url = "https://files.pythonhosted.org/packages/a0/8e/9adee63dfa3911be2382fb4d92e4b2e7d82610f9d9f668493bebaa2af50f/pyarrow-20.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:96d6a0a37d9c98be08f5ed6a10831d88d52cac7b13f5287f1e0f625a0de8062b", size = 25660496, upload-time = "2025-04-27T12:30:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d6/0c10e0d54f6c13eb464ee9b67a68b8c71bcf2f67760ef5b6fbcddd2ab05f/pyarrow-20.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:75a51a5b0eef32727a247707d4755322cb970be7e935172b6a3a9f9ae98404ba", size = 30815067 }, + { url = "https://files.pythonhosted.org/packages/7e/e2/04e9874abe4094a06fd8b0cbb0f1312d8dd7d707f144c2ec1e5e8f452ffa/pyarrow-20.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:211d5e84cecc640c7a3ab900f930aaff5cd2702177e0d562d426fb7c4f737781", size = 32297128 }, + { url = "https://files.pythonhosted.org/packages/31/fd/c565e5dcc906a3b471a83273039cb75cb79aad4a2d4a12f76cc5ae90a4b8/pyarrow-20.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ba3cf4182828be7a896cbd232aa8dd6a31bd1f9e32776cc3796c012855e1199", size = 41334890 }, + { url = "https://files.pythonhosted.org/packages/af/a9/3bdd799e2c9b20c1ea6dc6fa8e83f29480a97711cf806e823f808c2316ac/pyarrow-20.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c3a01f313ffe27ac4126f4c2e5ea0f36a5fc6ab51f8726cf41fee4b256680bd", size = 42421775 }, + { url = "https://files.pythonhosted.org/packages/10/f7/da98ccd86354c332f593218101ae56568d5dcedb460e342000bd89c49cc1/pyarrow-20.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a2791f69ad72addd33510fec7bb14ee06c2a448e06b649e264c094c5b5f7ce28", size = 40687231 }, + { url = "https://files.pythonhosted.org/packages/bb/1b/2168d6050e52ff1e6cefc61d600723870bf569cbf41d13db939c8cf97a16/pyarrow-20.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4250e28a22302ce8692d3a0e8ec9d9dde54ec00d237cff4dfa9c1fbf79e472a8", size = 42295639 }, + { url = "https://files.pythonhosted.org/packages/b2/66/2d976c0c7158fd25591c8ca55aee026e6d5745a021915a1835578707feb3/pyarrow-20.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:89e030dc58fc760e4010148e6ff164d2f44441490280ef1e97a542375e41058e", size = 42908549 }, + { url = "https://files.pythonhosted.org/packages/31/a9/dfb999c2fc6911201dcbf348247f9cc382a8990f9ab45c12eabfd7243a38/pyarrow-20.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6102b4864d77102dbbb72965618e204e550135a940c2534711d5ffa787df2a5a", size = 44557216 }, + { url = "https://files.pythonhosted.org/packages/a0/8e/9adee63dfa3911be2382fb4d92e4b2e7d82610f9d9f668493bebaa2af50f/pyarrow-20.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:96d6a0a37d9c98be08f5ed6a10831d88d52cac7b13f5287f1e0f625a0de8062b", size = 25660496 }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] [[package]] name = "pydub" version = "0.25.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327 }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] [[package]] name = "pyjwt" version = "2.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, ] [package.optional-dependencies] @@ -1293,22 +1011,13 @@ crypto = [ { name = "cryptography" }, ] -[[package]] -name = "pyparsing" -version = "3.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, -] - [[package]] name = "pyreadline3" version = "3.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 }, ] [[package]] @@ -1318,18 +1027,18 @@ 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" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } 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" }, + { 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 }, ] [[package]] name = "python-dotenv" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] [[package]] @@ -1342,81 +1051,81 @@ dependencies = [ { name = "typing-extensions" }, { name = "xlsxwriter" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload-time = "2024-08-07T17:33:37.772Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788 }, ] [[package]] name = "pytz" version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, ] [[package]] name = "rapidfuzz" version = "3.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226, upload-time = "2025-04-03T20:38:51.226Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226 } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/4b/a326f57a4efed8f5505b25102797a58e37ee11d94afd9d9422cb7c76117e/rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7", size = 1989501, upload-time = "2025-04-03T20:36:13.43Z" }, - { url = "https://files.pythonhosted.org/packages/b7/53/1f7eb7ee83a06c400089ec7cb841cbd581c2edd7a4b21eb2f31030b88daa/rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26", size = 1445379, upload-time = "2025-04-03T20:36:16.439Z" }, - { url = "https://files.pythonhosted.org/packages/07/09/de8069a4599cc8e6d194e5fa1782c561151dea7d5e2741767137e2a8c1f0/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69", size = 1405986, upload-time = "2025-04-03T20:36:18.447Z" }, - { url = "https://files.pythonhosted.org/packages/5d/77/d9a90b39c16eca20d70fec4ca377fbe9ea4c0d358c6e4736ab0e0e78aaf6/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97", size = 5310809, upload-time = "2025-04-03T20:36:20.324Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7d/14da291b0d0f22262d19522afaf63bccf39fc027c981233fb2137a57b71f/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981", size = 1629394, upload-time = "2025-04-03T20:36:22.256Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e4/79ed7e4fa58f37c0f8b7c0a62361f7089b221fe85738ae2dbcfb815e985a/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f", size = 1600544, upload-time = "2025-04-03T20:36:24.207Z" }, - { url = "https://files.pythonhosted.org/packages/4e/20/e62b4d13ba851b0f36370060025de50a264d625f6b4c32899085ed51f980/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f", size = 3052796, upload-time = "2025-04-03T20:36:26.279Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8d/55fdf4387dec10aa177fe3df8dbb0d5022224d95f48664a21d6b62a5299d/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87", size = 2464016, upload-time = "2025-04-03T20:36:28.525Z" }, - { url = "https://files.pythonhosted.org/packages/9b/be/0872f6a56c0f473165d3b47d4170fa75263dc5f46985755aa9bf2bbcdea1/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3", size = 7556725, upload-time = "2025-04-03T20:36:30.629Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f3/6c0750e484d885a14840c7a150926f425d524982aca989cdda0bb3bdfa57/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db", size = 2859052, upload-time = "2025-04-03T20:36:32.836Z" }, - { url = "https://files.pythonhosted.org/packages/6f/98/5a3a14701b5eb330f444f7883c9840b43fb29c575e292e09c90a270a6e07/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73", size = 3390219, upload-time = "2025-04-03T20:36:35.062Z" }, - { url = "https://files.pythonhosted.org/packages/e9/7d/f4642eaaeb474b19974332f2a58471803448be843033e5740965775760a5/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a", size = 4377924, upload-time = "2025-04-03T20:36:37.363Z" }, - { url = "https://files.pythonhosted.org/packages/8e/83/fa33f61796731891c3e045d0cbca4436a5c436a170e7f04d42c2423652c3/rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514", size = 1823915, upload-time = "2025-04-03T20:36:39.451Z" }, - { url = "https://files.pythonhosted.org/packages/03/25/5ee7ab6841ca668567d0897905eebc79c76f6297b73bf05957be887e9c74/rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e", size = 1616985, upload-time = "2025-04-03T20:36:41.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/5e/3f0fb88db396cb692aefd631e4805854e02120a2382723b90dcae720bcc6/rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7", size = 860116, upload-time = "2025-04-03T20:36:43.915Z" }, + { url = "https://files.pythonhosted.org/packages/13/4b/a326f57a4efed8f5505b25102797a58e37ee11d94afd9d9422cb7c76117e/rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7", size = 1989501 }, + { url = "https://files.pythonhosted.org/packages/b7/53/1f7eb7ee83a06c400089ec7cb841cbd581c2edd7a4b21eb2f31030b88daa/rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26", size = 1445379 }, + { url = "https://files.pythonhosted.org/packages/07/09/de8069a4599cc8e6d194e5fa1782c561151dea7d5e2741767137e2a8c1f0/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69", size = 1405986 }, + { url = "https://files.pythonhosted.org/packages/5d/77/d9a90b39c16eca20d70fec4ca377fbe9ea4c0d358c6e4736ab0e0e78aaf6/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97", size = 5310809 }, + { url = "https://files.pythonhosted.org/packages/1e/7d/14da291b0d0f22262d19522afaf63bccf39fc027c981233fb2137a57b71f/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981", size = 1629394 }, + { url = "https://files.pythonhosted.org/packages/b7/e4/79ed7e4fa58f37c0f8b7c0a62361f7089b221fe85738ae2dbcfb815e985a/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f", size = 1600544 }, + { url = "https://files.pythonhosted.org/packages/4e/20/e62b4d13ba851b0f36370060025de50a264d625f6b4c32899085ed51f980/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f", size = 3052796 }, + { url = "https://files.pythonhosted.org/packages/cd/8d/55fdf4387dec10aa177fe3df8dbb0d5022224d95f48664a21d6b62a5299d/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87", size = 2464016 }, + { url = "https://files.pythonhosted.org/packages/9b/be/0872f6a56c0f473165d3b47d4170fa75263dc5f46985755aa9bf2bbcdea1/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3", size = 7556725 }, + { url = "https://files.pythonhosted.org/packages/5d/f3/6c0750e484d885a14840c7a150926f425d524982aca989cdda0bb3bdfa57/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db", size = 2859052 }, + { url = "https://files.pythonhosted.org/packages/6f/98/5a3a14701b5eb330f444f7883c9840b43fb29c575e292e09c90a270a6e07/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73", size = 3390219 }, + { url = "https://files.pythonhosted.org/packages/e9/7d/f4642eaaeb474b19974332f2a58471803448be843033e5740965775760a5/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a", size = 4377924 }, + { url = "https://files.pythonhosted.org/packages/8e/83/fa33f61796731891c3e045d0cbca4436a5c436a170e7f04d42c2423652c3/rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514", size = 1823915 }, + { url = "https://files.pythonhosted.org/packages/03/25/5ee7ab6841ca668567d0897905eebc79c76f6297b73bf05957be887e9c74/rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e", size = 1616985 }, + { url = "https://files.pythonhosted.org/packages/76/5e/3f0fb88db396cb692aefd631e4805854e02120a2382723b90dcae720bcc6/rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7", size = 860116 }, ] [[package]] name = "regex" version = "2024.11.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781, upload-time = "2024-11-06T20:10:07.07Z" }, - { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455, upload-time = "2024-11-06T20:10:09.117Z" }, - { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759, upload-time = "2024-11-06T20:10:11.155Z" }, - { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976, upload-time = "2024-11-06T20:10:13.24Z" }, - { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077, upload-time = "2024-11-06T20:10:15.37Z" }, - { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160, upload-time = "2024-11-06T20:10:19.027Z" }, - { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896, upload-time = "2024-11-06T20:10:21.85Z" }, - { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997, upload-time = "2024-11-06T20:10:24.329Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725, upload-time = "2024-11-06T20:10:28.067Z" }, - { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481, upload-time = "2024-11-06T20:10:31.612Z" }, - { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896, upload-time = "2024-11-06T20:10:34.054Z" }, - { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138, upload-time = "2024-11-06T20:10:36.142Z" }, - { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692, upload-time = "2024-11-06T20:10:38.394Z" }, - { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135, upload-time = "2024-11-06T20:10:40.367Z" }, - { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567, upload-time = "2024-11-06T20:10:43.467Z" }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, ] [[package]] @@ -1429,9 +1138,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] [[package]] @@ -1442,104 +1151,73 @@ dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, -] - -[[package]] -name = "rouge-score" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "nltk" }, - { name = "numpy" }, - { name = "six" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/c5/9136736c37022a6ad27fea38f3111eb8f02fe75d067f9a985cc358653102/rouge_score-0.1.2.tar.gz", hash = "sha256:c7d4da2683e68c9abf0135ef915d63a46643666f848e558a1b9f7ead17ff0f04", size = 17400, upload-time = "2022-07-22T22:46:22.909Z" } [[package]] name = "ruff" version = "0.11.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/f6/adcf73711f31c9f5393862b4281c875a462d9f639f4ccdf69dc368311c20/ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8", size = 4086399, upload-time = "2025-05-01T14:53:24.459Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f6/adcf73711f31c9f5393862b4281c875a462d9f639f4ccdf69dc368311c20/ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8", size = 4086399 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/60/c6aa9062fa518a9f86cb0b85248245cddcd892a125ca00441df77d79ef88/ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3", size = 10272473, upload-time = "2025-05-01T14:52:37.252Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/0325e50d106dc87c00695f7bcd5044c6d252ed5120ebf423773e00270f50/ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835", size = 11040862, upload-time = "2025-05-01T14:52:41.022Z" }, - { url = "https://files.pythonhosted.org/packages/e6/27/b87ea1a7be37fef0adbc7fd987abbf90b6607d96aa3fc67e2c5b858e1e53/ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c", size = 10385273, upload-time = "2025-05-01T14:52:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f7/3346161570d789045ed47a86110183f6ac3af0e94e7fd682772d89f7f1a1/ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c", size = 10578330, upload-time = "2025-05-01T14:52:45.48Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c3/327fb950b4763c7b3784f91d3038ef10c13b2d42322d4ade5ce13a2f9edb/ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219", size = 10122223, upload-time = "2025-05-01T14:52:47.675Z" }, - { url = "https://files.pythonhosted.org/packages/de/c7/ba686bce9adfeb6c61cb1bbadc17d58110fe1d602f199d79d4c880170f19/ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f", size = 11697353, upload-time = "2025-05-01T14:52:50.264Z" }, - { url = "https://files.pythonhosted.org/packages/53/8e/a4fb4a1ddde3c59e73996bb3ac51844ff93384d533629434b1def7a336b0/ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474", size = 12375936, upload-time = "2025-05-01T14:52:52.394Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a1/9529cb1e2936e2479a51aeb011307e7229225df9ac64ae064d91ead54571/ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38", size = 11850083, upload-time = "2025-05-01T14:52:55.424Z" }, - { url = "https://files.pythonhosted.org/packages/3e/94/8f7eac4c612673ae15a4ad2bc0ee62e03c68a2d4f458daae3de0e47c67ba/ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458", size = 14005834, upload-time = "2025-05-01T14:52:58.056Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7c/6f63b46b2be870cbf3f54c9c4154d13fac4b8827f22fa05ac835c10835b2/ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5", size = 11503713, upload-time = "2025-05-01T14:53:01.244Z" }, - { url = "https://files.pythonhosted.org/packages/3a/91/57de411b544b5fe072779678986a021d87c3ee5b89551f2ca41200c5d643/ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948", size = 10457182, upload-time = "2025-05-01T14:53:03.726Z" }, - { url = "https://files.pythonhosted.org/packages/01/49/cfe73e0ce5ecdd3e6f1137bf1f1be03dcc819d1bfe5cff33deb40c5926db/ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb", size = 10101027, upload-time = "2025-05-01T14:53:06.555Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/a5cfe47c62b3531675795f38a0ef1c52ff8de62eaddf370d46634391a3fb/ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c", size = 11111298, upload-time = "2025-05-01T14:53:08.825Z" }, - { url = "https://files.pythonhosted.org/packages/36/98/f76225f87e88f7cb669ae92c062b11c0a1e91f32705f829bd426f8e48b7b/ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304", size = 11566884, upload-time = "2025-05-01T14:53:11.626Z" }, - { url = "https://files.pythonhosted.org/packages/de/7e/fff70b02e57852fda17bd43f99dda37b9bcf3e1af3d97c5834ff48d04715/ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2", size = 10451102, upload-time = "2025-05-01T14:53:14.303Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a9/eaa571eb70648c9bde3120a1d5892597de57766e376b831b06e7c1e43945/ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4", size = 11597410, upload-time = "2025-05-01T14:53:16.571Z" }, - { url = "https://files.pythonhosted.org/packages/cd/be/f6b790d6ae98f1f32c645f8540d5c96248b72343b0a56fab3a07f2941897/ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2", size = 10713129, upload-time = "2025-05-01T14:53:22.27Z" }, + { url = "https://files.pythonhosted.org/packages/9f/60/c6aa9062fa518a9f86cb0b85248245cddcd892a125ca00441df77d79ef88/ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3", size = 10272473 }, + { url = "https://files.pythonhosted.org/packages/a0/e4/0325e50d106dc87c00695f7bcd5044c6d252ed5120ebf423773e00270f50/ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835", size = 11040862 }, + { url = "https://files.pythonhosted.org/packages/e6/27/b87ea1a7be37fef0adbc7fd987abbf90b6607d96aa3fc67e2c5b858e1e53/ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c", size = 10385273 }, + { url = "https://files.pythonhosted.org/packages/d3/f7/3346161570d789045ed47a86110183f6ac3af0e94e7fd682772d89f7f1a1/ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c", size = 10578330 }, + { url = "https://files.pythonhosted.org/packages/c6/c3/327fb950b4763c7b3784f91d3038ef10c13b2d42322d4ade5ce13a2f9edb/ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219", size = 10122223 }, + { url = "https://files.pythonhosted.org/packages/de/c7/ba686bce9adfeb6c61cb1bbadc17d58110fe1d602f199d79d4c880170f19/ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f", size = 11697353 }, + { url = "https://files.pythonhosted.org/packages/53/8e/a4fb4a1ddde3c59e73996bb3ac51844ff93384d533629434b1def7a336b0/ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474", size = 12375936 }, + { url = "https://files.pythonhosted.org/packages/ad/a1/9529cb1e2936e2479a51aeb011307e7229225df9ac64ae064d91ead54571/ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38", size = 11850083 }, + { url = "https://files.pythonhosted.org/packages/3e/94/8f7eac4c612673ae15a4ad2bc0ee62e03c68a2d4f458daae3de0e47c67ba/ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458", size = 14005834 }, + { url = "https://files.pythonhosted.org/packages/1e/7c/6f63b46b2be870cbf3f54c9c4154d13fac4b8827f22fa05ac835c10835b2/ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5", size = 11503713 }, + { url = "https://files.pythonhosted.org/packages/3a/91/57de411b544b5fe072779678986a021d87c3ee5b89551f2ca41200c5d643/ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948", size = 10457182 }, + { url = "https://files.pythonhosted.org/packages/01/49/cfe73e0ce5ecdd3e6f1137bf1f1be03dcc819d1bfe5cff33deb40c5926db/ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb", size = 10101027 }, + { url = "https://files.pythonhosted.org/packages/56/21/a5cfe47c62b3531675795f38a0ef1c52ff8de62eaddf370d46634391a3fb/ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c", size = 11111298 }, + { url = "https://files.pythonhosted.org/packages/36/98/f76225f87e88f7cb669ae92c062b11c0a1e91f32705f829bd426f8e48b7b/ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304", size = 11566884 }, + { url = "https://files.pythonhosted.org/packages/de/7e/fff70b02e57852fda17bd43f99dda37b9bcf3e1af3d97c5834ff48d04715/ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2", size = 10451102 }, + { url = "https://files.pythonhosted.org/packages/7b/a9/eaa571eb70648c9bde3120a1d5892597de57766e376b831b06e7c1e43945/ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4", size = 11597410 }, + { url = "https://files.pythonhosted.org/packages/cd/be/f6b790d6ae98f1f32c645f8540d5c96248b72343b0a56fab3a07f2941897/ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2", size = 10713129 }, ] [[package]] -name = "safetensors" -version = "0.5.3" +name = "s3transfer" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/7e/2d5d6ee7b40c0682315367ec7475693d110f512922d582fef1bd4a63adc3/safetensors-0.5.3.tar.gz", hash = "sha256:b6b0d6ecacec39a4fdd99cc19f4576f5219ce858e6fd8dbe7609df0b8dc56965", size = 67210, upload-time = "2025-02-26T09:15:13.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/ae/88f6c49dbd0cc4da0e08610019a3c78a7d390879a919411a410a1876d03a/safetensors-0.5.3-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd20eb133db8ed15b40110b7c00c6df51655a2998132193de2f75f72d99c7073", size = 436917, upload-time = "2025-02-26T09:15:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/b8/3b/11f1b4a2f5d2ab7da34ecc062b0bc301f2be024d110a6466726bec8c055c/safetensors-0.5.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:21d01c14ff6c415c485616b8b0bf961c46b3b343ca59110d38d744e577f9cce7", size = 418419, upload-time = "2025-02-26T09:15:01.765Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9a/add3e6fef267658075c5a41573c26d42d80c935cdc992384dfae435feaef/safetensors-0.5.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11bce6164887cd491ca75c2326a113ba934be596e22b28b1742ce27b1d076467", size = 459493, upload-time = "2025-02-26T09:14:51.812Z" }, - { url = "https://files.pythonhosted.org/packages/df/5c/bf2cae92222513cc23b3ff85c4a1bb2811a2c3583ac0f8e8d502751de934/safetensors-0.5.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a243be3590bc3301c821da7a18d87224ef35cbd3e5f5727e4e0728b8172411e", size = 472400, upload-time = "2025-02-26T09:14:53.549Z" }, - { url = "https://files.pythonhosted.org/packages/58/11/7456afb740bd45782d0f4c8e8e1bb9e572f1bf82899fb6ace58af47b4282/safetensors-0.5.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8bd84b12b1670a6f8e50f01e28156422a2bc07fb16fc4e98bded13039d688a0d", size = 522891, upload-time = "2025-02-26T09:14:55.717Z" }, - { url = "https://files.pythonhosted.org/packages/57/3d/fe73a9d2ace487e7285f6e157afee2383bd1ddb911b7cb44a55cf812eae3/safetensors-0.5.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:391ac8cab7c829452175f871fcaf414aa1e292b5448bd02620f675a7f3e7abb9", size = 537694, upload-time = "2025-02-26T09:14:57.036Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f8/dae3421624fcc87a89d42e1898a798bc7ff72c61f38973a65d60df8f124c/safetensors-0.5.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cead1fa41fc54b1e61089fa57452e8834f798cb1dc7a09ba3524f1eb08e0317a", size = 471642, upload-time = "2025-02-26T09:15:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/ce/20/1fbe16f9b815f6c5a672f5b760951e20e17e43f67f231428f871909a37f6/safetensors-0.5.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1077f3e94182d72618357b04b5ced540ceb71c8a813d3319f1aba448e68a770d", size = 502241, upload-time = "2025-02-26T09:14:58.303Z" }, - { url = "https://files.pythonhosted.org/packages/5f/18/8e108846b506487aa4629fe4116b27db65c3dde922de2c8e0cc1133f3f29/safetensors-0.5.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:799021e78287bac619c7b3f3606730a22da4cda27759ddf55d37c8db7511c74b", size = 638001, upload-time = "2025-02-26T09:15:05.79Z" }, - { url = "https://files.pythonhosted.org/packages/82/5a/c116111d8291af6c8c8a8b40628fe833b9db97d8141c2a82359d14d9e078/safetensors-0.5.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df26da01aaac504334644e1b7642fa000bfec820e7cef83aeac4e355e03195ff", size = 734013, upload-time = "2025-02-26T09:15:07.892Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ff/41fcc4d3b7de837963622e8610d998710705bbde9a8a17221d85e5d0baad/safetensors-0.5.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:32c3ef2d7af8b9f52ff685ed0bc43913cdcde135089ae322ee576de93eae5135", size = 670687, upload-time = "2025-02-26T09:15:09.979Z" }, - { url = "https://files.pythonhosted.org/packages/40/ad/2b113098e69c985a3d8fbda4b902778eae4a35b7d5188859b4a63d30c161/safetensors-0.5.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:37f1521be045e56fc2b54c606d4455573e717b2d887c579ee1dbba5f868ece04", size = 643147, upload-time = "2025-02-26T09:15:11.185Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0c/95aeb51d4246bd9a3242d3d8349c1112b4ee7611a4b40f0c5c93b05f001d/safetensors-0.5.3-cp38-abi3-win32.whl", hash = "sha256:cfc0ec0846dcf6763b0ed3d1846ff36008c6e7290683b61616c4b040f6a54ace", size = 296677, upload-time = "2025-02-26T09:15:16.554Z" }, - { url = "https://files.pythonhosted.org/packages/69/e2/b011c38e5394c4c18fb5500778a55ec43ad6106126e74723ffaee246f56e/safetensors-0.5.3-cp38-abi3-win_amd64.whl", hash = "sha256:836cbbc320b47e80acd40e44c8682db0e8ad7123209f69b093def21ec7cafd11", size = 308878, upload-time = "2025-02-26T09:15:14.99Z" }, +dependencies = [ + { name = "botocore" }, ] - -[[package]] -name = "setuptools" -version = "80.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/dc/3976b322de9d2e87ed0007cf04cc7553969b6c7b3f48a565d0333748fbcd/setuptools-80.3.1.tar.gz", hash = "sha256:31e2c58dbb67c99c289f51c16d899afedae292b978f8051efaf6262d8212f927", size = 1315082, upload-time = "2025-05-04T18:47:04.397Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/5d/9dcc100abc6711e8247af5aa561fc07c4a046f72f659c3adea9a449e191a/s3transfer-0.13.0.tar.gz", hash = "sha256:f5e6db74eb7776a37208001113ea7aa97695368242b364d73e91c981ac522177", size = 150232 } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/7e/5d8af3317ddbf9519b687bd1c39d8737fde07d97f54df65553faca5cffb1/setuptools-80.3.1-py3-none-any.whl", hash = "sha256:ea8e00d7992054c4c592aeb892f6ad51fe1b4d90cc6947cc45c45717c40ec537", size = 1201172, upload-time = "2025-05-04T18:47:02.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/17/22bf8155aa0ea2305eefa3a6402e040df7ebe512d1310165eda1e233c3f8/s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:0148ef34d6dd964d0d8cf4311b2b21c474693e57c2e069ec708ce043d2b527be", size = 85152 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 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" }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } 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" }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] [[package]] name = "soupsieve" version = "2.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, ] [[package]] @@ -1549,9 +1227,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/81/2f98238647f409d8faa209a0cbe9a1e2be74eeca5f739971698a2b54b12d/speechrecognition-3.14.2.tar.gz", hash = "sha256:2daa467f0b5686017ff3f9a64dcfa1a789ee10d1b0ada3be74bfad10eaef5f49", size = 32857832, upload-time = "2025-03-23T02:18:09.274Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/81/2f98238647f409d8faa209a0cbe9a1e2be74eeca5f739971698a2b54b12d/speechrecognition-3.14.2.tar.gz", hash = "sha256:2daa467f0b5686017ff3f9a64dcfa1a789ee10d1b0ada3be74bfad10eaef5f49", size = 32857832 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/7a/ef9a0a3ddd7e8b304906bf0d7b1f3cd92759d7ea8be10d284183b2e8f47c/speechrecognition-3.14.2-py3-none-any.whl", hash = "sha256:42940b95295b358fdd7415daa01260c8b20025d6b4000fbbaa3458f005d912b7", size = 32853272, upload-time = "2025-03-23T02:18:03.056Z" }, + { url = "https://files.pythonhosted.org/packages/10/7a/ef9a0a3ddd7e8b304906bf0d7b1f3cd92759d7ea8be10d284183b2e8f47c/speechrecognition-3.14.2-py3-none-any.whl", hash = "sha256:42940b95295b358fdd7415daa01260c8b20025d6b4000fbbaa3458f005d912b7", size = 32853272 }, ] [[package]] @@ -1561,9 +1239,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mpmath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 }, ] [[package]] @@ -1573,9 +1251,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "rapidfuzz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/4b/d3eb25831590d6d7d38c2f2e3561d3ba41d490dc89cd91d9e65e7c812508/thefuzz-0.22.1.tar.gz", hash = "sha256:7138039a7ecf540da323792d8592ef9902b1d79eb78c147d4f20664de79f3680", size = 19993, upload-time = "2024-01-19T19:18:23.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/4b/d3eb25831590d6d7d38c2f2e3561d3ba41d490dc89cd91d9e65e7c812508/thefuzz-0.22.1.tar.gz", hash = "sha256:7138039a7ecf540da323792d8592ef9902b1d79eb78c147d4f20664de79f3680", size = 19993 } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/4f/1695e70ceb3604f19eda9908e289c687ea81c4fecef4d90a9d1d0f2f7ae9/thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481", size = 8245, upload-time = "2024-01-19T19:18:20.362Z" }, + { url = "https://files.pythonhosted.org/packages/82/4f/1695e70ceb3604f19eda9908e289c687ea81c4fecef4d90a9d1d0f2f7ae9/thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481", size = 8245 }, ] [[package]] @@ -1586,83 +1264,23 @@ dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991, upload-time = "2025-02-14T06:03:01.003Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073, upload-time = "2025-02-14T06:02:24.768Z" }, - { url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075, upload-time = "2025-02-14T06:02:26.92Z" }, - { url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754, upload-time = "2025-02-14T06:02:28.124Z" }, - { url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678, upload-time = "2025-02-14T06:02:29.845Z" }, - { url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283, upload-time = "2025-02-14T06:02:33.838Z" }, - { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897, upload-time = "2025-02-14T06:02:36.265Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073 }, + { url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075 }, + { url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754 }, + { url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678 }, + { url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283 }, + { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897 }, ] [[package]] name = "tld" version = "0.13.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/a1/5723b07a70c1841a80afc9ac572fdf53488306848d844cd70519391b0d26/tld-0.13.1.tar.gz", hash = "sha256:75ec00936cbcf564f67361c41713363440b6c4ef0f0c1592b5b0fbe72c17a350", size = 462000, upload-time = "2025-05-21T22:18:29.341Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/70/b2f38360c3fc4bc9b5e8ef429e1fde63749144ac583c2dbdf7e21e27a9ad/tld-0.13.1-py2.py3-none-any.whl", hash = "sha256:a2d35109433ac83486ddf87e3c4539ab2c5c2478230e5d9c060a18af4b03aa7c", size = 274718, upload-time = "2025-05-21T22:18:25.811Z" }, -] - -[[package]] -name = "tokenizers" -version = "0.21.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256, upload-time = "2025-03-13T10:51:18.189Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767, upload-time = "2025-03-13T10:51:09.459Z" }, - { url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555, upload-time = "2025-03-13T10:51:07.692Z" }, - { url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541, upload-time = "2025-03-13T10:50:56.679Z" }, - { url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058, upload-time = "2025-03-13T10:50:59.525Z" }, - { url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278, upload-time = "2025-03-13T10:51:04.678Z" }, - { url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253, upload-time = "2025-03-13T10:51:01.261Z" }, - { url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225, upload-time = "2025-03-13T10:51:03.243Z" }, - { url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874, upload-time = "2025-03-13T10:51:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448, upload-time = "2025-03-13T10:51:10.927Z" }, - { url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877, upload-time = "2025-03-13T10:51:12.688Z" }, - { url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645, upload-time = "2025-03-13T10:51:14.723Z" }, - { url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380, upload-time = "2025-03-13T10:51:16.526Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506, upload-time = "2025-03-13T10:51:20.643Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481, upload-time = "2025-03-13T10:51:19.243Z" }, -] - -[[package]] -name = "torch" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "jinja2" }, - { name = "networkx" }, - { 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-cufile-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" }, - { name = "sympy" }, - { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions" }, -] +sdist = { url = "https://files.pythonhosted.org/packages/df/a1/5723b07a70c1841a80afc9ac572fdf53488306848d844cd70519391b0d26/tld-0.13.1.tar.gz", hash = "sha256:75ec00936cbcf564f67361c41713363440b6c4ef0f0c1592b5b0fbe72c17a350", size = 462000 } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/5e/ac759f4c0ab7c01feffa777bd68b43d2ac61560a9770eeac074b450f81d4/torch-2.7.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:36a6368c7ace41ad1c0f69f18056020b6a5ca47bedaca9a2f3b578f5a104c26c", size = 99013250, upload-time = "2025-04-23T14:35:15.589Z" }, - { url = "https://files.pythonhosted.org/packages/9c/58/2d245b6f1ef61cf11dfc4aceeaacbb40fea706ccebac3f863890c720ab73/torch-2.7.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:15aab3e31c16feb12ae0a88dba3434a458874636f360c567caa6a91f6bfba481", size = 865042157, upload-time = "2025-04-23T14:32:56.011Z" }, - { url = "https://files.pythonhosted.org/packages/44/80/b353c024e6b624cd9ce1d66dcb9d24e0294680f95b369f19280e241a0159/torch-2.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:f56d4b2510934e072bab3ab8987e00e60e1262fb238176168f5e0c43a1320c6d", size = 212482262, upload-time = "2025-04-23T14:35:03.527Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8d/b2939e5254be932db1a34b2bd099070c509e8887e0c5a90c498a917e4032/torch-2.7.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:30b7688a87239a7de83f269333651d8e582afffce6f591fff08c046f7787296e", size = 68574294, upload-time = "2025-04-23T14:34:47.098Z" }, + { url = "https://files.pythonhosted.org/packages/dc/70/b2f38360c3fc4bc9b5e8ef429e1fde63749144ac583c2dbdf7e21e27a9ad/tld-0.13.1-py2.py3-none-any.whl", hash = "sha256:a2d35109433ac83486ddf87e3c4539ab2c5c2478230e5d9c060a18af4b03aa7c", size = 274718 }, ] [[package]] @@ -1672,9 +1290,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, ] [[package]] @@ -1690,41 +1308,9 @@ dependencies = [ { name = "lxml" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/25/e3ebeefdebfdfae8c4a4396f5a6ea51fc6fa0831d63ce338e5090a8003dc/trafilatura-2.0.0.tar.gz", hash = "sha256:ceb7094a6ecc97e72fea73c7dba36714c5c5b577b6470e4520dca893706d6247", size = 253404, upload-time = "2024-12-03T15:23:24.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/25/e3ebeefdebfdfae8c4a4396f5a6ea51fc6fa0831d63ce338e5090a8003dc/trafilatura-2.0.0.tar.gz", hash = "sha256:ceb7094a6ecc97e72fea73c7dba36714c5c5b577b6470e4520dca893706d6247", size = 253404 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/b6/097367f180b6383a3581ca1b86fcae284e52075fa941d1232df35293363c/trafilatura-2.0.0-py3-none-any.whl", hash = "sha256:77eb5d1e993747f6f20938e1de2d840020719735690c840b9a1024803a4cd51d", size = 132557, upload-time = "2024-12-03T15:23:21.41Z" }, -] - -[[package]] -name = "transformers" -version = "4.51.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "huggingface-hub" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "regex" }, - { name = "requests" }, - { name = "safetensors" }, - { name = "tokenizers" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f1/11/7414d5bc07690002ce4d7553602107bf969af85144bbd02830f9fb471236/transformers-4.51.3.tar.gz", hash = "sha256:e292fcab3990c6defe6328f0f7d2004283ca81a7a07b2de9a46d67fd81ea1409", size = 8941266, upload-time = "2025-04-14T08:15:00.485Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/b6/5257d04ae327b44db31f15cce39e6020cc986333c715660b1315a9724d82/transformers-4.51.3-py3-none-any.whl", hash = "sha256:fd3279633ceb2b777013234bbf0b4f5c2d23c4626b05497691f00cfda55e8a83", size = 10383940, upload-time = "2025-04-14T08:13:43.023Z" }, -] - -[[package]] -name = "triton" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/53/ce18470914ab6cfbec9384ee565d23c4d1c55f0548160b1c7b33000b11fd/triton-3.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b68c778f6c4218403a6bd01be7484f6dc9e20fe2083d22dd8aef33e3b87a10a3", size = 156504509, upload-time = "2025-04-09T20:27:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b6/097367f180b6383a3581ca1b86fcae284e52075fa941d1232df35293363c/trafilatura-2.0.0-py3-none-any.whl", hash = "sha256:77eb5d1e993747f6f20938e1de2d840020719735690c840b9a1024803a4cd51d", size = 132557 }, ] [[package]] @@ -1737,27 +1323,27 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/1a/5f36851f439884bcfe8539f6a20ff7516e7b60f319bbaf69a90dc35cc2eb/typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c", size = 101641, upload-time = "2025-04-28T21:40:59.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/1a/5f36851f439884bcfe8539f6a20ff7516e7b60f319bbaf69a90dc35cc2eb/typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c", size = 101641 } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/20/9d953de6f4367163d23ec823200eb3ecb0050a2609691e512c8b95827a9b/typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd", size = 45253, upload-time = "2025-04-28T21:40:56.269Z" }, + { url = "https://files.pythonhosted.org/packages/48/20/9d953de6f4367163d23ec823200eb3ecb0050a2609691e512c8b95827a9b/typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd", size = 45253 }, ] [[package]] name = "typing-extensions" version = "4.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, ] [[package]] name = "tzdata" version = "2025.2" source = { registry = "https://pypi.org/simple" } -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" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, ] [[package]] @@ -1767,68 +1353,68 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026 }, ] [[package]] name = "urllib3" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, ] [[package]] name = "win32-setctime" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, ] [[package]] name = "xlrd" version = "2.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/b3/19a2540d21dea5f908304375bd43f5ed7a4c28a370dc9122c565423e6b44/xlrd-2.0.1.tar.gz", hash = "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88", size = 100259, upload-time = "2020-12-11T10:14:22.201Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/b3/19a2540d21dea5f908304375bd43f5ed7a4c28a370dc9122c565423e6b44/xlrd-2.0.1.tar.gz", hash = "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88", size = 100259 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/0c/c2a72d51fe56e08a08acc85d13013558a2d793028ae7385448a6ccdfae64/xlrd-2.0.1-py2.py3-none-any.whl", hash = "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", size = 96531, upload-time = "2020-12-11T10:14:20.877Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0c/c2a72d51fe56e08a08acc85d13013558a2d793028ae7385448a6ccdfae64/xlrd-2.0.1-py2.py3-none-any.whl", hash = "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", size = 96531 }, ] [[package]] name = "xlsxwriter" version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/d1/e026d33dd5d552e5bf3a873dee54dad66b550230df8290d79394f09b2315/xlsxwriter-3.2.3.tar.gz", hash = "sha256:ad6fd41bdcf1b885876b1f6b7087560aecc9ae5a9cc2ba97dcac7ab2e210d3d5", size = 209135, upload-time = "2025-04-17T10:11:23.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/d1/e026d33dd5d552e5bf3a873dee54dad66b550230df8290d79394f09b2315/xlsxwriter-3.2.3.tar.gz", hash = "sha256:ad6fd41bdcf1b885876b1f6b7087560aecc9ae5a9cc2ba97dcac7ab2e210d3d5", size = 209135 } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/b1/a252d499f2760b314fcf264d2b36fcc4343a1ecdb25492b210cb0db70a68/XlsxWriter-3.2.3-py3-none-any.whl", hash = "sha256:593f8296e8a91790c6d0378ab08b064f34a642b3feb787cf6738236bd0a4860d", size = 169433, upload-time = "2025-04-17T10:11:21.329Z" }, + { url = "https://files.pythonhosted.org/packages/37/b1/a252d499f2760b314fcf264d2b36fcc4343a1ecdb25492b210cb0db70a68/XlsxWriter-3.2.3-py3-none-any.whl", hash = "sha256:593f8296e8a91790c6d0378ab08b064f34a642b3feb787cf6738236bd0a4860d", size = 169433 }, ] [[package]] name = "xxhash" version = "3.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/5e/d6e5258d69df8b4ed8c83b6664f2b47d30d2dec551a29ad72a6c69eafd31/xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f", size = 84241, upload-time = "2024-08-17T09:20:38.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/5e/d6e5258d69df8b4ed8c83b6664f2b47d30d2dec551a29ad72a6c69eafd31/xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f", size = 84241 } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/0e/1bfce2502c57d7e2e787600b31c83535af83746885aa1a5f153d8c8059d6/xxhash-3.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14470ace8bd3b5d51318782cd94e6f94431974f16cb3b8dc15d52f3b69df8e00", size = 31969, upload-time = "2024-08-17T09:18:24.025Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d6/8ca450d6fe5b71ce521b4e5db69622383d039e2b253e9b2f24f93265b52c/xxhash-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59aa1203de1cb96dbeab595ded0ad0c0056bb2245ae11fac11c0ceea861382b9", size = 30787, upload-time = "2024-08-17T09:18:25.318Z" }, - { url = "https://files.pythonhosted.org/packages/5b/84/de7c89bc6ef63d750159086a6ada6416cc4349eab23f76ab870407178b93/xxhash-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08424f6648526076e28fae6ea2806c0a7d504b9ef05ae61d196d571e5c879c84", size = 220959, upload-time = "2024-08-17T09:18:26.518Z" }, - { url = "https://files.pythonhosted.org/packages/fe/86/51258d3e8a8545ff26468c977101964c14d56a8a37f5835bc0082426c672/xxhash-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a1ff00674879725b194695e17f23d3248998b843eb5e933007ca743310f793", size = 200006, upload-time = "2024-08-17T09:18:27.905Z" }, - { url = "https://files.pythonhosted.org/packages/02/0a/96973bd325412feccf23cf3680fd2246aebf4b789122f938d5557c54a6b2/xxhash-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f2c61bee5844d41c3eb015ac652a0229e901074951ae48581d58bfb2ba01be", size = 428326, upload-time = "2024-08-17T09:18:29.335Z" }, - { url = "https://files.pythonhosted.org/packages/11/a7/81dba5010f7e733de88af9555725146fc133be97ce36533867f4c7e75066/xxhash-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d32a592cac88d18cc09a89172e1c32d7f2a6e516c3dfde1b9adb90ab5df54a6", size = 194380, upload-time = "2024-08-17T09:18:30.706Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7d/f29006ab398a173f4501c0e4977ba288f1c621d878ec217b4ff516810c04/xxhash-3.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70dabf941dede727cca579e8c205e61121afc9b28516752fd65724be1355cc90", size = 207934, upload-time = "2024-08-17T09:18:32.133Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6e/6e88b8f24612510e73d4d70d9b0c7dff62a2e78451b9f0d042a5462c8d03/xxhash-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5d0ddaca65ecca9c10dcf01730165fd858533d0be84c75c327487c37a906a27", size = 216301, upload-time = "2024-08-17T09:18:33.474Z" }, - { url = "https://files.pythonhosted.org/packages/af/51/7862f4fa4b75a25c3b4163c8a873f070532fe5f2d3f9b3fc869c8337a398/xxhash-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e5b5e16c5a480fe5f59f56c30abdeba09ffd75da8d13f6b9b6fd224d0b4d0a2", size = 203351, upload-time = "2024-08-17T09:18:34.889Z" }, - { url = "https://files.pythonhosted.org/packages/22/61/8d6a40f288f791cf79ed5bb113159abf0c81d6efb86e734334f698eb4c59/xxhash-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149b7914451eb154b3dfaa721315117ea1dac2cc55a01bfbd4df7c68c5dd683d", size = 210294, upload-time = "2024-08-17T09:18:36.355Z" }, - { url = "https://files.pythonhosted.org/packages/17/02/215c4698955762d45a8158117190261b2dbefe9ae7e5b906768c09d8bc74/xxhash-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:eade977f5c96c677035ff39c56ac74d851b1cca7d607ab3d8f23c6b859379cab", size = 414674, upload-time = "2024-08-17T09:18:38.536Z" }, - { url = "https://files.pythonhosted.org/packages/31/5c/b7a8db8a3237cff3d535261325d95de509f6a8ae439a5a7a4ffcff478189/xxhash-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa9f547bd98f5553d03160967866a71056a60960be00356a15ecc44efb40ba8e", size = 192022, upload-time = "2024-08-17T09:18:40.138Z" }, - { url = "https://files.pythonhosted.org/packages/78/e3/dd76659b2811b3fd06892a8beb850e1996b63e9235af5a86ea348f053e9e/xxhash-3.5.0-cp312-cp312-win32.whl", hash = "sha256:f7b58d1fd3551b8c80a971199543379be1cee3d0d409e1f6d8b01c1a2eebf1f8", size = 30170, upload-time = "2024-08-17T09:18:42.163Z" }, - { url = "https://files.pythonhosted.org/packages/d9/6b/1c443fe6cfeb4ad1dcf231cdec96eb94fb43d6498b4469ed8b51f8b59a37/xxhash-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0cafd3a2af231b4e113fba24a65d7922af91aeb23774a8b78228e6cd785e3e", size = 30040, upload-time = "2024-08-17T09:18:43.699Z" }, - { url = "https://files.pythonhosted.org/packages/0f/eb/04405305f290173acc0350eba6d2f1a794b57925df0398861a20fbafa415/xxhash-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:586886c7e89cb9828bcd8a5686b12e161368e0064d040e225e72607b43858ba2", size = 26796, upload-time = "2024-08-17T09:18:45.29Z" }, + { url = "https://files.pythonhosted.org/packages/07/0e/1bfce2502c57d7e2e787600b31c83535af83746885aa1a5f153d8c8059d6/xxhash-3.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14470ace8bd3b5d51318782cd94e6f94431974f16cb3b8dc15d52f3b69df8e00", size = 31969 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/8ca450d6fe5b71ce521b4e5db69622383d039e2b253e9b2f24f93265b52c/xxhash-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59aa1203de1cb96dbeab595ded0ad0c0056bb2245ae11fac11c0ceea861382b9", size = 30787 }, + { url = "https://files.pythonhosted.org/packages/5b/84/de7c89bc6ef63d750159086a6ada6416cc4349eab23f76ab870407178b93/xxhash-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08424f6648526076e28fae6ea2806c0a7d504b9ef05ae61d196d571e5c879c84", size = 220959 }, + { url = "https://files.pythonhosted.org/packages/fe/86/51258d3e8a8545ff26468c977101964c14d56a8a37f5835bc0082426c672/xxhash-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a1ff00674879725b194695e17f23d3248998b843eb5e933007ca743310f793", size = 200006 }, + { url = "https://files.pythonhosted.org/packages/02/0a/96973bd325412feccf23cf3680fd2246aebf4b789122f938d5557c54a6b2/xxhash-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f2c61bee5844d41c3eb015ac652a0229e901074951ae48581d58bfb2ba01be", size = 428326 }, + { url = "https://files.pythonhosted.org/packages/11/a7/81dba5010f7e733de88af9555725146fc133be97ce36533867f4c7e75066/xxhash-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d32a592cac88d18cc09a89172e1c32d7f2a6e516c3dfde1b9adb90ab5df54a6", size = 194380 }, + { url = "https://files.pythonhosted.org/packages/fb/7d/f29006ab398a173f4501c0e4977ba288f1c621d878ec217b4ff516810c04/xxhash-3.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70dabf941dede727cca579e8c205e61121afc9b28516752fd65724be1355cc90", size = 207934 }, + { url = "https://files.pythonhosted.org/packages/8a/6e/6e88b8f24612510e73d4d70d9b0c7dff62a2e78451b9f0d042a5462c8d03/xxhash-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5d0ddaca65ecca9c10dcf01730165fd858533d0be84c75c327487c37a906a27", size = 216301 }, + { url = "https://files.pythonhosted.org/packages/af/51/7862f4fa4b75a25c3b4163c8a873f070532fe5f2d3f9b3fc869c8337a398/xxhash-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e5b5e16c5a480fe5f59f56c30abdeba09ffd75da8d13f6b9b6fd224d0b4d0a2", size = 203351 }, + { url = "https://files.pythonhosted.org/packages/22/61/8d6a40f288f791cf79ed5bb113159abf0c81d6efb86e734334f698eb4c59/xxhash-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149b7914451eb154b3dfaa721315117ea1dac2cc55a01bfbd4df7c68c5dd683d", size = 210294 }, + { url = "https://files.pythonhosted.org/packages/17/02/215c4698955762d45a8158117190261b2dbefe9ae7e5b906768c09d8bc74/xxhash-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:eade977f5c96c677035ff39c56ac74d851b1cca7d607ab3d8f23c6b859379cab", size = 414674 }, + { url = "https://files.pythonhosted.org/packages/31/5c/b7a8db8a3237cff3d535261325d95de509f6a8ae439a5a7a4ffcff478189/xxhash-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa9f547bd98f5553d03160967866a71056a60960be00356a15ecc44efb40ba8e", size = 192022 }, + { url = "https://files.pythonhosted.org/packages/78/e3/dd76659b2811b3fd06892a8beb850e1996b63e9235af5a86ea348f053e9e/xxhash-3.5.0-cp312-cp312-win32.whl", hash = "sha256:f7b58d1fd3551b8c80a971199543379be1cee3d0d409e1f6d8b01c1a2eebf1f8", size = 30170 }, + { url = "https://files.pythonhosted.org/packages/d9/6b/1c443fe6cfeb4ad1dcf231cdec96eb94fb43d6498b4469ed8b51f8b59a37/xxhash-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0cafd3a2af231b4e113fba24a65d7922af91aeb23774a8b78228e6cd785e3e", size = 30040 }, + { url = "https://files.pythonhosted.org/packages/0f/eb/04405305f290173acc0350eba6d2f1a794b57925df0398861a20fbafa415/xxhash-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:586886c7e89cb9828bcd8a5686b12e161368e0064d040e225e72607b43858ba2", size = 26796 }, ] [[package]] @@ -1840,26 +1426,26 @@ dependencies = [ { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/51/c0edba5219027f6eab262e139f73e2417b0f4efffa23bf562f6e18f76ca5/yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", size = 185258, upload-time = "2025-04-17T00:45:14.661Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/e8/3efdcb83073df978bb5b1a9cc0360ce596680e6c3fac01f2a994ccbb8939/yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", size = 147089, upload-time = "2025-04-17T00:42:39.602Z" }, - { url = "https://files.pythonhosted.org/packages/60/c3/9e776e98ea350f76f94dd80b408eaa54e5092643dbf65fd9babcffb60509/yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", size = 97706, upload-time = "2025-04-17T00:42:41.469Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/45cdfb64a3b855ce074ae607b9fc40bc82e7613b94e7612b030255c93a09/yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", size = 95719, upload-time = "2025-04-17T00:42:43.666Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4e/929633b249611eeed04e2f861a14ed001acca3ef9ec2a984a757b1515889/yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", size = 343972, upload-time = "2025-04-17T00:42:45.391Z" }, - { url = "https://files.pythonhosted.org/packages/49/fd/047535d326c913f1a90407a3baf7ff535b10098611eaef2c527e32e81ca1/yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", size = 339639, upload-time = "2025-04-17T00:42:47.552Z" }, - { url = "https://files.pythonhosted.org/packages/48/2f/11566f1176a78f4bafb0937c0072410b1b0d3640b297944a6a7a556e1d0b/yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", size = 353745, upload-time = "2025-04-17T00:42:49.406Z" }, - { url = "https://files.pythonhosted.org/packages/26/17/07dfcf034d6ae8837b33988be66045dd52f878dfb1c4e8f80a7343f677be/yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", size = 354178, upload-time = "2025-04-17T00:42:51.588Z" }, - { url = "https://files.pythonhosted.org/packages/15/45/212604d3142d84b4065d5f8cab6582ed3d78e4cc250568ef2a36fe1cf0a5/yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", size = 349219, upload-time = "2025-04-17T00:42:53.674Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e0/a10b30f294111c5f1c682461e9459935c17d467a760c21e1f7db400ff499/yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", size = 337266, upload-time = "2025-04-17T00:42:55.49Z" }, - { url = "https://files.pythonhosted.org/packages/33/a6/6efa1d85a675d25a46a167f9f3e80104cde317dfdf7f53f112ae6b16a60a/yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", size = 360873, upload-time = "2025-04-17T00:42:57.895Z" }, - { url = "https://files.pythonhosted.org/packages/77/67/c8ab718cb98dfa2ae9ba0f97bf3cbb7d45d37f13fe1fbad25ac92940954e/yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", size = 360524, upload-time = "2025-04-17T00:43:00.094Z" }, - { url = "https://files.pythonhosted.org/packages/bd/e8/c3f18660cea1bc73d9f8a2b3ef423def8dadbbae6c4afabdb920b73e0ead/yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", size = 365370, upload-time = "2025-04-17T00:43:02.242Z" }, - { url = "https://files.pythonhosted.org/packages/c9/99/33f3b97b065e62ff2d52817155a89cfa030a1a9b43fee7843ef560ad9603/yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", size = 373297, upload-time = "2025-04-17T00:43:04.189Z" }, - { url = "https://files.pythonhosted.org/packages/3d/89/7519e79e264a5f08653d2446b26d4724b01198a93a74d2e259291d538ab1/yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", size = 378771, upload-time = "2025-04-17T00:43:06.609Z" }, - { url = "https://files.pythonhosted.org/packages/3a/58/6c460bbb884abd2917c3eef6f663a4a873f8dc6f498561fc0ad92231c113/yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", size = 375000, upload-time = "2025-04-17T00:43:09.01Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/dd7ed1aa23fea996834278d7ff178f215b24324ee527df53d45e34d21d28/yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", size = 86355, upload-time = "2025-04-17T00:43:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/ca/c6/333fe0338305c0ac1c16d5aa7cc4841208d3252bbe62172e0051006b5445/yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", size = 92904, upload-time = "2025-04-17T00:43:13.087Z" }, - { url = "https://files.pythonhosted.org/packages/ea/1f/70c57b3d7278e94ed22d85e09685d3f0a38ebdd8c5c73b65ba4c0d0fe002/yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", size = 46124, upload-time = "2025-04-17T00:45:12.199Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/62/51/c0edba5219027f6eab262e139f73e2417b0f4efffa23bf562f6e18f76ca5/yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", size = 185258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/e8/3efdcb83073df978bb5b1a9cc0360ce596680e6c3fac01f2a994ccbb8939/yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", size = 147089 }, + { url = "https://files.pythonhosted.org/packages/60/c3/9e776e98ea350f76f94dd80b408eaa54e5092643dbf65fd9babcffb60509/yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", size = 97706 }, + { url = "https://files.pythonhosted.org/packages/0c/5b/45cdfb64a3b855ce074ae607b9fc40bc82e7613b94e7612b030255c93a09/yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", size = 95719 }, + { url = "https://files.pythonhosted.org/packages/2d/4e/929633b249611eeed04e2f861a14ed001acca3ef9ec2a984a757b1515889/yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", size = 343972 }, + { url = "https://files.pythonhosted.org/packages/49/fd/047535d326c913f1a90407a3baf7ff535b10098611eaef2c527e32e81ca1/yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", size = 339639 }, + { url = "https://files.pythonhosted.org/packages/48/2f/11566f1176a78f4bafb0937c0072410b1b0d3640b297944a6a7a556e1d0b/yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", size = 353745 }, + { url = "https://files.pythonhosted.org/packages/26/17/07dfcf034d6ae8837b33988be66045dd52f878dfb1c4e8f80a7343f677be/yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", size = 354178 }, + { url = "https://files.pythonhosted.org/packages/15/45/212604d3142d84b4065d5f8cab6582ed3d78e4cc250568ef2a36fe1cf0a5/yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", size = 349219 }, + { url = "https://files.pythonhosted.org/packages/e6/e0/a10b30f294111c5f1c682461e9459935c17d467a760c21e1f7db400ff499/yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", size = 337266 }, + { url = "https://files.pythonhosted.org/packages/33/a6/6efa1d85a675d25a46a167f9f3e80104cde317dfdf7f53f112ae6b16a60a/yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", size = 360873 }, + { url = "https://files.pythonhosted.org/packages/77/67/c8ab718cb98dfa2ae9ba0f97bf3cbb7d45d37f13fe1fbad25ac92940954e/yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", size = 360524 }, + { url = "https://files.pythonhosted.org/packages/bd/e8/c3f18660cea1bc73d9f8a2b3ef423def8dadbbae6c4afabdb920b73e0ead/yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", size = 365370 }, + { url = "https://files.pythonhosted.org/packages/c9/99/33f3b97b065e62ff2d52817155a89cfa030a1a9b43fee7843ef560ad9603/yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", size = 373297 }, + { url = "https://files.pythonhosted.org/packages/3d/89/7519e79e264a5f08653d2446b26d4724b01198a93a74d2e259291d538ab1/yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", size = 378771 }, + { url = "https://files.pythonhosted.org/packages/3a/58/6c460bbb884abd2917c3eef6f663a4a873f8dc6f498561fc0ad92231c113/yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", size = 375000 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/dd7ed1aa23fea996834278d7ff178f215b24324ee527df53d45e34d21d28/yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", size = 86355 }, + { url = "https://files.pythonhosted.org/packages/ca/c6/333fe0338305c0ac1c16d5aa7cc4841208d3252bbe62172e0051006b5445/yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", size = 92904 }, + { url = "https://files.pythonhosted.org/packages/ea/1f/70c57b3d7278e94ed22d85e09685d3f0a38ebdd8c5c73b65ba4c0d0fe002/yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", size = 46124 }, ] [[package]] @@ -1868,6 +1454,7 @@ version = "0.3.1" source = { editable = "." } dependencies = [ { name = "asyncio" }, + { name = "boto3" }, { name = "click" }, { name = "datasets" }, { name = "hf-transfer" }, @@ -1875,6 +1462,7 @@ dependencies = [ { name = "loguru" }, { name = "markitdown", extra = ["all"] }, { name = "python-dotenv" }, + { name = "pyyaml" }, { name = "rich" }, { name = "ruff" }, { name = "thefuzz" }, @@ -1884,24 +1472,10 @@ dependencies = [ { name = "typer" }, ] -[package.optional-dependencies] -all = [ - { name = "bert-score" }, - { name = "rouge-score" }, - { name = "torch" }, - { name = "transformers" }, -] -semantic = [ - { name = "bert-score" }, - { name = "rouge-score" }, - { name = "torch" }, - { name = "transformers" }, -] - [package.metadata] requires-dist = [ { name = "asyncio", specifier = ">=3.4.3" }, - { name = "bert-score", marker = "extra == 'semantic'", specifier = ">=0.3.13" }, + { name = "boto3", specifier = ">=1.34.0" }, { name = "click", specifier = ">=8.1.7" }, { name = "datasets", specifier = ">=3.3.0" }, { name = "hf-transfer", specifier = ">=0.1.9" }, @@ -1909,19 +1483,15 @@ requires-dist = [ { name = "loguru", specifier = ">=0.7.3" }, { name = "markitdown", extras = ["all"], specifier = ">=0.0.2" }, { name = "python-dotenv", specifier = ">=1.0.1" }, + { name = "pyyaml", specifier = ">=6.0.1" }, { name = "rich", specifier = ">=13.7.0" }, - { name = "rouge-score", marker = "extra == 'semantic'", specifier = ">=0.1.2" }, { name = "ruff", specifier = ">=0.11.1" }, { name = "thefuzz", specifier = ">=0.22.1" }, { name = "tiktoken", specifier = ">=0.9.0" }, - { name = "torch", marker = "extra == 'semantic'", specifier = ">=2.6.0" }, { name = "tqdm", specifier = ">=4.67.1" }, { name = "trafilatura", specifier = ">=2.0.0" }, - { name = "transformers", marker = "extra == 'semantic'", specifier = ">=4.48.3" }, { name = "typer", specifier = ">=0.15.2" }, - { name = "yourbench", extras = ["semantic"], marker = "extra == 'all'" }, ] -provides-extras = ["semantic", "all"] [[package]] name = "youtube-transcript-api" @@ -1931,7 +1501,7 @@ dependencies = [ { name = "defusedxml" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/32/f60d87a99c05a53604c58f20f670c7ea6262b55e0bbeb836ffe4550b248b/youtube_transcript_api-1.0.3.tar.gz", hash = "sha256:902baf90e7840a42e1e148335e09fe5575dbff64c81414957aea7038e8a4db46", size = 2153252, upload-time = "2025-03-25T18:14:21.119Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/32/f60d87a99c05a53604c58f20f670c7ea6262b55e0bbeb836ffe4550b248b/youtube_transcript_api-1.0.3.tar.gz", hash = "sha256:902baf90e7840a42e1e148335e09fe5575dbff64c81414957aea7038e8a4db46", size = 2153252 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/44/40c03bb0f8bddfb9d2beff2ed31641f52d96c287ba881d20e0c074784ac2/youtube_transcript_api-1.0.3-py3-none-any.whl", hash = "sha256:d1874e57de65cf14c9d7d09b2b37c814d6287fa0e770d4922c4cd32a5b3f6c47", size = 2169911, upload-time = "2025-03-25T18:14:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/f0/44/40c03bb0f8bddfb9d2beff2ed31641f52d96c287ba881d20e0c074784ac2/youtube_transcript_api-1.0.3-py3-none-any.whl", hash = "sha256:d1874e57de65cf14c9d7d09b2b37c814d6287fa0e770d4922c4cd32a5b3f6c47", size = 2169911 }, ] From b0af320155ba198fb5e8679df4690e44e37724bb Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Fri, 13 Jun 2025 10:30:41 -0300 Subject: [PATCH 67/69] Added missing HF_HUB_ONLINE=1 to .env.template --- .env.template | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.env.template b/.env.template index 9df3c4ae..a9686e62 100644 --- a/.env.template +++ b/.env.template @@ -8,4 +8,6 @@ OUTPUT_S3_BUCKET="layerlens-private-test-organization" OUTPUT_S3_KEY="benchmarks/test-project/benchmark-name/" AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= \ No newline at end of file +AWS_SECRET_ACCESS_KEY= + +HF_HUB_OFFLINE=1 From 0cf07178bfbb508a639eb5619f81d971352d8483 Mon Sep 17 00:00:00 2001 From: Dmitry Stepanov Date: Thu, 17 Jul 2025 13:29:14 -0300 Subject: [PATCH 68/69] limit LLM query count to 50 for single-shot and 50 for multi-hop questions --- run_yourbench.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/run_yourbench.py b/run_yourbench.py index 6e77775e..8e1f426f 100644 --- a/run_yourbench.py +++ b/run_yourbench.py @@ -134,7 +134,13 @@ def main(): summarization: chunking: single_shot_question_generation: + chunk_sampling: + mode: "count" + value: 50 multi_hop_question_generation: + chunk_sampling: + mode: "count" + value: 50 lighteval: run: true include_document_text: false # Set to false to exclude full document text from the dataset (saves memory) From 2b86b62e45d25d8d768d39a70ebd6d73d5967c3b Mon Sep 17 00:00:00 2001 From: Robert Leonard <40375385+Robert-H-Leonard@users.noreply.github.com> Date: Tue, 2 Sep 2025 08:11:57 -0400 Subject: [PATCH 69/69] Update llm judge model for yourbench to 2.5-flash --- yourbench/utils/convert_to_atlas_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yourbench/utils/convert_to_atlas_module.py b/yourbench/utils/convert_to_atlas_module.py index c32bda4a..0dbc1d50 100644 --- a/yourbench/utils/convert_to_atlas_module.py +++ b/yourbench/utils/convert_to_atlas_module.py @@ -13,7 +13,7 @@ def _scorer_yaml(name: str) -> str: type: llm_judge options: regex_pattern: '' - judge_model: google_gemini-2.5-flash-preview-05-20 + judge_model: google_gemini-2.5-flash judge_prompt: |- Your job is to look at a question, a gold target, and a predicted answer, and then assign a grade of either ["CORRECT", "INCORRECT", "NOT_ATTEMPTED"]. First, I will give examples of each grade, and then you will grade a new example.