From 85b2b3b910c30915ba2ff58e8ec3de4923807f3e Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 29 Jan 2026 11:59:47 +0000 Subject: [PATCH] Add integration tests --- .github/workflows/integration-tests.yml | 32 +++++++ README.md | 11 +-- pyproject.toml | 7 +- tests/integration/__init__.py | 0 tests/integration/conftest.py | 115 ++++++++++++++++++++++++ tests/integration/test_agent_map.py | 82 +++++++++++++++++ tests/integration/test_dedupe.py | 104 +++++++++++++++++++++ tests/integration/test_merge.py | 108 ++++++++++++++++++++++ tests/integration/test_rank.py | 111 +++++++++++++++++++++++ tests/integration/test_screen.py | 89 ++++++++++++++++++ tests/integration/test_single_agent.py | 83 +++++++++++++++++ uv.lock | 24 +++++ 12 files changed, 760 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/integration-tests.yml create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/conftest.py create mode 100644 tests/integration/test_agent_map.py create mode 100644 tests/integration/test_dedupe.py create mode 100644 tests/integration/test_merge.py create mode 100644 tests/integration/test_rank.py create mode 100644 tests/integration/test_screen.py create mode 100644 tests/integration/test_single_agent.py diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 00000000..0d408cfb --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,32 @@ +name: Integration Tests + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + integration-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Install dependencies + run: uv sync + + - name: Run integration tests + env: + EVERYROW_API_KEY: ${{ secrets.EVERYROW_API_KEY }} + run: | + uv run pip install pytest-xdist + uv run pytest -n 8 tests/integration/ -v -m integration --tb=short diff --git a/README.md b/README.md index 232fc9b6..c8d4282b 100644 --- a/README.md +++ b/README.md @@ -342,11 +342,12 @@ lefthook install ``` ```bash -uv run pytest # tests -uv run ruff check . # lint -uv run ruff format . # format -uv run basedpyright # type check -./generate_openapi.sh # regenerate client +uv run pytest # unit tests +uv run --env-file .env pytest -m integration # integration tests (requires EVERYROW_API_KEY) +uv run ruff check . # lint +uv run ruff format . # format +uv run basedpyright # type check +./generate_openapi.sh # regenerate client ``` --- diff --git a/pyproject.toml b/pyproject.toml index 5f0c0204..4175f716 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dev = [ "basedpyright>=1.22.0", "ruff>=0.9.9", "jsonschema>=4.0.0", + "pytest-xdist>=3.8.0", ] [tool.basedpyright] @@ -72,5 +73,9 @@ ban-relative-imports = "all" exclude = ["src/everyrow/generated/**"] [tool.pytest.ini_options] -addopts = ["--import-mode=importlib"] +addopts = ["--import-mode=importlib", "-m", "not integration"] pythonpath = ["src"] +markers = [ + "integration: marks tests as integration tests (require EVERYROW_API_KEY)", +] +asyncio_mode = "auto" diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 00000000..5b96d2cd --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,115 @@ +"""Shared fixtures and configuration for integration tests.""" + +import os + +import pandas as pd +import pytest +from pydantic import BaseModel, Field + + +@pytest.fixture(scope="session", autouse=True) +def require_api_key(): + """Fail integration tests if EVERYROW_API_KEY is not set.""" + if not os.environ.get("EVERYROW_API_KEY"): + pytest.fail("EVERYROW_API_KEY environment variable not set") + + +# ============================================================================ +# Common Test Data - Small datasets to minimize cost/time +# ============================================================================ + + +@pytest.fixture +def companies_df(): + """Small company dataset for screen/rank tests.""" + return pd.DataFrame( + [ + {"company": "Apple", "industry": "Technology", "website": "apple.com"}, + { + "company": "Microsoft", + "industry": "Technology", + "website": "microsoft.com", + }, + { + "company": "Coca-Cola", + "industry": "Beverages", + "website": "coca-cola.com", + }, + ] + ) + + +@pytest.fixture +def papers_df(): + """Academic papers dataset for dedupe tests - contains known duplicates.""" + return pd.DataFrame( + [ + { + "title": "Attention Is All You Need", + "authors": "Vaswani et al.", + "venue": "NeurIPS 2017", + "identifier": "10.5555/3295222.3295349", + }, + { + "title": "Attention Is All You Need", + "authors": "Vaswani, Shazeer, Parmar et al.", + "venue": "arXiv", + "identifier": "1706.03762", + }, + { + "title": "BERT: Pre-training of Deep Bidirectional Transformers", + "authors": "Devlin et al.", + "venue": "NAACL 2019", + "identifier": "10.18653/v1/N19-1423", + }, + ] + ) + + +@pytest.fixture +def trials_df(): + """Clinical trials dataset for merge tests.""" + return pd.DataFrame( + [ + {"trial_id": "NCT001", "sponsor": "Genentech", "indication": "Lung cancer"}, + {"trial_id": "NCT002", "sponsor": "MSD", "indication": "Melanoma"}, + {"trial_id": "NCT003", "sponsor": "BMS", "indication": "Leukemia"}, + ] + ) + + +@pytest.fixture +def pharma_df(): + """Pharma companies dataset for merge tests.""" + return pd.DataFrame( + [ + {"company": "Roche Holding AG", "hq_country": "Switzerland"}, + {"company": "Merck & Co.", "hq_country": "United States"}, + {"company": "Bristol-Myers Squibb", "hq_country": "United States"}, + ] + ) + + +# ============================================================================ +# Common Response Models +# ============================================================================ + + +class RiskAssessment(BaseModel): + """Response model for screen tests.""" + + passes: bool = Field(description="Whether the company passes risk assessment") + risk_level: str = Field(description="Risk level: Low, Medium, or High") + + +class RevenueScore(BaseModel): + """Response model for rank tests.""" + + revenue_score: float = Field(description="Estimated annual revenue in billions USD") + + +class CompanyFinancials(BaseModel): + """Detailed response model for agent_map tests.""" + + annual_revenue_usd: int = Field(description="Most recent annual revenue in USD") + employee_count: int = Field(description="Current number of employees") diff --git a/tests/integration/test_agent_map.py b/tests/integration/test_agent_map.py new file mode 100644 index 00000000..6aca70a7 --- /dev/null +++ b/tests/integration/test_agent_map.py @@ -0,0 +1,82 @@ +"""Integration tests for agent_map operation.""" + +import pandas as pd +import pytest +from pydantic import BaseModel, Field + +from everyrow.ops import agent_map +from everyrow.result import TableResult + +pytestmark = [pytest.mark.integration, pytest.mark.asyncio] + + +async def test_agent_map_returns_table_result(): + """Test that agent_map returns a TableResult.""" + input_df = pd.DataFrame( + [ + {"company": "Apple"}, + {"company": "Microsoft"}, + ] + ) + + result = await agent_map( + task="What year was this company founded?", + input=input_df, + ) + + assert isinstance(result, TableResult) + assert result.artifact_id is not None + assert len(result.data) == len(input_df) + assert "answer" in result.data.columns + + +async def test_agent_map_with_custom_response_model(): + """Test agent_map with a custom response model.""" + + class FoundedYear(BaseModel): + founded_year: int = Field(description="Year the company was founded") + + input_df = pd.DataFrame( + [ + {"company": "Apple"}, + {"company": "Microsoft"}, + ] + ) + + result = await agent_map( + task="When was this company founded?", + input=input_df, + response_model=FoundedYear, + ) + + assert isinstance(result, TableResult) + assert len(result.data) == 2 + assert "founded_year" in result.data.columns + # Apple founded in 1976 + apple_row = result.data[result.data["company"] == "Apple"] + assert apple_row["founded_year"].iloc[0] == 1976 # pyright: ignore[reportAttributeAccessIssue] + # Microsoft founded in 1975 + msft_row = result.data[result.data["company"] == "Microsoft"] + assert msft_row["founded_year"].iloc[0] == 1975 # pyright: ignore[reportAttributeAccessIssue] + + +async def test_agent_map_preserves_input_columns(): + """Test that agent_map joins results with input columns.""" + input_df = pd.DataFrame( + [ + {"company": "Tesla", "industry": "Automotive"}, + {"company": "SpaceX", "industry": "Aerospace"}, + ] + ) + + result = await agent_map( + task="What city is the headquarters of this company located in?", + input=input_df, + ) + + assert isinstance(result, TableResult) + # Should preserve original columns + assert "company" in result.data.columns + assert "industry" in result.data.columns + # Should add new answer column + assert "answer" in result.data.columns diff --git a/tests/integration/test_dedupe.py b/tests/integration/test_dedupe.py new file mode 100644 index 00000000..7e21c139 --- /dev/null +++ b/tests/integration/test_dedupe.py @@ -0,0 +1,104 @@ +"""Integration tests for dedupe operation.""" + +import pandas as pd +import pytest + +from everyrow.ops import dedupe +from everyrow.result import TableResult + +pytestmark = [pytest.mark.integration, pytest.mark.asyncio] + + +async def test_dedupe_returns_table_with_equivalence_fields(papers_df): + """Test that dedupe returns a TableResult with equivalence class fields.""" + result = await dedupe( + equivalence_relation=""" + Two entries are duplicates if they represent the same research paper. + ArXiv preprints and published conference versions of the same paper + are considered duplicates. + """, + input=papers_df, + ) + + assert isinstance(result, TableResult) + assert result.artifact_id is not None + assert "equivalence_class_id" in result.data.columns + assert "selected" in result.data.columns + + +async def test_dedupe_identifies_duplicates(papers_df): + """Test that dedupe correctly identifies duplicate papers.""" + result = await dedupe( + equivalence_relation=""" + Two entries are duplicates if they represent the same research paper. + ArXiv preprints and published conference versions are duplicates. + "Attention Is All You Need" appears twice - once as NeurIPS and once as arXiv. + """, + input=papers_df, + ) + + assert isinstance(result, TableResult) + + # The two "Attention Is All You Need" papers should have same equivalence_class_id + attention_papers = result.data[ + result.data["title"].str.contains("Attention", case=False) + ] + assert len(attention_papers) == 2 + assert attention_papers["equivalence_class_id"].nunique() == 1 # pyright: ignore[reportAttributeAccessIssue] + + # BERT should have a different equivalence class + bert_papers = result.data[result.data["title"].str.contains("BERT", case=False)] + attention_class = attention_papers["equivalence_class_id"].iloc[0] # pyright: ignore[reportAttributeAccessIssue] + bert_class = bert_papers["equivalence_class_id"].iloc[0] # pyright: ignore[reportAttributeAccessIssue] + assert attention_class != bert_class + + +async def test_dedupe_selects_one_per_class(): + """Test that dedupe marks exactly one entry as selected per equivalence class.""" + input_df = pd.DataFrame( + [ + {"title": "Paper A - Preprint", "version": "v1"}, + {"title": "Paper A - Published", "version": "v2"}, + {"title": "Paper B", "version": "v1"}, + ] + ) + + result = await dedupe( + equivalence_relation=""" + Two entries are duplicates if they are versions of the same paper. + "Paper A - Preprint" and "Paper A - Published" are the same paper. + """, + input=input_df, + ) + + assert isinstance(result, TableResult) + + # For each equivalence class, exactly one should be selected + for class_id in result.data["equivalence_class_id"].unique(): + class_rows = result.data[result.data["equivalence_class_id"] == class_id] + selected_count = class_rows["selected"].sum() + assert selected_count == 1, ( + f"Class {class_id} has {selected_count} selected entries, expected 1" + ) + + +async def test_dedupe_unique_items_all_selected(): + """Test that unique (non-duplicate) items each get their own class and are selected.""" + input_df = pd.DataFrame( + [ + {"item": "Apple"}, + {"item": "Banana"}, + {"item": "Cherry"}, + ] + ) + + result = await dedupe( + equivalence_relation="Items are duplicates only if they are the exact same fruit name.", + input=input_df, + ) + + assert isinstance(result, TableResult) + # Each should have its own equivalence class + assert result.data["equivalence_class_id"].nunique() == 3 + # All should be selected since they're unique + assert result.data["selected"].all() # pyright: ignore[reportGeneralTypeIssues] diff --git a/tests/integration/test_merge.py b/tests/integration/test_merge.py new file mode 100644 index 00000000..521fa170 --- /dev/null +++ b/tests/integration/test_merge.py @@ -0,0 +1,108 @@ +"""Integration tests for merge operation.""" + +import pandas as pd +import pytest + +from everyrow.ops import merge +from everyrow.result import TableResult + +pytestmark = [pytest.mark.integration, pytest.mark.asyncio] + + +async def test_merge_returns_joined_table(trials_df, pharma_df): + """Test that merge returns a TableResult with joined data.""" + result = await merge( + task=""" + Merge clinical trial sponsors with parent pharmaceutical companies. + Genentech is owned by Roche, MSD is Merck's name outside the US, + BMS is Bristol-Myers Squibb. + """, + left_table=trials_df, + right_table=pharma_df, + merge_on_left="sponsor", + merge_on_right="company", + ) + + assert isinstance(result, TableResult) + assert result.artifact_id is not None + # Should have columns from both tables + assert "trial_id" in result.data.columns + assert "sponsor" in result.data.columns + assert "hq_country" in result.data.columns + + +async def test_merge_subsidiary_to_parent(): + """Test merge matching subsidiaries to parent companies.""" + subsidiaries = pd.DataFrame( + [ + {"subsidiary": "Instagram", "employees": 5000}, + {"subsidiary": "YouTube", "employees": 10000}, + {"subsidiary": "LinkedIn", "employees": 20000}, + ] + ) + + parents = pd.DataFrame( + [ + {"parent_company": "Meta Platforms", "market_cap_billions": 1200}, + {"parent_company": "Alphabet Inc", "market_cap_billions": 1800}, + {"parent_company": "Microsoft Corporation", "market_cap_billions": 3000}, + ] + ) + + result = await merge( + task=""" + Match each subsidiary to its parent company. + Instagram is owned by Meta, YouTube by Alphabet (Google), + LinkedIn by Microsoft. + """, + left_table=subsidiaries, + right_table=parents, + merge_on_left="subsidiary", + merge_on_right="parent_company", + ) + + assert isinstance(result, TableResult) + assert len(result.data) == 3 + # Verify correct matches + instagram_row = result.data[result.data["subsidiary"] == "Instagram"] + assert "Meta" in instagram_row["parent_company"].iloc[0] # pyright: ignore[reportAttributeAccessIssue] + + youtube_row = result.data[result.data["subsidiary"] == "YouTube"] + assert "Alphabet" in youtube_row["parent_company"].iloc[0] # pyright: ignore[reportAttributeAccessIssue] + + linkedin_row = result.data[result.data["subsidiary"] == "LinkedIn"] + assert "Microsoft" in linkedin_row["parent_company"].iloc[0] # pyright: ignore[reportAttributeAccessIssue] + + +async def test_merge_fuzzy_matches_abbreviations(): + """Test that merge correctly matches abbreviated names.""" + employees = pd.DataFrame( + [ + {"name": "John Smith", "dept": "Engineering"}, + {"name": "Jane Doe", "dept": "Marketing"}, + ] + ) + + departments = pd.DataFrame( + [ + {"department": "Engineering Department", "budget": 1000000}, + {"department": "Marketing & Sales", "budget": 500000}, + ] + ) + + result = await merge( + task=""" + Match employees to their department budgets. + Engineering matches Engineering Department. + Marketing matches Marketing & Sales. + """, + left_table=employees, + right_table=departments, + merge_on_left="dept", + merge_on_right="department", + ) + + assert isinstance(result, TableResult) + # Both employees should be matched + assert len(result.data) == 2 + assert "budget" in result.data.columns diff --git a/tests/integration/test_rank.py b/tests/integration/test_rank.py new file mode 100644 index 00000000..2997a01b --- /dev/null +++ b/tests/integration/test_rank.py @@ -0,0 +1,111 @@ +"""Integration tests for rank operation.""" + +import pandas as pd +import pytest +from pydantic import BaseModel, Field + +from everyrow.ops import rank +from everyrow.result import TableResult + +pytestmark = [pytest.mark.integration, pytest.mark.asyncio] + + +async def test_rank_returns_sorted_table_ascending(): + """Test that rank returns a TableResult sorted ascending.""" + input_df = pd.DataFrame( + [ + {"country": "China"}, + {"country": "Vatican City"}, + {"country": "Monaco"}, + ] + ) + + result = await rank( + task="Research the population of each country.", + input=input_df, + field_name="population", + field_type="int", + ascending_order=True, + ) + + assert isinstance(result, TableResult) + assert result.artifact_id is not None + assert "population" in result.data.columns + # Results should be sorted ascending (smallest first) + populations = result.data["population"].tolist() + assert populations == sorted(populations) + # Vatican City should be first (smallest population) + assert result.data.iloc[0]["country"] == "Vatican City" + + +async def test_rank_descending_order(): + """Test rank with descending order.""" + input_df = pd.DataFrame( + [ + {"country": "Vatican City"}, + {"country": "India"}, + {"country": "Monaco"}, + ] + ) + + result = await rank( + task="Research the population of each country.", + input=input_df, + field_name="population", + field_type="int", + ascending_order=False, + ) + + assert isinstance(result, TableResult) + # Results should be sorted descending (largest first) + populations = result.data["population"].tolist() + assert populations == sorted(populations, reverse=True) + # India should be first (largest population) + assert result.data.iloc[0]["country"] == "India" + + +async def test_rank_with_custom_response_model(): + """Test rank with a custom response model.""" + + class CountryMetrics(BaseModel): + population_millions: float = Field(description="Population in millions") + continent: str = Field(description="The continent where the country is located") + + input_df = pd.DataFrame( + [ + {"country": "Japan"}, + {"country": "Brazil"}, + {"country": "Australia"}, + ] + ) + + result = await rank( + task="Research the population and continent of each country.", + input=input_df, + field_name="population_millions", + response_model=CountryMetrics, + ascending_order=False, + ) + + assert isinstance(result, TableResult) + assert "population_millions" in result.data.columns + assert "continent" in result.data.columns + # Brazil has largest population of these three + assert result.data.iloc[0]["country"] == "Brazil" + + +async def test_rank_validates_field_in_response_model(): + """Test that rank validates field_name exists in response_model.""" + + class WrongModel(BaseModel): + score: int = Field(description="Some score") + + input_df = pd.DataFrame([{"item": "A"}, {"item": "B"}]) + + with pytest.raises(ValueError, match="not in response model"): + await rank( + task="Rank items", + input=input_df, + field_name="population", + response_model=WrongModel, + ) diff --git a/tests/integration/test_screen.py b/tests/integration/test_screen.py new file mode 100644 index 00000000..af29962b --- /dev/null +++ b/tests/integration/test_screen.py @@ -0,0 +1,89 @@ +"""Integration tests for screen operation.""" + +import pandas as pd +import pytest +from pydantic import BaseModel, Field + +from everyrow.ops import screen +from everyrow.result import TableResult + +pytestmark = [pytest.mark.integration, pytest.mark.asyncio] + + +async def test_screen_returns_table_with_passes_field(): + """Test that screen returns a TableResult with passes boolean.""" + input_df = pd.DataFrame( + [ + {"item": "Water", "category": "Beverage"}, + {"item": "Apple juice", "category": "Beverage"}, + ] + ) + + result = await screen( + task="Screen items for safety as food/drink. Pass only items safe for human consumption.", + input=input_df, + ) + + assert isinstance(result, TableResult) + assert result.artifact_id is not None + assert "passes" in result.data.columns + # Both safe items should pass and be in the result + assert len(result.data) == 2 + assert result.data["passes"].all() # pyright: ignore[reportGeneralTypeIssues] + + +async def test_screen_filters_out_failing_items(): + """Test that screen filters out items that don't pass.""" + input_df = pd.DataFrame( + [ + {"item": "Water", "category": "Beverage"}, + {"item": "Arsenic", "category": "Chemical"}, + {"item": "Apple juice", "category": "Beverage"}, + ] + ) + + result = await screen( + task="Screen items for safety as food/drink. Pass only items safe for human consumption. Arsenic is toxic and must fail.", + input=input_df, + ) + + assert isinstance(result, TableResult) + # Arsenic should be filtered out (screen returns only passing rows) + items_in_result = result.data["item"].tolist() + assert "Arsenic" not in items_in_result + + # Safe items should be present + assert "Water" in items_in_result + assert "Apple juice" in items_in_result + + # All returned rows should have passes=True + assert result.data["passes"].all() # pyright: ignore[reportGeneralTypeIssues] + + +async def test_screen_with_custom_response_model(): + """Test screen with a custom response model adds fields to research.""" + + class SafetyAssessment(BaseModel): + passes: bool = Field(description="Whether item is safe") + safety_rating: str = Field( + description="Safety rating: Safe, Caution, or Dangerous" + ) + reason: str = Field(description="Brief reason for the rating") + + input_df = pd.DataFrame( + [ + {"item": "Milk", "category": "Dairy"}, + ] + ) + + result = await screen( + task="Assess safety for human consumption. Milk is safe.", + input=input_df, + response_model=SafetyAssessment, + ) + + assert isinstance(result, TableResult) + assert "passes" in result.data.columns + # Milk should pass and be in the result + assert len(result.data) == 1 + assert result.data["passes"].iloc[0] == True # noqa: E712 diff --git a/tests/integration/test_single_agent.py b/tests/integration/test_single_agent.py new file mode 100644 index 00000000..b207a881 --- /dev/null +++ b/tests/integration/test_single_agent.py @@ -0,0 +1,83 @@ +"""Integration tests for single_agent operation.""" + +import pandas as pd +import pytest +from pydantic import BaseModel, Field + +from everyrow.ops import single_agent +from everyrow.result import ScalarResult, TableResult + +pytestmark = [pytest.mark.integration, pytest.mark.asyncio] + + +async def test_single_agent_returns_scalar_result(): + """Test that single_agent returns a ScalarResult by default.""" + result = await single_agent( + task="What is the capital of France? Answer with just the city name.", + ) + + assert isinstance(result, ScalarResult) + assert result.artifact_id is not None + assert result.data is not None + assert hasattr(result.data, "answer") + assert isinstance(result.data.answer, str) + assert len(result.data.answer) > 0 + assert "paris" in result.data.answer.lower() + + +async def test_single_agent_with_custom_response_model(): + """Test single_agent with a custom response model.""" + + class CapitalResponse(BaseModel): + capital: str = Field(description="The capital city") + country: str = Field(description="The country name") + + result = await single_agent( + task="What is the capital of Germany?", + response_model=CapitalResponse, + ) + + assert isinstance(result, ScalarResult) + assert hasattr(result.data, "capital") + assert hasattr(result.data, "country") + assert "berlin" in result.data.capital.lower() + assert "germany" in result.data.country.lower() + + +async def test_single_agent_return_table(): + """Test single_agent with return_table=True returns TableResult.""" + + class Country(BaseModel): + name: str = Field(description="Country name") + capital: str = Field(description="Capital city") + + result = await single_agent( + task="List exactly 3 countries in Europe with their capitals: France, Germany, and Italy.", + response_model=Country, + return_table=True, + ) + + assert isinstance(result, TableResult) + assert result.artifact_id is not None + assert len(result.data) == 3 + assert "name" in result.data.columns + assert "capital" in result.data.columns + + +async def test_single_agent_with_table_input(): + """Test single_agent can analyze a DataFrame input.""" + companies = pd.DataFrame( + [ + {"company": "Apple", "revenue_billions": 400}, + {"company": "Microsoft", "revenue_billions": 200}, + {"company": "Google", "revenue_billions": 300}, + ] + ) + + result = await single_agent( # pyright: ignore[reportCallIssue] + task="Which company has the highest revenue? Answer with just the company name.", + input=companies, # pyright: ignore[reportArgumentType] + ) + + assert isinstance(result, ScalarResult) + assert "apple" in result.data.answer.lower() diff --git a/uv.lock b/uv.lock index 72e5805a..68f1ab39 100644 --- a/uv.lock +++ b/uv.lock @@ -254,6 +254,7 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-mock" }, + { name = "pytest-xdist" }, { name = "ruff" }, ] @@ -280,9 +281,19 @@ dev = [ { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "ruff", specifier = ">=0.9.9" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -1119,6 +1130,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"