Skip to content

Instantly share code, notes, and snippets.

@wyattowalsh
Last active May 30, 2025 17:07
Show Gist options
  • Select an option

  • Save wyattowalsh/feaf0a44d06fda50eb2de45136eba84a to your computer and use it in GitHub Desktop.

Select an option

Save wyattowalsh/feaf0a44d06fda50eb2de45136eba84a to your computer and use it in GitHub Desktop.

Revisions

  1. wyattowalsh revised this gist May 30, 2025. 1 changed file with 1 addition and 49 deletions.
    50 changes: 1 addition & 49 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -107,60 +107,12 @@ Details based on §5 of the Infinity Loop - Async Coding Challenge Candidate Pro

    ## 🧪 Testing

    The solution includes comprehensive testing covering:

    - **Validation**: Input data validation and error handling
    - **Normalization**: Core business logic with various scenarios
    - **Edge Cases**: Zero values, long strings, special characters
    - **Integration**: End-to-end workflows and JSON serialization

    Run all tests:

    ```bash
    pytest tests.py -v
    uv run pytest
    ```

    ### Test Coverage

    - ✅ 20 test cases
    - ✅ 100% pass rate
    - ✅ Edge cases and error scenarios
    - ✅ Real-world contract variations

    ## 📊 Sample Output

    The following table represents the normalized output for the example input data, matching the structure specified in the challenge prompt:

    | Vendor | Renewal Terms | Termination Terms |
    | ------- | ------------------------------------- | ---------------------------------------------------- |
    | ACME | No auto-renewal | May be terminated by either party with 60 day notice |
    | Initech | Renews every 12 months, 30 day notice | Unknown |
    | Globex | Renewal period unknown, 60 day notice | May be terminated by vendor with 30 day notice |

    This output is generated by processing the sample input through `DataNormalizer.normalizeAll()` and formatting it for display. The actual JSON output would have keys `vendor`, `renewalTerms`, and `terminationTerms` as per the prompt's specification (if `main.py` is adjusted to output aliases, otherwise current `main.py` output is `renewal_terms`, `termination_terms`).

    ## ⚡ Features

    ### Advanced Pydantic v2 Features

    - **Field Validators**: Custom validation for vendor names and notice days
    - **Model Validators**: Cross-field validation and normalization
    - **Type Coercion**: Automatic type conversion with validation
    - **Detailed Error Messages**: Clear validation error reporting

    ### Rich Logging & UX

    - **Beautiful Tables**: Rich terminal tables for output display
    - **Structured Logging**: Detailed debug information with loguru
    - **Error Handling**: Graceful error handling with rich tracebacks
    - **Progress Feedback**: Real-time processing feedback

    ### Extensibility

    - **Modular Design**: Separation of concerns with clear interfaces
    - **Easy Extension**: Add new vendor formats by extending the models
    - **Configuration**: Configurable validation and normalization rules

    ## 📁 Project Structure

    ```text
  2. wyattowalsh created this gist May 30, 2025.
    1 change: 1 addition & 0 deletions .python-version
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    3.13
    184 changes: 184 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,184 @@
    # Infinity Loop Contract Normalization Challenge

    A robust solution for normalizing heterogeneous vendor contract metadata into a standardized format.

    ## 🎯 Challenge Overview

    This project solves the Infinity Loop coding challenge, which requires normalizing contract metadata from different vendors with varying JSON schemas into a standardized output format.

    ### Problem Statement

    - **Input**: Array of contract metadata objects with varying vendor-specific schemas
    - **Output**: Array of normalized objects with exactly 3 fields: `vendor`, `renewalTerms`, `terminationTerms`
    - **Goal**: Create a single function that handles the heterogeneous data normalization

    ## 🏗️ Architecture

    The solution is built using:

    - **Pydantic v2**: Advanced data validation and parsing with robust field validators
    - **Rich + Loguru**: Beautiful logging and terminal output with detailed debugging
    - **Type Safety**: Full type hints and validation throughout
    - **Comprehensive Testing**: 20 test cases covering edge cases and error scenarios

    ### Key Components

    1. **`RawContract`**: Flexible input model accepting heterogeneous vendor data
    2. **`NormalizedContract`**: Standardized output model with validation
    3. **`ContractNormalizer`**: Core service class handling the normalization logic
    4. **Rich Logging**: Beautiful terminal output and debugging information

    ## 🚀 Usage

    ### Basic Usage

    ```python
    from main import DataNormalizer

    # Sample input data
    contracts = [
    {
    "vendor": "ACME",
    "autoRenew": False,
    "terminationType": "either party",
    "terminationNoticeDays": 60
    },
    {
    "vendor": "Initech",
    "renewalPeriod": "12 months",
    "renewalNoticeDays": 30
    }
    ]

    # Normalize contracts
    normalized_data = DataNormalizer.normalizeAll(contracts)

    # Access results
    for contract in normalized_data:
    print(f"Vendor: {contract['vendor']}")
    print(f"Renewal: {contract['renewal_terms']}")
    print(f"Termination: {contract['termination_terms']}")
    ```

    ### Running the Demo

    ```bash
    python main.py
    ```

    This runs the main demo with the sample data from the challenge prompt and displays:

    - Beautiful terminal table output
    - Detailed logging information
    - JSON output for verification

    ## 📋 Normalization Rules

    Details based on §5 of the Infinity Loop - Async Coding Challenge Candidate Prompt.

    ### 5.1 Field `vendor`

    - Copied verbatim from the input.

    ### 5.2 Field `renewalTerms`

    - **If `autoRenew` is explicitly `false`**:
    - Result: `"No auto-renewal"` (any `renewalPeriod` / `renewalNoticeDays` fields are ignored).
    - **Otherwise (contract auto-renews by default or if `autoRenew` is `true` or absent)**:
    - The general template is: `"Renews every {renewal period}, {renewal notice period}"`.
    - If `renewalPeriod` is missing or unparseable: The renewal part becomes `"Renewal period unknown"`.
    - If `renewalNoticeDays` is missing: The notice part becomes `"notice period unknown"`.
    - The implementation in `main.py` first attempts to parse `renewalPeriod` (e.g., "12 months") into days. If successful, this day count is used.
    - _Key rule for auto-renewal determination: A contract auto-renews unless the `autoRenew` flag is present *and* its value is `false`._

    ### 5.3 Field `terminationTerms` / `terminationRights`

    - **General template**: `"May be terminated by {client | vendor | either party} with {X} days notice"`.
    - **If either `terminationType` or `terminationNoticeDays` is missing from the input**:
    - Result: `"Unknown"`.

    ### Additional Data Validation (as implemented in `main.py`)

    - **Notice Days**:
    - `renewalNoticeDays`: If provided, must be an integer greater than 0.
    - `terminationNoticeDays`: If provided, must be an integer greater than 0.
    - These are enforced by Pydantic validators in the `AgreementRaw` model.
    - **Termination Types**: Accepted string values are "client", "vendor", or "either party", mapping to the `TerminationType` enum.

    ## 🧪 Testing

    The solution includes comprehensive testing covering:

    - **Validation**: Input data validation and error handling
    - **Normalization**: Core business logic with various scenarios
    - **Edge Cases**: Zero values, long strings, special characters
    - **Integration**: End-to-end workflows and JSON serialization

    Run all tests:

    ```bash
    pytest tests.py -v
    ```

    ### Test Coverage

    - ✅ 20 test cases
    - ✅ 100% pass rate
    - ✅ Edge cases and error scenarios
    - ✅ Real-world contract variations

    ## 📊 Sample Output

    The following table represents the normalized output for the example input data, matching the structure specified in the challenge prompt:

    | Vendor | Renewal Terms | Termination Terms |
    | ------- | ------------------------------------- | ---------------------------------------------------- |
    | ACME | No auto-renewal | May be terminated by either party with 60 day notice |
    | Initech | Renews every 12 months, 30 day notice | Unknown |
    | Globex | Renewal period unknown, 60 day notice | May be terminated by vendor with 30 day notice |

    This output is generated by processing the sample input through `DataNormalizer.normalizeAll()` and formatting it for display. The actual JSON output would have keys `vendor`, `renewalTerms`, and `terminationTerms` as per the prompt's specification (if `main.py` is adjusted to output aliases, otherwise current `main.py` output is `renewal_terms`, `termination_terms`).

    ## ⚡ Features

    ### Advanced Pydantic v2 Features

    - **Field Validators**: Custom validation for vendor names and notice days
    - **Model Validators**: Cross-field validation and normalization
    - **Type Coercion**: Automatic type conversion with validation
    - **Detailed Error Messages**: Clear validation error reporting

    ### Rich Logging & UX

    - **Beautiful Tables**: Rich terminal tables for output display
    - **Structured Logging**: Detailed debug information with loguru
    - **Error Handling**: Graceful error handling with rich tracebacks
    - **Progress Feedback**: Real-time processing feedback

    ### Extensibility

    - **Modular Design**: Separation of concerns with clear interfaces
    - **Easy Extension**: Add new vendor formats by extending the models
    - **Configuration**: Configurable validation and normalization rules

    ## 📁 Project Structure

    ```text
    infinity-loop/
    ├── main.py # Core implementation
    ├── tests.py # Comprehensive test suite
    ├── pyproject.toml # Project configuration
    ├── requirements.txt # Python dependencies
    ├── uv.lock # Lock file for dependencies (if using uv)
    └── README.md # This documentation
    # Other files like logs.txt, pytest-logs.txt may also be present
    ```

    ## 🔧 Dependencies

    - **uv**: Package manager
    - **Python 3.13+**: Modern Python features and type system
    - **Pydantic 2.11+**: Advanced data validation and parsing
    - **Rich 14.0+**: Beautiful terminal output and formatting
    - **Loguru 0.7+**: Structured logging with rich integration
    - **Pytest 8.3+**: Comprehensive testing framework
    351 changes: 351 additions & 0 deletions main.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,351 @@
    """infinity-loop/main.py
    Main module for the Infinity Loop Coding Challenge.
    This module provides contract metadata normalization functionality,
    converting heterogeneous vendor contract data into a standardized format.
    """

    from concurrent.futures import ProcessPoolExecutor
    from enum import StrEnum
    from pprint import pprint
    from typing import Any, List, Optional

    from loguru import logger
    from pydantic import BaseModel, Field, field_validator
    from rich.console import Console
    from rich.logging import RichHandler
    from tqdm import tqdm

    CONSOLE_LOGGING = True
    FILE_LOGGING = True
    LOG_PATH = "./logs.txt"

    # Configure loguru
    logger.remove()

    console = Console()

    if CONSOLE_LOGGING:
    logger.add(
    RichHandler(
    console = console,
    show_time = True,
    show_level = True,
    show_path = True,
    markup = True,
    rich_tracebacks = True,
    tracebacks_show_locals = True,
    ),
    level = "DEBUG",
    format = (
    "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | "
    "{name}:{function}:{line} - {message}"
    ),
    )

    if FILE_LOGGING:
    logger.add(
    LOG_PATH,
    level = "DEBUG",
    format = (
    "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | "
    "{name}:{function}:{line} - {message}"
    ),
    )


    class TerminationType(StrEnum):
    """
    party that can terminate the contract
    """

    CLIENT = "client"
    VENDOR = "vendor"
    EITHER_PARTY = "either party"


    class AgreementRaw(BaseModel):
    """
    agreement data
    """

    vendor : str = Field(description="vendor name")
    auto_renew: bool = Field(
    default = True, description = "auto-renewal status", alias = "autoRenew"
    )
    renewal_period: Optional[str] = Field(
    default = None, description = "renewal period", alias = "renewalPeriod"
    )
    renewal_notice_days: Optional[int] = Field(
    default = None,
    description = "renewal notice days",
    alias = "renewalNoticeDays",
    )
    termination_type: Optional[TerminationType] = Field(
    default = None, description = "termination type", alias = "terminationType"
    )
    termination_notice_days: Optional[int] = Field(
    default = None,
    description = "termination notice days",
    alias = "terminationNoticeDays",
    )

    model_config = {"populate_by_name": True}

    @field_validator("renewal_notice_days")
    @classmethod
    def check_renewal_notice_days(cls, v: Optional[int]) -> Optional[int]:
    if v is None:
    return v
    if v <= 0:
    logger.error(f"Invalid renewal_notice_days: {v}. Must be greater than 0.")
    raise ValueError("renewal notice days must be greater than 0")
    return v

    @field_validator("termination_notice_days")
    @classmethod
    def check_termination_notice_days(cls, v: Optional[int]) -> Optional[int]:
    if v is None:
    return v
    if v <= 0:
    logger.error(
    f"Invalid termination_notice_days: {v}. " f"Must be greater than 0."
    )
    raise ValueError("termination notice period must be greater than 0")
    return v


    class AgreementNormalized(BaseModel):
    """
    normalized agreement data
    """

    vendor : str = Field(description="vendor name")
    renewal_terms : str = Field(description="renewal terms")
    termination_terms: str = Field(description="termination terms")


    class Agreements(BaseModel):
    """
    list of agreements
    """

    agreements: List[AgreementRaw]


    class DataNormalizer:
    """
    main class for data normalization
    """

    @staticmethod
    def compileRenewalTerms(
    auto_renew : Optional[bool],
    renewal_period : Optional[str],
    notice_period_days: Optional[int],
    ) -> str:
    """
    Compiles the renewal terms components (or placeholders) into the final
    renewal terms data string. Handles cases for missing renewal period,
    notice period, or their units, and auto-renewal status.
    """
    logger.debug(
    f"compileRenewalTerms: auto_renew={auto_renew}, "
    f"renewal_period='{renewal_period}', "
    f"notice_period_days={notice_period_days}"
    )
    if auto_renew is False:
    logger.debug("compileRenewalTerms: No auto-renewal.")
    return "No auto-renewal"

    parts = []

    # Renewal part
    if not renewal_period:
    parts.append("Renewal period unknown")
    else:
    parts.append(f"Renews every {renewal_period}")

    # Notice part
    if not notice_period_days:
    parts.append("notice period unknown")
    else:
    parts.append(f"{notice_period_days} days notice")

    compiled_terms = ", ".join(parts)
    logger.debug(f"compileRenewalTerms: Compiled to '{compiled_terms}'")
    return compiled_terms

    @staticmethod
    def compileTerminationTerms(
    termination_party: Optional[TerminationType],
    termination_notice_period_days: Optional[int],
    ) -> str:
    """
    Compiles the termination terms components (or placeholders) into the
    final termination terms data string.
    """
    logger.debug(
    f"compileTerminationTerms: termination_party={termination_party}, "
    f"termination_notice_period_days={termination_notice_period_days}"
    )
    parts = []

    # Termination part
    if not termination_party:
    parts.append("termination party unknown")
    else:
    parts.append(f"May be terminated by {termination_party} with")

    if not termination_notice_period_days:
    parts.append("notice period unknown")
    else:
    parts.append(f"{termination_notice_period_days} days notice")

    compiled_terms = ", ".join(parts)
    logger.debug(f"compileTerminationTerms: Compiled to '{compiled_terms}'")
    return compiled_terms

    @staticmethod
    def normalize(agreement: dict[str, Any]) -> dict[str, str]:
    """normalize a single agreement"""
    vendor_name = agreement.get("vendor", "Unknown Vendor")
    logger.debug(f"normalize: Starting normalization for vendor: {vendor_name}")

    try:
    data = AgreementRaw(**agreement)
    logger.debug(
    f"normalize: Successfully validated raw data for {data.vendor}"
    )
    except Exception as e:
    logger.error(f"normalize: Validation error for {vendor_name}: {e}")
    raise

    normalized_model = AgreementNormalized(
    vendor = data.vendor,
    renewal_terms = DataNormalizer.compileRenewalTerms(
    data.auto_renew,
    data.renewal_period,
    data.renewal_notice_days,
    ),
    termination_terms=DataNormalizer.compileTerminationTerms(
    data.termination_type, data.termination_notice_days
    ),
    )
    result = normalized_model.model_dump()
    logger.info(
    f"normalize: Successfully normalized agreement for vendor: {data.vendor}"
    )
    logger.debug(f"normalize: Normalized data for {data.vendor}: {result}")
    return result

    @staticmethod
    def normalizeAll(agreements: list[dict[str, Any]]) -> list[dict[str, str]]:
    """main method to normalize the agreements sequentially"""
    logger.info(
    f"normalizeAll: Starting sequential normalization for "
    f"{len(agreements)} agreements."
    )
    normalized_agreements = []
    for agreement in tqdm(agreements, desc="Normalizing agreements (sequential)"):
    try:
    normalized_agreements.append(DataNormalizer.normalize(agreement))
    except Exception as e:
    vendor_name = agreement.get("vendor", "Unknown Vendor")
    logger.error(
    f"normalizeAll: Failed to normalize agreement for "
    f"{vendor_name}: {e}. Skipping."
    )
    logger.info(
    f"normalizeAll: Finished sequential normalization. "
    f"Processed {len(normalized_agreements)} out of "
    f"{len(agreements)} agreements."
    )
    return normalized_agreements

    @staticmethod
    def normalizeAllParallel(
    agreements: list[dict[str, Any]], max_workers: Optional[int] = None
    ) -> list[dict[str, str]]:
    """main method to normalize the agreements in parallel"""
    num_agreements = len(agreements)
    logger.info(
    f"normalizeAllParallel: Starting parallel normalization for "
    f"{num_agreements} agreements with "
    f"max_workers={max_workers if max_workers is not None else 'default'}."
    )

    results: List[dict[str, str]] = []
    if not agreements:
    logger.info("normalizeAllParallel: No agreements to process.")
    return results

    with ProcessPoolExecutor(max_workers=max_workers) as executor:
    try:
    logger.debug(
    f"normalizeAllParallel: Submitting {num_agreements} "
    f"tasks to ProcessPoolExecutor."
    )
    results = list(
    tqdm(
    executor.map(DataNormalizer.normalize, agreements),
    total=num_agreements,
    desc="Normalizing agreements (parallel)",
    )
    )
    logger.debug(
    "normalizeAllParallel: All parallel tasks completed processing."
    )
    except Exception as e:
    logger.error(
    f"normalizeAllParallel: Halting due to an error in a worker "
    f"process: {e}. No agreements will be returned from this "
    f"parallel batch."
    )
    results = []

    processed_count = len(results)
    if not results and num_agreements > 0:
    logger.warning(
    f"normalizeAllParallel: Finished. 0/{num_agreements} agreements "
    f"successfully processed and returned, likely due to an error "
    f"during parallel execution (see error logs)."
    )
    else:
    logger.info(
    f"normalizeAllParallel: Finished. Successfully processed "
    f"{processed_count}/{num_agreements} agreements."
    )
    return results


    if __name__ == "__main__":
    agreements_data = [
    {
    "vendor" : "ACME",
    "autoRenew" : False,
    "terminationType" : "either party",
    "terminationNoticeDays": 60,
    },
    {
    "vendor" : "Initech",
    "renewalPeriod" : "12 months",
    "renewalNoticeDays": 30,
    },
    {
    "vendor" : "Globex",
    "autoRenew" : True,
    "renewalNoticeDays" : 60,
    "terminationType" : "vendor",
    "terminationNoticeDays": 30,
    },
    ]
    logger.info("\n--- Running Sequential Normalization ---")
    normalized_agreements_seq = DataNormalizer.normalizeAll(agreements_data)
    logger.info("Normalized agreements (sequential):")
    pprint(normalized_agreements_seq)

    logger.info("\n--- Running Parallel Normalization ---")
    normalized_agreements_par = DataNormalizer.normalizeAllParallel(agreements_data)
    logger.info("Normalized agreements (parallel):")
    pprint(normalized_agreements_par)
    60 changes: 60 additions & 0 deletions pyproject.toml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,60 @@
    [project]
    name = "infinity-loop"
    version = "0.1.0"
    description = "Add your description here"
    readme = "README.md"
    requires-python = ">=3.13"
    dependencies = [
    "autoflake>=2.3.1",
    "autopep8>=2.3.2",
    "black>=25.1.0",
    "hypothesis>=6.131.31",
    "isort>=6.0.1",
    "loguru>=0.7.3",
    "mypy>=1.16.0",
    "pydantic>=2.11.5",
    "pylint>=3.3.7",
    "pytest>=8.3.5",
    "pytest-cov>=6.1.1",
    "pytest-emoji>=0.2.0",
    "pytest-icdiff>=0.9",
    "pytest-instafail>=0.5.0",
    "pytest-mock>=3.14.1",
    "pytest-sugar>=1.0.0",
    "pytest-timeout>=2.4.0",
    "pytest-xdist>=3.7.0",
    "rich>=14.0.0",
    "ruff>=0.11.12",
    "tqdm>=4.67.1",
    ]

    [tool.pytest.ini_options]
    addopts = "-n auto --verbose --hypothesis-show-statistics --emoji --instafail"
    testpaths = ["tests.py"]
    console_output_style = "progress"
    junit_logging = "all"
    log_cli = true
    log_cli_date_format = "%Y-%m-%d %H:%M:%S"
    log_cli_format = "%(asctime)s - [%(levelname)s] - %(name)s - (%(filename)s).%(funcName)s(%(lineno)d) - %(message)s"
    log_cli_level = "DEBUG"
    log_file = "pytest-logs.txt"
    log_file_date_format = "%Y-%m-%d %H:%M:%S"
    log_file_format = "%(asctime)s - [%(levelname)s] - %(name)s - (%(filename)s).%(funcName)s(%(lineno)d) - %(message)s"
    log_file_level = "DEBUG"
    log_format = "%(asctime)s - [%(levelname)s] - %(name)s - (%(filename)s).%(funcName)s(%(lineno)d) - %(message)s"
    log_level = "DEBUG"
    required_plugins = ["pytest-sugar", "pytest-emoji", "pytest-icdiff", "pytest-instafail", "pytest-timeout"]
    timeout = 500

    [tool.coverage.run]
    data_file = ".coverage"

    [tool.isort]
    profile = "black"
    src_paths = ["./"]

    [tool.autoflake]
    remove-all-unused-imports = true
    remove-unused-variables = true
    in-place = true
    ignore-init-module-imports = true
    21 changes: 21 additions & 0 deletions requirements.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@
    autoflake>=2.3.1
    autopep8>=2.3.2
    black>=25.1.0
    hypothesis>=6.131.31
    isort>=6.0.1
    loguru>=0.7.3
    mypy>=1.16.0
    pydantic>=2.11.5
    pylint>=3.3.7
    pytest>=8.3.5
    pytest-cov>=6.1.1
    pytest-emoji>=0.2.0
    pytest-icdiff>=0.9
    pytest-instafail>=0.5.0
    pytest-mock>=3.14.1
    pytest-sugar>=1.0.0
    pytest-timeout>=2.4.0
    pytest-xdist>=3.7.0
    rich>=14.0.0
    ruff>=0.11.12
    tqdm>=4.67.1
    345 changes: 345 additions & 0 deletions tests.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,345 @@
    """
    Test suite for the Infinity Loop Contract Normalization Challenge.
    This module provides comprehensive testing for the contract metadata
    normalization functionality, including edge cases and error scenarios.
    """

    from typing import Any, Dict, List, Optional

    import pytest
    from hypothesis import HealthCheck, assume, given, settings
    from hypothesis import strategies as st
    from loguru import logger

    from main import AgreementNormalized, AgreementRaw, DataNormalizer, TerminationType


    @pytest.fixture
    def raw_contract_data() -> List[Dict[str, Any]]:
    """Provides the raw contract data as specified in the challenge prompt."""
    return [
    {
    "vendor": "ACME",
    "autoRenew": False,
    "terminationType": "either party",
    "terminationNoticeDays": 60,
    },
    {
    "vendor": "Initech",
    "renewalPeriod": "12 months",
    "renewalNoticeDays": 30,
    },
    {
    "vendor": "Globex",
    "autoRenew": True,
    "renewalNoticeDays": 60,
    "terminationType": "vendor",
    "terminationNoticeDays": 30,
    },
    ]


    # Hypothesis strategies
    st_duration_str = st.one_of(
    st.none(),
    st.just("12 months"),
    st.just("1 year"),
    st.just("30 days"),
    st.just("6 weeks"),
    st.builds(
    lambda val, unit: f"{val} {unit}",
    st.integers(min_value=1, max_value=100),
    st.sampled_from(
    ["day", "days", "week", "weeks", "month", "months", "year", "years"]
    ),
    ),
    )
    st_optional_int_gt_0 = st.one_of(st.none(), st.integers(min_value=1, max_value=1000))


    @given(
    auto_renew=st.one_of(st.none(), st.booleans()),
    renewal_period_str=st_duration_str,
    notice_period_days=st_optional_int_gt_0,
    )
    def test_compile_renewal_terms_hypothesis(
    auto_renew: Optional[bool],
    renewal_period_str: Optional[str],
    notice_period_days: Optional[int],
    ):
    """Tests `compileRenewalTerms` with Hypothesis-generated inputs."""

    result = DataNormalizer.compileRenewalTerms(
    auto_renew, renewal_period_str, notice_period_days
    )

    if auto_renew is False:
    assert result == "No auto-renewal"
    else:
    expected_parts = []
    if not renewal_period_str:
    expected_parts.append("Renewal period unknown")
    else:
    expected_parts.append(f"Renews every {renewal_period_str}")

    if not notice_period_days:
    expected_parts.append("notice period unknown")
    else:
    expected_parts.append(f"{notice_period_days} days notice")
    assert result == ", ".join(expected_parts)


    @given(
    termination_party=st.one_of(st.none(), st.sampled_from(TerminationType)),
    termination_notice_days=st_optional_int_gt_0,
    )
    def test_compile_termination_terms_hypothesis(
    termination_party: Optional[TerminationType],
    termination_notice_days: Optional[int],
    ):
    """Tests `compileTerminationTerms` with Hypothesis-generated inputs."""
    result = DataNormalizer.compileTerminationTerms(
    termination_party, termination_notice_days
    )

    parts = []
    if not termination_party:
    parts.append("termination party unknown")
    else:
    parts.append(f"May be terminated by {termination_party.value} with")

    if not termination_notice_days:
    parts.append("notice period unknown")
    else:
    parts.append(f"{termination_notice_days} days notice")
    assert result == ", ".join(parts)


    st_agreement_raw_dict: st.SearchStrategy[Dict[str, Any]] = st.builds(
    dict,
    vendor=st.text(min_size=1, max_size=50),
    autoRenew=st.booleans(),
    renewalPeriod=st_duration_str,
    renewalNoticeDays=st_optional_int_gt_0,
    terminationType=st.one_of(
    st.none(), st.sampled_from(TerminationType).map(lambda x: x.value)
    ),
    terminationNoticeDays=st_optional_int_gt_0,
    ).filter(
    lambda x: (x.get("autoRenew") is False)
    or (x.get("renewalPeriod") is not None or x.get("renewalNoticeDays") is not None)
    )


    @given(raw_agreement_dict=st_agreement_raw_dict)
    def test_normalize_hypothesis(raw_agreement_dict: Dict[str, Any]):
    """Tests `normalize` method with Hypothesis-generated raw agreement data."""
    # Validate raw_agreement_dict with Pydantic model to catch invalid inputs early
    # if Hypothesis generates something that AgreementRaw would reject.
    try:
    agreement_raw = AgreementRaw(**raw_agreement_dict)
    except ValueError:
    # Tell Hypothesis to skip this example if it's invalid raw data
    assume(False)
    return

    normalized_data = DataNormalizer.normalize(raw_agreement_dict)

    assert isinstance(normalized_data, dict)
    assert "vendor" in normalized_data
    assert "renewal_terms" in normalized_data
    assert "termination_terms" in normalized_data
    assert normalized_data["vendor"] == agreement_raw.vendor # Check aliasing

    # Check renewal terms logic
    expected_renewal_period_str = agreement_raw.renewal_period
    expected_renewal_terms = DataNormalizer.compileRenewalTerms(
    agreement_raw.auto_renew,
    expected_renewal_period_str,
    agreement_raw.renewal_notice_days,
    )
    assert normalized_data["renewal_terms"] == expected_renewal_terms

    # Check termination terms logic
    expected_termination_terms = DataNormalizer.compileTerminationTerms(
    agreement_raw.termination_type,
    agreement_raw.termination_notice_days,
    )
    assert normalized_data["termination_terms"] == expected_termination_terms

    # Validate output with Pydantic model
    AgreementNormalized(**normalized_data)


    def test_normalize_all_with_fixture(raw_contract_data: List[Dict[str, Any]]):
    """Tests `normalizeAll` with the fixture data."""
    logger.info("Testing normalizeAll with fixture data")
    normalized_list = DataNormalizer.normalizeAll(raw_contract_data)
    assert isinstance(normalized_list, list)
    assert len(normalized_list) == len(raw_contract_data)
    for i, normalized_item in enumerate(normalized_list):
    logger.debug(f"Item {i} (normalizeAll): {normalized_item}")
    assert isinstance(normalized_item, dict)
    AgreementNormalized(**normalized_item) # Validate structure


    def test_normalize_all_parallel_with_fixture(raw_contract_data: List[Dict[str, Any]]):
    """Tests `normalizeAllParallel` with the fixture data."""
    logger.info("Testing normalizeAllParallel with fixture data")
    normalized_list = DataNormalizer.normalizeAllParallel(
    raw_contract_data, max_workers=2
    )
    assert isinstance(normalized_list, list)
    # Results should be sorted by input order because executor.map preserves it
    assert len(normalized_list) == len(raw_contract_data)
    for i, normalized_item in enumerate(normalized_list):
    logger.debug(f"Item {i} (normalizeAllParallel): {normalized_item}")
    assert isinstance(normalized_item, dict)
    AgreementNormalized(**normalized_item)


    def test_normalize_all_parallel_empty_list():
    """Tests `normalizeAllParallel` with an empty list."""
    logger.info("Testing normalizeAllParallel with empty list")
    normalized_list = DataNormalizer.normalizeAllParallel([], max_workers=2)
    assert isinstance(normalized_list, list)
    assert len(normalized_list) == 0


    def test_normalize_all_parallel_with_errors(raw_contract_data: List[Dict[str, Any]]):
    """Tests `normalizeAllParallel` when some data causes errors."""
    logger.info("Testing normalizeAllParallel with data causing errors")
    # Add an invalid item that will cause AgreementRaw validation to fail
    error_data = raw_contract_data + [
    {"vendor": "ErrorVendor", "renewalNoticeDays": -1}
    ]

    # The current implementation of normalizeAllParallel will catch the first error
    # from a worker and return an empty list.
    normalized_list = DataNormalizer.normalizeAllParallel(error_data, max_workers=2)
    assert isinstance(normalized_list, list)
    # Depending on the desired behavior for errors in parallel processing:
    # Option 1: All or nothing (current behavior with executor.map and direct list conversion)
    # If any worker fails, the whole operation effectively fails, and an empty list is returned.
    assert (
    len(normalized_list) == 0
    ), "Expected empty list when an error occurs in parallel processing"

    # Option 2: Partial results (would require different implementation, e.g., submit and as_completed)
    # If partial results were expected, this assertion would be different.
    # For now, we test the current implemented behavior.


    # Example test cases for specific scenarios
    def test_normalize_specific_edge_cases():
    """Tests specific edge cases for the normalize method."""
    # Case 1: No auto-renewal
    case1_raw = {"vendor": "TestVendor1", "autoRenew": False}
    case1_norm = DataNormalizer.normalize(case1_raw)
    assert case1_norm["renewal_terms"] == "No auto-renewal"
    assert (
    "termination party unknown, notice period unknown"
    in case1_norm["termination_terms"]
    )

    # Case 2: Auto-renewal, but renewal period and notice days are missing
    case2_raw = {"vendor": "TestVendor2", "autoRenew": True}
    case2_norm = DataNormalizer.normalize(case2_raw)
    assert (
    "Renewal period unknown, notice period unknown" in case2_norm["renewal_terms"]
    )

    # Case 3: Renewal period present, notice days missing
    case3_raw = {
    "vendor": "TestVendor3",
    "autoRenew": True,
    "renewalPeriod": "30 days",
    }
    case3_norm = DataNormalizer.normalize(case3_raw)
    assert "Renews every 30 days, notice period unknown" in case3_norm["renewal_terms"]

    # Case 4: Renewal notice days present, period missing
    case4_raw = {
    "vendor": "TestVendor4",
    "autoRenew": True,
    "renewalNoticeDays": 15,
    }
    case4_norm = DataNormalizer.normalize(case4_raw)
    assert "Renewal period unknown, 15 days notice" in case4_norm["renewal_terms"]

    # Case 5: Termination type present, notice days missing
    case5_raw = {"vendor": "TestVendor5", "terminationType": "client"}
    case5_norm = DataNormalizer.normalize(case5_raw)
    assert (
    "May be terminated by client with, notice period unknown"
    in case5_norm["termination_terms"]
    )

    # Case 6: Termination notice days present, type missing
    case6_raw = {"vendor": "TestVendor6", "terminationNoticeDays": 45}
    case6_norm = DataNormalizer.normalize(case6_raw)
    assert (
    "termination party unknown, 45 days notice" in case6_norm["termination_terms"]
    )

    # Case 7: Invalid renewal period string
    case7_raw = {
    "vendor": "TestVendor7",
    "autoRenew": True,
    "renewalPeriod": "abc xyz",
    }
    case7_norm = DataNormalizer.normalize(case7_raw)
    assert "Renews every abc xyz, notice period unknown" in case7_norm["renewal_terms"]

    # Case 8: Zero renewal notice days (should be caught by validator, but test normalization path)
    # AgreementRaw validator will raise ValueError, so this path in normalize might not be hit
    # unless validators are bypassed or data comes from an untrusted source directly.
    # For this test, we assume it might pass validation or we're testing the compile function's robustness.
    # If AgreementRaw raises error, this test needs adjustment or separate test for compileRenewalTerms.
    # For now, we construct a dict that would create an AgreementRaw instance.
    # This case is primarily to ensure compileRenewalTerms handles it if it receives such values.
    raw_case8 = {
    "vendor": "TestVendor8",
    "autoRenew": True,
    "renewalPeriod": "1 year",
    "renewalNoticeDays": 0,
    } # Invalid
    # We expect DataNormalizer.normalize to pass data through Pydantic's AgreementRaw
    # which has validators. If renewalNoticeDays is 0, it should raise a ValueError.
    with pytest.raises(ValueError, match="renewal notice days must be greater than 0"):
    DataNormalizer.normalize(raw_case8)

    # Test scenario where renewalNoticeDays is None explicitly.
    raw_case8_none = {
    "vendor": "TestVendor8",
    "autoRenew": True,
    "renewalPeriod": "1 year",
    "renewalNoticeDays": None,
    }
    norm_case8_none = DataNormalizer.normalize(raw_case8_none)
    assert (
    norm_case8_none["renewal_terms"] == "Renews every 1 year, notice period unknown"
    )

    # Case 9: Zero termination notice days (similar to case 8)
    raw_case9 = {
    "vendor": "TestVendor9",
    "terminationType": "vendor",
    "terminationNoticeDays": 0,
    } # Invalid
    with pytest.raises(
    ValueError, match="termination notice period must be greater than 0"
    ):
    DataNormalizer.normalize(raw_case9)

    raw_case9_none = {
    "vendor": "TestVendor9",
    "terminationType": "vendor",
    "terminationNoticeDays": None,
    }
    norm_case9_none = DataNormalizer.normalize(raw_case9_none)
    assert (
    norm_case9_none["termination_terms"]
    == "May be terminated by vendor with, notice period unknown"
    )
    666 changes: 666 additions & 0 deletions uv.lock
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,666 @@
    version = 1
    revision = 2
    requires-python = ">=3.13"

    [[package]]
    name = "annotated-types"
    version = "0.7.0"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
    ]

    [[package]]
    name = "astroid"
    version = "3.3.10"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/00/c2/9b2de9ed027f9fe5734a6c0c0a601289d796b3caaf1e372e23fa88a73047/astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce", size = 398941, upload-time = "2025-05-10T13:33:10.405Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/15/58/5260205b9968c20b6457ed82f48f9e3d6edf2f1f95103161798b73aeccf0/astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb", size = 275388, upload-time = "2025-05-10T13:33:08.391Z" },
    ]

    [[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" }
    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" },
    ]

    [[package]]
    name = "autoflake"
    version = "2.3.1"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "pyflakes" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/2a/cb/486f912d6171bc5748c311a2984a301f4e2d054833a1da78485866c71522/autoflake-2.3.1.tar.gz", hash = "sha256:c98b75dc5b0a86459c4f01a1d32ac7eb4338ec4317a4469515ff1e687ecd909e", size = 27642, upload-time = "2024-03-13T03:41:28.977Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/a2/ee/3fd29bf416eb4f1c5579cf12bf393ae954099258abd7bde03c4f9716ef6b/autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840", size = 32483, upload-time = "2024-03-13T03:41:26.969Z" },
    ]

    [[package]]
    name = "autopep8"
    version = "2.3.2"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "pycodestyle" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/50/d8/30873d2b7b57dee9263e53d142da044c4600a46f2d28374b3e38b023df16/autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758", size = 92210, upload-time = "2025-01-14T14:46:18.454Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128", size = 45807, upload-time = "2025-01-14T14:46:15.466Z" },
    ]

    [[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/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" },
    { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" },
    { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" },
    { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" },
    { 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 = "click"
    version = "8.2.1"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "colorama", marker = "sys_platform == 'win32'" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
    ]

    [[package]]
    name = "colorama"
    version = "0.4.6"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
    ]

    [[package]]
    name = "coverage"
    version = "7.8.2"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759, upload-time = "2025-05-23T11:39:57.856Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898, upload-time = "2025-05-23T11:38:49.066Z" },
    { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171, upload-time = "2025-05-23T11:38:51.207Z" },
    { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564, upload-time = "2025-05-23T11:38:52.857Z" },
    { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719, upload-time = "2025-05-23T11:38:54.529Z" },
    { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634, upload-time = "2025-05-23T11:38:57.326Z" },
    { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824, upload-time = "2025-05-23T11:38:59.421Z" },
    { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872, upload-time = "2025-05-23T11:39:01.049Z" },
    { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179, upload-time = "2025-05-23T11:39:02.709Z" },
    { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393, upload-time = "2025-05-23T11:39:05.457Z" },
    { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194, upload-time = "2025-05-23T11:39:07.171Z" },
    { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580, upload-time = "2025-05-23T11:39:08.862Z" },
    { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734, upload-time = "2025-05-23T11:39:11.109Z" },
    { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959, upload-time = "2025-05-23T11:39:12.751Z" },
    { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024, upload-time = "2025-05-23T11:39:15.569Z" },
    { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867, upload-time = "2025-05-23T11:39:17.64Z" },
    { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096, upload-time = "2025-05-23T11:39:19.328Z" },
    { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276, upload-time = "2025-05-23T11:39:21.077Z" },
    { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478, upload-time = "2025-05-23T11:39:22.838Z" },
    { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255, upload-time = "2025-05-23T11:39:24.644Z" },
    { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109, upload-time = "2025-05-23T11:39:26.722Z" },
    { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268, upload-time = "2025-05-23T11:39:28.429Z" },
    { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071, upload-time = "2025-05-23T11:39:30.55Z" },
    { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623, upload-time = "2025-05-23T11:39:53.846Z" },
    ]

    [[package]]
    name = "dill"
    version = "0.4.0"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" },
    ]

    [[package]]
    name = "execnet"
    version = "2.1.1"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload-time = "2024-04-08T09:04:19.245Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload-time = "2024-04-08T09:04:17.414Z" },
    ]

    [[package]]
    name = "hypothesis"
    version = "6.131.31"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "attrs" },
    { name = "sortedcontainers" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/a9/2c/314f3d7b4089632c4bcf7059aa94b8a63e725b6963b3239f588ce157e9af/hypothesis-6.131.31.tar.gz", hash = "sha256:ab2fc07295b0e16d7cf7359edb4abb7c2436a0c324f1a371a64c755545a19d4f", size = 443699, upload-time = "2025-05-30T06:01:59.308Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/be/59/89a698dac595539722a00d7773ad35ebcce38cf8945a41cb30f5e083c31d/hypothesis-6.131.31-py3-none-any.whl", hash = "sha256:e7516a09deb22939b96857802ba9c4de1aa0a9e00ab7314693170c952a8c3218", size = 508318, upload-time = "2025-05-30T06:01:54.576Z" },
    ]

    [[package]]
    name = "icdiff"
    version = "2.0.7"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/fa/e4/43341832be5f2bcae71eb3ef08a07aaef9b74f74fe0b3675f62bd12057fe/icdiff-2.0.7.tar.gz", hash = "sha256:f79a318891adbf59a45e3a7694f5e1f18c5407065264637072ac8363b759866f", size = 16394, upload-time = "2023-08-21T15:00:55.742Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/7c/2a/b3178baa75a3ec75a33588252296c82a1332d2b83cd01061539b74bde9dd/icdiff-2.0.7-py3-none-any.whl", hash = "sha256:f05d1b3623223dd1c70f7848da7d699de3d9a2550b902a8234d9026292fb5762", size = 17018, upload-time = "2023-08-21T15:00:54.634Z" },
    ]

    [[package]]
    name = "infinity-loop"
    version = "0.1.0"
    source = { virtual = "." }
    dependencies = [
    { name = "autoflake" },
    { name = "autopep8" },
    { name = "black" },
    { name = "hypothesis" },
    { name = "isort" },
    { name = "loguru" },
    { name = "mypy" },
    { name = "pydantic" },
    { name = "pylint" },
    { name = "pytest" },
    { name = "pytest-cov" },
    { name = "pytest-emoji" },
    { name = "pytest-icdiff" },
    { name = "pytest-instafail" },
    { name = "pytest-mock" },
    { name = "pytest-sugar" },
    { name = "pytest-timeout" },
    { name = "pytest-xdist" },
    { name = "rich" },
    { name = "ruff" },
    { name = "tqdm" },
    ]

    [package.metadata]
    requires-dist = [
    { name = "autoflake", specifier = ">=2.3.1" },
    { name = "autopep8", specifier = ">=2.3.2" },
    { name = "black", specifier = ">=25.1.0" },
    { name = "hypothesis", specifier = ">=6.131.31" },
    { name = "isort", specifier = ">=6.0.1" },
    { name = "loguru", specifier = ">=0.7.3" },
    { name = "mypy", specifier = ">=1.16.0" },
    { name = "pydantic", specifier = ">=2.11.5" },
    { name = "pylint", specifier = ">=3.3.7" },
    { name = "pytest", specifier = ">=8.3.5" },
    { name = "pytest-cov", specifier = ">=6.1.1" },
    { name = "pytest-emoji", specifier = ">=0.2.0" },
    { name = "pytest-icdiff", specifier = ">=0.9" },
    { name = "pytest-instafail", specifier = ">=0.5.0" },
    { name = "pytest-mock", specifier = ">=3.14.1" },
    { name = "pytest-sugar", specifier = ">=1.0.0" },
    { name = "pytest-timeout", specifier = ">=2.4.0" },
    { name = "pytest-xdist", specifier = ">=3.7.0" },
    { name = "rich", specifier = ">=14.0.0" },
    { name = "ruff", specifier = ">=0.11.12" },
    { name = "tqdm", specifier = ">=4.67.1" },
    ]

    [[package]]
    name = "iniconfig"
    version = "2.1.0"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
    ]

    [[package]]
    name = "isort"
    version = "6.0.1"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" },
    ]

    [[package]]
    name = "loguru"
    version = "0.7.3"
    source = { registry = "https://pypi.org/simple" }
    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" }
    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" },
    ]

    [[package]]
    name = "markdown-it-py"
    version = "3.0.0"
    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" }
    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" },
    ]

    [[package]]
    name = "mccabe"
    version = "0.7.0"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
    ]

    [[package]]
    name = "mdurl"
    version = "0.1.2"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
    ]

    [[package]]
    name = "mypy"
    version = "1.16.0"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "mypy-extensions" },
    { name = "pathspec" },
    { name = "typing-extensions" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" },
    { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" },
    { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" },
    { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" },
    { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" },
    { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" },
    { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" },
    ]

    [[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 = "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" }
    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" },
    ]

    [[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 = "platformdirs"
    version = "4.3.8"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
    ]

    [[package]]
    name = "pluggy"
    version = "1.6.0"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
    ]

    [[package]]
    name = "pprintpp"
    version = "0.4.0"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/06/1a/7737e7a0774da3c3824d654993cf57adc915cb04660212f03406334d8c0b/pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403", size = 17995, upload-time = "2018-07-01T01:42:34.87Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/4e/d1/e4ed95fdd3ef13b78630280d9e9e240aeb65cc7c544ec57106149c3942fb/pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d", size = 16952, upload-time = "2018-07-01T01:42:36.496Z" },
    ]

    [[package]]
    name = "pycodestyle"
    version = "2.13.0"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/04/6e/1f4a62078e4d95d82367f24e685aef3a672abfd27d1a868068fed4ed2254/pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae", size = 39312, upload-time = "2025-03-29T17:33:30.669Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/07/be/b00116df1bfb3e0bb5b45e29d604799f7b91dd861637e4d448b4e09e6a3e/pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9", size = 31424, upload-time = "2025-03-29T17:33:29.405Z" },
    ]

    [[package]]
    name = "pydantic"
    version = "2.11.5"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "annotated-types" },
    { name = "pydantic-core" },
    { name = "typing-extensions" },
    { name = "typing-inspection" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" },
    ]

    [[package]]
    name = "pydantic-core"
    version = "2.33.2"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "typing-extensions" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
    { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
    { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
    { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
    { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
    { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
    { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
    { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
    { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
    { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
    { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
    { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
    { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
    { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
    { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
    { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
    { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
    ]

    [[package]]
    name = "pyflakes"
    version = "3.3.2"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/af/cc/1df338bd7ed1fa7c317081dcf29bf2f01266603b301e6858856d346a12b3/pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b", size = 64175, upload-time = "2025-03-31T13:21:20.34Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/15/40/b293a4fa769f3b02ab9e387c707c4cbdc34f073f945de0386107d4e669e6/pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a", size = 63164, upload-time = "2025-03-31T13:21:18.503Z" },
    ]

    [[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" }
    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" },
    ]

    [[package]]
    name = "pylint"
    version = "3.3.7"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "astroid" },
    { name = "colorama", marker = "sys_platform == 'win32'" },
    { name = "dill" },
    { name = "isort" },
    { name = "mccabe" },
    { name = "platformdirs" },
    { name = "tomlkit" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/1c/e4/83e487d3ddd64ab27749b66137b26dc0c5b5c161be680e6beffdc99070b3/pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559", size = 1520709, upload-time = "2025-05-04T17:07:51.089Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/e8/83/bff755d09e31b5d25cc7fdc4bf3915d1a404e181f1abf0359af376845c24/pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d", size = 522565, upload-time = "2025-05-04T17:07:48.714Z" },
    ]

    [[package]]
    name = "pytest"
    version = "8.3.5"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "colorama", marker = "sys_platform == 'win32'" },
    { name = "iniconfig" },
    { name = "packaging" },
    { name = "pluggy" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
    ]

    [[package]]
    name = "pytest-cov"
    version = "6.1.1"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "coverage" },
    { name = "pytest" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" },
    ]

    [[package]]
    name = "pytest-emoji"
    version = "0.2.0"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "pytest" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/88/4d/d489f939f0717a034cea7955d36bc2a7a5ba1b263871e63ad8cb16d47555/pytest-emoji-0.2.0.tar.gz", hash = "sha256:e1bd4790d87649c2d09c272c88bdfc4d37c1cc7c7a46583087d7c510944571e8", size = 6171, upload-time = "2019-02-19T09:33:17.454Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/f7/51/80af966c0aded877da7577d21c4601ca98c6f603c6e6073ddea071af01ec/pytest_emoji-0.2.0-py3-none-any.whl", hash = "sha256:6e34ed21970fa4b80a56ad11417456bd873eb066c02315fe9df0fafe6d4d4436", size = 5664, upload-time = "2019-02-19T09:33:15.771Z" },
    ]

    [[package]]
    name = "pytest-icdiff"
    version = "0.9"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "icdiff" },
    { name = "pprintpp" },
    { name = "pytest" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/5a/0c/66e1e2590e98f4428e374a3b6448dc086a908d15b1e24b914539d13b7ac4/pytest-icdiff-0.9.tar.gz", hash = "sha256:13aede616202e57fcc882568b64589002ef85438046f012ac30a8d959dac8b75", size = 7110, upload-time = "2023-12-05T11:18:30.192Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/e2/e1/cafe1edf7a30be6fa1bbbf43f7af12b34682eadcf19eb6e9f7352062c422/pytest_icdiff-0.9-py3-none-any.whl", hash = "sha256:efee0da3bd1b24ef2d923751c5c547fbb8df0a46795553fba08ef57c3ca03d82", size = 4994, upload-time = "2023-12-05T11:18:28.572Z" },
    ]

    [[package]]
    name = "pytest-instafail"
    version = "0.5.0"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "pytest" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/86/bd/e0ba6c3cd20b9aa445f0af229f3a9582cce589f083537978a23e6f14e310/pytest-instafail-0.5.0.tar.gz", hash = "sha256:33a606f7e0c8e646dc3bfee0d5e3a4b7b78ef7c36168cfa1f3d93af7ca706c9e", size = 5849, upload-time = "2023-03-31T17:17:32.161Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/e8/c0/c32dc39fc172e684fdb3d30169843efb65c067be1e12689af4345731126e/pytest_instafail-0.5.0-py3-none-any.whl", hash = "sha256:6855414487e9e4bb76a118ce952c3c27d3866af15487506c4ded92eb72387819", size = 4176, upload-time = "2023-03-31T17:17:30.065Z" },
    ]

    [[package]]
    name = "pytest-mock"
    version = "3.14.1"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "pytest" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" },
    ]

    [[package]]
    name = "pytest-sugar"
    version = "1.0.0"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "packaging" },
    { name = "pytest" },
    { name = "termcolor" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/f5/ac/5754f5edd6d508bc6493bc37d74b928f102a5fff82d9a80347e180998f08/pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a", size = 14992, upload-time = "2024-02-01T18:30:36.735Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/92/fb/889f1b69da2f13691de09a111c16c4766a433382d44aa0ecf221deded44a/pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd", size = 10171, upload-time = "2024-02-01T18:30:29.395Z" },
    ]

    [[package]]
    name = "pytest-timeout"
    version = "2.4.0"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "pytest" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" },
    ]

    [[package]]
    name = "pytest-xdist"
    version = "3.7.0"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "execnet" },
    { name = "pytest" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/49/dc/865845cfe987b21658e871d16e0a24e871e00884c545f246dd8f6f69edda/pytest_xdist-3.7.0.tar.gz", hash = "sha256:f9248c99a7c15b7d2f90715df93610353a485827bc06eefb6566d23f6400f126", size = 87550, upload-time = "2025-05-26T21:18:20.251Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/0d/b2/0e802fde6f1c5b2f7ae7e9ad42b83fd4ecebac18a8a8c2f2f14e39dce6e1/pytest_xdist-3.7.0-py3-none-any.whl", hash = "sha256:7d3fbd255998265052435eb9daa4e99b62e6fb9cfb6efd1f858d4d8c0c7f0ca0", size = 46142, upload-time = "2025-05-26T21:18:18.759Z" },
    ]

    [[package]]
    name = "rich"
    version = "14.0.0"
    source = { registry = "https://pypi.org/simple" }
    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" }
    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 = "ruff"
    version = "0.11.12"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" },
    { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" },
    { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" },
    { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" },
    { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" },
    { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" },
    { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" },
    { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" },
    { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" },
    { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" },
    { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" },
    { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" },
    { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" },
    { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" },
    { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" },
    { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" },
    { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" },
    ]

    [[package]]
    name = "sortedcontainers"
    version = "2.4.0"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
    ]

    [[package]]
    name = "termcolor"
    version = "3.1.0"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" },
    ]

    [[package]]
    name = "tomlkit"
    version = "0.13.2"
    source = { registry = "https://pypi.org/simple" }
    sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885, upload-time = "2024-08-14T08:19:41.488Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955, upload-time = "2024-08-14T08:19:40.05Z" },
    ]

    [[package]]
    name = "tqdm"
    version = "4.67.1"
    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" }
    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 = "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" }
    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" },
    ]

    [[package]]
    name = "typing-inspection"
    version = "0.4.1"
    source = { registry = "https://pypi.org/simple" }
    dependencies = [
    { name = "typing-extensions" },
    ]
    sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
    wheels = [
    { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
    ]

    [[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" }
    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" },
    ]