diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d1fe2f2..7d5e314 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,10 +42,10 @@ jobs: matrix: python: - - '3.10' - '3.11' - '3.12' - '3.13' + - '3.14' steps: - uses: actions/checkout@v4 - name: Install uv @@ -70,10 +70,10 @@ jobs: matrix: python: - - '3.10' - '3.11' - '3.12' - '3.13' + - '3.14' steps: - uses: actions/checkout@v4 - name: Install uv @@ -98,10 +98,10 @@ jobs: matrix: python: - - '3.10' - '3.11' - '3.12' - '3.13' + - '3.14' steps: - uses: actions/checkout@v4 - name: Install uv diff --git a/README.md b/README.md index a6f014c..c3a6998 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ aiomisc pytest plugin ===================== -This package contains a plugin for pytest. +This package is a pytest plugin that provides fixtures and utilities from `aiomisc.pytest` for testing async code. ## Installation @@ -9,13 +9,29 @@ This package contains a plugin for pytest. # pip install aiomisc-pytest ``` +## Architecture + +This package serves as a pytest plugin entry point for the `aiomisc.pytest` module. + +All fixtures and utilities are re-exported from `aiomisc.pytest`: +- `event_loop`, `entrypoint`, `services` +- `aiomisc_unused_port`, `aiomisc_unused_port_factory` +- `tcp_proxy`, `localhost` +- And more... + +The pytest hooks (`pytest_configure`, `pytest_addoption`) are defined in this +package because pytest discovers plugins through entry points. The plugin is +registered via the `pytest11` entry point in `pyproject.toml`. + +**Requires:** `aiomisc >= 18` + ## Basic usage Simple usage example: ```python async def test_sample(event_loop): - f = event_loop.crete_future() + f = event_loop.create_future() event_loop.call_soon(f.set_result, True) assert await f ``` @@ -30,7 +46,7 @@ import pytest @pytest.fixture -async def my_fixture(loop): +async def my_fixture(event_loop): await asyncio.sleep(0) # Requires python 3.6+ @@ -138,14 +154,14 @@ import aiomisc import pytest -class SimpleServie(aiomisc.Service): +class SimpleService(aiomisc.Service): async def start(self) -> None: pass @pytest.fixture def services(): - return [SimpleServie()] + return [SimpleService()] ``` ## Event loop policy overriding diff --git a/aiomisc_pytest.py b/aiomisc_pytest.py index 8638e84..84010be 100644 --- a/aiomisc_pytest.py +++ b/aiomisc_pytest.py @@ -1,491 +1,11 @@ -import abc -import asyncio -import concurrent.futures -import logging -import os -import platform -import socket -import sys -from collections.abc import ( - AsyncGenerator, - Awaitable, - Callable, - Generator, - Iterable, - Mapping, -) -from contextlib import contextmanager, suppress -from functools import partial, wraps -from inspect import isasyncgenfunction, iscoroutinefunction -from types import ModuleType -from typing import ( - Any, - NamedTuple, -) -from unittest.mock import MagicMock - -import aiomisc import pytest -from aiomisc.compat import set_current_loop, sock_set_reuseport -from aiomisc.utils import TimeoutType, bind_socket -from aiomisc_log import LOG_LEVEL, basic_config - -log = logging.getLogger("aiomisc_pytest") - - -USE_UVLOOP = os.getenv("AIOMISC_USE_UVLOOP", "1").lower() in ( - "1", - "yes", - "true", -) - -uvloop_module: ModuleType | None - -try: - if USE_UVLOOP: - import uvloop as _uvloop - - uvloop_module = _uvloop -except ImportError: - uvloop_module = None - - -log = logging.getLogger(__name__) -ProxyProcessorType = Callable[[bytes], Awaitable[bytes]] - - -class Delay: - __slots__ = "__timeout", "future", "lock" - - def __init__(self) -> None: - self.__timeout: int | float = 0 - self.future: asyncio.Future | None = None - self.lock = asyncio.Lock() - - @property - def timeout(self) -> int | float: - return self.__timeout - - @timeout.setter - def timeout(self, value: TimeoutType) -> None: - assert isinstance(value, (int, float)) - assert value >= 0 - - self.__timeout = value - - if self.future and not self.future.done(): - self.future.set_result(True) - - async def wait(self) -> None: - if self.__timeout == 0: - return - - async with self.lock: - try: - self.future = delayed_future(self.__timeout) - await self.future - finally: - self.future = None - - -def delayed_future( - timeout: int | float, - result: bool = True, -) -> asyncio.Future: - - loop = asyncio.get_event_loop() - - def resolve(f: asyncio.Future) -> None: - nonlocal result # noqa - - if f.done(): - return - f.set_result(result) - - future = loop.create_future() - handle = loop.call_later(timeout, resolve, future) - future.add_done_callback(lambda _: handle.cancel()) - - return future - - -class TCPProxy: - DEFAULT_TIMEOUT = 30 - - __slots__ = ( - "proxy_port", - "clients", - "server", - "proxy_host", - "target_port", - "target_host", - "listen_host", - "read_delay", - "write_delay", - "read_processor", - "write_processor", - "buffered", - ) - - def __init__( - self, - target_host: str, - target_port: int, - listen_host: str = "127.0.0.1", - buffered: bool = True, - ): - self.target_port = target_port - self.target_host = target_host - self.proxy_host = listen_host - self.proxy_port = unused_port() - self.read_delay: TimeoutType = 0 - self.write_delay: TimeoutType = 0 - self.buffered = buffered - - self.clients: set[TCPProxyClient] = set() - self.server: asyncio.AbstractServer | None = None - - self.read_processor: ProxyProcessorType | None = None - self.write_processor: ProxyProcessorType | None = None - - def __repr__(self) -> str: - return f"<{self.__class__.__name__}[{id(self):x}]: tcp://{self.proxy_host}:{self.proxy_port} => tcp://{self.target_host}:{self.target_port}>" - - async def start( - self, - timeout: TimeoutType | None = None, - ) -> asyncio.AbstractServer: - log.debug("Starting %r", self) - server = await asyncio.wait_for( - asyncio.start_server( - self._handle_client, - host=self.proxy_host, - port=self.proxy_port, - ), - timeout=timeout, - ) - self.server = server - return server - - ClientType = tuple[asyncio.StreamReader, asyncio.StreamWriter] - - async def create_client(self) -> ClientType: - log.debug("Creating client for %r", self) - return await asyncio.open_connection( - self.proxy_host, - self.proxy_port, - ) - - async def __aenter__(self) -> "TCPProxy": - if self.server is None: - await self.start(timeout=self.DEFAULT_TIMEOUT) - return self - - async def __aexit__( - self, - exc_type: type[Exception], - exc_val: Exception, - exc_tb: Any, - ) -> None: - await self.close(timeout=self.DEFAULT_TIMEOUT) - - async def close(self, timeout: TimeoutType | None = None) -> None: - async def close() -> None: - await self.disconnect_all() - - if self.server is None: - return - - self.server.close() - await self.server.wait_closed() - - await asyncio.wait_for(close(), timeout=timeout) - - def set_delay( - self, - read_delay: TimeoutType, - write_delay: TimeoutType = 0, - ) -> None: - log.debug("Setting delay [R/W %f %f]", read_delay, write_delay) - - for client in self.clients: - log.debug( - "Applying delays [R/W: %f %f] for %r", - read_delay, - write_delay, - client, - ) - client.read_delay.timeout = read_delay - client.write_delay.timeout = write_delay - - self.read_delay = read_delay - self.write_delay = write_delay - - def set_content_processors( - self, - read: ProxyProcessorType | None, - write: ProxyProcessorType | None, - ) -> None: - log.debug( - "Setting content processors for %r: read=%r write=%r", - self, - read, - write, - ) - - for client in self.clients: - log.debug( - "Applying context processors for %r: read=%r write=%r", - client, - read, - write, - ) - - client.read_processor = read - client.write_processor = write - self.read_processor = read - self.write_processor = write - - def disconnect_all(self) -> asyncio.Future: - log.debug( - "Disconnecting %s clients of %r", - len(self.clients), - self, - ) - return asyncio.ensure_future( - asyncio.gather( - *[client.close() for client in self.clients], - return_exceptions=True, - ), - ) - - async def _handle_client( - self, - reader: asyncio.StreamReader, - writer: asyncio.StreamWriter, - ) -> None: - client = TCPProxyClient(reader, writer, buffered=self.buffered) - self.clients.add(client) - - client.read_delay.timeout = self.read_delay - client.write_delay.timeout = self.write_delay - client.read_processor = self.read_processor - client.write_processor = self.write_processor - - await client.connect(self.target_host, self.target_port) - - client.closing.add_done_callback( - lambda _: self.clients.remove(client), - ) - - @contextmanager - def slowdown( - self, - read_delay: TimeoutType = 0, - write_delay: TimeoutType = 0, - ) -> Generator[None, None, None]: - old_read_delay = self.read_delay - old_write_delay = self.write_delay - - self.set_delay(read_delay, write_delay) - - try: - yield - finally: - self.set_delay(old_read_delay, old_write_delay) - - -class TCPProxyClient: - __slots__ = ( - "client_reader", - "client_writer", - "server_reader", - "server_writer", - "chunk_size", - "tasks", - "loop", - "read_delay", - "write_delay", - "closing", - "__processors", - "__client_repr", - "__server_repr", - "buffered", - ) - - @staticmethod - async def _blank_processor(body: bytes) -> bytes: - return body - - def __init__( - self, - client_reader: asyncio.StreamReader, - client_writer: asyncio.StreamWriter, - chunk_size: int = 64 * 1024, - buffered: bool = False, - ): - - self.loop = asyncio.get_event_loop() - self.client_reader: asyncio.StreamReader = client_reader - self.client_writer: asyncio.StreamWriter = client_writer - - self.server_reader: asyncio.StreamReader | None = None - self.server_writer: asyncio.StreamWriter | None = None - - self.tasks: Iterable[asyncio.Task] = () - self.chunk_size = chunk_size # type: int - self.read_delay = Delay() - self.write_delay = Delay() - - self.closing: asyncio.Future = self.loop.create_future() - self.__processors: dict[str, ProxyProcessorType] = { - "read": self._blank_processor, - "write": self._blank_processor, - } - - self.buffered = bool(buffered) - - self.__client_repr = "" - self.__server_repr = "" - - @property - def read_processor(self) -> ProxyProcessorType: - return self.__processors["read"] - - @read_processor.setter - def read_processor(self, value: ProxyProcessorType | None) -> None: - if value is None: - self.__processors["read"] = self._blank_processor - return - self.__processors["read"] = aiomisc.awaitable(value) - - @property - def write_processor(self) -> ProxyProcessorType: - return self.__processors["write"] - - @write_processor.setter - def write_processor(self, value: ProxyProcessorType | None) -> None: - if value is None: - self.__processors["write"] = self._blank_processor - return - self.__processors["write"] = aiomisc.awaitable(value) - - def __repr__(self) -> str: - return f"<{self.__class__.__name__}[{id(self):x}]: {self.__client_repr} => {self.__server_repr}>" - - @staticmethod - async def _close_writer(writer: asyncio.StreamWriter) -> None: - writer.close() - await writer.wait_closed() - - async def pipe( - self, - reader: asyncio.StreamReader, - writer: asyncio.StreamWriter, - processor: str, - delay: Delay, - ) -> None: - try: - while not reader.at_eof(): - chunk = await reader.read(self.chunk_size) - - if not chunk: - break - - if delay.timeout > 0: - log.debug( - "%r sleeping %.3f seconds on %s", - self, - delay.timeout, - processor, - ) - - await delay.wait() - - writer.write(await self.__processors[processor](chunk)) - - if not self.buffered: - await writer.drain() - finally: - await self._close_writer(writer) - - async def connect(self, target_host: str, target_port: int) -> None: - log.debug("Establishing connection for %r", self) - - self.server_reader, self.server_writer = await asyncio.open_connection( - host=target_host, - port=target_port, - ) - - self.__client_repr = ":".join( - map(str, self.client_writer.get_extra_info("peername")[:2]), - ) - self.__server_repr = ":".join( - map(str, self.server_writer.get_extra_info("peername")[:2]), - ) - - self.tasks = ( - self.loop.create_task( - self.pipe( - self.server_reader, - self.client_writer, - "write", - self.write_delay, - ), - ), - self.loop.create_task( - self.pipe( - self.client_reader, - self.server_writer, - "read", - self.read_delay, - ), - ), - ) - - async def close(self) -> None: - log.debug("Closing %r", self) - if self.closing.done(): - return - - await aiomisc.cancel_tasks(self.tasks) - self.loop.call_soon(self.closing.set_result, True) - await self.closing - - -def unused_port(*args: Any) -> int: - with socket.socket(*args) as sock: - sock.bind(("", 0)) - return sock.getsockname()[1] - - -@contextmanager -def mock_get_event_loop() -> Generator[Any, MagicMock, None]: - original_get_event_loop = asyncio.get_event_loop - getter_mock = MagicMock(asyncio.get_event_loop) - getter_mock.side_effect = original_get_event_loop - - try: - asyncio.get_event_loop = getter_mock - yield getter_mock - finally: - asyncio.get_event_loop = original_get_event_loop - - -@pytest.fixture(scope="session") -def tcp_proxy() -> type[TCPProxy]: - return TCPProxy - - -def isasyncgenerator(func: Callable[..., Any]) -> bool | None: - if isasyncgenfunction(func): - return True - if iscoroutinefunction(func): - return False - return None +# Re-export everything from aiomisc.pytest +from aiomisc.pytest import * # noqa: F401, F403 +from aiomisc.pytest import __all__ # noqa: F401 +# Pytest plugin hooks (must be here for pytest to find them via entry point) def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line( "markers", @@ -523,356 +43,3 @@ def pytest_addoption(parser: pytest.Parser) -> None: default=None, help="Test timeout", ) - - -def pytest_fixture_setup(fixturedef, request): # type: ignore - func = fixturedef.func - - is_async_gen = isasyncgenerator(func) - - if is_async_gen is None: - return - - strip_request = False - if "request" not in fixturedef.argnames: - fixturedef.argnames += ("request",) - strip_request = True - - if "event_loop" not in request.fixturenames: - raise Exception("`event_loop` fixture required") - - # noinspection PyProtectedMember - loop_fixturedef = request._get_active_fixturedef("event_loop") - - def wrapper(*args, **kwargs): # type: ignore - request = kwargs.pop("request") if strip_request else kwargs["request"] - - event_loop = request.getfixturevalue("event_loop") - - if not is_async_gen: - return event_loop.run_until_complete(func(*args, **kwargs)) - - gen = func(*args, **kwargs) - - def finalizer(): # type: ignore - try: - return event_loop.run_until_complete(gen.__anext__()) - except StopAsyncIteration: # NOQA - pass - - loop_fixturedef.addfinalizer( - partial(fixturedef.finish, request=request), - ) - - request.addfinalizer(finalizer) - return event_loop.run_until_complete(gen.__anext__()) - - fixturedef.func = wrapper - - -@pytest.fixture(scope="session") -def localhost() -> str: - params = ( - (socket.AF_INET, "127.0.0.1"), - (socket.AF_INET6, "::1"), - ) - for family, addr in params: - with socket.socket(family, socket.SOCK_STREAM) as sock: - try: - sock.bind((addr, 0)) - except Exception: - pass - else: - return addr - raise RuntimeError("localhost unavailable") - - -@pytest.fixture(scope="session") -def loop_debug(pytestconfig: pytest.Config) -> bool: - return pytestconfig.getoption("--aiomisc-debug") - - -@pytest.fixture(scope="session") -def aiomisc_test_timeout( - pytestconfig: pytest.Config, -) -> int | float | None: - return pytestconfig.getoption("--aiomisc-test-timeout") - - -@pytest.fixture(autouse=True) -def aiomisc_func_wrap() -> Callable: - return aiomisc.awaitable - - -def pytest_pycollect_makeitem(collector, name, obj): # type: ignore - if collector.funcnamefilter(name) and iscoroutinefunction(obj): - return list(collector._genfunctions(name, obj)) - return None - - -@pytest.mark.tryfirst -def pytest_pyfunc_call(pyfuncitem): # type: ignore - if not iscoroutinefunction(pyfuncitem.function): - return None - - event_loop = pyfuncitem.funcargs.get("event_loop", None) - func_wraper = pyfuncitem.funcargs.get( - "aiomisc_func_wrap", - aiomisc.awaitable, - ) - - aiomisc_test_timeout = pyfuncitem.funcargs.get( - "aiomisc_test_timeout", - None, - ) - - kwargs = { - arg: pyfuncitem.funcargs[arg] - for arg in pyfuncitem._fixtureinfo.argnames - } - - @wraps(pyfuncitem.obj) - async def func() -> Any: - return await asyncio.wait_for( - func_wraper(pyfuncitem.obj)(**kwargs), - timeout=aiomisc_test_timeout, - ) - - event_loop.run_until_complete(func()) - - return True - - -@pytest.fixture(scope="session") -def thread_pool_size(request: pytest.FixtureRequest) -> int: - return request.config.getoption("--aiomisc-pool-size") - - -@pytest.fixture -def services() -> Iterable[aiomisc.Service]: - return [] - - -@pytest.fixture -def default_context() -> Mapping[str, Any]: - return {} - - -loop_autouse = os.getenv("AIOMISC_LOOP_AUTOUSE", "1") == "1" - - -@pytest.fixture(name="thread_pool_executor") -def _thread_pool_executor() -> type[concurrent.futures.ThreadPoolExecutor]: - from aiomisc.thread_pool import ThreadPoolExecutor - - return ThreadPoolExecutor - - -@pytest.fixture(name="entrypoint_kwargs") -def _entrypoint_kwargs() -> dict: - return {"log_config": False} - - -def _create_event_loop() -> asyncio.AbstractEventLoop: - """Create a new event loop, using uvloop if available and enabled.""" - if USE_UVLOOP and uvloop_module is not None: - return uvloop_module.new_event_loop() - return asyncio.new_event_loop() - - -@pytest.fixture(autouse=loop_autouse) -def event_loop( - request: pytest.FixtureRequest, - caplog: pytest.LogCaptureFixture, - thread_pool_size: int, - loop_debug: bool, - thread_pool_executor: Callable[..., concurrent.futures.ThreadPoolExecutor], -) -> Generator[Any, asyncio.AbstractEventLoop, None]: - basic_config( - log_format="plain", - stream=caplog.handler.stream, - ) - - get_marker = request.node.get_closest_marker - forbid_loop_getter_marker = get_marker("forbid_get_event_loop") - catch_unhandled_marker = get_marker("catch_loop_exceptions") - - exceptions: list[dict[str, Any]] = [] - - def catch_exceptions(_: Any, catched: dict[str, Any]) -> None: - nonlocal exceptions # noqa - exceptions.append(catched) - - loop = _create_event_loop() - loop.set_debug(loop_debug) - - asyncio.set_event_loop(loop) - set_current_loop(loop) - - pool = thread_pool_executor(thread_pool_size) - loop.set_default_executor(pool) - - if catch_unhandled_marker: - loop.set_exception_handler(catch_exceptions) - - if LOG_LEVEL: - LOG_LEVEL.set(logging.getLogger().getEffectiveLevel()) - - try: - with mock_get_event_loop() as event_loop_getter_mock: - if forbid_loop_getter_marker: - event_loop_getter_mock.side_effect = partial( - pytest.fail, - "get_event_loop is forbidden", - ) - yield loop - finally: - if exceptions: - logging.error( - "Unhandled exceptions found:\n\n\t%s", - "\n\t".join( - ("Message: {m}\n\tFuture: {f}\n\tException: {e}").format( - m=e["message"], - f=repr(e.get("future")), - e=repr(e.get("exception")), - ) - for e in exceptions - ), - ) - pytest.fail("Unhandled exceptions found. See logs.") - - basic_config( - log_format="plain", - stream=sys.stderr, - ) - - if not loop.is_closed(): - with suppress(Exception): - loop.run_until_complete(loop.shutdown_asyncgens()) - with suppress(Exception): - loop.close() - - asyncio.set_event_loop(None) - - -@pytest.fixture(autouse=loop_autouse) -async def entrypoint( - services: Iterable[aiomisc.Service], - loop_debug: bool, - default_context: Mapping[str, Any], - entrypoint_kwargs: Mapping[str, Any], - thread_pool_size: int, - thread_pool_executor: Callable[..., concurrent.futures.ThreadPoolExecutor], - event_loop: asyncio.AbstractEventLoop, -) -> AsyncGenerator[aiomisc.entrypoint, None]: - from aiomisc.context import get_context - from aiomisc.entrypoint import entrypoint - - async with entrypoint( - *services, loop=event_loop, **entrypoint_kwargs - ) as ep: - ctx = get_context() - for key, value in default_context.items(): - ctx[key] = value - yield ep - - -class PortSocket(NamedTuple): - port: int - socket: socket.socket - - -@pytest.fixture -def aiomisc_socket_factory( - request: pytest.FixtureRequest, - localhost: str, -) -> Callable[..., PortSocket]: - """Returns a""" - - def factory(*args: Any, **kwargs: Any) -> PortSocket: - sock = bind_socket(*args, address=localhost, port=0, **kwargs) - port = sock.getsockname()[1] - - # Close socket after teardown - request.addfinalizer(sock.close) - - return PortSocket(port=port, socket=sock) - - return factory - - -class SocketWrapper(abc.ABC): - address: str - port: int - - def __init__(self, *args: Any): - self._socket_args: tuple[Any, ...] = args - self.address: str = "" - self.port: int = 0 - - @abc.abstractmethod - def prepare(self, address: str) -> None: - raise NotImplementedError - - @abc.abstractmethod - def close(self) -> None: - pass - - -class SocketWrapperUnix(SocketWrapper): - socket: socket.socket - fd: int - - def __init__(self, *args: Any): - super().__init__(*args) - self.socket = socket.socket(*args) - self.fd = -1 - - def close(self) -> None: - self.socket.close() - if self.fd > 0: - os.close(self.fd) - - def prepare(self, address: str) -> None: - self.socket.bind((address, 0)) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock_set_reuseport(self.socket, True) - self.address, self.port = self.socket.getsockname()[:2] - # detach socket object and save file descriptor - self.fd = self.socket.detach() - - -class SocketWrapperWindows(SocketWrapper): - def close(self) -> None: - pass - - def prepare(self, address: str) -> None: - self.address = address - self.port = unused_port(*self._socket_args) - - -socket_wrapper: type[SocketWrapper] - -if platform.system() == "Windows": - socket_wrapper = SocketWrapperWindows -else: - socket_wrapper = SocketWrapperUnix - - -@pytest.fixture -def aiomisc_unused_port_factory( - request: pytest.FixtureRequest, - localhost: str, -) -> Callable[[], int]: - def port_factory(*args: Any) -> int: - wrapper = socket_wrapper(*args) - wrapper.prepare("::" if ":" in localhost else "0.0.0.0") - request.addfinalizer(wrapper.close) - return wrapper.port - - return port_factory - - -@pytest.fixture -def aiomisc_unused_port(aiomisc_unused_port_factory: Callable[..., int]) -> int: - return aiomisc_unused_port_factory() diff --git a/pyproject.toml b/pyproject.toml index 99b76c7..4895836 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "aiomisc-pytest" -version = "1.4.0" +version = "2.0.0" description = "pytest integration for aiomisc" readme = "README.md" license = "MIT" @@ -19,17 +19,16 @@ classifiers = [ "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries", "Topic :: Software Development", ] -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "pytest>=8.4", - "aiomisc>=17", + "aiomisc>=18", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 87b6b19..96d70ca 100644 --- a/uv.lock +++ b/uv.lock @@ -1,24 +1,24 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.11" [[package]] name = "aiomisc" -version = "17.10.3" +version = "18.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "aiothreads" }, { name = "colorlog" }, { name = "logging-journald", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/c1/c3f0838d23e60eb7695a0054ac9a964db6d2b5f0649d96068e0515f5bb0b/aiomisc-17.10.3.tar.gz", hash = "sha256:db891af7cd96c78064d93956bb0ea9bdf44b878ee5f1026f1c3fac73e2cec556", size = 466598, upload-time = "2026-01-13T10:04:21.684Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/35/e4ae5dd8bf72ad0476438623fa6ec0293e8d0523d23d69471934fdca29e5/aiomisc-18.0.0.tar.gz", hash = "sha256:efad5898f72c7f338acee26aa3b64e20837236be74beccc04e89debc92e8b46c", size = 461925, upload-time = "2026-02-09T00:01:51.84Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/33/548305964fc871edc9f579a5a880946d09393cd966ba06ad6416449f3925/aiomisc-17.10.3-py3-none-any.whl", hash = "sha256:f05ca9fd3d8c3ff0cded91c7bfb439da87486c47fd94abcec3f3dac53734c15f", size = 92402, upload-time = "2026-01-13T10:04:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/5a/80/96200c8ba053c33f5c98e360851eb93339491c35dfd7d9b03999e46724f1/aiomisc-18.0.0-py3-none-any.whl", hash = "sha256:e12b6616e2ae91b200a73dc9f903ae40effd093f8dfdf1ed580d46f599f89473", size = 94635, upload-time = "2026-02-09T00:01:49.721Z" }, ] [[package]] name = "aiomisc-pytest" -version = "1.4.0" +version = "2.0.0" source = { editable = "." } dependencies = [ { name = "aiomisc" }, @@ -42,7 +42,7 @@ uvloop = [ [package.metadata] requires-dist = [ - { name = "aiomisc", specifier = ">=17" }, + { name = "aiomisc", specifier = ">=18" }, { name = "pytest", specifier = ">=8.4" }, { name = "uvloop", marker = "extra == 'uvloop'", specifier = ">=0.21" }, ] @@ -56,6 +56,15 @@ dev = [ ] uvloop = [{ name = "uvloop", specifier = ">=0.21" }] +[[package]] +name = "aiothreads" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/56/98896ab8d2ad3a959298e1e3f4d4749b6a56fc99d81639817ee5129be7bc/aiothreads-1.0.0.tar.gz", hash = "sha256:f8c3513af29ceea4abe860cc72233aea8dae1d109bed633dac064da317bab145", size = 42910, upload-time = "2025-08-03T11:15:49.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f1/0d6fe4045c07cd9ff092ff0529075453739f00e1be5b4af6902a883f9c9d/aiothreads-1.0.0-py3-none-any.whl", hash = "sha256:ccdb35fc9a04a3be12cb715fd12b3b1855c7da5bd45cf0259b69e7f82515e06e", size = 20200, upload-time = "2025-08-03T11:15:47.959Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -83,18 +92,6 @@ version = "7.13.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" }, - { url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663, upload-time = "2026-02-03T13:59:41.951Z" }, - { url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424, upload-time = "2026-02-03T13:59:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228, upload-time = "2026-02-03T13:59:44.816Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103, upload-time = "2026-02-03T13:59:46.271Z" }, - { url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107, upload-time = "2026-02-03T13:59:48.53Z" }, - { url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143, upload-time = "2026-02-03T13:59:50.027Z" }, - { url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148, upload-time = "2026-02-03T13:59:52.025Z" }, - { url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414, upload-time = "2026-02-03T13:59:53.859Z" }, - { url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023, upload-time = "2026-02-03T13:59:55.454Z" }, - { url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751, upload-time = "2026-02-03T13:59:57.383Z" }, - { url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686, upload-time = "2026-02-03T13:59:58.825Z" }, { url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" }, { url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" }, { url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" }, @@ -181,18 +178,6 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -208,16 +193,6 @@ version = "0.7.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, - { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, - { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, - { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, - { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, - { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, - { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, @@ -292,17 +267,10 @@ dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, @@ -381,12 +349,10 @@ version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ @@ -501,12 +467,6 @@ version = "0.22.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, - { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, - { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, - { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" },