diff --git a/conftest.py b/conftest.py index 3f67101..7f66d34 100644 --- a/conftest.py +++ b/conftest.py @@ -4,7 +4,7 @@ from injection import Module from cq import CQ, Bus, CommandBus, EventBus, QueryBus -from cq._core.dispatcher.bus import SimpleBus +from cq._core.dispatchers.bus import SimpleBus from cq.ext.injection import InjectionAdapter from tests.helpers.history import HistoryMiddleware diff --git a/cq/__init__.py b/cq/__init__.py index 694b0ec..49b82b8 100644 --- a/cq/__init__.py +++ b/cq/__init__.py @@ -1,9 +1,9 @@ from ._core.cq import CQ from ._core.di import DIAdapter from ._core.di import NoDI as _NoDI -from ._core.dispatcher.base import Dispatcher -from ._core.dispatcher.bus import Bus -from ._core.dispatcher.pipe import ContextPipeline, Pipe +from ._core.dispatchers.abc import Dispatcher +from ._core.dispatchers.bus import Bus +from ._core.dispatchers.pipe import ContextPipeline, Pipe from ._core.message import ( AnyCommandBus, Command, @@ -15,6 +15,9 @@ ) from ._core.middleware import Middleware, MiddlewareResult, resolve_handler_source from ._core.pipetools import ContextCommandPipeline as _ContextCommandPipeline +from ._core.pump import Pump +from ._core.queues.abc import Consumer, Producer, Queue +from ._core.queues.memory import MemoryQueue from ._core.related_events import AnyIORelatedEvents, RelatedEvents __all__ = ( @@ -24,17 +27,22 @@ "CQ", "Command", "CommandBus", + "Consumer", "ContextCommandPipeline", "ContextPipeline", "DIAdapter", "Dispatcher", "Event", "EventBus", + "MemoryQueue", "Middleware", "MiddlewareResult", "Pipe", + "Producer", + "Pump", "Query", "QueryBus", + "Queue", "RelatedEvents", "command_handler", "event_handler", diff --git a/cq/_core/cq.py b/cq/_core/cq.py index fec32d1..d999455 100644 --- a/cq/_core/cq.py +++ b/cq/_core/cq.py @@ -1,7 +1,7 @@ from typing import Any, Self from cq._core.di import DIAdapter -from cq._core.dispatcher.bus import Bus, SimpleBus, TaskBus +from cq._core.dispatchers.bus import Bus, SimpleBus, TaskBus from cq._core.handler import ( HandlerDecorator, HandlerRegistry, diff --git a/cq/_core/di.py b/cq/_core/di.py index ac15af7..5a982f9 100644 --- a/cq/_core/di.py +++ b/cq/_core/di.py @@ -5,7 +5,7 @@ from contextlib import nullcontext from typing import TYPE_CHECKING, Any, AsyncContextManager, Protocol, runtime_checkable -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from cq import CommandBus, EventBus, QueryBus diff --git a/cq/_core/dispatcher/__init__.py b/cq/_core/dispatchers/__init__.py similarity index 100% rename from cq/_core/dispatcher/__init__.py rename to cq/_core/dispatchers/__init__.py diff --git a/cq/_core/dispatcher/base.py b/cq/_core/dispatchers/abc.py similarity index 84% rename from cq/_core/dispatcher/base.py rename to cq/_core/dispatchers/abc.py index d594a47..38386a1 100644 --- a/cq/_core/dispatcher/base.py +++ b/cq/_core/dispatchers/abc.py @@ -9,11 +9,11 @@ class Dispatcher[I, O](Protocol): __slots__ = () - async def __call__(self, input_value: I, /) -> O: - return await self.dispatch(input_value) + async def __call__(self, message: I, /) -> O: + return await self.dispatch(message) @abstractmethod - async def dispatch(self, input_value: I, /) -> O: + async def dispatch(self, message: I, /) -> O: raise NotImplementedError @@ -32,12 +32,12 @@ def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self: async def _invoke_with_middlewares( self, handler: Callable[[I], Awaitable[O]], - input_value: I, + message: I, /, fail_silently: bool = False, ) -> O: try: - return await self.__middleware_group.invoke(handler, input_value) + return await self.__middleware_group.invoke(handler, message) except Exception: if fail_silently: return NotImplemented diff --git a/cq/_core/dispatcher/bus.py b/cq/_core/dispatchers/bus.py similarity index 73% rename from cq/_core/dispatcher/bus.py rename to cq/_core/dispatchers/bus.py index 04c0a9c..12fd9f2 100644 --- a/cq/_core/dispatcher/bus.py +++ b/cq/_core/dispatchers/bus.py @@ -5,7 +5,7 @@ import anyio from anyio.abc import TaskGroup -from cq._core.dispatcher.base import BaseDispatcher, Dispatcher +from cq._core.dispatchers.abc import BaseDispatcher, Dispatcher from cq._core.handler import ( HandleFunction, HandlerFactory, @@ -33,7 +33,7 @@ def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self: @abstractmethod def subscribe( self, - input_type: type[I], + message_type: type[I], factory: HandlerFactory[[I], O], fail_silently: bool = ..., ) -> Self: @@ -57,19 +57,19 @@ def add_listeners(self, *listeners: Listener[I]) -> Self: def subscribe( self, - input_type: type[I], + message_type: type[I], factory: HandlerFactory[[I], O], fail_silently: bool = False, ) -> Self: - self.__registry.subscribe(input_type, factory, fail_silently=fail_silently) + self.__registry.subscribe(message_type, factory, fail_silently=fail_silently) return self - def _handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]: - return self.__registry.handlers_from(input_type) + def _handlers_from(self, message_type: type[I]) -> Iterator[HandleFunction[[I], O]]: + return self.__registry.handlers_from(message_type) - def _trigger_listeners(self, input_value: I, /, task_group: TaskGroup) -> None: + def _trigger_listeners(self, message: I, /, task_group: TaskGroup) -> None: for listener in self.__listeners: - task_group.start_soon(listener, input_value) + task_group.start_soon(listener, message) class SimpleBus[I, O](BaseBus[I, O]): @@ -78,14 +78,14 @@ class SimpleBus[I, O](BaseBus[I, O]): def __init__(self, registry: HandlerRegistry[I, O] | None = None, /) -> None: super().__init__(registry or SingleHandlerRegistry()) - async def dispatch(self, input_value: I, /) -> O: + async def dispatch(self, message: I, /) -> O: async with anyio.create_task_group() as task_group: - self._trigger_listeners(input_value, task_group) + self._trigger_listeners(message, task_group) - for handler in self._handlers_from(type(input_value)): + for handler in self._handlers_from(type(message)): return await self._invoke_with_middlewares( handler, - input_value, + message, handler.fail_silently, ) @@ -98,14 +98,14 @@ class TaskBus[I](BaseBus[I, None]): def __init__(self, registry: HandlerRegistry[I, None] | None = None, /) -> None: super().__init__(registry or MultipleHandlerRegistry()) - async def dispatch(self, input_value: I, /) -> None: + async def dispatch(self, message: I, /) -> None: async with anyio.create_task_group() as task_group: - self._trigger_listeners(input_value, task_group) + self._trigger_listeners(message, task_group) - for handler in self._handlers_from(type(input_value)): + for handler in self._handlers_from(type(message)): task_group.start_soon( self._invoke_with_middlewares, handler, - input_value, + message, handler.fail_silently, ) diff --git a/cq/_core/dispatcher/lazy.py b/cq/_core/dispatchers/lazy.py similarity index 78% rename from cq/_core/dispatcher/lazy.py rename to cq/_core/dispatchers/lazy.py index ee60763..54edfd3 100644 --- a/cq/_core/dispatcher/lazy.py +++ b/cq/_core/dispatchers/lazy.py @@ -3,7 +3,7 @@ from typing import TypeAliasType from cq._core.di import DIAdapter -from cq._core.dispatcher.base import Dispatcher +from cq._core.dispatchers.abc import Dispatcher class LazyDispatcher[I, O](Dispatcher[I, O]): @@ -19,6 +19,6 @@ def __init__( ) -> None: self.__resolve = di.lazy(dispatcher_type) # type: ignore[arg-type] - async def dispatch(self, input_value: I, /) -> O: + async def dispatch(self, message: I, /) -> O: dispatcher = await self.__resolve() - return await dispatcher.dispatch(input_value) + return await dispatcher.dispatch(message) diff --git a/cq/_core/dispatcher/pipe.py b/cq/_core/dispatchers/pipe.py similarity index 81% rename from cq/_core/dispatcher/pipe.py rename to cq/_core/dispatchers/pipe.py index 705c280..aa320ff 100644 --- a/cq/_core/dispatcher/pipe.py +++ b/cq/_core/dispatchers/pipe.py @@ -14,7 +14,7 @@ ) from cq._core.common.typing import Decorator, Method -from cq._core.dispatcher.base import BaseDispatcher, Dispatcher +from cq._core.dispatchers.abc import BaseDispatcher, Dispatcher from cq._core.middleware import Middleware, MiddlewareGroup type ConvertAsync[**P, I, O] = Callable[Concatenate[O, P], Awaitable[I]] @@ -31,7 +31,7 @@ class PipelineConverter[**P, I, O](Protocol): __slots__ = () @abstractmethod - async def convert(self, output_value: O, /, *args: P.args, **kwargs: P.kwargs) -> I: + async def convert(self, result: O, /, *args: P.args, **kwargs: P.kwargs) -> I: raise NotImplementedError @@ -54,28 +54,24 @@ def add[T]( self.__steps.append(PipelineStep(converter, dispatcher)) return self - def add_static[T]( - self, - input_value: T, - dispatcher: Dispatcher[T, Any] | None, - ) -> Self: - converter = _StaticPipelineConverter(input_value) + def add_static[T](self, message: T, dispatcher: Dispatcher[T, Any] | None) -> Self: + converter = _StaticPipelineConverter(message) self.add(converter, dispatcher) # type: ignore[arg-type] return self - async def execute(self, input_value: I, /, *args: P.args, **kwargs: P.kwargs) -> O: + async def execute(self, message: I, /, *args: P.args, **kwargs: P.kwargs) -> O: dispatcher = self.default_dispatcher for step in self.__steps: - output_value = await dispatcher.dispatch(input_value) - input_value = await step.converter.convert(output_value, *args, **kwargs) + result = await dispatcher.dispatch(message) + message = await step.converter.convert(result, *args, **kwargs) - if input_value is None: + if message is None: return NotImplemented dispatcher = step.dispatcher or self.default_dispatcher - return await dispatcher.dispatch(input_value) + return await dispatcher.dispatch(message) class Pipe[I, O](BaseDispatcher[I, O]): @@ -136,15 +132,15 @@ def decorator(wp: Convert[[], T, Any]) -> Convert[[], T, Any]: def add_static_step[T]( self, - input_value: T, + message: T, /, dispatcher: Dispatcher[T, Any] | None = None, ) -> Self: - self.__steps.add_static(input_value, dispatcher) + self.__steps.add_static(message, dispatcher) return self - async def dispatch(self, input_value: I, /) -> O: - return await self._invoke_with_middlewares(self.__steps.execute, input_value) + async def dispatch(self, message: I, /) -> O: + return await self._invoke_with_middlewares(self.__steps.execute, message) class ContextPipeline[I]: @@ -199,11 +195,11 @@ def add_middlewares(self, *middlewares: Middleware[[I], Any]) -> Self: def add_static_step[T]( self, - input_value: T, + message: T, /, dispatcher: Dispatcher[T, Any] | None = None, ) -> Self: - self.__steps.add_static(input_value, dispatcher) + self.__steps.add_static(message, dispatcher) return self if TYPE_CHECKING: # pragma: no cover @@ -255,49 +251,49 @@ def decorator(wp: ConvertMethod[T, Any]) -> ConvertMethod[T, Any]: async def __execute[Context]( self, - input_value: I, + message: I, /, *, context: Context, context_type: type[Context] | None, ) -> Context: - async def handler(i: I, /) -> Context: - await self.__steps.execute(i, context, context_type) + async def handler(first_message: I, /) -> Context: + await self.__steps.execute(first_message, context, context_type) return context - return await self.__middleware_group.invoke(handler, input_value) + return await self.__middleware_group.invoke(handler, message) @dataclass(repr=False, eq=False, frozen=True, slots=True) class BoundContextPipeline[I, O](Dispatcher[I, O]): dispatch_method: Callable[[I], Awaitable[O]] - async def dispatch(self, input_value: I, /) -> O: - return await self.dispatch_method(input_value) + async def dispatch(self, message: I, /) -> O: + return await self.dispatch_method(message) @dataclass(repr=False, eq=False, frozen=True, slots=True) class _AsyncPipelineConverter[**P, I, O](PipelineConverter[P, I, O]): converter: ConvertAsync[P, I, O] - async def convert(self, output_value: O, /, *args: P.args, **kwargs: P.kwargs) -> I: - return await self.converter(output_value, *args, **kwargs) + async def convert(self, result: O, /, *args: P.args, **kwargs: P.kwargs) -> I: + return await self.converter(result, *args, **kwargs) @dataclass(repr=False, eq=False, frozen=True, slots=True) class _SyncPipelineConverter[**P, I, O](PipelineConverter[P, I, O]): converter: ConvertSync[P, I, O] - async def convert(self, output_value: O, /, *args: P.args, **kwargs: P.kwargs) -> I: - return self.converter(output_value, *args, **kwargs) + async def convert(self, result: O, /, *args: P.args, **kwargs: P.kwargs) -> I: + return self.converter(result, *args, **kwargs) @dataclass(repr=False, eq=False, frozen=True, slots=True) class _StaticPipelineConverter[I](PipelineConverter[..., I, Any]): - input_value: I + message: I - async def convert(self, output_value: Any, /, *args: Any, **kwargs: Any) -> I: - return self.input_value + async def convert(self, result: Any, /, *args: Any, **kwargs: Any) -> I: + return self.message @dataclass(repr=False, eq=False, frozen=True, slots=True) @@ -308,13 +304,13 @@ class _AsyncContextPipelineConverter[I, O]( async def convert( self, - output_value: O, + result: O, /, context: object, context_type: type | None, ) -> I: method = self.converter.__get__(context, context_type) - return await method(output_value) + return await method(result) @dataclass(repr=False, eq=False, frozen=True, slots=True) @@ -325,10 +321,10 @@ class _SyncContextPipelineConverter[I, O]( async def convert( self, - output_value: O, + result: O, /, context: object, context_type: type | None, ) -> I: method = self.converter.__get__(context, context_type) - return method(output_value) + return method(result) diff --git a/cq/_core/handler.py b/cq/_core/handler.py index 155d5c9..ce5f2bc 100644 --- a/cq/_core/handler.py +++ b/cq/_core/handler.py @@ -50,13 +50,13 @@ class HandlerRegistry[I, O](Protocol): __slots__ = () @abstractmethod - def handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]: + def handlers_from(self, message_type: type[I]) -> Iterator[HandleFunction[[I], O]]: raise NotImplementedError @abstractmethod def subscribe( self, - input_type: type[I], + message_type: type[I], handler_factory: HandlerFactory[[I], O], handler_type: HandlerType[[I], O] | None = ..., fail_silently: bool = ..., @@ -71,20 +71,20 @@ class MultipleHandlerRegistry[I, O](HandlerRegistry[I, O]): init=False, ) - def handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]: - for key_type in _iter_key_types(input_type): + def handlers_from(self, message_type: type[I]) -> Iterator[HandleFunction[[I], O]]: + for key_type in _iter_key_types(message_type): yield from self.__values.get(key_type, ()) def subscribe( self, - input_type: type[I], + message_type: type[I], handler_factory: HandlerFactory[[I], O], handler_type: HandlerType[[I], O] | None = None, fail_silently: bool = False, ) -> Self: function = HandleFunction.create(handler_factory, handler_type, fail_silently) - for key_type in _build_key_types(input_type): + for key_type in _build_key_types(message_type): self.__values[key_type].append(function) return self @@ -97,26 +97,26 @@ class SingleHandlerRegistry[I, O](HandlerRegistry[I, O]): init=False, ) - def handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]: - for key_type in _iter_key_types(input_type): + def handlers_from(self, message_type: type[I]) -> Iterator[HandleFunction[[I], O]]: + for key_type in _iter_key_types(message_type): function = self.__values.get(key_type, None) if function is not None: yield function def subscribe( self, - input_type: type[I], + message_type: type[I], handler_factory: HandlerFactory[[I], O], handler_type: HandlerType[[I], O] | None = None, fail_silently: bool = False, ) -> Self: function = HandleFunction.create(handler_factory, handler_type, fail_silently) - entries = {key_type: function for key_type in _build_key_types(input_type)} + entries = {key_type: function for key_type in _build_key_types(message_type)} for key_type in entries: if key_type in self.__values: raise RuntimeError( - f"A handler is already registered for the input type: `{key_type}`." + f"A handler is already registered for the message type: `{key_type}`." ) self.__values.update(entries) @@ -133,7 +133,7 @@ class HandlerDecorator[I, O]: @overload def __call__( self, - input_or_handler_type: type[I], + message_or_handler_type: type[I], /, *, fail_silently: bool = ..., @@ -142,7 +142,7 @@ def __call__( @overload def __call__[T]( self, - input_or_handler_type: T, + message_or_handler_type: T, /, *, fail_silently: bool = ..., @@ -151,7 +151,7 @@ def __call__[T]( @overload def __call__( self, - input_or_handler_type: None = ..., + message_or_handler_type: None = ..., /, *, fail_silently: bool = ..., @@ -159,24 +159,24 @@ def __call__( def __call__[T]( self, - input_or_handler_type: type[I] | T | None = None, + message_or_handler_type: type[I] | T | None = None, /, *, fail_silently: bool = False, ) -> Any: if ( - input_or_handler_type is not None - and isclass(input_or_handler_type) - and issubclass(input_or_handler_type, Handler) + message_or_handler_type is not None + and isclass(message_or_handler_type) + and issubclass(message_or_handler_type, Handler) ): return self.__decorator( - input_or_handler_type, + message_or_handler_type, fail_silently=fail_silently, ) return partial( self.__decorator, - input_type=input_or_handler_type, # type: ignore[arg-type] + message_type=message_or_handler_type, # type: ignore[arg-type] fail_silently=fail_silently, ) @@ -185,42 +185,42 @@ def __decorator( wrapped: HandlerType[[I], O], /, *, - input_type: type[I] | None = None, + message_type: type[I] | None = None, fail_silently: bool = False, ) -> HandlerType[[I], O]: factory = self.di.wire(wrapped) - input_type = input_type or _resolve_input_type(wrapped) - self.registry.subscribe(input_type, factory, wrapped, fail_silently) + message_type = message_type or _resolve_message_type(wrapped) + self.registry.subscribe(message_type, factory, wrapped, fail_silently) return wrapped -def _build_key_types(input_type: Any) -> tuple[Any, ...]: +def _build_key_types(message_type: Any) -> tuple[Any, ...]: config = MatchingTypesConfig(ignore_none=True) - return matching_types(input_type, config) + return matching_types(message_type, config) -def _iter_key_types(input_type: Any) -> Iterator[Any]: +def _iter_key_types(message_type: Any) -> Iterator[Any]: config = MatchingTypesConfig( with_bases=True, with_origin=True, with_type_alias_value=True, ) - return iter_matching_types(input_type, config) + return iter_matching_types(message_type, config) -def _resolve_input_type[I, O](handler_type: HandlerType[[I], O]) -> type[I]: +def _resolve_message_type[I, O](handler_type: HandlerType[[I], O]) -> type[I]: fake_method = handler_type.handle.__get__(NotImplemented, handler_type) signature = inspect_signature(fake_method, eval_str=True) for parameter in signature.parameters.values(): - input_type = parameter.annotation + message_type = parameter.annotation - if input_type is Parameter.empty: + if message_type is Parameter.empty: break - return input_type + return message_type raise TypeError( - f"Unable to resolve input type for handler `{handler_type}`, " + f"Unable to resolve message type for handler `{handler_type}`, " "`handle` method must have a type annotation for its first parameter." ) diff --git a/cq/_core/message.py b/cq/_core/message.py index 43f0cf6..a9d2d62 100644 --- a/cq/_core/message.py +++ b/cq/_core/message.py @@ -1,6 +1,6 @@ from typing import Any -from cq._core.dispatcher.base import Dispatcher +from cq._core.dispatchers.abc import Dispatcher Command = object Event = object diff --git a/cq/_core/pipetools.py b/cq/_core/pipetools.py index e135f95..389da1c 100644 --- a/cq/_core/pipetools.py +++ b/cq/_core/pipetools.py @@ -3,8 +3,8 @@ from cq import Dispatcher from cq._core.common.typing import Decorator from cq._core.di import DIAdapter -from cq._core.dispatcher.lazy import LazyDispatcher -from cq._core.dispatcher.pipe import ( +from cq._core.dispatchers.lazy import LazyDispatcher +from cq._core.dispatchers.pipe import ( ContextPipeline, ConvertMethod, ConvertMethodAsync, diff --git a/cq/_core/pump.py b/cq/_core/pump.py new file mode 100644 index 0000000..c0cb19d --- /dev/null +++ b/cq/_core/pump.py @@ -0,0 +1,34 @@ +from collections.abc import AsyncIterator, Awaitable, Callable +from contextlib import asynccontextmanager +from dataclasses import dataclass, field +from typing import Any + +import anyio + +from cq._core.queues.abc import Consumer + + +@dataclass(repr=False, eq=False, frozen=True, slots=True) +class Pump[T]: + consumer: Consumer[T] + dispatcher: Callable[[T], Awaitable[Any]] + fail_silently: bool = field(default=False) + + async def drain(self) -> None: + async for message in self.consumer: + try: + await self.dispatcher(message) + except Exception: + if not self.fail_silently: + raise + + @asynccontextmanager + async def draining(self, /, *, graceful: bool = False) -> AsyncIterator[None]: + async with anyio.create_task_group() as task_group: + task_group.start_soon(self.drain) + + try: + yield + finally: + if not graceful: + task_group.cancel_scope.cancel() diff --git a/cq/_core/queues/__init__.py b/cq/_core/queues/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cq/_core/queues/abc.py b/cq/_core/queues/abc.py new file mode 100644 index 0000000..b38086a --- /dev/null +++ b/cq/_core/queues/abc.py @@ -0,0 +1,29 @@ +from abc import abstractmethod +from collections.abc import AsyncIterator +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class Producer[T](Protocol): + __slots__ = () + + async def __call__(self, message: T, /) -> None: + return await self.send(message) + + @abstractmethod + async def send(self, message: T, /) -> None: + raise NotImplementedError + + +@runtime_checkable +class Consumer[T](Protocol): + __slots__ = () + + @abstractmethod + def __aiter__(self) -> AsyncIterator[T]: + raise NotImplementedError + + +@runtime_checkable +class Queue[T](Producer[T], Consumer[T], Protocol): + __slots__ = () diff --git a/cq/_core/queues/memory.py b/cq/_core/queues/memory.py new file mode 100644 index 0000000..6344652 --- /dev/null +++ b/cq/_core/queues/memory.py @@ -0,0 +1,42 @@ +from collections.abc import AsyncIterator, Awaitable, Callable +from contextlib import asynccontextmanager +from typing import Any, Self + +import anyio +from anyio.abc import ObjectReceiveStream, ObjectSendStream + +from cq._core.pump import Pump +from cq._core.queues.abc import Queue + + +class MemoryQueue[T](Queue[T]): + __slots__ = ("__consumer", "__producer") + + __consumer: ObjectReceiveStream[T] + __producer: ObjectSendStream[T] + + def __init__(self, maxsize: int = 0) -> None: + self.__producer, self.__consumer = anyio.create_memory_object_stream(maxsize) + + def __aiter__(self) -> AsyncIterator[T]: + return aiter(self.__consumer) + + async def close(self) -> None: + await self.__producer.aclose() + + @asynccontextmanager + async def draining( + self, + dispatcher: Callable[[T], Awaitable[Any]], + /, + *, + fail_silently: bool = False, + ) -> AsyncIterator[Self]: + async with Pump(self, dispatcher, fail_silently).draining(graceful=True): + try: + yield self + finally: + await self.close() + + async def send(self, message: T, /) -> None: + await self.__producer.send(message) diff --git a/cq/_core/related_events.py b/cq/_core/related_events.py index aa88345..0f4fd05 100644 --- a/cq/_core/related_events.py +++ b/cq/_core/related_events.py @@ -1,12 +1,13 @@ from abc import abstractmethod +from collections.abc import Awaitable, Callable from dataclasses import dataclass, field from types import TracebackType -from typing import Protocol, Self, runtime_checkable +from typing import Any, Protocol, Self, runtime_checkable from anyio import create_task_group from anyio.abc import TaskGroup -from cq._core.message import Event, EventBus +from cq._core.message import Event @runtime_checkable @@ -20,7 +21,7 @@ def add(self, *events: Event) -> None: @dataclass(repr=False, eq=False, frozen=True, slots=True) class AnyIORelatedEvents(RelatedEvents): - event_bus: EventBus + emit: Callable[[Event], Awaitable[Any]] task_group: TaskGroup = field(default_factory=create_task_group) history: list[Event] = field(default_factory=list, init=False) @@ -41,7 +42,5 @@ async def __aexit__( def add(self, *events: Event) -> None: self.history.extend(events) - dispatch_method = self.event_bus.dispatch - for event in events: - self.task_group.start_soon(dispatch_method, event) + self.task_group.start_soon(self.emit, event) diff --git a/docs/guides/queues.md b/docs/guides/queues.md new file mode 100644 index 0000000..e54d329 --- /dev/null +++ b/docs/guides/queues.md @@ -0,0 +1,89 @@ +# Queueing messages + +Some workloads benefit from putting a queue between the producer of a message and its dispatcher: smoothing bursts, isolating slow handlers from the request path, or simply detaching event publication from the lifecycle of the caller. **python-cq** ships two building blocks for that pattern: a `Queue` protocol and a `Pump` that drains it into any dispatcher. + +## The `Queue` protocol + +`Queue` is the combination of two protocols, kept separate so you can type producer-side and consumer-side ends independently: + +* `Producer[T]` exposes `send(message)` and is callable. +* `Consumer[T]` exposes `__aiter__` and yields messages as they arrive. + +Any object that satisfies these protocols can act as a queue. The library provides `MemoryQueue` as the default in-process implementation, and you are free to write your own without changing the rest of the API. + +!!! note + If you'd like a `Queue` implementation for a specific library, feel free to open a [discussion on GitHub](https://github.com/100nm/python-cq/discussions). + +## `MemoryQueue` + +`MemoryQueue` is a thin wrapper around `anyio.create_memory_object_stream`. It is bounded by an optional `maxsize`, in which case `send` waits until a slot is available. + +```python +from cq import Command, MemoryQueue + +queue: MemoryQueue[Command] = MemoryQueue(maxsize=100) +await queue.send(command) +``` + +`MemoryQueue` is the right tool when producer and consumer live in the same process. It is not thread-safe: both `send` and consumption must run on the event loop that created the queue. For cross-process or persistent queues, implement `Queue` against your transport of choice. + +## Draining a queue with `Pump` + +`Pump` connects a `Consumer` to a dispatcher. The dispatcher is any async callable that accepts one message; in practice this is usually a bus. + +```python +from cq import Command, CommandBus, MemoryQueue, Pump +from typing import Any + +command_bus: CommandBus[Any] = ... +queue: MemoryQueue[Command] = MemoryQueue() + +async with Pump(queue, command_bus).draining(): + # ... + await queue.send(command) +``` + +While the `draining` context is open, a background task consumes the queue and forwards each message to the dispatcher. On exit, the task group is cancelled by default, which stops the pump immediately even if some messages are still in flight. + +### Graceful shutdown + +Pass `graceful=True` to let the pump finish draining whatever is already queued before the context exits. This relies on the queue being closed so the async iterator can terminate; otherwise the pump would wait forever for the next message. + +```python +async with Pump(queue, command_bus).draining(graceful=True): + # ... + await queue.send(command_1) + await queue.send(command_2) + # ... + await queue.close() +``` + +### Suppressing dispatcher errors + +By default, an exception raised by the dispatcher stops the pump and propagates out of `draining`. Set `fail_silently=True` on the `Pump` if you want individual failures to be swallowed and the pump to keep consuming the rest of the queue: + +```python +Pump(queue, command_bus, fail_silently=True) +``` + +Use this when each message is independent and a failed dispatch should not take down the whole consumer. Logging or alerting on failures is the dispatcher's responsibility, typically through a middleware on the underlying bus. + +## `MemoryQueue.draining` shortcut + +`MemoryQueue` exposes a `draining` helper that combines `Pump` with the queue's own lifecycle. It opens a pump, yields the queue, and closes it on exit so the pump terminates gracefully without an explicit `close` call: + +```python +from cq import CommandBus, MemoryQueue +from injection import inject +from typing import Any + +@inject +async def main(command_bus: CommandBus[Any]) -> None: + async with MemoryQueue().draining(command_bus) as queue: + # ... + await queue.send(command_1) + await queue.send(command_2) + # Both commands have been dispatched here. +``` + +`fail_silently` is forwarded to the underlying `Pump`. diff --git a/mkdocs.yml b/mkdocs.yml index a80db85..472b7ed 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,6 +21,7 @@ nav: - Dispatching messages: guides/dispatching.md - Configuring a bus: guides/configuring.md - Executing multiple commands: guides/pipeline.md + - Queueing messages: guides/queues.md - Custom DI adapter: di.md plugins: diff --git a/tests/core/dispatcher/test_bus.py b/tests/core/dispatcher/test_bus.py index 24cfdd8..c5d3bfc 100644 --- a/tests/core/dispatcher/test_bus.py +++ b/tests/core/dispatcher/test_bus.py @@ -3,7 +3,7 @@ import pytest from cq import MiddlewareResult -from cq._core.dispatcher.bus import SimpleBus, TaskBus +from cq._core.dispatchers.bus import SimpleBus, TaskBus class TestSimpleBus: diff --git a/tests/core/dispatcher/test_pipe.py b/tests/core/dispatcher/test_pipe.py index d3f11aa..faed491 100644 --- a/tests/core/dispatcher/test_pipe.py +++ b/tests/core/dispatcher/test_pipe.py @@ -1,7 +1,7 @@ from typing import Any, Self from cq import Bus, Pipe -from cq._core.dispatcher.bus import SimpleBus +from cq._core.dispatchers.bus import SimpleBus class TestPipe: diff --git a/tests/core/queue/__init__.py b/tests/core/queue/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/queue/test_memory.py b/tests/core/queue/test_memory.py new file mode 100644 index 0000000..52b649c --- /dev/null +++ b/tests/core/queue/test_memory.py @@ -0,0 +1,50 @@ +from dataclasses import dataclass, field + +import pytest +from anyio import ClosedResourceError + +from cq import MemoryQueue + + +class TestMemoryQueue: + async def test_draining_with_success(self) -> None: + history = _History[str]() + + async with MemoryQueue[str]().draining(history) as queue: + await queue.send("message_1") + await queue.send("message_2") + + with pytest.raises(ClosedResourceError): + await queue.send("message_3") + + assert len(history) == 2 + + async def test_draining_with_fail_silently(self) -> None: + async def dispatcher(message: str) -> None: + raise NotImplementedError + + async with MemoryQueue[str]().draining(dispatcher, fail_silently=True) as queue: + await queue.send("message_1") + await queue.send("message_2") + + async def test_draining_without_fail_silently(self) -> None: + async def dispatcher(message: str) -> None: + raise NotImplementedError + + with pytest.raises(ExceptionGroup): + async with MemoryQueue[str]().draining( + dispatcher, + fail_silently=False, + ) as queue: + await queue.send("message") + + +@dataclass +class _History[T]: + __records: list[T] = field(default_factory=list, init=False) + + async def __call__(self, message: T) -> None: + self.__records.append(message) + + def __len__(self) -> int: + return len(self.__records) diff --git a/tests/core/test_middleware.py b/tests/core/test_middleware.py index a5cdf48..3f3692a 100644 --- a/tests/core/test_middleware.py +++ b/tests/core/test_middleware.py @@ -3,7 +3,7 @@ import pytest -from cq._core.dispatcher.bus import SimpleBus +from cq._core.dispatchers.bus import SimpleBus from cq._core.handler import HandlerDecorator, SingleHandlerRegistry from cq._core.middleware import ( MiddlewareGroup, diff --git a/tests/core/test_pump.py b/tests/core/test_pump.py new file mode 100644 index 0000000..24ccd30 --- /dev/null +++ b/tests/core/test_pump.py @@ -0,0 +1,19 @@ +import anyio + +from cq import MemoryQueue, Pump + + +class TestPump: + async def test_draining_without_graceful(self) -> None: + dispatched = anyio.Event() + + async def dispatcher(message: str) -> None: + await anyio.sleep(60) + dispatched.set() + + queue = MemoryQueue[str]() + + async with Pump(queue, dispatcher).draining(graceful=False): + await queue.send("message") + + assert not dispatched.is_set() diff --git a/uv.lock b/uv.lock index 5441900..6d54c6c 100644 --- a/uv.lock +++ b/uv.lock @@ -17,40 +17,40 @@ wheels = [ [[package]] name = "ast-serialize" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/9d/912fefab0e30aee6a3af8a62bbea4a81b29afa4ba2c973d31170620a26de/ast_serialize-0.3.0.tar.gz", hash = "sha256:1bc3ca09a63a021376527c4e938deedd11d11d675ce850e6f9c7487f5889992b", size = 60689, upload-time = "2026-04-30T23:24:48.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/57/a54d4de491d6cdd7a4e4b0952cc3ca9f60dcefa7b5fb48d6d492debe1649/ast_serialize-0.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a867927df59f76a18dc1d874a0b2c079b42c58972dca637905576deb0912e14", size = 1182966, upload-time = "2026-04-30T23:23:57.376Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/a5db014bb0f91b209236b57c429389e31290c0093532b8436d577699b2fa/ast_serialize-0.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a6fb063bf040abf8321e7b8113a0554eda445ffc508aa51287f8808886a5ae22", size = 1171316, upload-time = "2026-04-30T23:23:59.63Z" }, - { url = "https://files.pythonhosted.org/packages/15/59/fd55133e478c4326f60a11df02573bf7ccb2ac685810b50f1803d0f68053/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5075cd8482573d743586779e5f9b652a015e37d4e95132d7e5a9bc5c8f483d8f", size = 1232234, upload-time = "2026-04-30T23:24:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/cc/79/0ca1d26357ecb4a697d74d00b73ef3137f24c140424125393a0de820eb09/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41560b27794f4553b0f77811e9fb325b77db4a2b39018d437e09932275306e66", size = 1233437, upload-time = "2026-04-30T23:24:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/7078ec94dd6e124b8e028ac77016a4f13c83fa1c145790f2e68f3816998b/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b967c01ca74909c5d90e0fe4393401e2cc5da5ebd9a6262a19e45ffd3757dec8", size = 1440188, upload-time = "2026-04-30T23:24:04.717Z" }, - { url = "https://files.pythonhosted.org/packages/21/16/cca7195ef55a012f8013c3442afa91d287a0a36dcf88b480b262475135b3/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:424ebb8f46cd993f7cec4009d119312d8433dd90e6b0df0499cd2c91bdcc5af9", size = 1254211, upload-time = "2026-04-30T23:24:06.18Z" }, - { url = "https://files.pythonhosted.org/packages/a0/0f/f3d4dfae67dee6580534361a6343367d34217e7d25cff858bd1d8f03b8ed/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14b1d566b56e2ee70b11fec1de7e0b94ec7cd83717ec7d189967841a361190e", size = 1255973, upload-time = "2026-04-30T23:24:07.772Z" }, - { url = "https://files.pythonhosted.org/packages/14/41/55fbfe02c42f40fbe3e74eda167d977d555ff720ce1abfa08515236efd88/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ba30b18735f047ec11103d1ab92f4789cf1fea1e0dc89b04a2f5a0632fd79de", size = 1298629, upload-time = "2026-04-30T23:24:09.4Z" }, - { url = "https://files.pythonhosted.org/packages/28/36/7d2501cacc7989fb8504aa9da2a2022a174200a59d4e6639de4367a57fdd/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ea0754cb7b0f682ebb005ffb0d18f8d17993490d9c289863cd69cacc4ab8df", size = 1408435, upload-time = "2026-04-30T23:24:11.013Z" }, - { url = "https://files.pythonhosted.org/packages/03/e7/54e3b469c3fa0bf9cd532fa643d1d33b73303f8d70beac3e366b68dd64b7/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a0c5aa1073a5ba7b2abaa4b54abe8b8d75c4d1e2d54a2ff70b0ca6222fea5728", size = 1508174, upload-time = "2026-04-30T23:24:12.635Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/9b9621865b02c60539e26d9b114a312b4fa46aa703e33e79317174bfea21/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4e52650d834c1ea7791969a361de2c54c13b2fb4c519ec79445fa8b9021a147d", size = 1502354, upload-time = "2026-04-30T23:24:14.186Z" }, - { url = "https://files.pythonhosted.org/packages/34/dd/f138bc5c43b0c414fdd12eefe15677839323078b6e75301ad7f96cd26d45/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15bd6af3f136c61dae27805eb6b8f3269e85a545c4c27ffe9e530ead78d2b36d", size = 1450504, upload-time = "2026-04-30T23:24:16.076Z" }, - { url = "https://files.pythonhosted.org/packages/68/cf/97ef9e1c315601db74365955c8edd3292e3055500d6317602815dbdf08ae/ast_serialize-0.3.0-cp314-cp314t-win32.whl", hash = "sha256:d188bfe37b674b49708497683051d4b571366a668799c9b8e8a94513694969d9", size = 1058662, upload-time = "2026-04-30T23:24:17.535Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d6/e2c3483c31580fdb623f92ad38d2f856cde4b9205a3e6bd84760f3de7d82/ast_serialize-0.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5832c2fdf8f8a6cf682b4cfcf677f5eaf39b4ddbc490f5480cfccdd1e7ce8fa1", size = 1100349, upload-time = "2026-04-30T23:24:18.992Z" }, - { url = "https://files.pythonhosted.org/packages/ab/89/29abcb1fe18a429cda60c6e0bbd1d6e90499339842a2f548d7567542357e/ast_serialize-0.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:670f177188d128fb7f9f15b5ad0e1b553d22c34e3f584dcb83eb8077600437f0", size = 1072895, upload-time = "2026-04-30T23:24:20.706Z" }, - { url = "https://files.pythonhosted.org/packages/bc/93/72abad83966ed6235647c9f956417dc1e17e997696388521910e3d1fa3f4/ast_serialize-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ec2fafa5e4313cc8feed96e436ebe19ac7bc6fa41fbc2827e826c48b9e4c3a9", size = 1190024, upload-time = "2026-04-30T23:24:22.486Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/eb88584b2f0234e581762011208ca203252bf6c98e59b4769daa571f3576/ast_serialize-0.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef6d3c08b7b4cd29b48410338e134764a00e76d25841eb02c1084e868c888ecc", size = 1178633, upload-time = "2026-04-30T23:24:24.35Z" }, - { url = "https://files.pythonhosted.org/packages/56/51/cf1ec1ff3e616373d0dcbd5fad502e0029dc541f13ab642259762a7d127f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d841424f41b886e98044abc80769c14a956e6e5ccd5fb5b0d9f5ead72be18a4", size = 1241351, upload-time = "2026-04-30T23:24:25.987Z" }, - { url = "https://files.pythonhosted.org/packages/0d/44/68fcf50478cf1093f2d423f034ae06453122c8b415d8e21a44668eca485d/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d21453734ad39367ede5d37efe4f59f830ce1c09f432fc72a90e368f77a4a3e7", size = 1239582, upload-time = "2026-04-30T23:24:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/9d/c1/a6c9fa284eceb5fc6f21347e968445a051d7ca2c4d34e6a04314646dbcee/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5e110cdce2a347e1dd987529c88ef54d26f67848dce3eba1b3b2cc2cf085c94", size = 1448853, upload-time = "2026-04-30T23:24:29.534Z" }, - { url = "https://files.pythonhosted.org/packages/23/5f/8ad3829a09e4e8c5328a53ce7d4711d660944e3e164c5f6abcc2c8f27167/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6e23a98e57560a055f5c4b68700a0fd5ce483d2814c23140b3638c7f5d1e61", size = 1262204, upload-time = "2026-04-30T23:24:31.482Z" }, - { url = "https://files.pythonhosted.org/packages/25/13/44aa28d97f10e25247e8576b5f6b2795d4fa1a80acc88acc942c508d06f7/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c9e763d70293d65ce1e1ea8c943140c68d0953f0268c7ee0998f2e07f77dd0", size = 1266458, upload-time = "2026-04-30T23:24:33.088Z" }, - { url = "https://files.pythonhosted.org/packages/d8/58/b3a8be3777cd3744324fd5cec0d80d37cd96fc7cbb0fb010e03dff1e870f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4388a1796c228f1ce5c391426f7d21a0003ad3b47f677dbeded9bd1a85c7209f", size = 1308700, upload-time = "2026-04-30T23:24:34.657Z" }, - { url = "https://files.pythonhosted.org/packages/13/03/f8312d6b57f5471a9dc7946f22b8798a1fc296d38c25766223aacadec42c/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5283cdcc0c64c3d8b9b688dc6aaa012d9c0cf1380a7f774a6bae6a1c01b3205a", size = 1416724, upload-time = "2026-04-30T23:24:36.562Z" }, - { url = "https://files.pythonhosted.org/packages/50/5d/13fc3789a7abac00559da2e2e9f386db4612aa1f84fc53d09bf714c37545/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ef88cc5842a5d7a6ac09dc0d5fc2c98f5d276c1f076f866d55047ce886785b", size = 1515441, upload-time = "2026-04-30T23:24:38.018Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b9/7ab43fc7a23b1f970281093228f5f79bed6edeed7a3e672bde6d7a832a58/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cc14bf402bdc0978594ecce783793de2c7470cd4f5cd7eb286ca97ed8ff7cba9", size = 1510522, upload-time = "2026-04-30T23:24:39.798Z" }, - { url = "https://files.pythonhosted.org/packages/56/ec/d75fc2b788d319f1fad77c14156896f31afdfc68af85b505e5bdebcb9592/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11eae0cf1b7b3e0678133cc2daa974ea972caf02eb4b3aa062af6fa9acd52c57", size = 1460917, upload-time = "2026-04-30T23:24:41.305Z" }, - { url = "https://files.pythonhosted.org/packages/95/74/f99c81193a2725911e1911ae567ed27c2f2419332c7f3537366f9d238cac/ast_serialize-0.3.0-cp39-abi3-win32.whl", hash = "sha256:2db3dd99de5e6a5a11d7dda73de8750eb6e5baaf25245adf7bdcfe64b6108ae2", size = 1067804, upload-time = "2026-04-30T23:24:43.091Z" }, - { url = "https://files.pythonhosted.org/packages/16/81/76af00c47daa151e89f98ae21fbbcb2840aaa9f5766579c4da76a3c57188/ast_serialize-0.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:a2cd125adccf7969470621905d302750cd25951f22ea430d9a25b7be031e5549", size = 1105561, upload-time = "2026-04-30T23:24:44.578Z" }, - { url = "https://files.pythonhosted.org/packages/bd/46/d3ec57ad500f598d1554bd14ce4df615960549ab2844961bc4e1f5fbd174/ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a", size = 1077165, upload-time = "2026-04-30T23:24:46.377Z" }, +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/1f/50f241d4e01fe75f4bba6a209edd4047c4b26acf70992ff885fd161f79cb/ast_serialize-0.4.0.tar.gz", hash = "sha256:74e4e634ab82d1466acf0be27043178570b98ebeaa3165f9240a6fad4c286471", size = 60687, upload-time = "2026-05-14T22:44:38.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/85/232631c59b5ca7152c08f026e9a46f47d852298acff74edd04a1fc1d0005/ast_serialize-0.4.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a6f26937ce0293aafbece0e39019e020369a5a70486ff4088227f0cc888844a9", size = 1182685, upload-time = "2026-05-14T22:43:40.205Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/4838d4d3ddc4425555601467d4e2a565e4340899e45feee4e32c80fbc911/ast_serialize-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:074032142777e3e6091977dc3c5146a8ca58ae6825b7f64e9a0b604153ddabd8", size = 1173113, upload-time = "2026-05-14T22:43:41.937Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/d622b19fc1c79a62028ec17f4ad4323177af25b174d32b07c84d61ef9d47/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:404f3462b4532e13a70b8849bba241dbd82e30043ff58d98c7e762fd925b116a", size = 1234117, upload-time = "2026-05-14T22:43:43.977Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b5/72f8c8659da0b64562e6d97f852d5c2022c74577df27c922e1e7065039ce/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:97c55336e16f5c4ca2bde7be94cca4b8f7d665d64f7008925a82e02707ba14ac", size = 1231703, upload-time = "2026-05-14T22:43:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/7b/98/ccc51ee4f90f97a1ed0a0848bd4c9d77a80969849db8a262b7d2970a6a15/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:732b4ef76adcb0f298a7d18c4558336d83b1384f9ae0c7eaa1dc8d031b0a4390", size = 1441574, upload-time = "2026-05-14T22:43:47.784Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ce/668c4efe79e09c9cc97a4d0a1c29e61fe6f78857fe1e57c086772af55f89/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b3db87c4772097c0782250bcd550d66b1189a8c889793c7bcf153f4fee70005c", size = 1254040, upload-time = "2026-05-14T22:43:49.879Z" }, + { url = "https://files.pythonhosted.org/packages/3d/be/38b27bc2909b7236939801ca9f0d97cdc6198da4f435a81658e0db506fdb/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43729a5e369ebbe7750635c0c206bc616fcd36e703cb9c4497d6b4df0291ee64", size = 1257847, upload-time = "2026-05-14T22:43:51.607Z" }, + { url = "https://files.pythonhosted.org/packages/68/df/360ebccc361235c167a8be2a0476870cb9ef44c42413bf1289b885684052/ast_serialize-0.4.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:91d3786f3929786cdc4eeedfd110abb4603e7f6c1390c5af398f333a947b742d", size = 1298683, upload-time = "2026-05-14T22:43:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/51/5c/7d5e0b4d47aafa1600c19e3670f962f81a9bf3da1bc25a1382529a447cf3/ast_serialize-0.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7fba7315fd4bd87cb5560792709f6e66e0606402d362c0a38dd32dfb66ba6066", size = 1409438, upload-time = "2026-05-14T22:43:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/8875b2f1af3ec1539b88ff193dfbfa5573084ef7fcab27ea4cd09b6dc829/ast_serialize-0.4.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4db9769d57deb5545ce56ebbbbe3436dcc0ae2688ce14c295cd14e106624ece7", size = 1507922, upload-time = "2026-05-14T22:43:56.959Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/5ec6927eb493ece7ba64263cdc556be889e0c62a013b1851bbe674a0dcda/ast_serialize-0.4.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:dcd04f85a29deb80400e8987cfaceb9907140f763453cbffdbd6ff36f1b32c12", size = 1502817, upload-time = "2026-05-14T22:43:59.081Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c8/40cb818a08396b1f34d6189c0c42aec917dd331e11fb7c3b870cc61b795a/ast_serialize-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:905fc11940831454d93589bd7ce2acb6a5eb01c2936156f751d2a21087c98cd3", size = 1454318, upload-time = "2026-05-14T22:44:01.377Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/d51494b60cc52f4792be5ddc951631cddb17a2990154634549abdbdbb5bf/ast_serialize-0.4.0-cp314-cp314t-win32.whl", hash = "sha256:3bdde2c4570143791f636aed4e3ef868f5b46eb90a18f8d5c41dd045aab08bef", size = 1060098, upload-time = "2026-05-14T22:44:03.265Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c9/b0086257c79ff95743a3621448a01fc71b234ae359d3d54cda383aa43939/ast_serialize-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6551d55b8607b97a7755683d743200b398c61a0b71a11b7f00c89c335a11d0f4", size = 1101015, upload-time = "2026-05-14T22:44:05.055Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6d/3dfddef4990fda47745af6615a3e51c4de711eda56c3a8072a0d8b6181c7/ast_serialize-0.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7234ff086cb152ea2a3b7ef895b5ebeb6d80779df049d5c6431c8e3536d5b03c", size = 1074495, upload-time = "2026-05-14T22:44:07.186Z" }, + { url = "https://files.pythonhosted.org/packages/be/d5/044c5f995ef75807a0effb56fc288cfdedeeb571222450fb6f7d94fd52f1/ast_serialize-0.4.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dcded5056d9f3d201df7833082c07ebcbc566ffc3d4105c9fc9fe278fa086ecb", size = 1189800, upload-time = "2026-05-14T22:44:09.333Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5a/52163557789d59a8197c10912ab4a1791c9143731ba0e3d9283ac0791db6/ast_serialize-0.4.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bd50d201098aae0d202805fe9606c0545492f69a3ec4403337e32c54ad29fc41", size = 1181713, upload-time = "2026-05-14T22:44:11.286Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c3/678ce3b6cb594b01c361da87f6c5679d26c1dae1583a082a8cd190e7232e/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6615b39cd747967c3aabe68bf3f5f26748e823cc6b474ddc1510ed188a824149", size = 1243258, upload-time = "2026-05-14T22:44:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dd/4810fbeb81c47b7e4e65db15ca65c71330efc59b460bd10c12338dc6012e/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91362c0a9fdf1c344b7f50a5b0508b11a0732102998fbd754a191f7187e77031", size = 1239226, upload-time = "2026-05-14T22:44:15.811Z" }, + { url = "https://files.pythonhosted.org/packages/28/38/13a88d90b664c009ed208346ec2ed248b0ab2cb0b582ae467acaa7f44fa4/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70d9c5d527bbfa69bd3c7d17dac11fb6781e36186a434a06d7d5892e0b2f88f9", size = 1448867, upload-time = "2026-05-14T22:44:17.99Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/a069dba1a634b703bf07fb49df8f7e3c04e9ba8ef3f0d9f4495f72630f92/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4738790cf54d8b416de992b87ee567056980bc82134d52458bd4985f389d1658", size = 1264135, upload-time = "2026-05-14T22:44:19.8Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/76ec4279fecd7e78b60c3c99321f944c43cd11e5ff09c952746f5f9c0f4c/ast_serialize-0.4.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:faa008dccfcb793ae9101325e4d6d026caaa5d845c2182f03749c759834b0a3a", size = 1269060, upload-time = "2026-05-14T22:44:21.894Z" }, + { url = "https://files.pythonhosted.org/packages/33/c5/9230ef7481e5cb63b93a1f7738e959586202b081caf32b8bc5d9f673ef56/ast_serialize-0.4.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c5245228e65d38cb48e1251f0ca71b0fa417e527141491e8c92f740e8e2d121", size = 1309654, upload-time = "2026-05-14T22:44:23.725Z" }, + { url = "https://files.pythonhosted.org/packages/b9/54/7d7397528d181ad68e476e0c81aa3ceff7d1f1b5c7fa958d6be28628ef16/ast_serialize-0.4.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8f5153e9c44a02e61f4042c5f9249d2e8a759773d621a0b2f445a899e536e181", size = 1418855, upload-time = "2026-05-14T22:44:25.415Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8f/87d6428adaa0986b817404f09329b64f8d2614cfe061ebf4951b4a7e0d19/ast_serialize-0.4.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1e1fb90def261f6a0db885876f7e1a49ad2dbac38ad9f2f62dba2f9543af16e7", size = 1516040, upload-time = "2026-05-14T22:44:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/b5/bb/5aaa41a21314c8b0d6dee54867b16535682c6660dd28cac64dba1380062d/ast_serialize-0.4.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf2ff7b654c8e95143e20f5d75878cbb78b65b928b26c4d58ef71cdba9d6d981", size = 1511450, upload-time = "2026-05-14T22:44:29.522Z" }, + { url = "https://files.pythonhosted.org/packages/87/16/cc729b5bb4b21da99db1379266cc367512e82ba10f9b3300a6f3e9941325/ast_serialize-0.4.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:90fc5c0d35a22f1a92dd33635508626d50f8fc64deb897c23e78e666a60804c9", size = 1463654, upload-time = "2026-05-14T22:44:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/43/97/7198321b0244d011093387b41affea934d58bda08d59a2adfde72976b6c4/ast_serialize-0.4.0-cp39-abi3-win32.whl", hash = "sha256:9ecd6a1fc1b86f1f4e8ae206759b6319c10019706b3496b01b54d02b9b2cd918", size = 1068636, upload-time = "2026-05-14T22:44:33.189Z" }, + { url = "https://files.pythonhosted.org/packages/10/09/3b868f6d8df4bbe452903a5e0e039ebcec9ea0045f1a77951546205097e8/ast_serialize-0.4.0-cp39-abi3-win_amd64.whl", hash = "sha256:79c8d015c771c8bfdb1208003b227b27c40034790a2c29c09f2317a041825ce2", size = 1107137, upload-time = "2026-05-14T22:44:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/78/9387dffccdc55a12734f83aaccc4a987404a217a2a12a1920d8d4585950b/ast_serialize-0.4.0-cp39-abi3-win_arm64.whl", hash = "sha256:1026f565a7ab846337c630909089b3346a2fe417bf1552b1581ab01852137407", size = 1079199, upload-time = "2026-05-14T22:44:36.816Z" }, ] [[package]] @@ -981,15 +981,15 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "10.21.2" +version = "10.21.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, ] [[package]] @@ -1125,14 +1125,14 @@ wheels = [ [[package]] name = "python-injection" -version = "0.25.15" +version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "type-analyzer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/71/19b96f631b7cd5168603c5ecdb382e7d23c423007b9bdaf1becf7076ef98/python_injection-0.25.15.tar.gz", hash = "sha256:1cd107ecf65cd8c0ba0de58190e8dd2f30489d941ff3aa179319ebe1dddf6917", size = 24517, upload-time = "2026-02-03T12:32:51.941Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/a3/92d298bed487d83a49fc2f49b717a5c97cb63056291ee14e3177fb56049c/python_injection-0.26.0.tar.gz", hash = "sha256:70ae0327b88a7bde26bfbc35013dc8477f25267467f482b93ce3895a7b1d6612", size = 24640, upload-time = "2026-05-15T08:41:49.979Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/92/2affad30bdda30daac51e6fbd3ce3e64fc6bdc87085ae85fa4ad45a95185/python_injection-0.25.15-py3-none-any.whl", hash = "sha256:fbf87b5032382255c2dd761a030a02314013e40338bbe6ee5b665ef0937674dd", size = 31241, upload-time = "2026-02-03T12:32:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/51/45/b5795255ab61a8e42f0b34e542f2ebf3dd8c8d3da4ab09f4fbce8529a97e/python_injection-0.26.0-py3-none-any.whl", hash = "sha256:fa87dbef570bd0c67755b04296075e5d019af389878e55208c446016e859d90e", size = 31360, upload-time = "2026-05-15T08:41:51.103Z" }, ] [package.optional-dependencies] @@ -1209,7 +1209,7 @@ wheels = [ [[package]] name = "requests" -version = "2.34.0" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1217,9 +1217,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/b8/7a707d60fea4c49094e40262cc0e2ca6c768cca21587e34d3f705afec47e/requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a", size = 142436, upload-time = "2026-05-11T19:29:51.717Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60", size = 73021, upload-time = "2026-05-11T19:29:49.923Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -1237,27 +1237,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, - { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, - { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, - { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, - { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, - { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, - { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, - { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, - { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, - { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, - { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, +version = "0.15.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" }, + { url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" }, + { url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" }, + { url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" }, + { url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" }, + { url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" }, + { url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" }, + { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, ] [[package]] @@ -1385,7 +1385,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.3.2" +version = "21.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -1393,9 +1393,9 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/e1/665267cea4767debd19f584667a9197c2098b5e7f67a502da9f3a086ab37/virtualenv-21.3.2.tar.gz", hash = "sha256:3ecda97894a6fc1c53106356f488690e5c86278c1f693f3fc0805ac85a513686", size = 7613810, upload-time = "2026-05-12T14:44:18.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl", hash = "sha256:c58ea748fa50bb2a4367da5ba3d30b02458ed40b4ea888faad94021f3309f764", size = 7594558, upload-time = "2026-05-12T14:44:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl", hash = "sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3", size = 7594554, upload-time = "2026-05-13T18:01:27.815Z" }, ] [[package]]