diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 0000000000..02abbfad25 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,45 @@ +name: CodSpeed + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + benchmarks: + name: Run benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + - name: Install uv with caching + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true + cache-dependency-glob: | + **/pyproject.toml + **/uv.lock + - name: Create and activate virtual environment + run: | + uv venv .venv + echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH" + - name: Install dependencies + run: uv sync --dev -p .venv --extra dev + - name: Run benchmarks + uses: CodSpeedHQ/action@b16b7f2241a8564d005126c814839e9e990045a0 # v4 + with: + mode: simulation + run: uv run -p .venv pytest tests/benchmarks/ --codspeed diff --git a/README.md b/README.md index fe73d8cc89..cfbcbbf7d8 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ **Documentation:** [DSPy Docs](https://dspy.ai/) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/dspy?period=monthly)](https://pepy.tech/projects/dspy) +[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/cmpnd-ai/dspy?utm_source=badge) ---- @@ -85,4 +86,3 @@ If you use DSPy or DSP in a research paper, please cite our work as follows: * [**Releasing the DSP Compiler (v0.1)**](https://twitter.com/lateinteraction/status/1625231662849073160) (Twitter Thread, Feb 2023) * [**Introducing DSP**](https://twitter.com/lateinteraction/status/1617953413576425472) (Twitter Thread, Jan 2023) * [**Demonstrate-Search-Predict: Composing retrieval and language models for knowledge-intensive NLP**](https://arxiv.org/abs/2212.14024.pdf) (Academic Paper, Dec 2022) --> - diff --git a/pyproject.toml b/pyproject.toml index b0d0bd9933..81b9d9940d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,6 +168,11 @@ indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" +[dependency-groups] +dev = [ + "pytest-codspeed>=5.0.3", +] + [tool.ruff.lint.isort] known-first-party = ["dspy"] diff --git a/tests/benchmarks/__init__.py b/tests/benchmarks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/benchmarks/test_benchmarks.py b/tests/benchmarks/test_benchmarks.py new file mode 100644 index 0000000000..6f8b3f6687 --- /dev/null +++ b/tests/benchmarks/test_benchmarks.py @@ -0,0 +1,328 @@ +"""Performance benchmarks for DSPy core operations. + +These benchmarks cover the most performance-critical, CPU-bound code paths +in DSPy: data container operations, signature parsing and manipulation, +adapter formatting and parsing, and serialization. +""" + +import copy +import json + +import pytest + +import dspy +from dspy.adapters.chat_adapter import ChatAdapter +from dspy.adapters.utils import ( + format_field_value, + get_annotation_name, + get_field_description_string, + parse_value, + serialize_for_json, + translate_field_type, +) +from dspy.primitives.example import Example +from dspy.primitives.prediction import Prediction +from dspy.signatures.signature import Signature, infer_prefix, make_signature + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def simple_example(): + return Example(question="What is the capital of France?", answer="Paris") + + +@pytest.fixture +def large_example(): + return Example( + **{f"field_{i}": f"value_{i}" for i in range(50)}, + ) + + +@pytest.fixture +def nested_example(): + inner = Example(detail="nested_value", score=42) + return Example( + question="What is DSPy?", + answer="A framework", + context=["paragraph one", "paragraph two", "paragraph three"], + metadata={"source": "test", "nested": inner}, + ) + + +@pytest.fixture +def simple_signature(): + return make_signature("question -> answer") + + +@pytest.fixture +def complex_signature(): + return make_signature("question, context: list[str], hint -> answer, reasoning") + + +@pytest.fixture +def chat_adapter(): + return ChatAdapter() + + +@pytest.fixture +def qa_signature_class(): + class QA(Signature): + """Answer the question based on the context.""" + + question: str = dspy.InputField(desc="The question to answer") + context: str = dspy.InputField(desc="Relevant context") + answer: str = dspy.OutputField(desc="The answer") + + return QA + + +@pytest.fixture +def chat_completion_text(qa_signature_class): + return "[[ ## answer ## ]]\nParis is the capital of France.\n\n[[ ## completed ## ]]" + + +# --------------------------------------------------------------------------- +# Example / Prediction benchmarks +# --------------------------------------------------------------------------- + + +class TestExampleBenchmarks: + def test_example_creation(self, benchmark): + """Benchmark creating an Example from keyword arguments.""" + benchmark(Example, question="What is 2+2?", answer="4") + + def test_example_creation_from_dict(self, benchmark): + """Benchmark creating an Example from a base dictionary.""" + data = {f"field_{i}": f"value_{i}" for i in range(20)} + benchmark(Example, base=data) + + def test_example_field_access(self, benchmark, simple_example): + """Benchmark attribute-style field access.""" + + def access_fields(): + _ = simple_example.question + _ = simple_example.answer + + benchmark(access_fields) + + def test_example_copy(self, benchmark, simple_example): + """Benchmark shallow copy with field override.""" + benchmark(simple_example.copy, answer="London") + + def test_example_with_inputs(self, benchmark, simple_example): + """Benchmark marking input fields.""" + benchmark(simple_example.with_inputs, "question") + + def test_example_inputs_labels(self, benchmark): + """Benchmark splitting an Example into inputs and labels.""" + ex = Example(question="Why?", answer="Because.", context="info").with_inputs("question") + + def split(): + _ = ex.inputs() + _ = ex.labels() + + benchmark(split) + + def test_example_to_dict(self, benchmark, nested_example): + """Benchmark recursive serialization to dict.""" + benchmark(nested_example.toDict) + + def test_example_hash(self, benchmark, simple_example): + """Benchmark hash computation.""" + benchmark(hash, simple_example) + + def test_example_keys_values_items(self, benchmark, large_example): + """Benchmark dict-like iteration over a large Example.""" + + def iterate(): + _ = large_example.keys() + _ = large_example.values() + _ = large_example.items() + + benchmark(iterate) + + def test_prediction_creation(self, benchmark): + """Benchmark creating a Prediction.""" + benchmark(Prediction, answer="Paris", score=0.95) + + def test_prediction_arithmetic(self, benchmark): + """Benchmark Prediction score arithmetic.""" + p = Prediction(answer="Paris", score=0.8) + + def arithmetic(): + _ = p + p + _ = p / 2 + + benchmark(arithmetic) + + +# --------------------------------------------------------------------------- +# Signature benchmarks +# --------------------------------------------------------------------------- + + +class TestSignatureBenchmarks: + def test_make_signature_simple(self, benchmark): + """Benchmark parsing a simple signature string.""" + benchmark(make_signature, "question -> answer") + + def test_make_signature_complex(self, benchmark): + """Benchmark parsing a complex signature with typed fields.""" + benchmark(make_signature, "question: str, context: list[str], hint: str -> answer: str, confidence: float") + + def test_signature_with_instructions(self, benchmark, simple_signature): + """Benchmark creating a signature with new instructions.""" + benchmark(simple_signature.with_instructions, "Translate the question to French.") + + def test_signature_append_field(self, benchmark, simple_signature): + """Benchmark appending an output field.""" + benchmark(simple_signature.append, "confidence", dspy.OutputField(desc="Confidence score"), float) + + def test_signature_prepend_field(self, benchmark, simple_signature): + """Benchmark prepending an input field.""" + benchmark(simple_signature.prepend, "context", dspy.InputField(desc="Context")) + + def test_signature_delete_field(self, benchmark, complex_signature): + """Benchmark deleting a field from a signature.""" + benchmark(complex_signature.delete, "hint") + + def test_signature_dump_state(self, benchmark, complex_signature): + """Benchmark serializing signature state.""" + benchmark(complex_signature.dump_state) + + def test_signature_load_state(self, benchmark, complex_signature): + """Benchmark deserializing signature state.""" + state = complex_signature.dump_state() + benchmark(complex_signature.load_state, state) + + def test_signature_equals(self, benchmark, simple_signature): + """Benchmark comparing two signatures.""" + other = make_signature("question -> answer") + benchmark(simple_signature.equals, other) + + def test_infer_prefix_camel_case(self, benchmark): + """Benchmark prefix inference from camelCase.""" + benchmark(infer_prefix, "camelCaseFieldName") + + def test_infer_prefix_snake_case(self, benchmark): + """Benchmark prefix inference from snake_case.""" + benchmark(infer_prefix, "snake_case_field_name") + + +# --------------------------------------------------------------------------- +# Adapter formatting / parsing benchmarks +# --------------------------------------------------------------------------- + + +class TestAdapterBenchmarks: + def test_chat_adapter_format_field_description(self, benchmark, chat_adapter, qa_signature_class): + """Benchmark formatting field descriptions for a signature.""" + benchmark(chat_adapter.format_field_description, qa_signature_class) + + def test_chat_adapter_format_field_structure(self, benchmark, chat_adapter, qa_signature_class): + """Benchmark formatting the field structure section.""" + benchmark(chat_adapter.format_field_structure, qa_signature_class) + + def test_chat_adapter_format_user_message(self, benchmark, chat_adapter, qa_signature_class): + """Benchmark formatting a user message from inputs.""" + inputs = {"question": "What is the capital of France?", "context": "France is a country in Europe."} + benchmark(chat_adapter.format_user_message_content, qa_signature_class, inputs) + + def test_chat_adapter_format_assistant_message(self, benchmark, chat_adapter, qa_signature_class): + """Benchmark formatting an assistant response message.""" + outputs = {"answer": "Paris is the capital of France."} + benchmark(chat_adapter.format_assistant_message_content, qa_signature_class, outputs) + + def test_chat_adapter_parse(self, benchmark, chat_adapter): + """Benchmark parsing a completion into structured fields.""" + sig = make_signature("question -> answer") + completion = "[[ ## answer ## ]]\nParis is the capital of France.\n\n[[ ## completed ## ]]" + benchmark(chat_adapter.parse, sig, completion) + + def test_chat_adapter_parse_multifield(self, benchmark, chat_adapter): + """Benchmark parsing a multi-field completion.""" + sig = make_signature("question -> answer: str, reasoning: str, confidence: float") + completion = ( + "[[ ## answer ## ]]\nParis\n\n" + "[[ ## reasoning ## ]]\nFrance's capital is Paris.\n\n" + "[[ ## confidence ## ]]\n0.95\n\n" + "[[ ## completed ## ]]" + ) + benchmark(chat_adapter.parse, sig, completion) + + def test_format_field_value_string(self, benchmark): + """Benchmark formatting a simple string field value.""" + sig = make_signature("question -> answer") + field_info = sig.fields["answer"] + benchmark(format_field_value, field_info, "Paris is the capital of France.") + + def test_format_field_value_list(self, benchmark): + """Benchmark formatting a list field value.""" + sig = make_signature("question -> answer") + field_info = sig.fields["question"] + value = [f"Paragraph {i}: Some context about the topic." for i in range(10)] + benchmark(format_field_value, field_info, value) + + def test_serialize_for_json_complex(self, benchmark): + """Benchmark JSON serialization of a complex nested structure.""" + data = { + "results": [{"text": f"result {i}", "score": 0.9 - i * 0.1} for i in range(10)], + "metadata": {"source": "test", "count": 10}, + } + benchmark(serialize_for_json, data) + + def test_translate_field_type_string(self, benchmark, qa_signature_class): + """Benchmark field type translation for a string field.""" + field_info = qa_signature_class.fields["answer"] + benchmark(translate_field_type, "answer", field_info) + + def test_parse_value_string(self, benchmark): + """Benchmark parsing a string value.""" + benchmark(parse_value, "Paris", str) + + def test_parse_value_int(self, benchmark): + """Benchmark parsing an integer value from a string.""" + benchmark(parse_value, "42", int) + + def test_parse_value_list(self, benchmark): + """Benchmark parsing a JSON list value.""" + benchmark(parse_value, '["a", "b", "c"]', list[str]) + + def test_get_annotation_name_simple(self, benchmark): + """Benchmark getting the name of a simple type annotation.""" + benchmark(get_annotation_name, str) + + def test_get_annotation_name_generic(self, benchmark): + """Benchmark getting the name of a generic type annotation.""" + benchmark(get_annotation_name, list[str]) + + def test_get_field_description_string(self, benchmark, qa_signature_class): + """Benchmark generating field description strings.""" + benchmark(get_field_description_string, qa_signature_class.output_fields) + + +# --------------------------------------------------------------------------- +# Module state serialization benchmarks +# --------------------------------------------------------------------------- + + +class TestSerializationBenchmarks: + def test_example_deepcopy(self, benchmark, nested_example): + """Benchmark deep copying a nested Example.""" + benchmark(copy.deepcopy, nested_example) + + def test_signature_deepcopy(self, benchmark, complex_signature): + """Benchmark deep copying a complex signature's fields.""" + benchmark(copy.deepcopy, complex_signature.fields) + + def test_example_json_roundtrip(self, benchmark, nested_example): + """Benchmark JSON serialization and deserialization of an Example.""" + + def roundtrip(): + d = nested_example.toDict() + _ = json.dumps(d) + + benchmark(roundtrip) diff --git a/uv.lock b/uv.lock index aa4975c6a9..cbb09748db 100644 --- a/uv.lock +++ b/uv.lock @@ -832,6 +832,11 @@ weaviate = [ { name = "weaviate-client" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest-codspeed" }, +] + [package.metadata] requires-dist = [ { name = "anthropic", marker = "extra == 'anthropic'", specifier = ">=0.18.0,<1.0.0" }, @@ -875,6 +880,9 @@ requires-dist = [ ] provides-extras = ["anthropic", "weaviate", "mcp", "langchain", "optuna", "numpy", "litellm", "dev", "test-extras"] +[package.metadata.requires-dev] +dev = [{ name = "pytest-codspeed", specifier = ">=5.0.3" }] + [[package]] name = "email-validator" version = "2.2.0" @@ -1610,6 +1618,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + [[package]] name = "markupsafe" version = "3.0.2" @@ -1720,6 +1740,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0", size = 130232, upload-time = "2025-06-12T08:20:28.551Z" }, ] +[[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 = "more-itertools" version = "10.7.0" @@ -2793,6 +2822,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "pytest-codspeed" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/b4/cf932fcd1960a2fd6d9b09eb403253a8709aeee975961afa6299239a830e/pytest_codspeed-5.0.3.tar.gz", hash = "sha256:91afef90e6a96b013495e4702ef5d6358614a449e71008cdc194ef668778b92f", size = 324571, upload-time = "2026-05-22T16:20:49.231Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/f5/a8f70147216e4b84046ca406d03ecc8e83e3ea56ba1bdca0bb79cca79fee/pytest_codspeed-5.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:005348ea52ace3ede2e2f595913912ad2564cca7b124211a88dc78a9cb1fca63", size = 366249, upload-time = "2026-05-22T16:20:39.985Z" }, + { url = "https://files.pythonhosted.org/packages/f6/bd/7a4dbcf457fcc3ed788c55d402f3af2671e0e342b6098090fd590aa8712e/pytest_codspeed-5.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbe6a4a00b449b6ba2771f644cbc38bdf55acf5c812e60e5659110e19dd9f510", size = 932229, upload-time = "2026-05-22T16:20:37.283Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/414ea4c66559f24ec06aeb6db62bfc7079582dac1452e648affe1eb5cfb4/pytest_codspeed-5.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac4344f34bbcdd17f6f8c30dbac3da2f80d223dd112e568fd7f7c2cd4cbc693", size = 934647, upload-time = "2026-05-22T16:20:31.997Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ef/32ce60d42a4aa43e728d988e13eb6568fbc7b10a514517b459bafd3f2b94/pytest_codspeed-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f56d0339cd98d26f6e561987be25bdd2761a5d53d8f73493b1ebe02d0d451093", size = 366253, upload-time = "2026-05-22T16:21:10.013Z" }, + { url = "https://files.pythonhosted.org/packages/2a/15/c66ef90a793c5d2c039e63a1726a5e55c678be2618b0f5f1660d0f79e25f/pytest_codspeed-5.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c682f6645d4eb472f3bd95dbda1805e3af4243610572cb7d6bf94a88e8a0b6c", size = 932465, upload-time = "2026-05-22T16:20:34.265Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7b/d231279301967f05b7909160489e85ee3a1b9da76094ea25343faba1abc2/pytest_codspeed-5.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f852bee785a7a124cb1720b1915670c6742af87747dc4d838f3ffdbd365ce9d9", size = 934925, upload-time = "2026-05-22T16:20:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/c2/22/456c48160b761d5028c8afa119f085a9fc42855a783a13d73918078969f0/pytest_codspeed-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2eeb25fb1ac3f73c4de50e739e78fea396b89782bdb740bf2a7cd2df21f8d4ee", size = 366255, upload-time = "2026-05-22T16:20:56.214Z" }, + { url = "https://files.pythonhosted.org/packages/74/33/ac7441fa937c9d9f158083a8c46920a5a5c81ed3c5f96240fc8d650db5c2/pytest_codspeed-5.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73c5c9d98a3372a42611989ccfa437cce3842431ac6d6b9ab42c4f0e59c070f7", size = 932325, upload-time = "2026-05-22T16:21:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/77/bc/8b994adcb9e9016e7d9a808056a3dd9cca21441e432ef456eae2b697d7fe/pytest_codspeed-5.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2e0ab65df73e837666d12357280ca50ff6d6ac03ea5266703be518b68170edf", size = 934885, upload-time = "2026-05-22T16:21:01.444Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/e032451e9e0a06b0c4bff53105f62b693d9a54595dd8c024693741ce3380/pytest_codspeed-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6524c57fec279a22ffef6112af404036afc71b4704758ae9f0abda429b8478d4", size = 366253, upload-time = "2026-05-22T16:20:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7b/ae76fd8ac656b9695806a6aafd5f22ec32e6ce20e266a58f9112e01d3cd8/pytest_codspeed-5.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c383c9121deb58a69f174188e9e4488ffc0daced0ed276abf87747182511901", size = 932360, upload-time = "2026-05-22T16:20:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4a/dfd43d943fdb143be4fd62f34c2793ba349dc27aa188e521d19d629aa7ab/pytest_codspeed-5.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4bcdb4b6522738152885ef067e0c8524d5699828d780fb6f464cdb3db44369c", size = 934928, upload-time = "2026-05-22T16:20:38.62Z" }, + { url = "https://files.pythonhosted.org/packages/04/6a/fdcec19c7f267c195f147c51d3fd2245f6b8d09b80495ed0a90c008e0842/pytest_codspeed-5.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:25464363c7f9b9bd5022e969c0addba616fa40ac9b8f0fc9e030c4538863b32d", size = 366259, upload-time = "2026-05-22T16:21:06.039Z" }, + { url = "https://files.pythonhosted.org/packages/6a/96/c6b03b81dcd21ae3d6b32cca0b3c10149fa378eb21b338d4b63c9eb8050b/pytest_codspeed-5.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efd43f82ea03ced8488a767ded9473f050791ab7783ea8654107e1e0ac66af40", size = 932395, upload-time = "2026-05-22T16:21:04.804Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/56ad8f1cc7d6962f8a680141b361e93467a2abc53d976cd9d5e1edd740e3/pytest_codspeed-5.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:782f9985b6f6b45b8bc20152d206d3a52b56dd088ba81cb70a71f0b39841be9e", size = 934994, upload-time = "2026-05-22T16:20:28.809Z" }, + { url = "https://files.pythonhosted.org/packages/0b/54/9096c4545f09da94b1b00f3be2fe4952949e86c9bcafca9a29b26aed1a75/pytest_codspeed-5.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9aa0815b90196f3c20d736ea8691381e97f12bbe8c7d87af10a351e434b452cb", size = 366311, upload-time = "2026-05-22T16:20:41.791Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3c/24c53f67a38ad48cb087105ac30a8aa0923223ee274ea9bf2dc705edaa59/pytest_codspeed-5.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85505c96a3477c346ec2d2b7dced8478f4c651e2b1666ee102d53a832b511853", size = 933169, upload-time = "2026-05-22T16:20:43.178Z" }, + { url = "https://files.pythonhosted.org/packages/d1/de/2213f868fa7694f743f96cccbc07e757f45c920c523cccc2da97bc8652df/pytest_codspeed-5.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20eba63765be9d1b6cacbbfad84b87d49eb04b357a7045a0899880da181f81e3", size = 935522, upload-time = "2026-05-22T16:21:03.398Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b2/1d2a993c532146dce9eca5b5942d51898021c3579ce18b2454f932a915f8/pytest_codspeed-5.0.3-py3-none-any.whl", hash = "sha256:fe2ea83c924c2250675b75686c3ee456b8cf0208d83d552e182a195fdf467378", size = 74033, upload-time = "2026-05-22T16:20:26.814Z" }, +] + [[package]] name = "pytest-mock" version = "3.14.1" @@ -3070,6 +3130,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "rpds-py" version = "0.25.1"