diff --git a/.github/workflows/codspeed_benchmarks.yaml b/.github/workflows/codspeed_benchmarks.yaml new file mode 100644 index 0000000000..0f7dbe310c --- /dev/null +++ b/.github/workflows/codspeed_benchmarks.yaml @@ -0,0 +1,65 @@ +name: Benchmarks + +permissions: + contents: read + id-token: write # for OpenID Connect authentication with CodSpeed + +on: + workflow_dispatch: + +jobs: + benchmarks: + name: Run benchmarks + runs-on: codspeed-macro + services: + kafka: + image: confluentinc/cp-kafka:8.0.0 + ports: + - 9092:9092 + env: + KAFKA_NODE_ID: "1" + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" + KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://127.0.0.1:9092" + KAFKA_PROCESS_ROLES: "broker,controller" + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_BROKER_ID: "1" + KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka:9093" + KAFKA_LISTENERS: "PLAINTEXT://:9092,CONTROLLER://:9093" + KAFKA_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" + KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER" + CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk" + rabbitmq: + image: rabbitmq:alpine + ports: + - 5672:5672 + nats: + image: diementros/nats:js + ports: + - 4222:4222 + redis: + image: redis:alpine + ports: + - 6379:6379 + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + - name: Install uv + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + with: + version: "latest" + - name: Set up Python + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: "3.13" + - name: Install Dependencies + run: | + uv pip install --system --group optionals --group testing . + - name: Install Pydantic v2 + run: uv pip install --system --prerelease=disallow "pydantic>=2.0.0,<3.0.0" + - name: Run the benchmarks + uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # v4.5.2 + with: + mode: walltime + run: uv run pytest benchmarks/ --codspeed + token: ${{ secrets.CODSPEED_TOKEN }} diff --git a/.gitignore b/.gitignore index 150f825b31..63b93e3b53 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ htmlcov token .DS_Store *.egg-info +.codspeed docs/site/ docs/site_build/ diff --git a/README.md b/README.md index a0788fae87..6fd04710e0 100644 --- a/README.md +++ b/README.md @@ -396,6 +396,9 @@ More integration features can be found [here](https://faststream.ag2.ai/latest/g --- +## Benchmarks +We use codspeed to run benchmarks for both FastStream itself and raw clients. + ## Stay in touch Please show your support and stay in touch by: diff --git a/benchmarks/README.md b/benchmarks/README.md index 9a0282496d..cd33a09b9d 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -13,6 +13,6 @@ docker run --rm \ --network=host \ python:3.12-slim \ /bin/bash -c " - pip install 'faststream[rabbit,redis,nats,kafka,confluent]==0.6.0rc0' fast-depends psutil && \ + pip install 'faststream[rabbit,redis,nats,kafka,confluent]==0.6.0rc0' fast-depends psutil pytest && \ python bench.py" ``` diff --git a/benchmarks/bench.py b/benchmarks/bench.py index baa5f33a99..c4484eb1d8 100644 --- a/benchmarks/bench.py +++ b/benchmarks/bench.py @@ -20,9 +20,14 @@ class TestCase(Protocol): broker_type: str comment: str + def setup_method(self) -> None: ... + @asynccontextmanager async def start(self) -> AsyncIterator[float]: ... + @asynccontextmanager + async def test_consume_message(self) -> None: ... + @dataclass class MeasureResult: @@ -57,9 +62,11 @@ async def main(case: TestCase, measure_time: int) -> MeasureResult: if __name__ == "__main__": - from rabbit_cases.basic import RabbitTestCase + from rabbit_cases.test_aiopika import TestRabbitCase + + case: TestCase = TestRabbitCase() - case: TestCase = RabbitTestCase() + case.setup_method() bench_file = Path(__file__).resolve().parent / "benches.csv" diff --git a/benchmarks/benches.csv b/benchmarks/benches.csv index 4a5dc67c75..bcbe91a1cc 100644 --- a/benchmarks/benches.csv +++ b/benchmarks/benches.csv @@ -1,11 +1,11 @@ -FastStream Version;Broker;Total Events;Event per second;Elapsed Time;Measure Time;Python Version;Comments;Host Memory -0.5.48;RabbitMQ;530557;883.1;600.7908401489258;2025-08-15T16:27:12.832939+00:00;3.12.11;Consume Any Message;15.03 GB -0.6.0rc0;RabbitMQ;584133;972.42;600.7002210617065;2025-08-15T14:55:56.580142+00:00;3.12.11;Consume Any Message;15.03 GB -0.5.48;NATS;804013;1339.69;600.1491076946259;2025-08-15T17:13:45.211546+00:00;3.12.11;Consume Any Message;15.03 GB -0.6.0rc0;NATS;954535;1590.36;600.2000441551208;2025-08-15T17:37:55.896376+00:00;3.12.11;Consume Any Message;15.03 GB -0.5.48;Redis;683286;1138.22;600.3099718093872;2025-08-15T18:29:38.791560+00:00;3.12.11;Consume Any Message;15.03 GB -0.6.0rc0;Redis;653702;1088.94;600.3076493740082;2025-08-15T18:42:33.665307+00:00;3.12.11;Consume Any Message;15.03 GB -0.5.48;Kafka;315477;525.64;600.1727740764618;2025-08-17T12:15:45.879776+00:00;3.12.11;Consume Any Message;15.03 GB -0.6.0rc0;Kafka;351082;584.99;600.1487319469452;2025-08-17T12:33:43.495466+00:00;3.12.11;Consume Any Message;15.03 GB -0.5.48;Confluent;357784;595.93;600.3742282390594;2025-08-17T14:55:24.771995+00:00;3.12.11;Consume Any Message;15.03 GB -0.6.0rc0;Confluent;385732;642.49;600.3732979297638;2025-08-17T15:37:25.498074+00:00;3.12.11;Consume Any Message;15.03 GB +FastStream Version;Broker;Total Events;Event per second;Elapsed Time;Measure Time;Python Version;Comments;Host Memory +0.5.48;RabbitMQ;530557;883.1;600.7908401489258;2025-08-15T16:27:12.832939+00:00;3.12.11;Consume Any Message;15.03 GB +0.6.0rc0;RabbitMQ;584133;972.42;600.7002210617065;2025-08-15T14:55:56.580142+00:00;3.12.11;Consume Any Message;15.03 GB +0.5.48;NATS;804013;1339.69;600.1491076946259;2025-08-15T17:13:45.211546+00:00;3.12.11;Consume Any Message;15.03 GB +0.6.0rc0;NATS;954535;1590.36;600.2000441551208;2025-08-15T17:37:55.896376+00:00;3.12.11;Consume Any Message;15.03 GB +0.5.48;Redis;683286;1138.22;600.3099718093872;2025-08-15T18:29:38.791560+00:00;3.12.11;Consume Any Message;15.03 GB +0.6.0rc0;Redis;653702;1088.94;600.3076493740082;2025-08-15T18:42:33.665307+00:00;3.12.11;Consume Any Message;15.03 GB +0.5.48;Kafka;315477;525.64;600.1727740764618;2025-08-17T12:15:45.879776+00:00;3.12.11;Consume Any Message;15.03 GB +0.6.0rc0;Kafka;351082;584.99;600.1487319469452;2025-08-17T12:33:43.495466+00:00;3.12.11;Consume Any Message;15.03 GB +0.5.48;Confluent;357784;595.93;600.3742282390594;2025-08-17T14:55:24.771995+00:00;3.12.11;Consume Any Message;15.03 GB +0.6.0rc0;Confluent;385732;642.49;600.3732979297638;2025-08-17T15:37:25.498074+00:00;3.12.11;Consume Any Message;15.03 GB diff --git a/benchmarks/confluent_cases/schemas/__init__.py b/benchmarks/confluent_cases/schemas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/benchmarks/confluent_cases/schemas/msgspec.py b/benchmarks/confluent_cases/schemas/msgspec.py new file mode 100644 index 0000000000..8ee3206ba8 --- /dev/null +++ b/benchmarks/confluent_cases/schemas/msgspec.py @@ -0,0 +1,11 @@ +from msgspec import Struct + + +class BaseSchema(Struct): + name: str + age: int + fullname: str + + +class Schema(BaseSchema): + children: list[BaseSchema] diff --git a/benchmarks/confluent_cases/schemas/pydantic.py b/benchmarks/confluent_cases/schemas/pydantic.py new file mode 100644 index 0000000000..1f9e789e93 --- /dev/null +++ b/benchmarks/confluent_cases/schemas/pydantic.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + + +class BaseSchema(BaseModel): + name: str + age: int + fullname: str + + +class Schema(BaseSchema): + children: list[BaseSchema] diff --git a/benchmarks/confluent_cases/basic.py b/benchmarks/confluent_cases/test_basic.py similarity index 77% rename from benchmarks/confluent_cases/basic.py rename to benchmarks/confluent_cases/test_basic.py index 162181b1c2..dd811d604c 100644 --- a/benchmarks/confluent_cases/basic.py +++ b/benchmarks/confluent_cases/test_basic.py @@ -1,16 +1,24 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager from typing import Any +import pytest + from faststream.confluent import KafkaBroker, TopicPartition -class ConfluentTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestConfluentCase: comment = "Consume Any Message" broker_type = "Confluent" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = KafkaBroker(logger=None, graceful_timeout=10) @@ -41,3 +49,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(6.0) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/confluent_cases/test_confluent.py b/benchmarks/confluent_cases/test_confluent.py new file mode 100644 index 0000000000..b54f254c4e --- /dev/null +++ b/benchmarks/confluent_cases/test_confluent.py @@ -0,0 +1,88 @@ +import asyncio +import json +import time +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager + +import pytest +from confluent_kafka import Consumer, Producer, TopicPartition + +from faststream._internal.utils.functions import run_in_executor + +from .schemas.pydantic import Schema + + +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestConfluentCase: + comment = "Pure confluent client with pydantic" + broker_type = "Confluent" + + def setup_method(self) -> None: + self.EVENTS_PROCESSED = 0 + + self.producer = Producer({ + "bootstrap.servers": "localhost:9092", + }) + + self.consumer = Consumer({ + "bootstrap.servers": "localhost:9092", + "group.id": "test-group", + "enable.auto.commit": True, + "auto.offset.reset": "earliest", + }) + self.consumer.assign([TopicPartition("in", 0, 0)]) + + @asynccontextmanager + async def start(self) -> AsyncIterator[float]: + stop_event = asyncio.Event() + + def acked(err, msg) -> None: # noqa: ANN001 + if err is not None: + print(f"Failed to deliver message: {msg!s}: {err!s}") + + def handle() -> None: + while not stop_event.is_set(): + try: + msg = self.consumer.poll(timeout=0.01) + except RuntimeError: + break + if msg is None: + continue + self.EVENTS_PROCESSED += 1 + data = json.loads(msg.value().decode("utf-8")) + parsed = Schema(**data) + self.producer.produce( + "in", value=parsed.model_dump_json().encode("utf-8"), callback=acked + ) + self.producer.flush() + + loop = asyncio.get_event_loop() + start_time = time.time() + executor_task = loop.run_in_executor(None, handle) + + value = json.dumps({ + "name": "John", + "age": 39, + "fullname": "LongString" * 8, + "children": [{"name": "Mike", "age": 8, "fullname": "LongString" * 8}], + }).encode("utf-8") + + await run_in_executor(None, self.producer.produce, "in", value=value) + await run_in_executor(None, self.producer.poll, 0) + + try: + yield start_time + finally: + stop_event.set() + await executor_task + await run_in_executor(None, self.producer.flush) + await run_in_executor(None, self.consumer.close) + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(6.0) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/confluent_cases/msgspec.py b/benchmarks/confluent_cases/test_msgspec.py similarity index 76% rename from benchmarks/confluent_cases/msgspec.py rename to benchmarks/confluent_cases/test_msgspec.py index f322c547cc..2a96cadd92 100644 --- a/benchmarks/confluent_cases/msgspec.py +++ b/benchmarks/confluent_cases/test_msgspec.py @@ -1,28 +1,26 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager +import pytest from fast_depends.msgspec import MsgSpecSerializer -from msgspec import Struct from faststream.confluent import KafkaBroker +from .schemas.msgspec import Schema -class BaseSchema(Struct): - name: str - age: int - fullname: str - -class Schema(BaseSchema): - children: list[BaseSchema] - - -class ConfluentTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestConfluentCase: comment = "Consume Msgspec Struct" broker_type = "Confluent" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = KafkaBroker( @@ -55,3 +53,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(6.0) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/confluent_cases/pydantic.py b/benchmarks/confluent_cases/test_pydantic.py similarity index 73% rename from benchmarks/confluent_cases/pydantic.py rename to benchmarks/confluent_cases/test_pydantic.py index 3d9335d5cc..9e0ac130dc 100644 --- a/benchmarks/confluent_cases/pydantic.py +++ b/benchmarks/confluent_cases/test_pydantic.py @@ -1,27 +1,25 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from pydantic import BaseModel +import pytest from faststream.confluent import KafkaBroker +from .schemas.pydantic import Schema -class BaseSchema(BaseModel): - name: str - age: int - fullname: str - -class Schema(BaseSchema): - children: list[BaseSchema] - - -class ConfluentTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestConfluentCase: comment = "Consume Pydantic Model" broker_type = "Confluent" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = KafkaBroker(logger=None, graceful_timeout=10) @@ -50,3 +48,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(6.0) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/kafka_cases/schemas/__init__.py b/benchmarks/kafka_cases/schemas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/benchmarks/kafka_cases/schemas/msgspec.py b/benchmarks/kafka_cases/schemas/msgspec.py new file mode 100644 index 0000000000..8ee3206ba8 --- /dev/null +++ b/benchmarks/kafka_cases/schemas/msgspec.py @@ -0,0 +1,11 @@ +from msgspec import Struct + + +class BaseSchema(Struct): + name: str + age: int + fullname: str + + +class Schema(BaseSchema): + children: list[BaseSchema] diff --git a/benchmarks/kafka_cases/schemas/pydantic.py b/benchmarks/kafka_cases/schemas/pydantic.py new file mode 100644 index 0000000000..1f9e789e93 --- /dev/null +++ b/benchmarks/kafka_cases/schemas/pydantic.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + + +class BaseSchema(BaseModel): + name: str + age: int + fullname: str + + +class Schema(BaseSchema): + children: list[BaseSchema] diff --git a/benchmarks/kafka_cases/test_aiokafka.py b/benchmarks/kafka_cases/test_aiokafka.py new file mode 100644 index 0000000000..d0a8952c65 --- /dev/null +++ b/benchmarks/kafka_cases/test_aiokafka.py @@ -0,0 +1,88 @@ +import asyncio +import json +import time +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager, suppress + +import pytest +from aiokafka import AIOKafkaConsumer, AIOKafkaProducer +from aiokafka.admin import AIOKafkaAdminClient, NewTopic + +from .schemas.pydantic import Schema + + +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestKafkaCase: + comment = "Pure aio-kafka client with pydantic" + broker_type = "Kafka" + + def setup_method(self) -> None: + self.EVENTS_PROCESSED = 0 + + async def create_topic(self) -> None: + admin = AIOKafkaAdminClient(bootstrap_servers="localhost:9092") + await admin.start() + try: + await admin.create_topics([ + NewTopic(name="in", num_partitions=1, replication_factor=1) + ]) + finally: + await admin.close() + + @asynccontextmanager + async def start(self) -> AsyncIterator[float]: + await self.create_topic() + producer = AIOKafkaProducer(bootstrap_servers="localhost:9092") + consumer = AIOKafkaConsumer( + "in", + bootstrap_servers="localhost:9092", + auto_offset_reset="earliest", + enable_auto_commit=True, + ) + await producer.start() + await consumer.start() + + start_time = time.time() + stop_event = asyncio.Event() + + await producer.send_and_wait( + "in", + json.dumps({ + "name": "John", + "age": 39, + "fullname": "LongString" * 8, + "children": [{"name": "Mike", "age": 8, "fullname": "LongString" * 8}], + }).encode(), + ) + + async def message_loop() -> None: + try: + async for msg in consumer: + if stop_event.is_set(): + break + self.EVENTS_PROCESSED += 1 + data = json.loads(msg.value.decode()) + parsed = Schema(**data) + await producer.send_and_wait("in", parsed.model_dump_json().encode()) + except asyncio.CancelledError: + pass + + task = asyncio.create_task(message_loop()) + try: + yield start_time + finally: + stop_event.set() + task.cancel() + with suppress(asyncio.CancelledError): + await task + await producer.stop() + await consumer.stop() + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/kafka_cases/basic.py b/benchmarks/kafka_cases/test_basic.py similarity index 75% rename from benchmarks/kafka_cases/basic.py rename to benchmarks/kafka_cases/test_basic.py index 2bffa1fb2c..5099d05928 100644 --- a/benchmarks/kafka_cases/basic.py +++ b/benchmarks/kafka_cases/test_basic.py @@ -1,16 +1,24 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager from typing import Any +import pytest + from faststream.kafka import KafkaBroker -class KafkaTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestKafkaCase: comment = "Consume Any Message" broker_type = "Kafka" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = KafkaBroker(logger=None, graceful_timeout=10) @@ -39,3 +47,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/kafka_cases/msgspec.py b/benchmarks/kafka_cases/test_msgspec.py similarity index 76% rename from benchmarks/kafka_cases/msgspec.py rename to benchmarks/kafka_cases/test_msgspec.py index 167fe8b63b..22db76a4e2 100644 --- a/benchmarks/kafka_cases/msgspec.py +++ b/benchmarks/kafka_cases/test_msgspec.py @@ -1,28 +1,26 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager +import pytest from fast_depends.msgspec import MsgSpecSerializer -from msgspec import Struct from faststream.kafka import KafkaBroker +from .schemas.msgspec import Schema -class BaseSchema(Struct): - name: str - age: int - fullname: str - -class Schema(BaseSchema): - children: list[BaseSchema] - - -class KafkaTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestKafkaCase: comment = "Consume Msgspec Struct" broker_type = "Kafka" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = KafkaBroker( @@ -55,3 +53,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/kafka_cases/pydantic.py b/benchmarks/kafka_cases/test_pydantic.py similarity index 73% rename from benchmarks/kafka_cases/pydantic.py rename to benchmarks/kafka_cases/test_pydantic.py index c206737981..f1818023a7 100644 --- a/benchmarks/kafka_cases/pydantic.py +++ b/benchmarks/kafka_cases/test_pydantic.py @@ -1,27 +1,25 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from pydantic import BaseModel +import pytest from faststream.kafka import KafkaBroker +from .schemas.pydantic import Schema -class BaseSchema(BaseModel): - name: str - age: int - fullname: str - -class Schema(BaseSchema): - children: list[BaseSchema] - - -class KafkaTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestKafkaCase: comment = "Consume Pydantic Model" broker_type = "Kafka" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = KafkaBroker(logger=None, graceful_timeout=10) @@ -50,3 +48,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/nats_cases/schemas/__init__.py b/benchmarks/nats_cases/schemas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/benchmarks/nats_cases/schemas/msgspec.py b/benchmarks/nats_cases/schemas/msgspec.py new file mode 100644 index 0000000000..8ee3206ba8 --- /dev/null +++ b/benchmarks/nats_cases/schemas/msgspec.py @@ -0,0 +1,11 @@ +from msgspec import Struct + + +class BaseSchema(Struct): + name: str + age: int + fullname: str + + +class Schema(BaseSchema): + children: list[BaseSchema] diff --git a/benchmarks/nats_cases/schemas/pydantic.py b/benchmarks/nats_cases/schemas/pydantic.py new file mode 100644 index 0000000000..1f9e789e93 --- /dev/null +++ b/benchmarks/nats_cases/schemas/pydantic.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + + +class BaseSchema(BaseModel): + name: str + age: int + fullname: str + + +class Schema(BaseSchema): + children: list[BaseSchema] diff --git a/benchmarks/nats_cases/basic.py b/benchmarks/nats_cases/test_basic.py similarity index 75% rename from benchmarks/nats_cases/basic.py rename to benchmarks/nats_cases/test_basic.py index 162caf32d2..1fa3a1415d 100644 --- a/benchmarks/nats_cases/basic.py +++ b/benchmarks/nats_cases/test_basic.py @@ -1,19 +1,26 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager from typing import Any +import pytest + from faststream.nats import NatsBroker -class NatsTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestNatsCase: comment = "Consume Any Message" broker_type = "NATS" - def __init__(self) -> None: - self.EVENTS_PROCESSED = 0 - + def setup_method(self) -> None: broker = self.broker = NatsBroker(logger=None, graceful_timeout=10) + self.EVENTS_PROCESSED = 0 p = self.publisher = broker.publisher("in") @@ -39,3 +46,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/nats_cases/msgspec.py b/benchmarks/nats_cases/test_msgspec.py similarity index 76% rename from benchmarks/nats_cases/msgspec.py rename to benchmarks/nats_cases/test_msgspec.py index 566c92eb81..5cd4d7b19a 100644 --- a/benchmarks/nats_cases/msgspec.py +++ b/benchmarks/nats_cases/test_msgspec.py @@ -1,28 +1,26 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager +import pytest from fast_depends.msgspec import MsgSpecSerializer -from msgspec import Struct from faststream.nats import NatsBroker +from .schemas.msgspec import Schema -class BaseSchema(Struct): - name: str - age: int - fullname: str - -class Schema(BaseSchema): - children: list[BaseSchema] - - -class NatsTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestNatsTestCase: comment = "Consume Msgspec Struct" broker_type = "NATS" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = NatsBroker( @@ -55,3 +53,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/nats_cases/test_nats.py b/benchmarks/nats_cases/test_nats.py new file mode 100644 index 0000000000..e074c92320 --- /dev/null +++ b/benchmarks/nats_cases/test_nats.py @@ -0,0 +1,56 @@ +import asyncio +import json +import time +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from typing import Any + +import nats +import pytest + +from .schemas.pydantic import Schema + + +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestNatsTestCase: + comment = "Pure nats_py client with pydantic" + broker_type = "NATS" + + def setup_method(self) -> None: + self.EVENTS_PROCESSED = 0 + + @asynccontextmanager + async def start(self) -> AsyncIterator[float]: + nc = await nats.connect(servers=["nats://localhost:4222"]) + + async def message_handler(msg: Any) -> None: + self.EVENTS_PROCESSED += 1 + data = json.loads(msg.data.decode("utf-8")) + parsed = Schema(**data) + + await nc.publish("in", parsed.model_dump_json().encode()) + + await nc.subscribe("in", cb=message_handler) + start_time = time.time() + + await nc.publish( + "in", + json.dumps({ + "name": "John", + "age": 39, + "fullname": "LongString" * 8, + "children": [{"name": "Mike", "age": 8, "fullname": "LongString" * 8}], + }).encode("utf-8"), + ) + yield start_time + + await nc.close() + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/nats_cases/pydantic.py b/benchmarks/nats_cases/test_pydantic.py similarity index 73% rename from benchmarks/nats_cases/pydantic.py rename to benchmarks/nats_cases/test_pydantic.py index 5fd6a37d3f..ea8b1f0099 100644 --- a/benchmarks/nats_cases/pydantic.py +++ b/benchmarks/nats_cases/test_pydantic.py @@ -1,27 +1,25 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from pydantic import BaseModel +import pytest from faststream.nats import NatsBroker +from .schemas.pydantic import Schema -class BaseSchema(BaseModel): - name: str - age: int - fullname: str - -class Schema(BaseSchema): - children: list[BaseSchema] - - -class NatsTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestNatsTestCase: comment = "Consume Pydantic Model" broker_type = "NATS" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = NatsBroker(logger=None, graceful_timeout=10) @@ -50,3 +48,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/nats_cases/stream.py b/benchmarks/nats_cases/test_stream.py similarity index 76% rename from benchmarks/nats_cases/stream.py rename to benchmarks/nats_cases/test_stream.py index 02ef6e083d..5d683b663d 100644 --- a/benchmarks/nats_cases/stream.py +++ b/benchmarks/nats_cases/test_stream.py @@ -1,16 +1,24 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager from typing import Any +import pytest + from faststream.nats import NatsBroker -class NatsTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestNatsTestCase: comment = "Consume from JetStream" broker_type = "NATS" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = NatsBroker(logger=None, graceful_timeout=10) @@ -39,3 +47,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/rabbit_cases/schemas/__init__.py b/benchmarks/rabbit_cases/schemas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/benchmarks/rabbit_cases/schemas/msgspec.py b/benchmarks/rabbit_cases/schemas/msgspec.py new file mode 100644 index 0000000000..8ee3206ba8 --- /dev/null +++ b/benchmarks/rabbit_cases/schemas/msgspec.py @@ -0,0 +1,11 @@ +from msgspec import Struct + + +class BaseSchema(Struct): + name: str + age: int + fullname: str + + +class Schema(BaseSchema): + children: list[BaseSchema] diff --git a/benchmarks/rabbit_cases/schemas/pydantic.py b/benchmarks/rabbit_cases/schemas/pydantic.py new file mode 100644 index 0000000000..1f9e789e93 --- /dev/null +++ b/benchmarks/rabbit_cases/schemas/pydantic.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + + +class BaseSchema(BaseModel): + name: str + age: int + fullname: str + + +class Schema(BaseSchema): + children: list[BaseSchema] diff --git a/benchmarks/rabbit_cases/aiopika.py b/benchmarks/rabbit_cases/test_aiopika.py similarity index 66% rename from benchmarks/rabbit_cases/aiopika.py rename to benchmarks/rabbit_cases/test_aiopika.py index 53cb0e4f05..30776a11c1 100644 --- a/benchmarks/rabbit_cases/aiopika.py +++ b/benchmarks/rabbit_cases/test_aiopika.py @@ -1,16 +1,25 @@ +import asyncio import json import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager import aio_pika +import pytest +from .schemas.pydantic import Schema -class RabbitTestCase: - comment = "Pure aio-pika" + +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestRabbitCase: + comment = "Pure aio-pika with pydantic" broker_type = "RabbitMQ" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 @asynccontextmanager @@ -19,11 +28,12 @@ async def start(self) -> AsyncIterator[float]: channel = await connection.channel() async def handler(msg: aio_pika.IncomingMessage) -> None: - self.EVENTS_PROCESSED += 1 - async with msg.process(): + self.EVENTS_PROCESSED += 1 + data = json.loads(msg.body.decode()) + parsed = Schema(**data) await channel.default_exchange.publish( - aio_pika.Message(msg.body), + aio_pika.Message(parsed.model_dump_json().encode()), routing_key="in", ) @@ -47,5 +57,9 @@ async def handler(msg: aio_pika.IncomingMessage) -> None: ) yield start_time - await connection.close() + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/rabbit_cases/basic.py b/benchmarks/rabbit_cases/test_basic.py similarity index 75% rename from benchmarks/rabbit_cases/basic.py rename to benchmarks/rabbit_cases/test_basic.py index 2199378006..f202cf86f5 100644 --- a/benchmarks/rabbit_cases/basic.py +++ b/benchmarks/rabbit_cases/test_basic.py @@ -1,16 +1,24 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager from typing import Any +import pytest + from faststream.rabbit import RabbitBroker -class RabbitTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestRabbitCase: comment = "Consume Any Message" broker_type = "RabbitMQ" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = RabbitBroker(logger=None, graceful_timeout=10) @@ -39,3 +47,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/rabbit_cases/msgspec.py b/benchmarks/rabbit_cases/test_msgspec.py similarity index 76% rename from benchmarks/rabbit_cases/msgspec.py rename to benchmarks/rabbit_cases/test_msgspec.py index 2f19b996d2..2e8e553de8 100644 --- a/benchmarks/rabbit_cases/msgspec.py +++ b/benchmarks/rabbit_cases/test_msgspec.py @@ -1,28 +1,26 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager +import pytest from fast_depends.msgspec import MsgSpecSerializer -from msgspec import Struct from faststream.rabbit import RabbitBroker +from .schemas.msgspec import Schema -class BaseSchema(Struct): - name: str - age: int - fullname: str - -class Schema(BaseSchema): - children: list[BaseSchema] - - -class RabbitTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestRabbitCase: comment = "Consume Msgspec Struct" broker_type = "RabbitMQ" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = RabbitBroker( @@ -55,3 +53,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/rabbit_cases/pydantic.py b/benchmarks/rabbit_cases/test_pydantic.py similarity index 73% rename from benchmarks/rabbit_cases/pydantic.py rename to benchmarks/rabbit_cases/test_pydantic.py index 8edcdaf5cd..af13f47622 100644 --- a/benchmarks/rabbit_cases/pydantic.py +++ b/benchmarks/rabbit_cases/test_pydantic.py @@ -1,27 +1,25 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from pydantic import BaseModel +import pytest from faststream.rabbit import RabbitBroker +from .schemas.pydantic import Schema -class BaseSchema(BaseModel): - name: str - age: int - fullname: str - -class Schema(BaseSchema): - children: list[BaseSchema] - - -class RabbitTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestRabbitCase: comment = "Consume Pydantic Model" broker_type = "RabbitMQ" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = RabbitBroker(logger=None, graceful_timeout=10) @@ -50,3 +48,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/redis_cases/schemas/__init__.py b/benchmarks/redis_cases/schemas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/benchmarks/redis_cases/schemas/msgspec.py b/benchmarks/redis_cases/schemas/msgspec.py new file mode 100644 index 0000000000..8ee3206ba8 --- /dev/null +++ b/benchmarks/redis_cases/schemas/msgspec.py @@ -0,0 +1,11 @@ +from msgspec import Struct + + +class BaseSchema(Struct): + name: str + age: int + fullname: str + + +class Schema(BaseSchema): + children: list[BaseSchema] diff --git a/benchmarks/redis_cases/schemas/pydantic.py b/benchmarks/redis_cases/schemas/pydantic.py new file mode 100644 index 0000000000..1f9e789e93 --- /dev/null +++ b/benchmarks/redis_cases/schemas/pydantic.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + + +class BaseSchema(BaseModel): + name: str + age: int + fullname: str + + +class Schema(BaseSchema): + children: list[BaseSchema] diff --git a/benchmarks/redis_cases/basic.py b/benchmarks/redis_cases/test_basic.py similarity index 75% rename from benchmarks/redis_cases/basic.py rename to benchmarks/redis_cases/test_basic.py index 8db33c1b05..350ec865f5 100644 --- a/benchmarks/redis_cases/basic.py +++ b/benchmarks/redis_cases/test_basic.py @@ -1,16 +1,24 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager from typing import Any +import pytest + from faststream.redis import RedisBroker -class RedisTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestRedisCase: comment = "Consume Any Message" broker_type = "Redis" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = RedisBroker(logger=None, graceful_timeout=10) @@ -39,3 +47,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/redis_cases/msgspec.py b/benchmarks/redis_cases/test_msgspec.py similarity index 76% rename from benchmarks/redis_cases/msgspec.py rename to benchmarks/redis_cases/test_msgspec.py index 32fdde72cd..11a7181eec 100644 --- a/benchmarks/redis_cases/msgspec.py +++ b/benchmarks/redis_cases/test_msgspec.py @@ -1,28 +1,26 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager +import pytest from fast_depends.msgspec import MsgSpecSerializer -from msgspec import Struct from faststream.redis import RedisBroker +from .schemas.msgspec import Schema -class BaseSchema(Struct): - name: str - age: int - fullname: str - -class Schema(BaseSchema): - children: list[BaseSchema] - - -class RedisTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestRedisCase: comment = "Consume Msgspec Struct" broker_type = "Redis" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = RedisBroker( @@ -55,3 +53,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/redis_cases/pydantic.py b/benchmarks/redis_cases/test_pydantic.py similarity index 73% rename from benchmarks/redis_cases/pydantic.py rename to benchmarks/redis_cases/test_pydantic.py index 7afcf678f9..c5dfc3f008 100644 --- a/benchmarks/redis_cases/pydantic.py +++ b/benchmarks/redis_cases/test_pydantic.py @@ -1,27 +1,25 @@ +import asyncio import time from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from pydantic import BaseModel +import pytest from faststream.redis import RedisBroker +from .schemas.pydantic import Schema -class BaseSchema(BaseModel): - name: str - age: int - fullname: str - -class Schema(BaseSchema): - children: list[BaseSchema] - - -class RedisTestCase: +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestRedisCase: comment = "Consume Pydantic Model" broker_type = "Redis" - def __init__(self) -> None: + def setup_method(self) -> None: self.EVENTS_PROCESSED = 0 broker = self.broker = RedisBroker(logger=None, graceful_timeout=10) @@ -50,3 +48,8 @@ async def start(self) -> AsyncIterator[float]: }) yield start_time + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/benchmarks/redis_cases/test_redis.py b/benchmarks/redis_cases/test_redis.py new file mode 100644 index 0000000000..0c5a5579b6 --- /dev/null +++ b/benchmarks/redis_cases/test_redis.py @@ -0,0 +1,65 @@ +import asyncio +import json +import time +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager + +import pytest +import redis.asyncio as redis + +from .schemas.pydantic import Schema + + +@pytest.mark.asyncio() +@pytest.mark.benchmark( + min_time=150, + max_time=300, +) +class TestRedisCase: + comment = "Pure redis client with pydantic" + broker_type = "Redis" + + def setup_method(self) -> None: + self.EVENTS_PROCESSED = 0 + + @asynccontextmanager + async def start(self) -> AsyncIterator[float]: + client = redis.Redis(host="localhost", port=6379, decode_responses=False) + pubsub = client.pubsub() + await pubsub.subscribe("in") + + async def handler() -> None: + async for msg in pubsub.listen(): + if msg["type"] != "message": + continue + self.EVENTS_PROCESSED += 1 + data = json.loads(msg["data"].decode()) + validated = Schema(**data) + await client.publish("in", validated.model_dump_json()) + + start_time = time.time() + + await client.publish( + "in", + json.dumps({ + "name": "John", + "age": 39, + "fullname": "LongString" * 8, + "children": [{"name": "Mike", "age": 8, "fullname": "LongString" * 8}], + }), + ) + + handler_task = asyncio.create_task(handler()) + + try: + yield start_time + finally: + handler_task.cancel() + await pubsub.unsubscribe("in") + await pubsub.aclose() + await client.aclose() + + async def test_consume_message(self) -> None: + async with self.start(): + await asyncio.sleep(1) + assert self.EVENTS_PROCESSED > 1 diff --git a/faststream/_internal/fastapi/_compat.py b/faststream/_internal/fastapi/_compat.py index dc3a9b1a00..61ff679164 100644 --- a/faststream/_internal/fastapi/_compat.py +++ b/faststream/_internal/fastapi/_compat.py @@ -57,13 +57,6 @@ class SolvedDependency: def raise_fastapi_validation_error(errors: list[Any], body: dict[str, Any]) -> Never: raise RequestValidationError(errors, body=body) -elif FASTAPI_V2: - from fastapi._compat import _normalize_errors - from fastapi.exceptions import RequestValidationError - - def raise_fastapi_validation_error(errors: list[Any], body: dict[str, Any]) -> Never: - raise RequestValidationError(_normalize_errors(errors), body=body) - else: from pydantic import ( # type: ignore[assignment] ValidationError as RequestValidationError, diff --git a/pyproject.toml b/pyproject.toml index 095c070511..837581e942 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,6 +154,8 @@ dev = [ {include-group = "docs"}, "pre-commit==4.5.1", "detect-secrets==1.5.0", + "pytest-codspeed>=2.2.1", + "setuptools>=80.9.0", ] [project.urls] @@ -205,6 +207,7 @@ markers = [ "slow", "connected", "all", + "benchmark", ] asyncio_default_fixture_loop_scope = "function" diff --git a/uv.lock b/uv.lock index 71aaa11c25..6dd921c047 100644 --- a/uv.lock +++ b/uv.lock @@ -884,6 +884,7 @@ dev = [ { name = "pydantic-settings" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-codspeed" }, { name = "pytest-cov" }, { name = "pytest-rerunfailures" }, { name = "pytest-timeout" }, @@ -891,6 +892,7 @@ dev = [ { name = "pyyaml" }, { name = "ruff" }, { name = "semgrep" }, + { name = "setuptools" }, { name = "types-aiofiles" }, { name = "types-deprecated" }, { name = "types-docutils" }, @@ -1018,6 +1020,7 @@ dev = [ { name = "pydantic-settings", specifier = ">=2.0.0,<3.0.0" }, { name = "pytest", specifier = "==9.0.2" }, { name = "pytest-asyncio", specifier = "==1.3.0" }, + { name = "pytest-codspeed", specifier = ">=2.2.1" }, { name = "pytest-cov", specifier = ">=6.2.1" }, { name = "pytest-rerunfailures", specifier = ">=15.1" }, { name = "pytest-timeout", specifier = ">=2.4.0" }, @@ -1025,6 +1028,7 @@ dev = [ { name = "pyyaml", specifier = "==6.0.3" }, { name = "ruff", specifier = "==0.14.10" }, { name = "semgrep", specifier = "==1.146.0" }, + { name = "setuptools", specifier = ">=80.9.0" }, { name = "types-aiofiles" }, { name = "types-deprecated" }, { name = "types-docutils" }, @@ -2761,6 +2765,21 @@ 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 = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "filelock" }, + { name = "pytest" }, + { name = "setuptools", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/6a/ba0b067dba286443a04c9d32ff75ef3bc169617ee347d1a97972ada62665/pytest_codspeed-2.2.1.tar.gz", hash = "sha256:0adc24baf01c64a6ca0a0b83b3cd704351708997e09ec086b7776c32227d4e0a", size = 9163, upload-time = "2024-03-19T21:59:57.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/0b/e3541064afcf24ed54bcabfa6eb5f8083eb335d5c58c7b5b95bc31127f86/pytest_codspeed-2.2.1-py3-none-any.whl", hash = "sha256:aad08033015f3e6c8c14c8bf0eca475921a9b088e92c98b626bf8af8f516471e", size = 10126, upload-time = "2024-03-19T21:59:55.388Z" }, +] + [[package]] name = "pytest-cov" version = "7.0.0" @@ -3319,6 +3338,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/e9/4d6f31e21803c571a54cfeeeaaa6c3a7909645a2421e4d247a2ca44543fd/semgrep-1.146.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:57e2590ada20fba58376fe541d6babaae9fb95763f0fe132df0729563af4ce4c", size = 43021046, upload-time = "2025-12-17T20:44:39.742Z" }, ] +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + [[package]] name = "shellingham" version = "1.5.4"