diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..95bf860 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,25 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands +- Install dependencies: `uv sync` +- Run tests: `uv run pytest` +- Run specific test: `uv run pytest tests/path/to/test_file.py::test_name -v` +- Run all example tests: `uv run pytest --all` +- Type check: `uv run mypy src tests examples/team_recommender/src` +- Lint: `uv run ruff check src tests examples` +- Format: `uv run ruff format src tests examples` + +## Code Style +- Python 3.13+ required +- Use type annotations for all functions and methods (checked by mypy) +- Max line length: 120 characters +- Use pytest fixtures in `conftest.py` for test setup +- Follow black formatting conventions +- Import order: stdlib, third-party, local +- Use proper error handling with try/except blocks +- Use snake_case for functions, variables, and modules +- Use PascalCase for class names +- Maintain test coverage for all new code +- Use `CAT_AI_SAMPLE_SIZE` environment variable for test iterations \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c55d989..f5d8b86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ test = [ ] examples = ["openai>=1.63.2,<2", "python-dotenv>=1.0.1,<2"] dev = [ + "ipython>=9.0.0", "sphinx>=8.1.3,<9", "sphinx-rtd-theme>=3.0.2,<4", "sphinx-markdown-builder>=0.6.8,<0.7", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..9d3d6e3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,90 @@ +import csv +import io +import os +from typing import Callable, Generator, Optional + +import matplotlib +import pytest + +from cat_ai.helpers.helpers import root_dir +from cat_ai.reporter import Reporter +from cat_ai.statistical_analysis import StatisticalAnalysis, analyse_measure_from_test_sample + + +@pytest.fixture +def test_name(request: pytest.FixtureRequest) -> str: + return str(request.node.name) + + +@pytest.fixture +def reporter_factory(test_name: str) -> Callable: + """Factory fixture for creating Reporter instances with default settings.""" + + def _create_reporter( + unique_id: Optional[str] = None, + ) -> Reporter: + return Reporter(test_name=test_name, output_dir=root_dir(), unique_id=unique_id) + + return _create_reporter + + +@pytest.fixture +def tmp_reporter() -> "Reporter": + """Creates a reporter that writes to /tmp.""" + return Reporter(test_name="test_fixture", output_dir="/tmp") + + +@pytest.fixture +def analyze_failure_rate() -> Callable: + """Helper fixture to analyze failure rates.""" + + def _analyze(failure_count: int, sample_size: int) -> StatisticalAnalysis: + return analyse_measure_from_test_sample(failure_count, sample_size) + + return _analyze + + +def export_results_to_csv(results: list[StatisticalAnalysis]) -> str: + output = io.StringIO(newline="\n") + writer = csv.writer(output, lineterminator="\n") + + # Write header + writer.writerow(StatisticalAnalysis.get_csv_headers()) + + # Write rows + for result in results: + writer.writerow(result.as_csv_row()) + + return output.getvalue() + + +@pytest.fixture +def configure_matplotlib() -> Generator[None, None, None]: + """Configure matplotlib for consistent snapshot testing.""" + matplotlib.use("Agg") # Force CPU-based renderer + + # Configure for deterministic rendering + matplotlib.rcParams.update( + { + "figure.max_open_warning": 0, + "svg.hashsalt": "matplotlib", + "figure.dpi": 100, + "savefig.dpi": 100, + "path.simplify": False, + "agg.path.chunksize": 0, + "pdf.fonttype": 42, # Ensures text is stored as text, not paths + "ps.fonttype": 42, + } + ) + + yield + + # Clean up any open figures + import matplotlib.pyplot as plt + + plt.close("all") + + +def running_in_ci() -> bool: + """Check if tests are running in CI environment.""" + return os.getenv("CI") is not None diff --git a/tests/snapshots/test_reporter/test_report_creates_correct_json/expected_report.json b/tests/snapshots/test_reporter/test_report_creates_correct_json/expected_report.json new file mode 100644 index 0000000..3417a17 --- /dev/null +++ b/tests/snapshots/test_reporter/test_report_creates_correct_json/expected_report.json @@ -0,0 +1,11 @@ +{ + "test_name": "test_report_creates_correct_json", + "folder_path": "/tmp/test_runs/test_report_creates_correct_json-20231001_120000", + "output_file": "fail-0.json", + "metadata_path": "/tmp/test_runs/test_report_creates_correct_json-20231001_120000/metadata.json", + "validations": { + "can-talk": true, + "can-think": false + }, + "response": "Alice is the oldest." +} \ No newline at end of file diff --git a/tests/test_reporter.py b/tests/test_reporter.py index 8070a6c..5da4f4a 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -1,60 +1,71 @@ import json +import os import time -from unittest.mock import MagicMock, mock_open, patch +from pathlib import Path +from typing import Any, Callable from cat_ai.helpers.helpers import root_dir from cat_ai.reporter import Reporter -from cat_ai.statistical_analysis import analyse_measure_from_test_sample -def test_reporter_creates_a_unique_folder_path() -> None: - test_name = "unique_folder_path" - reporter1 = Reporter(test_name=test_name, output_dir=root_dir()) - expected_dir_path = f"{root_dir()}/test_runs/{test_name}" +def test_reporter_creates_a_unique_folder_path(reporter_factory: Callable) -> None: + reporter1 = reporter_factory() + expected_dir_path = f"{root_dir()}/test_runs/test_reporter_creates_a_unique_folder_path" assert expected_dir_path in reporter1.folder_path + time.sleep(2) - reporter2 = Reporter(test_name=test_name, output_dir=root_dir()) + reporter2 = reporter_factory() assert str(reporter1.folder_path) != str(reporter2.folder_path) -def test_reporter_can_accept_unique_id_override() -> None: - test_name = "example_test" +def test_reporter_can_accept_unique_id_override(reporter_factory: Callable) -> None: unique_id = "timestamp_or_any_unique_id" - reporter1 = Reporter(test_name=test_name, output_dir=root_dir(), unique_id=unique_id) - expected_dir_path = f"{root_dir()}/test_runs/{test_name}-{unique_id}" - assert str(expected_dir_path) == str(reporter1.folder_path) + reporter = reporter_factory(unique_id=unique_id) + + expected_dir_path = ( + f"{root_dir()}/test_runs/test_reporter_can_accept_unique_id_override-{unique_id}" + ) + assert str(expected_dir_path) == str(reporter.folder_path) -@patch("os.makedirs") -@patch("builtins.open", new_callable=mock_open) -def test_report_creates_correct_json(mock_open: MagicMock, mock_makedirs: MagicMock) -> None: - test_name = "report_creates_correct_json" +def test_report_creates_correct_json(test_name: str, snapshot: Any) -> None: + temp_dir = "/tmp" unique_id = "20231001_120000" - reporter = Reporter(test_name=test_name, output_dir=root_dir(), unique_id=unique_id) + metadata = {"ai-model": "champion-1"} + reporter = Reporter( + test_name=test_name, + output_dir=temp_dir, + unique_id=unique_id, + metadata=metadata, + ) - response = "Sample response" - results = {"test1": True, "test2": False} + # Generate test data + response = "Alice is the oldest." + results = {"can-talk": True, "can-think": False} + # Call report method final_result = reporter.report(response, results) + # Verify return value (should be False because not all results are True) assert final_result is False - expected_metadata = { - "test_name": test_name, - "folder_path": f"{root_dir()}/test_runs/{test_name}-{unique_id}", - "output_file": "fail-0.json", - "metadata_path": f"{root_dir()}/test_runs/{test_name}-{unique_id}/metadata.json", - "validations": results, - "response": response, - } - expected_json_string = json.dumps(expected_metadata, indent=4) - mock_makedirs.assert_called_once_with(reporter.folder_path, exist_ok=True) + # Expected output paths + expected_dir_path = Path(temp_dir) / "test_runs" / (test_name + "-" + unique_id) + expected_metadata_path = expected_dir_path / "metadata.json" + with open(expected_metadata_path, "r") as file: + contents = json.load(file) + assert contents == metadata + expected_output_path = expected_dir_path / "fail-0.json" + assert os.path.isfile(expected_metadata_path) + assert os.path.isfile(expected_output_path) - mock_open().write.assert_called_with(expected_json_string) + with open(expected_output_path, "r") as file: + content = json.load(file) + snapshot.assert_match(json.dumps(content, indent=2), "expected_report.json") -def test_format_summary_with_failure_analysis(): - failure_analysis = analyse_measure_from_test_sample(6, 100) +def test_format_summary_with_failure_analysis(analyze_failure_rate: Callable) -> None: + failure_analysis = analyze_failure_rate(6, 100) assert Reporter.format_summary(failure_analysis) == ( "> [!NOTE]\n" "> ## 6 ± 3 failures detected (100 samples)\n" diff --git a/tests/test_runner.py b/tests/test_runner.py index 9a4ff4b..998afbe 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -1,12 +1,6 @@ -from cat_ai.reporter import Reporter -from cat_ai.runner import Runner - +import pytest -# Dummy test function that will be passed to Runner -def dummy_test_function(reporter: Reporter) -> bool: - # Imagine that this function does something meaningful - # Simply returning True instead of trying to log - return True +from cat_ai.runner import Runner def test_runner_sample_size(monkeypatch): @@ -19,46 +13,39 @@ def test_runner_sample_size(monkeypatch): assert Runner.get_sample_size(default_size=3) == 3 -def test_run_once(): - # Create a Reporter with necessary arguments - reporter = Reporter(test_name="test_run_once", output_dir="/tmp") - +@pytest.mark.parametrize("return_value", [True, False]) +def test_run_once(tmp_reporter, return_value): # Initialize Runner with dummy test function and Reporter - runner = Runner(test_function=dummy_test_function, reporter=reporter) + runner = Runner(test_function=lambda x: return_value, reporter=tmp_reporter) # Test run_once result = runner.run_once() - assert result is True - assert reporter.run_number == 0 - + assert result is return_value + assert tmp_reporter.run_number == 0 -def test_run_multiple(): - # Create a Reporter with necessary arguments - reporter = Reporter(test_name="test_run", output_dir="/tmp") +@pytest.mark.parametrize("return_value", [True, False]) +def test_run_multiple(tmp_reporter, return_value): # Initialize Runner with dummy test function and Reporter - runner = Runner(test_function=dummy_test_function, reporter=reporter) + runner = Runner(test_function=lambda _: return_value, reporter=tmp_reporter) # Test with explicit sample size parameter results = runner.run_multiple(sample_size=2) assert len(results) == 2 - assert all(results) - expected_results = [True, True] + expected_results = [return_value, return_value] assert results == expected_results -def test_run_with_env_variable(monkeypatch): +@pytest.mark.parametrize("sample_size", [3, 5]) +def test_run_with_env_variable(monkeypatch, tmp_reporter, sample_size): # Set the environment variable for a controlled test - monkeypatch.setenv("CAT_AI_SAMPLE_SIZE", "3") - - # Create a Reporter with necessary arguments - reporter = Reporter(test_name="test_run_with_env", output_dir="/tmp") + monkeypatch.setenv("CAT_AI_SAMPLE_SIZE", str(sample_size)) # Initialize Runner with dummy test function and Reporter - runner = Runner(test_function=dummy_test_function, reporter=reporter) + runner = Runner(test_function=lambda x: True, reporter=tmp_reporter) # Test without explicit sample size (should use environment variable) results = runner.run_multiple() - assert len(results) == 3 - expected_results = [True, True, True] + assert len(results) == sample_size + expected_results = [True] * sample_size assert results == expected_results diff --git a/tests/test_statistical_analysis.py b/tests/test_statistical_analysis.py index 439d191..db83974 100644 --- a/tests/test_statistical_analysis.py +++ b/tests/test_statistical_analysis.py @@ -1,33 +1,23 @@ -import csv import io import math -import os from statistics import NormalDist -import matplotlib - -matplotlib.use("Agg") # Force CPU-based renderer before any pyplot import - import matplotlib.pyplot as plt import numpy as np import pytest -from cat_ai.statistical_analysis import StatisticalAnalysis, analyse_measure_from_test_sample - - -def analyse_failure_rate_from_test_sample( - failure_count: int, sample_size: int -) -> StatisticalAnalysis: - return analyse_measure_from_test_sample(failure_count, sample_size) +from tests.conftest import export_results_to_csv, running_in_ci @pytest.mark.parametrize( "failure_count,sample_size,expected_proportion", [(0, 100, 0.0), (6, 100, 0.06), (100, 100, 1.0), (1, 47, 0.0213)], ) -def test_analyse_sample_from_test(failure_count, sample_size, expected_proportion): +def test_analyse_sample_from_test( + analyze_failure_rate, failure_count, sample_size, expected_proportion +): """Test the statistical analysis function with various edge cases.""" - result = analyse_failure_rate_from_test_sample(failure_count, sample_size) + result = analyze_failure_rate(failure_count, sample_size) # Basic assertions assert result.observation == failure_count @@ -73,17 +63,16 @@ def test_analyse_sample_from_test(failure_count, sample_size, expected_proportio (100, 100, 0.0, (100, 100)), ], ) -def test_edges_cases(failures, total, expected_error, expected_ci): - result = analyse_failure_rate_from_test_sample(failures, total) - print("hello world") +def test_edges_cases(analyze_failure_rate, failures, total, expected_error, expected_ci): + result = analyze_failure_rate(failures, total) assert math.isclose(result.standard_error, expected_error, abs_tol=0.0001) assert result.confidence_interval_count == expected_ci +# Constants for success rate tests next_success_after_97 = 0.9709704495337362 next_success_after_90 = 0.925327195595728 digress_from_0_999 = 0.9986779845027858 - progress_from_0_999 = 0.9992400558756847 @@ -113,71 +102,38 @@ def test_measured_constants(): (1, 10000, 0.999, progress_from_0_999), ], ) -def test_next_success_rate(failures, total, current_success_rate, next_success_rate): - result = analyse_failure_rate_from_test_sample(failures, total) +def test_next_success_rate( + analyze_failure_rate, failures, total, current_success_rate, next_success_rate +): + result = analyze_failure_rate(failures, total) assert result.next_success_rate(current_success_rate) == pytest.approx( next_success_rate, rel=0.001 ) -def export_results_to_csv_string(results: list[StatisticalAnalysis]) -> str: - """Export a list of StatisticalAnalysis objects to a CSV-formatted string.""" - # Create a CSV writer with MacOS-style newlines to match the snapshot - output = io.StringIO(newline="\n") # Let CSV writer handle newline translation - writer = csv.writer(output, lineterminator="\n") # Explicitly set line terminator - - # Write header - writer.writerow(StatisticalAnalysis.get_csv_headers()) - - # Write rows - for result in results: - writer.writerow(result.as_csv_row()) - - return output.getvalue() - - -def running_in_ci() -> bool: - return os.getenv("CI") is not None - - # This test is skipped on CI as it fails to render the difference due to Timeout >10.0s # https://github.com/thisisartium/continuous-alignment-testing/issues/53 @pytest.mark.skipif(running_in_ci(), reason="Image comparison fails to produce diff on CI") -def test_failure_rate_bar_graph(snapshot): +def test_failure_rate_bar_graph(snapshot, analyze_failure_rate, configure_matplotlib): # Sample data points - choosing strategic values to test boundary conditions sample_size = 100 failure_counts = list(range(sample_size + 1)) - assert failure_counts[0] == 0 - assert failure_counts[sample_size] == sample_size # Calculate results for each data point - results = [analyse_failure_rate_from_test_sample(f, sample_size) for f in failure_counts] - csv = export_results_to_csv_string(results) - csv_bytes = io.BytesIO(csv.encode("utf-8")) - snapshot.assert_match(csv_bytes.getvalue(), "failure_rate_results.csv") + results = [analyze_failure_rate(f, sample_size) for f in failure_counts] + + # Export to CSV and validate + snapshot.assert_match(export_results_to_csv(results), "failure_rate_results.csv") + # Extract data for plotting rates = [r.proportion for r in results] errors = [r.margin_of_error for r in results] # Create the bar plot - # When creating the figure, set specific dimensions - fig, ax = plt.subplots(figsize=(10, 6), dpi=100) # Explicitly set DPI here too + fig, ax = plt.subplots(figsize=(10, 6), dpi=100) # Plot bars with error bars ax.bar(failure_counts, rates, yerr=errors, capsize=5, color="steelblue", alpha=0.7, width=8) - # # Add annotations on top of each bar - # for bar, rate, error in zip(bars, rates, errors): - # height = bar.get_height() - # ax.text( - # bar.get_x() + bar.get_width() / 2.0, - # height + error + 0.01, - # f"{rate:.2f}±{error:.2f}", - # ha="center", - # va="bottom", - # rotation=0, - # fontsize=9, - # ) - # Add labels and title ax.set_xlabel("Number of Failures") ax.set_ylabel("Failure Rate") @@ -185,53 +141,34 @@ def test_failure_rate_bar_graph(snapshot): ax.set_ylim(0, 1.2) # Set y-axis to accommodate annotations ax.grid(True, linestyle="--", alpha=0.7, axis="both") - # Deterministic rendering for snapshot testing + # Save for snapshot comparison plt.tight_layout() buf = io.BytesIO() - # Add these parameters before saving the figure - plt.rcParams["svg.hashsalt"] = "matplotlib" # Fix the hash salt for deterministic rendering - plt.rcParams["figure.dpi"] = 100 # Fix the DPI - plt.rcParams["savefig.dpi"] = 100 # Fix the saving DPI - plt.rcParams["path.simplify"] = False # Don't simplify paths - plt.rcParams["agg.path.chunksize"] = 0 # Disable path chunking - - # Before saving, set more explicit parameters fig.savefig( buf, format="png", metadata={"CreationDate": None}, dpi=100, - bbox_inches="tight", # Consistent bounding box + bbox_inches="tight", pad_inches=0.1, - ) # Consistent padding - - # fig.savefig(buf, format="png", metadata={"CreationDate": None}) + ) buf.seek(0) # Compare with snapshot snapshot.assert_match(buf.read(), "failure_rate_bar_graph.png") - plt.close() - # This test is skipped on CI as it fails to render the difference due to Timeout >10.0s # https://github.com/thisisartium/continuous-alignment-testing/issues/53 @pytest.mark.skipif(running_in_ci(), reason="Image comparison fails to produce diff on CI") -def test_failure_rate_graph(snapshot): - # Also useful to ensure thread safety and determinism - matplotlib.rcParams["figure.max_open_warning"] = 0 - matplotlib.rcParams["pdf.fonttype"] = 42 # Ensures text is stored as text, not paths - matplotlib.rcParams["ps.fonttype"] = 42 - +def test_failure_rate_graph(snapshot, analyze_failure_rate, configure_matplotlib): # Generate a series of failure rates totals = [100] * 100 failures = list(range(100)) - assert len(failures) == len(totals) + # Calculate results for each rate - results = [ - analyse_failure_rate_from_test_sample(f, t) for f, t in zip(failures, totals, strict=True) - ] + results = [analyze_failure_rate(f, t) for f, t in zip(failures, totals, strict=True)] # Extract data for plotting rates = [r.proportion for r in results] @@ -263,11 +200,8 @@ def test_failure_rate_graph(snapshot): # Save to buffer for snapshot comparison plt.tight_layout() buf = io.BytesIO() - plt.rcParams["svg.hashsalt"] = "matplotlib" fig.savefig(buf, format="png", metadata={"CreationDate": None}) buf.seek(0) # Compare with snapshot snapshot.assert_match(buf.read(), "failure_rate_graph.png") - - plt.close() diff --git a/uv.lock b/uv.lock index baafd81..fdc3bf8 100644 --- a/uv.lock +++ b/uv.lock @@ -33,6 +33,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, ] +[[package]] +name = "anyioutils" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "outcome" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/29/3386d5be378ef1a8fb8b8879b12010e7b2b2953bc7798bf451be84f2010b/anyioutils-0.7.3.tar.gz", hash = "sha256:68443be47c3d8589ddc4906d0d43f0bcc1187d750cc0b59355805af6cd57a1fe", size = 15022 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/5a/eb249853684de193b39d69359d20f825e7babe5497e4eb54cd88ee9976f5/anyioutils-0.7.3-py3-none-any.whl", hash = "sha256:8698f031d90ad346c950939ffa9ba072d00826f1c09bcd52e1cde49784b96c6a", size = 14341 }, +] + [[package]] name = "appnope" version = "0.1.4" @@ -170,6 +183,7 @@ source = { virtual = "." } [package.dev-dependencies] dev = [ + { name = "ipython" }, { name = "notebook" }, { name = "pydantic" }, { name = "pydrive2" }, @@ -196,6 +210,7 @@ test = [ [package.metadata.requires-dev] dev = [ + { name = "ipython", specifier = ">=9.0.0" }, { name = "notebook", specifier = ">=7.3.2" }, { name = "pydantic", specifier = ">=2.10.6,<3" }, { name = "pydrive2", specifier = ">=1.21.3,<2" }, @@ -603,9 +618,10 @@ wheels = [ [[package]] name = "ipykernel" -version = "6.29.5" +version = "7.0.0a1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "anyio" }, { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, @@ -617,17 +633,17 @@ dependencies = [ { name = "packaging" }, { name = "psutil" }, { name = "pyzmq" }, - { name = "tornado" }, { name = "traitlets" }, + { name = "zmq-anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/e8/147a81083d3200403d9465330e0daa226a3c6d8130f6bc8c275baab406c2/ipykernel-7.0.0a1.tar.gz", hash = "sha256:7986c167f4501d4fab4873ab878196fd7dcc189a5cdea4bed9f973bbe3859098", size = 169472 } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173 }, + { url = "https://files.pythonhosted.org/packages/a6/40/3e78506d35c863979aea77a125b42125cccaa0ed4199ad20b2bfcba613d7/ipykernel-7.0.0a1-py3-none-any.whl", hash = "sha256:fa4a1401d3f1a86dc2c8e47592ae60d59818d3b597f68198bb99fd41d789d09a", size = 116622 }, ] [[package]] name = "ipython" -version = "9.0.0" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -641,9 +657,9 @@ dependencies = [ { name = "stack-data" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/39/264894738a202ddaf6abae39b3f84671ddee23fd292dbb3e10039e70300c/ipython-9.0.0.tar.gz", hash = "sha256:9368d65b3d4a471e9a698fed3ea486bbf6737e45111e915279c971b77f974397", size = 4364165 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ce/012a0f40ca58a966f87a6e894d6828e2817657cbdf522b02a5d3a87d92ce/ipython-9.0.2.tar.gz", hash = "sha256:ec7b479e3e5656bf4f58c652c120494df1820f4f28f522fb7ca09e213c2aab52", size = 4366102 } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/a1/894e4c0b6ac994045c6edeb2b6fdf69c59f20fcd2e348a42f4e40889181c/ipython-9.0.0-py3-none-any.whl", hash = "sha256:2cce23069b830a54a5b9d3d66ccd6433047c1503a7b9a3b34593c0b5c2c08477", size = 592040 }, + { url = "https://files.pythonhosted.org/packages/20/3a/917cb9e72f4e1a4ea13c862533205ae1319bd664119189ee5cc9e4e95ebf/ipython-9.0.2-py3-none-any.whl", hash = "sha256:143ef3ea6fb1e1bffb4c74b114051de653ffb7737a3f7ab1670e657ca6ae8c44", size = 600524 }, ] [[package]] @@ -1228,6 +1244,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/64/db3462b358072387b8e93e6e6a38d3c741a17b4a84171ef01d6c85c63f25/openai-1.63.2-py3-none-any.whl", hash = "sha256:1f38b27b5a40814c2b7d8759ec78110df58c4a614c25f182809ca52b080ff4d4", size = 472282 }, ] +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 }, +] + [[package]] name = "overrides" version = "7.7.0" @@ -2151,3 +2179,17 @@ sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf594 wheels = [ { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, ] + +[[package]] +name = "zmq-anyio" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "anyioutils" }, + { name = "pyzmq" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/88/dcebf3052e2dbe9b81f1d13ad6c1b41eb0e742c7f75a73ce00a78375cb76/zmq_anyio-0.3.9.tar.gz", hash = "sha256:7dc0cdbf039834f16c0dc6412b560e689ca406e4d8b93cb94e0fa8763a87e947", size = 13175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/0b/137721c37bbe04d0a379d349342382edc0c48a0feefd52e69c63b2c7bfe9/zmq_anyio-0.3.9-py3-none-any.whl", hash = "sha256:a922b13409ae49cf51730958f0a37b38d9ce9ebde05f046fec9f8639b58ac709", size = 10513 }, +]