Compare commits
4 Commits
main
...
add-codspe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b383969ef | ||
|
|
9b22e53e28 | ||
|
|
b5e326ab0e | ||
|
|
90bd7126b2 |
34
.github/workflows/benchmark.yml
vendored
Normal file
34
.github/workflows/benchmark.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: CodSpeed
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
benchmarks:
|
||||
name: Run benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install dependencies
|
||||
run: scripts/install
|
||||
shell: bash
|
||||
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@281164b0f014a4e7badd2c02cecad9b595b70537 # v4
|
||||
with:
|
||||
mode: instrumentation
|
||||
run: uv run pytest tests/benchmarks/ --codspeed
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,3 +8,4 @@ venv/
|
||||
htmlcov/
|
||||
site/
|
||||
dist/
|
||||
.codspeed/
|
||||
|
||||
@ -56,6 +56,7 @@ dev = [
|
||||
"pytest==9.0.2",
|
||||
"pytest-mock==3.15.1",
|
||||
"pytest-xdist[psutil]==3.8.0",
|
||||
"pytest-codspeed>=4.1.1",
|
||||
"mypy==1.19.1",
|
||||
"types-click==7.1.8",
|
||||
"types-pyyaml==6.0.12.20250915",
|
||||
@ -135,7 +136,7 @@ filterwarnings = [
|
||||
parallel = true
|
||||
source_pkgs = ["uvicorn", "tests"]
|
||||
plugins = ["coverage_conditional_plugin"]
|
||||
omit = ["uvicorn/workers.py", "uvicorn/__main__.py", "uvicorn/_compat.py"]
|
||||
omit = ["uvicorn/workers.py", "uvicorn/__main__.py", "uvicorn/_compat.py", "tests/benchmarks/*"]
|
||||
|
||||
[tool.coverage.report]
|
||||
precision = 2
|
||||
|
||||
0
tests/benchmarks/__init__.py
Normal file
0
tests/benchmarks/__init__.py
Normal file
174
tests/benchmarks/http.py
Normal file
174
tests/benchmarks/http.py
Normal file
@ -0,0 +1,174 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any, TypeAlias
|
||||
|
||||
from uvicorn._types import ASGIApplication, Scope
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.lifespan.off import LifespanOff
|
||||
from uvicorn.lifespan.on import LifespanOn
|
||||
from uvicorn.protocols.http.h11_impl import H11Protocol
|
||||
from uvicorn.server import ServerState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
|
||||
from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
|
||||
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol as _WSProtocol
|
||||
|
||||
WSProtocol: TypeAlias = WebSocketProtocol | _WSProtocol
|
||||
HTTPProtocol: TypeAlias = H11Protocol | HttpToolsProtocol
|
||||
|
||||
|
||||
SIMPLE_GET_REQUEST = b"\r\n".join([b"GET / HTTP/1.1", b"Host: example.org", b"", b""])
|
||||
|
||||
SIMPLE_POST_REQUEST = b"\r\n".join(
|
||||
[
|
||||
b"POST / HTTP/1.1",
|
||||
b"Host: example.org",
|
||||
b"Content-Type: application/json",
|
||||
b"Content-Length: 18",
|
||||
b"",
|
||||
b'{"hello": "world"}',
|
||||
]
|
||||
)
|
||||
|
||||
LARGE_POST_REQUEST = b"\r\n".join(
|
||||
[
|
||||
b"POST / HTTP/1.1",
|
||||
b"Host: example.org",
|
||||
b"Content-Type: text/plain",
|
||||
b"Content-Length: 100000",
|
||||
b"",
|
||||
b"x" * 100000,
|
||||
]
|
||||
)
|
||||
|
||||
HTTP10_GET_REQUEST = b"\r\n".join([b"GET / HTTP/1.0", b"Host: example.org", b"", b""])
|
||||
|
||||
CONNECTION_CLOSE_REQUEST = b"\r\n".join([b"GET / HTTP/1.1", b"Host: example.org", b"Connection: close", b"", b""])
|
||||
|
||||
START_POST_REQUEST = b"\r\n".join(
|
||||
[
|
||||
b"POST / HTTP/1.1",
|
||||
b"Host: example.org",
|
||||
b"Content-Type: application/json",
|
||||
b"Content-Length: 18",
|
||||
b"",
|
||||
b"",
|
||||
]
|
||||
)
|
||||
|
||||
FINISH_POST_REQUEST = b'{"hello": "world"}'
|
||||
|
||||
BODY_CHUNK_SIZE = 256
|
||||
FRAGMENTED_BODY_SIZE = 100_000
|
||||
FRAGMENTED_POST_HEADERS = b"\r\n".join(
|
||||
[
|
||||
b"POST / HTTP/1.1",
|
||||
b"Host: example.org",
|
||||
b"Content-Type: application/octet-stream",
|
||||
b"Content-Length: " + str(FRAGMENTED_BODY_SIZE).encode(),
|
||||
b"",
|
||||
b"",
|
||||
]
|
||||
)
|
||||
FRAGMENTED_BODY_CHUNKS = [b"x" * BODY_CHUNK_SIZE] * (FRAGMENTED_BODY_SIZE // BODY_CHUNK_SIZE)
|
||||
|
||||
|
||||
class MockTransport:
|
||||
def __init__(self) -> None:
|
||||
self.buffer = b""
|
||||
self.closed = False
|
||||
self.read_paused = False
|
||||
|
||||
def get_extra_info(self, key: Any) -> Any:
|
||||
return {
|
||||
"sockname": ("127.0.0.1", 8000),
|
||||
"peername": ("127.0.0.1", 8001),
|
||||
"sslcontext": False,
|
||||
}.get(key)
|
||||
|
||||
def write(self, data: bytes) -> None:
|
||||
self.buffer += data
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
def pause_reading(self) -> None:
|
||||
self.read_paused = True
|
||||
|
||||
def resume_reading(self) -> None:
|
||||
self.read_paused = False
|
||||
|
||||
def is_closing(self) -> bool:
|
||||
return self.closed
|
||||
|
||||
def clear_buffer(self) -> None:
|
||||
self.buffer = b""
|
||||
|
||||
def set_protocol(self, protocol: asyncio.Protocol) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class MockTimerHandle:
|
||||
def __init__(
|
||||
self, loop_later_list: list[MockTimerHandle], delay: float, callback: Callable[[], None], args: tuple[Any, ...]
|
||||
) -> None:
|
||||
self.loop_later_list = loop_later_list
|
||||
self.delay = delay
|
||||
self.callback = callback
|
||||
self.args = args
|
||||
self.cancelled = False
|
||||
|
||||
def cancel(self) -> None:
|
||||
if not self.cancelled:
|
||||
self.cancelled = True
|
||||
self.loop_later_list.remove(self)
|
||||
|
||||
|
||||
class MockLoop:
|
||||
def __init__(self) -> None:
|
||||
self._tasks: list[asyncio.Task[Any]] = []
|
||||
self._later: list[MockTimerHandle] = []
|
||||
|
||||
def create_task(self, coroutine: Any) -> Any:
|
||||
self._tasks.insert(0, coroutine)
|
||||
return MockTask()
|
||||
|
||||
def call_later(self, delay: float, callback: Callable[[], None], *args: Any) -> MockTimerHandle:
|
||||
handle = MockTimerHandle(self._later, delay, callback, args)
|
||||
self._later.insert(0, handle)
|
||||
return handle
|
||||
|
||||
async def run_one(self) -> Any:
|
||||
return await self._tasks.pop()
|
||||
|
||||
|
||||
class MockTask:
|
||||
def add_done_callback(self, callback: Callable[[], None]) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class MockProtocol(asyncio.Protocol):
|
||||
loop: MockLoop
|
||||
transport: MockTransport
|
||||
timeout_keep_alive_task: asyncio.TimerHandle | None
|
||||
ws_protocol_class: type[WSProtocol] | None
|
||||
scope: Scope
|
||||
|
||||
|
||||
def get_connected_protocol(
|
||||
app: ASGIApplication,
|
||||
http_protocol_cls: type[HTTPProtocol],
|
||||
lifespan: LifespanOff | LifespanOn | None = None,
|
||||
**kwargs: Any,
|
||||
) -> MockProtocol:
|
||||
loop = MockLoop()
|
||||
transport = MockTransport()
|
||||
config = Config(app=app, **kwargs)
|
||||
lifespan = lifespan or LifespanOff(config)
|
||||
server_state = ServerState()
|
||||
protocol = http_protocol_cls(config=config, server_state=server_state, app_state=lifespan.state, _loop=loop) # type: ignore
|
||||
protocol.connection_made(transport) # type: ignore[arg-type]
|
||||
return protocol # type: ignore[return-value]
|
||||
108
tests/benchmarks/test_http.py
Normal file
108
tests/benchmarks/test_http.py
Normal file
@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.benchmarks.http import (
|
||||
CONNECTION_CLOSE_REQUEST,
|
||||
FINISH_POST_REQUEST,
|
||||
FRAGMENTED_BODY_CHUNKS,
|
||||
FRAGMENTED_POST_HEADERS,
|
||||
HTTP10_GET_REQUEST,
|
||||
LARGE_POST_REQUEST,
|
||||
SIMPLE_GET_REQUEST,
|
||||
SIMPLE_POST_REQUEST,
|
||||
START_POST_REQUEST,
|
||||
get_connected_protocol,
|
||||
)
|
||||
from tests.response import Response
|
||||
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tests.benchmarks.http import HTTPProtocol
|
||||
|
||||
pytestmark = [pytest.mark.anyio, pytest.mark.benchmark]
|
||||
|
||||
_plain_text_app = Response("Hello, world", media_type="text/plain")
|
||||
_no_content_app = Response(b"", status_code=204)
|
||||
_chunked_app = Response(b"Hello, world!", status_code=200, headers={"transfer-encoding": "chunked"})
|
||||
|
||||
|
||||
async def _body_echo_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
body = b""
|
||||
while True:
|
||||
message = await receive()
|
||||
body += message.get("body", b"") # type: ignore[operator]
|
||||
if not message.get("more_body", False):
|
||||
break
|
||||
headers = [(b"content-length", str(len(body)).encode())]
|
||||
await send({"type": "http.response.start", "status": 200, "headers": headers})
|
||||
await send({"type": "http.response.body", "body": body})
|
||||
|
||||
|
||||
async def test_bench_simple_get(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_app, http_protocol_cls)
|
||||
protocol.data_received(SIMPLE_GET_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_simple_post(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_app, http_protocol_cls)
|
||||
protocol.data_received(SIMPLE_POST_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_large_post(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_app, http_protocol_cls)
|
||||
protocol.data_received(LARGE_POST_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_pipelined_requests(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_app, http_protocol_cls)
|
||||
protocol.data_received(SIMPLE_GET_REQUEST * 3)
|
||||
await protocol.loop.run_one()
|
||||
await protocol.loop.run_one()
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_keepalive_reuse(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_app, http_protocol_cls)
|
||||
protocol.data_received(SIMPLE_GET_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
protocol.data_received(SIMPLE_GET_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_chunked_response(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_chunked_app, http_protocol_cls)
|
||||
protocol.data_received(SIMPLE_GET_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_http10(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_app, http_protocol_cls)
|
||||
protocol.data_received(HTTP10_GET_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_connection_close(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_app, http_protocol_cls)
|
||||
protocol.data_received(CONNECTION_CLOSE_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_fragmented_body(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_app, http_protocol_cls)
|
||||
protocol.data_received(FRAGMENTED_POST_HEADERS)
|
||||
for chunk in FRAGMENTED_BODY_CHUNKS:
|
||||
protocol.data_received(chunk)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_post_body_receive(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_body_echo_app, http_protocol_cls)
|
||||
protocol.data_received(START_POST_REQUEST)
|
||||
protocol.data_received(FINISH_POST_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
30
uv.lock
generated
30
uv.lock
generated
@ -1284,6 +1284,32 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-codspeed"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
{ name = "pytest" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/ab/eca41967d11c95392829a8b4bfa9220a51cffc4a33ec4653358000356918/pytest_codspeed-4.3.0.tar.gz", hash = "sha256:5230d9d65f39063a313ed1820df775166227ec5c20a1122968f85653d5efee48", size = 124745, upload-time = "2026-02-09T15:23:34.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/64/800bdaeabd3eb126aff7e3e22dc45b2826305f61cbfd093284caf8d9ca01/pytest_codspeed-4.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2acecc4126658abebc683b38121adec405a46e18a619d49d6154c6e60c5deb2", size = 347077, upload-time = "2026-02-09T15:23:17.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/f1/d69707440829adab86d078d5f1c8c070df116b1624f8eae4ff36933ba612/pytest_codspeed-4.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:619120775e92a3f43fb4ff4c256a251b1554c904d95e2154a382484283f0388a", size = 342234, upload-time = "2026-02-09T15:23:18.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/15/ec0ac1f022173b3134c9638f2a35f21fbb3142c75da066d9e49e5a8bb4bd/pytest_codspeed-4.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbeff1eb2f2e36df088658b556fa993e6937bf64ffb07406de4db16fd2b26874", size = 347076, upload-time = "2026-02-09T15:23:19.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/e8/1fe375794ad02b7835f378a7bcfa8fbac9acadefe600a782a7c4a7064db7/pytest_codspeed-4.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:878aad5e4bb7b401ad8d82f3af5186030cd2bd0d0446782e10dabb9db8827466", size = 342215, upload-time = "2026-02-09T15:23:20.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/58/50df94e9a78e1c77818a492c90557eeb1309af025120c9a21e6375950c52/pytest_codspeed-4.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527a3a02eaa3e4d4583adc4ba2327eef79628f3e1c682a4b959439551a72588e", size = 347395, upload-time = "2026-02-09T15:23:21.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/56/7dfbd3eefd112a14e6fb65f9ff31dacf2e9c381cb94b27332b81d2b13f8d/pytest_codspeed-4.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9858c2a6e1f391d5696757e7b6e9484749a7376c46f8b4dd9aebf093479a9667", size = 342625, upload-time = "2026-02-09T15:23:23.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/53/7255f6a25bc56ff1745b254b21545dfe0be2268f5b91ce78f7e8a908f0ad/pytest_codspeed-4.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34f2fd8497456eefbd325673f677ea80d93bb1bc08a578c1fa43a09cec3d1879", size = 347325, upload-time = "2026-02-09T15:23:23.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/f8/82ae570d8b9ad30f33c9d4002a7a1b2740de0e090540c69a28e4f711ebe2/pytest_codspeed-4.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df6a36a2a9da1406bc50428437f657f0bd8c842ae54bee5fb3ad30e01d50c0f5", size = 342558, upload-time = "2026-02-09T15:23:25.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/e1/55cfe9474f91d174c7a4b04d257b5fc6d4d06f3d3680f2da672ee59ccc10/pytest_codspeed-4.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bec30f4fc9c4973143cd80f0d33fa780e9fa3e01e4dbe8cedf229e72f1212c62", size = 347383, upload-time = "2026-02-09T15:23:26.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3b/8fd781d959bbe789b3de8ce4c50d5706a684a0df377147dfb27b200c20c1/pytest_codspeed-4.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6584e641cadf27d894ae90b87c50377232a97cbfd76ee0c7ecd0c056fa3f7f4", size = 342481, upload-time = "2026-02-09T15:23:27.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/0c/368045133c6effa2c665b1634b7b8a9c88b307f877fa31f1f8df47885b51/pytest_codspeed-4.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0d1f6ea594f29b745c634d66d5f5f1caa1c3abd2af82fea49d656038e8fc77", size = 353680, upload-time = "2026-02-09T15:23:28.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/21/e543abcd72244294e25ae88ec3a9311ade24d6913f8c8f42569d671700bc/pytest_codspeed-4.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2f5bb6d8898bea7db45e3c8b916ee48e36905b929477bb511b79c5a3ccacda4", size = 347888, upload-time = "2026-02-09T15:23:30.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/d9/b8a53c20cf5b41042c205bb9d36d37da00418d30fd1a94bf9eb147820720/pytest_codspeed-4.3.0-py3-none-any.whl", hash = "sha256:05baff2a61dc9f3e92b92b9c2ab5fb45d9b802438f5373073f5766a91319ed7a", size = 125224, upload-time = "2026-02-09T15:23:33.774Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-mock"
|
||||
version = "3.15.1"
|
||||
@ -1696,6 +1722,7 @@ dev = [
|
||||
{ name = "httpx" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-codspeed" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-xdist", extra = ["psutil"] },
|
||||
{ name = "ruff" },
|
||||
@ -1724,7 +1751,7 @@ requires-dist = [
|
||||
{ name = "pyyaml", marker = "extra == 'standard'", specifier = ">=5.1" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'", specifier = ">=4.0" },
|
||||
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' and extra == 'standard'", specifier = ">=0.15.1" },
|
||||
{ name = "watchfiles", marker = "extra == 'standard'", specifier = ">=0.13" },
|
||||
{ name = "watchfiles", marker = "extra == 'standard'", specifier = ">=0.20" },
|
||||
{ name = "websockets", marker = "extra == 'standard'", specifier = ">=10.4" },
|
||||
]
|
||||
provides-extras = ["standard"]
|
||||
@ -1739,6 +1766,7 @@ dev = [
|
||||
{ name = "httpx", specifier = "==0.28.1" },
|
||||
{ name = "mypy", specifier = "==1.19.1" },
|
||||
{ name = "pytest", specifier = "==9.0.2" },
|
||||
{ name = "pytest-codspeed", specifier = ">=4.1.1" },
|
||||
{ name = "pytest-mock", specifier = "==3.15.1" },
|
||||
{ name = "pytest-xdist", extras = ["psutil"], specifier = "==3.8.0" },
|
||||
{ name = "ruff", specifier = "==0.15.1" },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user