From 1feb2715dccea1bf02bf7c9c8a8e81abcfd49af9 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Sat, 1 Feb 2025 12:05:46 +0100 Subject: [PATCH 01/60] prepare for next release cycle --- CHANGELOG.rst | 11 +++++++++++ src/h2/__init__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 65cebf456..a4175f799 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,17 @@ Release History =============== +dev +--- + +**API Changes (Backward Incompatible)** + +- + +**Bugfixes** + +- + 4.2.0 (2025-02-01) ------------------ diff --git a/src/h2/__init__.py b/src/h2/__init__.py index 0764daadb..35bfe397a 100644 --- a/src/h2/__init__.py +++ b/src/h2/__init__.py @@ -3,4 +3,4 @@ """ from __future__ import annotations -__version__ = "4.2.0" +__version__ = "4.3.0+dev" From 748eef93fffbf74049c90a10647fadf83f0c9e25 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 7 Feb 2025 19:23:25 +0100 Subject: [PATCH 02/60] enforce stricter types for `H2StreamStateMachine` --- src/h2/stream.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/h2/stream.py b/src/h2/stream.py index 7d4a12e35..a3c99e351 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -46,7 +46,7 @@ from .windows import WindowManager if TYPE_CHECKING: # pragma: no cover - from collections.abc import Generator, Iterable + from collections.abc import Callable, Generator, Iterable from hpack.hpack import Encoder from hpack.struct import Header, HeaderWeaklyTyped @@ -131,7 +131,7 @@ def __init__(self, stream_id: int) -> None: # How the stream was closed. One of StreamClosedBy. self.stream_closed_by: StreamClosedBy | None = None - def process_input(self, input_: StreamInputs) -> Any: + def process_input(self, input_: StreamInputs) -> list[Event]: """ Process a specific input in the state machine. """ @@ -315,21 +315,23 @@ def recv_push_promise(self, previous_state: StreamState) -> list[Event]: event.parent_stream_id = self.stream_id return [event] - def send_end_stream(self, previous_state: StreamState) -> None: + def send_end_stream(self, previous_state: StreamState) -> list[Event]: """ Called when an attempt is made to send END_STREAM in the HALF_CLOSED_REMOTE state. """ self.stream_closed_by = StreamClosedBy.SEND_END_STREAM + return [] - def send_reset_stream(self, previous_state: StreamState) -> None: + def send_reset_stream(self, previous_state: StreamState) -> list[Event]: """ Called when an attempt is made to send RST_STREAM in a non-closed stream state. """ self.stream_closed_by = StreamClosedBy.SEND_RST_STREAM + return [] - def reset_stream_on_error(self, previous_state: StreamState) -> None: + def reset_stream_on_error(self, previous_state: StreamState) -> list[Event]: """ Called when we need to forcefully emit another RST_STREAM frame on behalf of the state machine. @@ -350,7 +352,7 @@ def reset_stream_on_error(self, previous_state: StreamState) -> None: error._events = [event] raise error - def recv_on_closed_stream(self, previous_state: StreamState) -> None: + def recv_on_closed_stream(self, previous_state: StreamState) -> list[Event]: """ Called when an unexpected frame is received on an already-closed stream. @@ -362,7 +364,7 @@ def recv_on_closed_stream(self, previous_state: StreamState) -> None: """ raise StreamClosedError(self.stream_id) - def send_on_closed_stream(self, previous_state: StreamState) -> None: + def send_on_closed_stream(self, previous_state: StreamState) -> list[Event]: """ Called when an attempt is made to send data on an already-closed stream. @@ -374,7 +376,7 @@ def send_on_closed_stream(self, previous_state: StreamState) -> None: """ raise StreamClosedError(self.stream_id) - def recv_push_on_closed_stream(self, previous_state: StreamState) -> None: + def recv_push_on_closed_stream(self, previous_state: StreamState) -> list[Event]: """ Called when a PUSH_PROMISE frame is received on a full stop stream. @@ -393,7 +395,7 @@ def recv_push_on_closed_stream(self, previous_state: StreamState) -> None: msg = "Attempted to push on closed stream." raise ProtocolError(msg) - def send_push_on_closed_stream(self, previous_state: StreamState) -> None: + def send_push_on_closed_stream(self, previous_state: StreamState) -> list[Event]: """ Called when an attempt is made to push on an already-closed stream. @@ -473,7 +475,7 @@ def recv_alt_svc(self, previous_state: StreamState) -> list[Event]: # the event and let it get populated. return [AlternativeServiceAvailable()] - def send_alt_svc(self, previous_state: StreamState) -> None: + def send_alt_svc(self, previous_state: StreamState) -> list[Event]: """ Called when sending an ALTSVC frame on this stream. @@ -489,6 +491,7 @@ def send_alt_svc(self, previous_state: StreamState) -> None: if self.headers_sent: msg = "Cannot send ALTSVC after sending response headers." raise ProtocolError(msg) + return [] @@ -561,7 +564,10 @@ def send_alt_svc(self, previous_state: StreamState) -> None: # (state, input) to tuples of (side_effect_function, end_state). This # map contains all allowed transitions: anything not in this map is # invalid and immediately causes a transition to ``closed``. -_transitions = { +_transitions: dict[ + tuple[StreamState, StreamInputs], + tuple[Callable[[H2StreamStateMachine, StreamState], list[Event]] | None, StreamState], +] = { # State: idle (StreamState.IDLE, StreamInputs.SEND_HEADERS): (H2StreamStateMachine.request_sent, StreamState.OPEN), From 6901794827338c14ffdea4ad608a4af1d8078af8 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 7 Feb 2025 23:22:10 +0100 Subject: [PATCH 03/60] fix up type errors with stricter state machine --- src/h2/events.py | 14 +++++++------- src/h2/stream.py | 34 ++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/h2/events.py b/src/h2/events.py index b81fd1a63..7a22f152c 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -16,7 +16,7 @@ from .settings import ChangedSetting, SettingCodes, Settings, _setting_code_from_int if TYPE_CHECKING: # pragma: no cover - from hpack import HeaderTuple + from hpack.struct import Header from hyperframe.frame import Frame from .errors import ErrorCodes @@ -52,7 +52,7 @@ def __init__(self) -> None: self.stream_id: int | None = None #: The request headers. - self.headers: list[HeaderTuple] | None = None + self.headers: list[Header] | None = None #: If this request also ended the stream, the associated #: :class:`StreamEnded ` event will be available @@ -91,7 +91,7 @@ def __init__(self) -> None: self.stream_id: int | None = None #: The response headers. - self.headers: list[HeaderTuple] | None = None + self.headers: list[Header] | None = None #: If this response also ended the stream, the associated #: :class:`StreamEnded ` event will be available @@ -133,7 +133,7 @@ def __init__(self) -> None: self.stream_id: int | None = None #: The trailers themselves. - self.headers: list[HeaderTuple] | None = None + self.headers: list[Header] | None = None #: Trailers always end streams. This property has the associated #: :class:`StreamEnded ` in it. @@ -237,7 +237,7 @@ def __init__(self) -> None: self.stream_id: int | None = None #: The headers for this informational response. - self.headers: list[HeaderTuple] | None = None + self.headers: list[Header] | None = None #: If this response also had associated priority information, the #: associated :class:`PriorityUpdated ` @@ -436,7 +436,7 @@ def __init__(self) -> None: #: The error code given. Either one of :class:`ErrorCodes #: ` or ``int`` - self.error_code: ErrorCodes | None = None + self.error_code: ErrorCodes | int | None = None #: Whether the remote peer sent a RST_STREAM or we did. self.remote_reset = True @@ -460,7 +460,7 @@ def __init__(self) -> None: self.parent_stream_id: int | None = None #: The request headers, sent by the remote party in the push. - self.headers: list[HeaderTuple] | None = None + self.headers: list[Header] | None = None def __repr__(self) -> str: return ( diff --git a/src/h2/stream.py b/src/h2/stream.py index a3c99e351..3f6c97cd1 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -7,7 +7,7 @@ from __future__ import annotations from enum import Enum, IntEnum -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Union, cast from hpack import HeaderTuple from hyperframe.frame import AltSvcFrame, ContinuationFrame, DataFrame, Frame, HeadersFrame, PushPromiseFrame, RstStreamFrame, WindowUpdateFrame @@ -1046,10 +1046,11 @@ def receive_push_promise_in_band(self, events = self.state_machine.process_input( StreamInputs.RECV_PUSH_PROMISE, ) - events[0].pushed_stream_id = promised_stream_id + push_event = cast(PushedStreamReceived, events[0]) + push_event.pushed_stream_id = promised_stream_id hdr_validation_flags = self._build_hdr_validation_flags(events) - events[0].headers = self._process_received_headers( + push_event.headers = self._process_received_headers( headers, hdr_validation_flags, header_encoding, ) return [], events @@ -1083,22 +1084,30 @@ def receive_headers(self, input_ = StreamInputs.RECV_HEADERS events = self.state_machine.process_input(input_) + headers_event = cast( + Union[RequestReceived, ResponseReceived, TrailersReceived, InformationalResponseReceived], + events[0], + ) if end_stream: es_events = self.state_machine.process_input( StreamInputs.RECV_END_STREAM, ) - events[0].stream_ended = es_events[0] + # We ensured it's not an information response at the beginning of the method. + cast( + Union[RequestReceived, ResponseReceived, TrailersReceived], + headers_event, + ).stream_ended = cast(StreamEnded, es_events[0]) events += es_events self._initialize_content_length(headers) - if isinstance(events[0], TrailersReceived) and not end_stream: + if isinstance(headers_event, TrailersReceived) and not end_stream: msg = "Trailers must have END_STREAM set" raise ProtocolError(msg) hdr_validation_flags = self._build_hdr_validation_flags(events) - events[0].headers = self._process_received_headers( + headers_event.headers = self._process_received_headers( headers, hdr_validation_flags, header_encoding, ) return [], events @@ -1112,6 +1121,7 @@ def receive_data(self, data: bytes, end_stream: bool, flow_control_len: int) -> "set to %d", self, end_stream, flow_control_len, ) events = self.state_machine.process_input(StreamInputs.RECV_DATA) + data_event = cast(DataReceived, events[0]) self._inbound_window_manager.window_consumed(flow_control_len) self._track_content_length(len(data), end_stream) @@ -1119,11 +1129,11 @@ def receive_data(self, data: bytes, end_stream: bool, flow_control_len: int) -> es_events = self.state_machine.process_input( StreamInputs.RECV_END_STREAM, ) - events[0].stream_ended = es_events[0] + data_event.stream_ended = cast(StreamEnded, es_events[0]) events.extend(es_events) - events[0].data = data - events[0].flow_controlled_length = flow_control_len + data_event.data = data + data_event.flow_controlled_length = flow_control_len return [], events def receive_window_update(self, increment: int) -> tuple[list[Frame], list[Event]]: @@ -1143,7 +1153,7 @@ def receive_window_update(self, increment: int) -> tuple[list[Frame], list[Event # this should be treated as a *stream* error, not a *connection* error. # That means we need to catch the error and forcibly close the stream. if events: - events[0].delta = increment + cast(WindowUpdated, events[0]).delta = increment try: self.outbound_flow_control_window = guard_increment_window( self.outbound_flow_control_window, @@ -1226,7 +1236,7 @@ def stream_reset(self, frame: RstStreamFrame) -> tuple[list[Frame], list[Event]] if events: # We don't fire an event if this stream is already closed. - events[0].error_code = _error_code_from_int(frame.error_code) + cast(StreamReset, events[0]).error_code = _error_code_from_int(frame.error_code) return [], events @@ -1328,7 +1338,7 @@ def _build_headers_frames(self, def _process_received_headers(self, headers: Iterable[Header], header_validation_flags: HeaderValidationFlags, - header_encoding: bool | str | None) -> Iterable[Header]: + header_encoding: bool | str | None) -> list[Header]: """ When headers have been received from the remote peer, run a processing pipeline on them to transform them into the appropriate form for From 4ccc768800a2c77402bbbcfbd0c0decd0b29d1ee Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 4 Feb 2025 00:46:22 +0100 Subject: [PATCH 04/60] convert `events.WindowUpdated` into a dataclass --- src/h2/connection.py | 4 +--- src/h2/events.py | 17 +++++++++++------ src/h2/stream.py | 4 +--- tests/test_events.py | 4 +--- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/h2/connection.py b/src/h2/connection.py index 28be9fca2..d9ff0862f 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -1806,9 +1806,7 @@ def _receive_window_update_frame(self, frame: WindowUpdateFrame) -> tuple[list[F ) # FIXME: Should we split this into one event per active stream? - window_updated_event = WindowUpdated() - window_updated_event.stream_id = 0 - window_updated_event.delta = frame.window_increment + window_updated_event = WindowUpdated(stream_id=0, delta=frame.window_increment) stream_events = [window_updated_event] frames = [] diff --git a/src/h2/events.py b/src/h2/events.py index 7a22f152c..9a852e8ac 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -11,6 +11,7 @@ from __future__ import annotations import binascii +from dataclasses import dataclass from typing import TYPE_CHECKING from .settings import ChangedSetting, SettingCodes, Settings, _setting_code_from_int @@ -292,6 +293,7 @@ def __repr__(self) -> str: ) +@dataclass(kw_only=True) class WindowUpdated(Event): """ The WindowUpdated event is fired whenever a flow control window changes @@ -301,13 +303,16 @@ class WindowUpdated(Event): the connection), and the delta in the window size. """ - def __init__(self) -> None: - #: The Stream ID of the stream whose flow control window was changed. - #: May be ``0`` if the connection window was changed. - self.stream_id: int | None = None + stream_id: int + """ + The Stream ID of the stream whose flow control window was changed. + May be ``0`` if the connection window was changed. + """ - #: The window delta. - self.delta: int | None = None + delta: int | None = None + """ + The window delta. + """ def __repr__(self) -> str: return f"" diff --git a/src/h2/stream.py b/src/h2/stream.py index 3f6c97cd1..50c2f6024 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -232,9 +232,7 @@ def window_updated(self, previous_state: StreamState) -> list[Event]: """ Fires when a window update frame is received. """ - event = WindowUpdated() - event.stream_id = self.stream_id - return [event] + return [WindowUpdated(stream_id=self.stream_id)] def stream_half_closed(self, previous_state: StreamState) -> list[Event]: """ diff --git a/tests/test_events.py b/tests/test_events.py index aac913586..3c4f3cefd 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -186,9 +186,7 @@ def test_windowupdated_repr(self) -> None: """ WindowUpdated has a useful debug representation. """ - e = h2.events.WindowUpdated() - e.stream_id = 0 - e.delta = 2**16 + e = h2.events.WindowUpdated(stream_id=0, delta=2**16) assert repr(e) == "" From 11a01a3b83084bd702e073e167cad5f77cc81acb Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 5 Feb 2025 11:57:02 +0100 Subject: [PATCH 05/60] ignore kw_only on Python 3.9 --- src/h2/events.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/h2/events.py b/src/h2/events.py index 9a852e8ac..ce1730436 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -11,6 +11,7 @@ from __future__ import annotations import binascii +import sys from dataclasses import dataclass from typing import TYPE_CHECKING @@ -23,6 +24,12 @@ from .errors import ErrorCodes +if sys.version_info < (3, 10): # pragma: no cover + kw_only: dict[str, bool] = {} +else: # pragma: no cover + kw_only = {"kw_only": True} + + class Event: """ Base class for h2 events. @@ -293,7 +300,7 @@ def __repr__(self) -> str: ) -@dataclass(kw_only=True) +@dataclass(**kw_only) class WindowUpdated(Event): """ The WindowUpdated event is fired whenever a flow control window changes From 8d64bfc4087564b36272b22fee5ff0a11f5895e7 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 7 Feb 2025 18:55:41 +0100 Subject: [PATCH 06/60] convert `events.StreamReset` into a dataclass --- src/h2/events.py | 21 +++++++++++++-------- src/h2/stream.py | 32 ++++++++++++++++---------------- tests/test_events.py | 9 +++++---- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/h2/events.py b/src/h2/events.py index ce1730436..c4815cc71 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -431,6 +431,7 @@ def __repr__(self) -> str: return f"" +@dataclass(**kw_only) class StreamReset(Event): """ The StreamReset event is fired in two situations. The first is when the @@ -442,16 +443,20 @@ class StreamReset(Event): This event is now fired when h2 automatically resets a stream. """ - def __init__(self) -> None: - #: The Stream ID of the stream that was reset. - self.stream_id: int | None = None + stream_id: int + """ + The Stream ID of the stream that was reset. + """ - #: The error code given. Either one of :class:`ErrorCodes - #: ` or ``int`` - self.error_code: ErrorCodes | int | None = None + error_code: ErrorCodes | int = _LAZY_INIT + """ + The error code given. + """ - #: Whether the remote peer sent a RST_STREAM or we did. - self.remote_reset = True + remote_reset: bool = True + """ + Whether the remote peer sent a RST_STREAM or we did. + """ def __repr__(self) -> str: return f"" diff --git a/src/h2/stream.py b/src/h2/stream.py index 50c2f6024..0a231ad08 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -257,9 +257,7 @@ def stream_reset(self, previous_state: StreamState) -> list[Event]: Fired when a stream is forcefully reset. """ self.stream_closed_by = StreamClosedBy.RECV_RST_STREAM - event = StreamReset() - event.stream_id = self.stream_id - return [event] + return [StreamReset(stream_id=self.stream_id)] def send_new_pushed_stream(self, previous_state: StreamState) -> list[Event]: """ @@ -342,12 +340,13 @@ def reset_stream_on_error(self, previous_state: StreamState) -> list[Event]: self.stream_closed_by = StreamClosedBy.SEND_RST_STREAM error = StreamClosedError(self.stream_id) - - event = StreamReset() - event.stream_id = self.stream_id - event.error_code = ErrorCodes.STREAM_CLOSED - event.remote_reset = False - error._events = [event] + error._events = [ + StreamReset( + stream_id=self.stream_id, + error_code=ErrorCodes.STREAM_CLOSED, + remote_reset=False, + ), + ] raise error def recv_on_closed_stream(self, previous_state: StreamState) -> list[Event]: @@ -1160,13 +1159,14 @@ def receive_window_update(self, increment: int) -> tuple[list[Frame], list[Event except FlowControlError: # Ok, this is bad. We're going to need to perform a local # reset. - event = StreamReset() - event.stream_id = self.stream_id - event.error_code = ErrorCodes.FLOW_CONTROL_ERROR - event.remote_reset = False - - events = [event] - frames = self.reset_stream(event.error_code) + events = [ + StreamReset( + stream_id=self.stream_id, + error_code=ErrorCodes.FLOW_CONTROL_ERROR, + remote_reset=False, + ), + ] + frames = self.reset_stream(ErrorCodes.FLOW_CONTROL_ERROR) return frames, events diff --git a/tests/test_events.py b/tests/test_events.py index 3c4f3cefd..319d8a658 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -246,10 +246,11 @@ def test_streamreset_repr(self) -> None: """ StreamEnded has a useful debug representation. """ - e = h2.events.StreamReset() - e.stream_id = 919 - e.error_code = h2.errors.ErrorCodes.ENHANCE_YOUR_CALM - e.remote_reset = False + e = h2.events.StreamReset( + stream_id=919, + error_code=h2.errors.ErrorCodes.ENHANCE_YOUR_CALM, + remote_reset=False, + ) if sys.version_info >= (3, 11): assert repr(e) == ( From e40bfe74d6891fef2873ca85aed12885a450a3bc Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 7 Feb 2025 19:04:57 +0100 Subject: [PATCH 07/60] convert `events.DataReceived` into a dataclass --- src/h2/events.py | 47 ++++++++++++++++++++++++++++---------------- src/h2/stream.py | 3 +-- tests/test_events.py | 9 +++++---- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/h2/events.py b/src/h2/events.py index c4815cc71..add38af0a 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -13,7 +13,7 @@ import binascii import sys from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING from .settings import ChangedSetting, SettingCodes, Settings, _setting_code_from_int @@ -30,6 +30,15 @@ kw_only = {"kw_only": True} +_LAZY_INIT: Any = object() +""" +Some h2 events are instantiated by the state machine, but its attributes are +subsequently populated by H2Stream. To make this work with strict type annotations +on the events, they are temporarily set to this placeholder value. +This value should never be exposed to users. +""" + + class Event: """ Base class for h2 events. @@ -258,6 +267,7 @@ def __repr__(self) -> str: return f"" +@dataclass(**kw_only) class DataReceived(Event): """ The DataReceived event is fired whenever data is received on a stream from @@ -268,25 +278,28 @@ class DataReceived(Event): Added ``stream_ended`` property. """ - def __init__(self) -> None: - #: The Stream ID for the stream this data was received on. - self.stream_id: int | None = None + stream_id: int + """The Stream ID for the stream this data was received on.""" + + data: bytes = _LAZY_INIT + """The data itself.""" - #: The data itself. - self.data: bytes | None = None + flow_controlled_length: int = _LAZY_INIT + """ + The amount of data received that counts against the flow control + window. Note that padding counts against the flow control window, so + when adjusting flow control you should always use this field rather + than ``len(data)``. + """ - #: The amount of data received that counts against the flow control - #: window. Note that padding counts against the flow control window, so - #: when adjusting flow control you should always use this field rather - #: than ``len(data)``. - self.flow_controlled_length: int | None = None + stream_ended: StreamEnded | None = None + """ + If this data chunk also completed the stream, the associated + :class:`StreamEnded ` event will be available + here. - #: If this data chunk also completed the stream, the associated - #: :class:`StreamEnded ` event will be available - #: here. - #: - #: .. versionadded:: 2.4.0 - self.stream_ended: StreamEnded | None = None + .. versionadded:: 2.4.0 + """ def __repr__(self) -> str: return ( diff --git a/src/h2/stream.py b/src/h2/stream.py index 0a231ad08..c1715cbaa 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -224,8 +224,7 @@ def data_received(self, previous_state: StreamState) -> list[Event]: if not self.headers_received: msg = "cannot receive data before headers" raise ProtocolError(msg) - event = DataReceived() - event.stream_id = self.stream_id + event = DataReceived(stream_id=self.stream_id) return [event] def window_updated(self, previous_state: StreamState) -> list[Event]: diff --git a/tests/test_events.py b/tests/test_events.py index 319d8a658..27efe5b48 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -172,10 +172,11 @@ def test_datareceived_repr(self) -> None: """ DataReceived has a useful debug representation. """ - e = h2.events.DataReceived() - e.stream_id = 888 - e.data = b"abcdefghijklmnopqrstuvwxyz" - e.flow_controlled_length = 88 + e = h2.events.DataReceived( + stream_id=888, + data=b"abcdefghijklmnopqrstuvwxyz", + flow_controlled_length=88, + ) assert repr(e) == ( " Date: Fri, 7 Feb 2025 23:42:03 +0100 Subject: [PATCH 08/60] convert `events.TrailerReceived` into a dataclass --- src/h2/events.py | 38 +++++++++++++++++++++----------------- src/h2/stream.py | 2 +- tests/test_events.py | 4 +--- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/h2/events.py b/src/h2/events.py index add38af0a..c5d0708e3 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -13,7 +13,7 @@ import binascii import sys from dataclasses import dataclass -from typing import Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any from .settings import ChangedSetting, SettingCodes, Settings, _setting_code_from_int @@ -128,6 +128,7 @@ def __repr__(self) -> str: return f"" +@dataclass(**kw_only) class TrailersReceived(Event): """ The TrailersReceived event is fired whenever trailers are received on a @@ -145,25 +146,28 @@ class TrailersReceived(Event): Added ``stream_ended`` and ``priority_updated`` properties. """ - def __init__(self) -> None: - #: The Stream ID for the stream on which these trailers were received. - self.stream_id: int | None = None + stream_id: int + """The Stream ID for the stream on which these trailers were received.""" - #: The trailers themselves. - self.headers: list[Header] | None = None + headers: list[Header] = _LAZY_INIT + """The trailers themselves.""" - #: Trailers always end streams. This property has the associated - #: :class:`StreamEnded ` in it. - #: - #: .. versionadded:: 2.4.0 - self.stream_ended: StreamEnded | None = None + stream_ended: StreamEnded | None = None + """ + Trailers always end streams. This property has the associated + :class:`StreamEnded ` in it. - #: If the trailers also set associated priority information, the - #: associated :class:`PriorityUpdated ` - #: event will be available here. - #: - #: .. versionadded:: 2.4.0 - self.priority_updated: PriorityUpdated | None = None + .. versionadded:: 2.4.0 + """ + + priority_updated: PriorityUpdated | None = None + """ + If the trailers also set associated priority information, the + associated :class:`PriorityUpdated ` + event will be available here. + + .. versionadded:: 2.4.0 + """ def __repr__(self) -> str: return f"" diff --git a/src/h2/stream.py b/src/h2/stream.py index c1715cbaa..1c42f0c17 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -212,7 +212,7 @@ def response_received(self, previous_state: StreamState) -> list[Event]: else: assert not self.trailers_received self.trailers_received = True - event = TrailersReceived() + event = TrailersReceived(stream_id=self.stream_id) event.stream_id = self.stream_id return [event] diff --git a/tests/test_events.py b/tests/test_events.py index 27efe5b48..9dfe3abc6 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -144,9 +144,7 @@ def test_trailersreceived_repr(self) -> None: """ TrailersReceived has a useful debug representation. """ - e = h2.events.TrailersReceived() - e.stream_id = 62 - e.headers = self.example_response_headers + e = h2.events.TrailersReceived(stream_id=62, headers=self.example_response_headers) assert repr(e) == ( " Date: Fri, 7 Feb 2025 23:47:38 +0100 Subject: [PATCH 09/60] convert `events.StreamEnded` into a dataclass --- src/h2/events.py | 6 +++--- src/h2/stream.py | 6 ++---- tests/test_events.py | 3 +-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/h2/events.py b/src/h2/events.py index c5d0708e3..65d69d14d 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -433,6 +433,7 @@ def __repr__(self) -> str: return f"" +@dataclass(**kw_only) class StreamEnded(Event): """ The StreamEnded event is fired whenever a stream is ended by a remote @@ -440,9 +441,8 @@ class StreamEnded(Event): locally, but no further data or headers should be expected on that stream. """ - def __init__(self) -> None: - #: The Stream ID of the stream that was closed. - self.stream_id: int | None = None + stream_id: int + """The Stream ID of the stream that was closed.""" def __repr__(self) -> str: return f"" diff --git a/src/h2/stream.py b/src/h2/stream.py index 1c42f0c17..91263b7b7 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -238,8 +238,7 @@ def stream_half_closed(self, previous_state: StreamState) -> list[Event]: Fires when an END_STREAM flag is received in the OPEN state, transitioning this stream to a HALF_CLOSED_REMOTE state. """ - event = StreamEnded() - event.stream_id = self.stream_id + event = StreamEnded(stream_id=self.stream_id) return [event] def stream_ended(self, previous_state: StreamState) -> list[Event]: @@ -247,8 +246,7 @@ def stream_ended(self, previous_state: StreamState) -> list[Event]: Fires when a stream is cleanly ended. """ self.stream_closed_by = StreamClosedBy.RECV_END_STREAM - event = StreamEnded() - event.stream_id = self.stream_id + event = StreamEnded(stream_id=self.stream_id) return [event] def stream_reset(self, previous_state: StreamState) -> list[Event]: diff --git a/tests/test_events.py b/tests/test_events.py index 9dfe3abc6..17136aa12 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -236,8 +236,7 @@ def test_streamended_repr(self) -> None: """ StreamEnded has a useful debug representation. """ - e = h2.events.StreamEnded() - e.stream_id = 99 + e = h2.events.StreamEnded(stream_id=99) assert repr(e) == "" From f7c4c530b4f9e9cf44f79e611828a8b43f7c99e9 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 7 Feb 2025 23:53:49 +0100 Subject: [PATCH 10/60] convert `events.UnknownFrame` into a dataclass --- src/h2/connection.py | 3 +-- src/h2/events.py | 5 ++--- tests/test_events.py | 3 ++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/h2/connection.py b/src/h2/connection.py index d9ff0862f..5dad57da7 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -1972,8 +1972,7 @@ def _receive_unknown_frame(self, frame: ExtensionFrame) -> tuple[list[Frame], li self.config.logger.debug( "Received unknown extension frame (ID %d)", frame.stream_id, ) - event = UnknownFrameReceived() - event.frame = frame + event = UnknownFrameReceived(frame=frame) return [], [event] def _local_settings_acked(self) -> dict[SettingCodes | int, ChangedSetting]: diff --git a/src/h2/events.py b/src/h2/events.py index 65d69d14d..e3364e949 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -635,6 +635,7 @@ def __repr__(self) -> str: ) +@dataclass(**kw_only) class UnknownFrameReceived(Event): """ The UnknownFrameReceived event is fired when the remote peer sends a frame @@ -650,9 +651,7 @@ class UnknownFrameReceived(Event): .. versionadded:: 2.7.0 """ - def __init__(self) -> None: - #: The hyperframe Frame object that encapsulates the received frame. - self.frame: Frame | None = None + frame: Frame def __repr__(self) -> str: return "" diff --git a/tests/test_events.py b/tests/test_events.py index 17136aa12..1eacb3acd 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -7,6 +7,7 @@ import inspect import sys +import hyperframe.frame import pytest from hypothesis import given from hypothesis.strategies import integers, lists, tuples @@ -360,7 +361,7 @@ def test_unknownframereceived_repr(self) -> None: """ UnknownFrameReceived has a useful debug representation. """ - e = h2.events.UnknownFrameReceived() + e = h2.events.UnknownFrameReceived(frame=hyperframe.frame.Frame(1)) assert repr(e) == "" From 68925801ae322ae6553126be15a8e1ff1b3902cb Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sat, 8 Feb 2025 00:01:10 +0100 Subject: [PATCH 11/60] convert `events.RequestReceived` into a dataclass --- src/h2/events.py | 39 +++++++++++++++++++++------------------ src/h2/stream.py | 3 +-- tests/test_events.py | 7 ++++--- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/h2/events.py b/src/h2/events.py index e3364e949..f12c04288 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -45,7 +45,7 @@ class Event: """ - +@dataclass(**kw_only) class RequestReceived(Event): """ The RequestReceived event is fired whenever all of a request's headers @@ -64,26 +64,29 @@ class RequestReceived(Event): Added ``stream_ended`` and ``priority_updated`` properties. """ - def __init__(self) -> None: - #: The Stream ID for the stream this request was made on. - self.stream_id: int | None = None + stream_id: int + """The Stream ID for the stream this request was made on.""" - #: The request headers. - self.headers: list[Header] | None = None + headers: list[Header] = _LAZY_INIT + """The request headers.""" - #: If this request also ended the stream, the associated - #: :class:`StreamEnded ` event will be available - #: here. - #: - #: .. versionadded:: 2.4.0 - self.stream_ended: StreamEnded | None = None + stream_ended: StreamEnded | None = None + """ + If this request also ended the stream, the associated + :class:`StreamEnded ` event will be available + here. - #: If this request also had associated priority information, the - #: associated :class:`PriorityUpdated ` - #: event will be available here. - #: - #: .. versionadded:: 2.4.0 - self.priority_updated: PriorityUpdated | None = None + .. versionadded:: 2.4.0 + """ + + priority_updated: PriorityUpdated | None = None + """ + If this request also had associated priority information, the + associated :class:`PriorityUpdated ` + event will be available here. + + .. versionadded:: 2.4.0 + """ def __repr__(self) -> str: return f"" diff --git a/src/h2/stream.py b/src/h2/stream.py index 91263b7b7..b047a3d39 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -195,8 +195,7 @@ def request_received(self, previous_state: StreamState) -> list[Event]: self.client = False self.headers_received = True - event = RequestReceived() - event.stream_id = self.stream_id + event = RequestReceived(stream_id=self.stream_id) return [event] def response_received(self, previous_state: StreamState) -> list[Event]: diff --git a/tests/test_events.py b/tests/test_events.py index 1eacb3acd..7e9764ba7 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -115,9 +115,10 @@ def test_requestreceived_repr(self) -> None: """ RequestReceived has a useful debug representation. """ - e = h2.events.RequestReceived() - e.stream_id = 5 - e.headers = self.example_request_headers + e = h2.events.RequestReceived( + stream_id=5, + headers=self.example_request_headers + ) assert repr(e) == ( " Date: Sat, 8 Feb 2025 00:03:15 +0100 Subject: [PATCH 12/60] convert `events.ResponseReceived` into a dataclass --- src/h2/events.py | 38 +++++++++++++++++++++----------------- src/h2/stream.py | 2 +- tests/test_events.py | 7 ++++--- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/h2/events.py b/src/h2/events.py index f12c04288..162b99c34 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -92,6 +92,7 @@ def __repr__(self) -> str: return f"" +@dataclass(**kw_only) class ResponseReceived(Event): """ The ResponseReceived event is fired whenever response headers are received. @@ -106,26 +107,29 @@ class ResponseReceived(Event): Added ``stream_ended`` and ``priority_updated`` properties. """ - def __init__(self) -> None: - #: The Stream ID for the stream this response was made on. - self.stream_id: int | None = None + stream_id: int + """The Stream ID for the stream this response was made on.""" - #: The response headers. - self.headers: list[Header] | None = None + headers: list[Header] = _LAZY_INIT + """The response headers.""" - #: If this response also ended the stream, the associated - #: :class:`StreamEnded ` event will be available - #: here. - #: - #: .. versionadded:: 2.4.0 - self.stream_ended: StreamEnded | None = None + stream_ended: StreamEnded | None = None + """ + If this response also ended the stream, the associated + :class:`StreamEnded ` event will be available + here. - #: If this response also had associated priority information, the - #: associated :class:`PriorityUpdated ` - #: event will be available here. - #: - #: .. versionadded:: 2.4.0 - self.priority_updated: PriorityUpdated | None = None + .. versionadded:: 2.4.0 + """ + + priority_updated: PriorityUpdated | None = None + """ + If this response also had associated priority information, the + associated :class:`PriorityUpdated ` + event will be available here. + + .. versionadded:: 2.4.0 + """ def __repr__(self) -> str: return f"" diff --git a/src/h2/stream.py b/src/h2/stream.py index b047a3d39..0fae6eb0f 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -207,7 +207,7 @@ def response_received(self, previous_state: StreamState) -> list[Event]: if not self.headers_received: assert self.client is True self.headers_received = True - event = ResponseReceived() + event = ResponseReceived(stream_id=self.stream_id) else: assert not self.trailers_received self.trailers_received = True diff --git a/tests/test_events.py b/tests/test_events.py index 7e9764ba7..24f801f97 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -132,9 +132,10 @@ def test_responsereceived_repr(self) -> None: """ ResponseReceived has a useful debug representation. """ - e = h2.events.ResponseReceived() - e.stream_id = 500 - e.headers = self.example_response_headers + e = h2.events.ResponseReceived( + stream_id=500, + headers=self.example_response_headers, + ) assert repr(e) == ( " Date: Sat, 8 Feb 2025 00:05:43 +0100 Subject: [PATCH 13/60] convert `events.InformationalResponseReceived` into a dataclass --- src/h2/events.py | 26 +++++++++++++------------- src/h2/stream.py | 4 +--- tests/test_events.py | 7 ++++--- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/h2/events.py b/src/h2/events.py index 162b99c34..c7d7f32f5 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -235,7 +235,7 @@ class _PushedRequestSent(_HeadersSent): """ - +@dataclass(**kw_only) class InformationalResponseReceived(Event): """ The InformationalResponseReceived event is fired when an informational @@ -259,20 +259,20 @@ class InformationalResponseReceived(Event): Added ``priority_updated`` property. """ - def __init__(self) -> None: - #: The Stream ID for the stream this informational response was made - #: on. - self.stream_id: int | None = None + stream_id: int + """The Stream ID for the stream this informational response was made on.""" - #: The headers for this informational response. - self.headers: list[Header] | None = None + headers: list[Header] = _LAZY_INIT + """The headers for this informational response.""" - #: If this response also had associated priority information, the - #: associated :class:`PriorityUpdated ` - #: event will be available here. - #: - #: .. versionadded:: 2.4.0 - self.priority_updated: PriorityUpdated | None = None + priority_updated: PriorityUpdated | None = None + """ + If this response also had associated priority information, the + associated :class:`PriorityUpdated ` + event will be available here. + + .. versionadded:: 2.4.0 + """ def __repr__(self) -> str: return f"" diff --git a/src/h2/stream.py b/src/h2/stream.py index 0fae6eb0f..d102b056c 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -424,9 +424,7 @@ def recv_informational_response(self, previous_state: StreamState) -> list[Event msg = "Informational response after final response" raise ProtocolError(msg) - event = InformationalResponseReceived() - event.stream_id = self.stream_id - return [event] + return [InformationalResponseReceived(stream_id=self.stream_id)] def recv_alt_svc(self, previous_state: StreamState) -> list[Event]: """ diff --git a/tests/test_events.py b/tests/test_events.py index 24f801f97..904578adb 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -159,9 +159,10 @@ def test_informationalresponsereceived_repr(self) -> None: """ InformationalResponseReceived has a useful debug representation. """ - e = h2.events.InformationalResponseReceived() - e.stream_id = 62 - e.headers = self.example_informational_headers + e = h2.events.InformationalResponseReceived( + stream_id=62, + headers=self.example_informational_headers, + ) assert repr(e) == ( " Date: Sat, 8 Feb 2025 00:08:32 +0100 Subject: [PATCH 14/60] convert ping events into a dataclasses --- src/h2/connection.py | 5 ++--- src/h2/events.py | 12 ++++++------ tests/test_events.py | 6 ++---- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/h2/connection.py b/src/h2/connection.py index 5dad57da7..aa773e891 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -1823,9 +1823,9 @@ def _receive_ping_frame(self, frame: PingFrame) -> tuple[list[Frame], list[Event evt: PingReceived | PingAckReceived if "ACK" in frame.flags: - evt = PingAckReceived() + evt = PingAckReceived(ping_data=frame.opaque_data) else: - evt = PingReceived() + evt = PingReceived(ping_data=frame.opaque_data) # automatically ACK the PING with the same 'opaque data' f = PingFrame(0) @@ -1833,7 +1833,6 @@ def _receive_ping_frame(self, frame: PingFrame) -> tuple[list[Frame], list[Event f.opaque_data = frame.opaque_data frames.append(f) - evt.ping_data = frame.opaque_data events.append(evt) return frames, events diff --git a/src/h2/events.py b/src/h2/events.py index c7d7f32f5..a68a88ce7 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -403,6 +403,7 @@ def __repr__(self) -> str: ) +@dataclass(**kw_only) class PingReceived(Event): """ The PingReceived event is fired whenever a PING is received. It contains @@ -412,14 +413,14 @@ class PingReceived(Event): .. versionadded:: 3.1.0 """ - def __init__(self) -> None: - #: The data included on the ping. - self.ping_data: bytes | None = None + ping_data: bytes + """The data included on the ping.""" def __repr__(self) -> str: return f"" +@dataclass(**kw_only) class PingAckReceived(Event): """ The PingAckReceived event is fired whenever a PING acknowledgment is @@ -432,9 +433,8 @@ class PingAckReceived(Event): Removed deprecated but equivalent ``PingAcknowledged``. """ - def __init__(self) -> None: - #: The data included on the ping. - self.ping_data: bytes | None = None + ping_data: bytes + """The data included on the ping.""" def __repr__(self) -> str: return f"" diff --git a/tests/test_events.py b/tests/test_events.py index 904578adb..a43543c86 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -222,8 +222,7 @@ def test_pingreceived_repr(self) -> None: """ PingReceived has a useful debug representation. """ - e = h2.events.PingReceived() - e.ping_data = b"abcdefgh" + e = h2.events.PingReceived(ping_data=b"abcdefgh") assert repr(e) == "" @@ -231,8 +230,7 @@ def test_pingackreceived_repr(self) -> None: """ PingAckReceived has a useful debug representation. """ - e = h2.events.PingAckReceived() - e.ping_data = b"abcdefgh" + e = h2.events.PingAckReceived(ping_data=b"abcdefgh") assert repr(e) == "" From f39dd8971ea36269e972e5eb851bc3f2efa97802 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sun, 9 Feb 2025 19:07:54 +0100 Subject: [PATCH 15/60] fixup `events.WindowUpdated.delta` type annotation --- src/h2/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/h2/events.py b/src/h2/events.py index a68a88ce7..6aab0713d 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -340,7 +340,7 @@ class WindowUpdated(Event): May be ``0`` if the connection window was changed. """ - delta: int | None = None + delta: int = _LAZY_INIT """ The window delta. """ From 1ed6c610e02cde7eb3a8555a76da94ccfddd59d1 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Mon, 10 Feb 2025 15:50:35 +0100 Subject: [PATCH 16/60] add changelog entry --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a4175f799..9aad5387f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,11 @@ dev - +**API Changes (Backward Compatible)** + +- h2 events now have tighter type bounds, e.g. `stream_id` is guaranteed to not be `None` for most events now. + This simplifies downstream type checking. + **Bugfixes** - From 500a3488d4920d051a5851bc4555dc35f1a64bed Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Thu, 13 Feb 2025 22:09:46 +0100 Subject: [PATCH 17/60] fix error value when opening a new stream --- src/h2/connection.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/h2/connection.py b/src/h2/connection.py index aa773e891..313efc146 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -793,8 +793,9 @@ def send_headers(self, # Check we can open the stream. if stream_id not in self.streams: max_open_streams = self.remote_settings.max_concurrent_streams - if (self.open_outbound_streams + 1) > max_open_streams: - msg = f"Max outbound streams is {max_open_streams}, {self.open_outbound_streams} open" + value = self.open_outbound_streams # take a copy due to the property accessor having side affects + if (value + 1) > max_open_streams: + msg = f"Max outbound streams is {max_open_streams}, {value} open" raise TooManyStreamsError(msg) self.state_machine.process_input(ConnectionInputs.SEND_HEADERS) @@ -1593,8 +1594,9 @@ def _receive_headers_frame(self, frame: HeadersFrame) -> tuple[list[Frame], list # stream ID is valid. if frame.stream_id not in self.streams: max_open_streams = self.local_settings.max_concurrent_streams - if (self.open_inbound_streams + 1) > max_open_streams: - msg = f"Max outbound streams is {max_open_streams}, {self.open_outbound_streams} open" + value = self.open_inbound_streams # take a copy due to the property accessor having side affects + if (value + 1) > max_open_streams: + msg = f"Max inbound streams is {max_open_streams}, {value} open" raise TooManyStreamsError(msg) # Let's decode the headers. We handle headers as bytes internally up From 42ee8d75777bdd227aa95226dd039604e96e1703 Mon Sep 17 00:00:00 2001 From: deedy5 <65482418+deedy5@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:46:57 +0300 Subject: [PATCH 18/60] perf(FrameBuffer.data): use bytearray() --- src/h2/frame_buffer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/h2/frame_buffer.py b/src/h2/frame_buffer.py index 30d96e816..e87e03092 100644 --- a/src/h2/frame_buffer.py +++ b/src/h2/frame_buffer.py @@ -30,7 +30,7 @@ class FrameBuffer: """ def __init__(self, server: bool = False) -> None: - self.data = b"" + self.data = bytearray() self.max_frame_size = 0 self._preamble = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" if server else b"" self._preamble_len = len(self._preamble) From 191ac06e0949fcfe3367b06eeb101a5a1a335964 Mon Sep 17 00:00:00 2001 From: deedy5 <65482418+deedy5@users.noreply.github.com> Date: Wed, 26 Feb 2025 22:45:48 +0300 Subject: [PATCH 19/60] refactor(FrameBuffer): rename self.data to self._data --- src/h2/frame_buffer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/h2/frame_buffer.py b/src/h2/frame_buffer.py index e87e03092..e7b0a7126 100644 --- a/src/h2/frame_buffer.py +++ b/src/h2/frame_buffer.py @@ -30,7 +30,7 @@ class FrameBuffer: """ def __init__(self, server: bool = False) -> None: - self.data = bytearray() + self._data = bytearray() self.max_frame_size = 0 self._preamble = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" if server else b"" self._preamble_len = len(self._preamble) @@ -54,7 +54,7 @@ def add_data(self, data: bytes) -> None: self._preamble_len -= of_which_preamble self._preamble = self._preamble[of_which_preamble:] - self.data += data + self._data += data def _validate_frame_length(self, length: int) -> None: """ @@ -119,18 +119,18 @@ def __iter__(self) -> FrameBuffer: def __next__(self) -> Frame: # First, check that we have enough data to successfully parse the # next frame header. If not, bail. Otherwise, parse it. - if len(self.data) < 9: + if len(self._data) < 9: raise StopIteration try: - f, length = Frame.parse_frame_header(memoryview(self.data[:9])) + f, length = Frame.parse_frame_header(memoryview(self._data[:9])) except (InvalidDataError, InvalidFrameError) as err: # pragma: no cover msg = f"Received frame with invalid header: {err!s}" raise ProtocolError(msg) from err # Next, check that we have enough length to parse the frame body. If # not, bail, leaving the frame header data in the buffer for next time. - if len(self.data) < length + 9: + if len(self._data) < length + 9: raise StopIteration # Confirm the frame has an appropriate length. @@ -138,7 +138,7 @@ def __next__(self) -> Frame: # Try to parse the frame body try: - f.parse_body(memoryview(self.data[9:9+length])) + f.parse_body(memoryview(self._data[9:9+length])) except InvalidDataError as err: msg = "Received frame with non-compliant data" raise ProtocolError(msg) from err @@ -148,7 +148,7 @@ def __next__(self) -> Frame: # At this point, as we know we'll use or discard the entire frame, we # can update the data. - self.data = self.data[9+length:] + self._data = self._data[9+length:] # Pass the frame through the header buffer. new_frame = self._update_header_buffer(f) From 428b0b5d74a3456eb9d5551530284a00cd5dfa2b Mon Sep 17 00:00:00 2001 From: Jan Brasna <1784648+janbrasna@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:04:30 +0200 Subject: [PATCH 20/60] Update README link to https --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2b221d777..dd01fe3a2 100644 --- a/README.rst +++ b/README.rst @@ -62,7 +62,7 @@ to large feature requests and changes. Before you contribute (either by opening an issue or filing a pull request), please `read the contribution guidelines`_. -.. _read the contribution guidelines: http://python-hyper.org/en/latest/contributing.html +.. _read the contribution guidelines: https://python-hyper.org/en/latest/contributing.html License ======= From 243461d500822d1e78972946ce0dbc5905ab1ad9 Mon Sep 17 00:00:00 2001 From: Jan Brasna <1784648+janbrasna@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:20:03 +0200 Subject: [PATCH 21/60] Create RTD config --- .readthedocs.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..45b3f5527 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,10 @@ +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.9" + +sphinx: + configuration: docs/source/conf.py From 492d3db0a206d22ccabb99faeb53924f4af7a982 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Wed, 30 Apr 2025 22:30:45 +0200 Subject: [PATCH 22/60] Update .readthedocs.yaml --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 45b3f5527..3480c6fc3 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,7 +4,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.9" + python: "3.13" sphinx: configuration: docs/source/conf.py From 9ce83ff7d77522ed30aea3b052990c43c1218104 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Wed, 30 Apr 2025 22:47:24 +0200 Subject: [PATCH 23/60] exclude RDT from sdist --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 25d6814dc..ff2f15801 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,3 +11,4 @@ recursive-include examples *.py *.crt *.key *.pem *.csr include README.rst LICENSE CHANGELOG.rst pyproject.toml global-exclude *.pyc *.pyo *.swo *.swp *.map *.yml *.DS_Store +exclude .readthedocs.yaml From ea3140f484a6646ec09abccd0d3c7e955bce4f4c Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Wed, 30 Apr 2025 23:04:21 +0200 Subject: [PATCH 24/60] cleanup --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9035d3955..694f71c3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -183,9 +183,6 @@ python = """ """ [tool.tox.env_run_base] -pass_env = [ - "GITHUB_*", -] dependency_groups = ["testing"] commands = [ ["python", "-bb", "-m", "pytest", "--cov-report=xml", "--cov-report=term", "--cov=h2", { replace = "posargs", extend = true }] From bbd3d90c66cce51fbbe6b9af75cb0bef7004f9d5 Mon Sep 17 00:00:00 2001 From: Jan Brasna <1784648+janbrasna@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:46:56 +0200 Subject: [PATCH 25/60] fix(packaging): bump twine to pass meta check wildcard bugs --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 694f71c3b..0830a91d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ packaging = [ "check-manifest==0.50", "readme-renderer==44.0", "build>=1.2.2,<2", - "twine>=5.1.1,<6", + "twine>=6.1.0,<7", "wheel>=0.45.0,<1", ] From 0583911b29d05764bbe3f7691d59f4d5e83e249b Mon Sep 17 00:00:00 2001 From: Jan Brasna <1784648+janbrasna@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:41:26 +0200 Subject: [PATCH 26/60] lint: fix TC006 --- src/h2/stream.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/h2/stream.py b/src/h2/stream.py index d102b056c..d6f5845c3 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -1037,7 +1037,7 @@ def receive_push_promise_in_band(self, events = self.state_machine.process_input( StreamInputs.RECV_PUSH_PROMISE, ) - push_event = cast(PushedStreamReceived, events[0]) + push_event = cast("PushedStreamReceived", events[0]) push_event.pushed_stream_id = promised_stream_id hdr_validation_flags = self._build_hdr_validation_flags(events) @@ -1076,7 +1076,7 @@ def receive_headers(self, events = self.state_machine.process_input(input_) headers_event = cast( - Union[RequestReceived, ResponseReceived, TrailersReceived, InformationalResponseReceived], + "Union[RequestReceived, ResponseReceived, TrailersReceived, InformationalResponseReceived]", events[0], ) @@ -1086,9 +1086,9 @@ def receive_headers(self, ) # We ensured it's not an information response at the beginning of the method. cast( - Union[RequestReceived, ResponseReceived, TrailersReceived], + "Union[RequestReceived, ResponseReceived, TrailersReceived]", headers_event, - ).stream_ended = cast(StreamEnded, es_events[0]) + ).stream_ended = cast("StreamEnded", es_events[0]) events += es_events self._initialize_content_length(headers) @@ -1112,7 +1112,7 @@ def receive_data(self, data: bytes, end_stream: bool, flow_control_len: int) -> "set to %d", self, end_stream, flow_control_len, ) events = self.state_machine.process_input(StreamInputs.RECV_DATA) - data_event = cast(DataReceived, events[0]) + data_event = cast("DataReceived", events[0]) self._inbound_window_manager.window_consumed(flow_control_len) self._track_content_length(len(data), end_stream) @@ -1120,7 +1120,7 @@ def receive_data(self, data: bytes, end_stream: bool, flow_control_len: int) -> es_events = self.state_machine.process_input( StreamInputs.RECV_END_STREAM, ) - data_event.stream_ended = cast(StreamEnded, es_events[0]) + data_event.stream_ended = cast("StreamEnded", es_events[0]) events.extend(es_events) data_event.data = data @@ -1144,7 +1144,7 @@ def receive_window_update(self, increment: int) -> tuple[list[Frame], list[Event # this should be treated as a *stream* error, not a *connection* error. # That means we need to catch the error and forcibly close the stream. if events: - cast(WindowUpdated, events[0]).delta = increment + cast("WindowUpdated", events[0]).delta = increment try: self.outbound_flow_control_window = guard_increment_window( self.outbound_flow_control_window, @@ -1228,7 +1228,7 @@ def stream_reset(self, frame: RstStreamFrame) -> tuple[list[Frame], list[Event]] if events: # We don't fire an event if this stream is already closed. - cast(StreamReset, events[0]).error_code = _error_code_from_int(frame.error_code) + cast("StreamReset", events[0]).error_code = _error_code_from_int(frame.error_code) return [], events From 883ed37be42592b2f0aa0caddab6ca5e3d668fa3 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Mon, 18 Aug 2025 22:46:12 +0200 Subject: [PATCH 27/60] reject header names and values containing unpermitted characters `\r`, `\n`, or `\0x00` --- CHANGELOG.rst | 2 +- src/h2/utilities.py | 25 +++++++++++++++++++++++++ tests/test_invalid_headers.py | 8 +++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9aad5387f..1a2deb8d5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,7 +6,7 @@ dev **API Changes (Backward Incompatible)** -- +- Reject header names and values containing unpermitted characters `\r`, `\n`, or `\0x00`. **API Changes (Backward Compatible)** diff --git a/src/h2/utilities.py b/src/h2/utilities.py index 8cafdbd50..b874aa8fe 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -24,6 +24,12 @@ SIGIL = ord(b":") INFORMATIONAL_START = ord(b"1") +HEADER_UNPERMITTED_CHARACTERS = frozenset([ + b"\r", + b"\n", + b"\x00", +]) + # A set of headers that are hop-by-hop or connection-specific and thus # forbidden in HTTP/2. This list comes from RFC 7540 § 8.1.2.2. @@ -201,6 +207,9 @@ def validate_headers(headers: Iterable[Header], hdr_validation_flags: HeaderVali # For example, we avoid tuple unpacking in loops because it represents a # fixed cost that we don't want to spend, instead indexing into the header # tuples. + headers = _reject_unpermitted_characters( + headers, hdr_validation_flags, + ) headers = _reject_empty_header_names( headers, hdr_validation_flags, ) @@ -225,6 +234,22 @@ def validate_headers(headers: Iterable[Header], hdr_validation_flags: HeaderVali return _check_path_header(headers, hdr_validation_flags) +def _reject_unpermitted_characters(headers: Iterable[Header], + hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]: + """ + Raises a ProtocolError if any header names or values contain unpermitted characters. + See RFC 7540, section 10.3 and 8.1.2.6. + """ + for header in headers: + for c in HEADER_UNPERMITTED_CHARACTERS: + if c in header[0]: + msg = f"Unpermitted character '{c}' in header name: {header[0]!r}" + raise ProtocolError(msg) + if c in header[1]: + msg = f"Unpermitted character '{c}' in header value: {header[1]!r}" + raise ProtocolError(msg) + yield header + def _reject_empty_header_names(headers: Iterable[Header], hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]: diff --git a/tests/test_invalid_headers.py b/tests/test_invalid_headers.py index 192ba10d4..e8f1a13e4 100644 --- a/tests/test_invalid_headers.py +++ b/tests/test_invalid_headers.py @@ -48,6 +48,12 @@ class TestInvalidFrameSequences: [*base_request_headers, ("name ", "name with trailing space")], [*base_request_headers, ("name", " value with leading space")], [*base_request_headers, ("name", "value with trailing space ")], + [*base_request_headers, ("unpermitted-\r-characters", "value")], + [*base_request_headers, ("unpermitted-\n-characters", "value")], + [*base_request_headers, ("unpermitted-\x00-characters", "value")], + [*base_request_headers, ("unpermitted-characters", "some \r value")], + [*base_request_headers, ("unpermitted-characters", "some \n value")], + [*base_request_headers, ("unpermitted-characters", "some \x00 value")], [header for header in base_request_headers if header[0] != ":authority"], [(":protocol", "websocket"), *base_request_headers], @@ -665,7 +671,7 @@ def test_inbound_header_name_length(self, hdr_validation_flags) -> None: def test_inbound_header_name_length_full_frame_decode(self, frame_factory) -> None: f = frame_factory.build_headers_frame([]) - f.data = b"\x00\x00\x05\x00\x00\x00\x00\x04" + f.data = b"\x00\x00\x01\x04" data = f.serialize() c = h2.connection.H2Connection(config=h2.config.H2Configuration(client_side=False)) From 035e9899f95e3709af098f578bfc3cd302298e3a Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 19 Aug 2025 08:35:52 +0200 Subject: [PATCH 28/60] be stricter about which characters to accept for headers This now adheres to the minimal requirements laid out in RFC 9113. We could consider putting additional restrictions on the header value, but in order to keep breakage at a minimum let's do that in a later release if at all. --- src/h2/utilities.py | 41 +++++++++++++++++++++-------------- tests/test_invalid_headers.py | 14 +++++++----- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/h2/utilities.py b/src/h2/utilities.py index b874aa8fe..1bd48af65 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -24,12 +24,6 @@ SIGIL = ord(b":") INFORMATIONAL_START = ord(b"1") -HEADER_UNPERMITTED_CHARACTERS = frozenset([ - b"\r", - b"\n", - b"\x00", -]) - # A set of headers that are hop-by-hop or connection-specific and thus # forbidden in HTTP/2. This list comes from RFC 7540 § 8.1.2.2. @@ -207,7 +201,7 @@ def validate_headers(headers: Iterable[Header], hdr_validation_flags: HeaderVali # For example, we avoid tuple unpacking in loops because it represents a # fixed cost that we don't want to spend, instead indexing into the header # tuples. - headers = _reject_unpermitted_characters( + headers = _reject_illegal_characters( headers, hdr_validation_flags, ) headers = _reject_empty_header_names( @@ -234,20 +228,35 @@ def validate_headers(headers: Iterable[Header], hdr_validation_flags: HeaderVali return _check_path_header(headers, hdr_validation_flags) -def _reject_unpermitted_characters(headers: Iterable[Header], - hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]: +def _reject_illegal_characters(headers: Iterable[Header], + hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]: """ - Raises a ProtocolError if any header names or values contain unpermitted characters. - See RFC 7540, section 10.3 and 8.1.2.6. + Raises a ProtocolError if any header names or values contain illegal characters. + See RFC 9113, section 8.2.1. """ for header in headers: - for c in HEADER_UNPERMITTED_CHARACTERS: - if c in header[0]: - msg = f"Unpermitted character '{c}' in header name: {header[0]!r}" + # > A field name MUST NOT contain characters in the ranges 0x00-0x20, 0x41-0x5a, + # > or 0x7f-0xff (all ranges inclusive). + for c in header[0]: + if c <= 0x20 or 0x41 <= c <= 0x5a or 0x7f <= c: + msg = f"Illegal character '{chr(c)}' in header name: {header[0]!r}" raise ProtocolError(msg) - if c in header[1]: - msg = f"Unpermitted character '{c}' in header value: {header[1]!r}" + + # > With the exception of pseudo-header fields (Section 8.3), which have a name + # > that starts with a single colon, field names MUST NOT include a colon (ASCII + # > COLON, 0x3a). + if header[0].find(b":", 1) != -1: + msg = f"Illegal character ':' in header name: {header[0]!r}" + raise ProtocolError(msg) + + # > A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed + # > (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position. + for c in header[1]: + if c == 0 or c == 0x0a or c == 0x0d: + msg = f"Illegal character '{chr(c)}' in header value: {header[1]!r}" raise ProtocolError(msg) + + # Surrounding whitespace is enforced in `_reject_surrounding_whitespace`. yield header diff --git a/tests/test_invalid_headers.py b/tests/test_invalid_headers.py index e8f1a13e4..96876c6f5 100644 --- a/tests/test_invalid_headers.py +++ b/tests/test_invalid_headers.py @@ -48,12 +48,14 @@ class TestInvalidFrameSequences: [*base_request_headers, ("name ", "name with trailing space")], [*base_request_headers, ("name", " value with leading space")], [*base_request_headers, ("name", "value with trailing space ")], - [*base_request_headers, ("unpermitted-\r-characters", "value")], - [*base_request_headers, ("unpermitted-\n-characters", "value")], - [*base_request_headers, ("unpermitted-\x00-characters", "value")], - [*base_request_headers, ("unpermitted-characters", "some \r value")], - [*base_request_headers, ("unpermitted-characters", "some \n value")], - [*base_request_headers, ("unpermitted-characters", "some \x00 value")], + [*base_request_headers, ("illegal:characters", "value")], + [*base_request_headers, ("illegal-\r-characters", "value")], + [*base_request_headers, ("illegal-\n-characters", "value")], + [*base_request_headers, ("illegal-\x00-characters", "value")], + [*base_request_headers, ("illegal-\x01-characters", "value")], + [*base_request_headers, ("illegal-characters", "some \r value")], + [*base_request_headers, ("illegal-characters", "some \n value")], + [*base_request_headers, ("illegal-characters", "some \x00 value")], [header for header in base_request_headers if header[0] != ":authority"], [(":protocol", "websocket"), *base_request_headers], From 9e4bbed6138c825cd43d519674d00bd267650f30 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Sat, 23 Aug 2025 19:49:53 +0200 Subject: [PATCH 29/60] merge surrounding whitespace and uppercase validators into illegal character validation --- src/h2/utilities.py | 80 ++++++++++++++------------------------------- 1 file changed, 25 insertions(+), 55 deletions(-) diff --git a/src/h2/utilities.py b/src/h2/utilities.py index 1bd48af65..a7409b388 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -7,8 +7,6 @@ from __future__ import annotations import collections -import re -from string import whitespace from typing import TYPE_CHECKING, Any, NamedTuple from hpack.struct import HeaderTuple, NeverIndexedHeaderTuple @@ -20,7 +18,6 @@ from hpack.struct import Header, HeaderWeaklyTyped -UPPER_RE = re.compile(b"[A-Z]") SIGIL = ord(b":") INFORMATIONAL_START = ord(b"1") @@ -70,9 +67,6 @@ _CONNECT_REQUEST_ONLY_HEADERS = frozenset([b":protocol"]) -_WHITESPACE = frozenset(map(ord, whitespace)) - - def _secure_headers(headers: Iterable[Header], hdr_validation_flags: HeaderValidationFlags | None) -> Generator[Header, None, None]: """ @@ -207,12 +201,6 @@ def validate_headers(headers: Iterable[Header], hdr_validation_flags: HeaderVali headers = _reject_empty_header_names( headers, hdr_validation_flags, ) - headers = _reject_uppercase_header_fields( - headers, hdr_validation_flags, - ) - headers = _reject_surrounding_whitespace( - headers, hdr_validation_flags, - ) headers = _reject_te( headers, hdr_validation_flags, ) @@ -232,13 +220,16 @@ def _reject_illegal_characters(headers: Iterable[Header], hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]: """ Raises a ProtocolError if any header names or values contain illegal characters. - See RFC 9113, section 8.2.1. + See . """ for header in headers: # > A field name MUST NOT contain characters in the ranges 0x00-0x20, 0x41-0x5a, # > or 0x7f-0xff (all ranges inclusive). for c in header[0]: - if c <= 0x20 or 0x41 <= c <= 0x5a or 0x7f <= c: + if 0x41 <= c <= 0x5a: + msg = f"Received uppercase header name {header[0]!r}." + raise ProtocolError(msg) + if c <= 0x20 or c >= 0x7f: msg = f"Illegal character '{chr(c)}' in header name: {header[0]!r}" raise ProtocolError(msg) @@ -249,14 +240,28 @@ def _reject_illegal_characters(headers: Iterable[Header], msg = f"Illegal character ':' in header name: {header[0]!r}" raise ProtocolError(msg) - # > A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed - # > (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position. - for c in header[1]: - if c == 0 or c == 0x0a or c == 0x0d: - msg = f"Illegal character '{chr(c)}' in header value: {header[1]!r}" + # For compatibility with RFC 7230 header fields, we need to allow the field + # value to be an empty string. This is ludicrous, but technically allowed. + if field_value := header[1]: + + # > A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed + # > (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position. + for c in field_value: + if c == 0 or c == 0x0a or c == 0x0d: # noqa: PLR1714 + msg = f"Illegal character '{chr(c)}' in header value: {field_value!r}" + raise ProtocolError(msg) + + # > A field value MUST NOT start or end with an ASCII whitespace character + # > (ASCII SP or HTAB, 0x20 or 0x09). + if ( + field_value[0] == 0x20 or + field_value[0] == 0x09 or + field_value[-1] == 0x20 or + field_value[-1] == 0x09 + ): + msg = f"Received header value surrounded by whitespace {field_value!r}" raise ProtocolError(msg) - # Surrounding whitespace is enforced in `_reject_surrounding_whitespace`. yield header @@ -275,41 +280,6 @@ def _reject_empty_header_names(headers: Iterable[Header], yield header -def _reject_uppercase_header_fields(headers: Iterable[Header], - hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]: - """ - Raises a ProtocolError if any uppercase character is found in a header - block. - """ - for header in headers: - if UPPER_RE.search(header[0]): - msg = f"Received uppercase header name {header[0]!r}." - raise ProtocolError(msg) - yield header - - -def _reject_surrounding_whitespace(headers: Iterable[Header], - hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]: - """ - Raises a ProtocolError if any header name or value is surrounded by - whitespace characters. - """ - # For compatibility with RFC 7230 header fields, we need to allow the field - # value to be an empty string. This is ludicrous, but technically allowed. - # The field name may not be empty, though, so we can safely assume that it - # must have at least one character in it and throw exceptions if it - # doesn't. - for header in headers: - if header[0][0] in _WHITESPACE or header[0][-1] in _WHITESPACE: - msg = f"Received header name surrounded by whitespace {header[0]!r}" - raise ProtocolError(msg) - if header[1] and ((header[1][0] in _WHITESPACE) or - (header[1][-1] in _WHITESPACE)): - msg = f"Received header value surrounded by whitespace {header[1]!r}" - raise ProtocolError(msg) - yield header - - def _reject_te(headers: Iterable[Header], hdr_validation_flags: HeaderValidationFlags) -> Generator[Header, None, None]: """ Raises a ProtocolError if the TE header is present in a header block and From 1aae569315eb170cdff00582644aca37ee38db62 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Sat, 23 Aug 2025 19:06:57 +0200 Subject: [PATCH 30/60] v4.3.0 --- CHANGELOG.rst | 12 ++++++++---- src/h2/__init__.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1a2deb8d5..06111c33d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,21 +1,25 @@ Release History =============== -dev ---- +4.3.0 (2025-08-23) +------------------ **API Changes (Backward Incompatible)** -- Reject header names and values containing unpermitted characters `\r`, `\n`, or `\0x00`. +- Reject header names and values containing illegal characters, based on RFC 9113, section 8.2.1. + The main Python API is compatible, but some previously valid requests/response headers might now be blocked. + Use the `validate_inbound_headers` config option if needed. + Thanks to Sebastiano Sartor (sebsrt) for the report. **API Changes (Backward Compatible)** - h2 events now have tighter type bounds, e.g. `stream_id` is guaranteed to not be `None` for most events now. This simplifies downstream type checking. +- Various typing-related improvements. **Bugfixes** -- +- Fix error value when opening a new stream on too many open streams. 4.2.0 (2025-02-01) ------------------ diff --git a/src/h2/__init__.py b/src/h2/__init__.py index 35bfe397a..7f0ccd774 100644 --- a/src/h2/__init__.py +++ b/src/h2/__init__.py @@ -3,4 +3,4 @@ """ from __future__ import annotations -__version__ = "4.3.0+dev" +__version__ = "4.3.0" From 9268b7235f345e6a335733d56d298594e8069aa3 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Sat, 20 Sep 2025 14:22:44 +0200 Subject: [PATCH 31/60] read the docs: fix dependency installation --- .readthedocs.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 3480c6fc3..f74877669 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,6 +5,9 @@ build: os: ubuntu-22.04 tools: python: "3.13" + jobs: + install: + - pip install --group 'docs' . sphinx: configuration: docs/source/conf.py From 91ef398efbf717628aa3e52d5d023c8d4cb3c991 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Sat, 20 Sep 2025 14:25:30 +0200 Subject: [PATCH 32/60] the docs lied - need to manually update pip to get `pip install --group` --- .readthedocs.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f74877669..0da08136a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,6 +7,7 @@ build: python: "3.13" jobs: install: + - pip install -U pip - pip install --group 'docs' . sphinx: From 8e0bf8317d5915d64390c73784b9ce8cff534605 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Sat, 4 Oct 2025 17:20:07 +0200 Subject: [PATCH 33/60] prepare for next release cycle --- .readthedocs.yaml | 4 +++- CHANGELOG.rst | 15 +++++++++++++++ pyproject.toml | 20 ++++++++++---------- src/h2/__init__.py | 2 +- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0da08136a..1092bf085 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,8 +1,10 @@ +# Read the Docs configuration file for Sphinx projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + version: 2 build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: python: "3.13" jobs: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 06111c33d..dc194624a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,21 @@ Release History =============== +dev +--- + +**API Changes (Backward Incompatible)** + +- Support for Python 3.9 has been removed. + +**API Changes (Backward Compatible)** + +- Support for Python 3.14 has been added. + +**Bugfixes** + +- + 4.3.0 (2025-08-23) ------------------ diff --git a/pyproject.toml b/pyproject.toml index 0830a91d4..249dabb4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ maintainers = [ { name = "Thomas Kriechbaumer", email = "thomas@kriechbaumer.name" }, ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "hyperframe>=6.1,<7", "hpack>=4.1,<5", @@ -33,11 +33,11 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] @@ -92,7 +92,7 @@ version = { attr = "h2.__version__" } [tool.ruff] line-length = 150 -target-version = "py39" +target-version = "py310" format.preview = true format.docstring-code-line-length = 100 format.docstring-code-format = true @@ -170,15 +170,15 @@ source = [ [tool.tox] min_version = "4.23.2" -env_list = [ "py39", "py310", "py311", "py312", "py313", "pypy3", "lint", "docs", "packaging" ] +env_list = [ "py310", "py311", "py312", "py313", "py314", "pypy3", "lint", "docs", "packaging" ] [tool.tox.gh-actions] python = """ - 3.9: py39, h2spec, lint, docs, packaging - 3.10: py310 + 3.10: py310, h2spec, lint, docs, packaging 3.11: py311 3.12: py312 3.13: py313 + 3.14: py314 pypy3: pypy3 """ @@ -211,7 +211,7 @@ commands = [ ] [tool.tox.env.packaging] -base_python = ["python39"] +base_python = ["python3.10"] dependency_groups = ["packaging"] allowlist_externals = ["rm"] commands = [ @@ -222,7 +222,7 @@ commands = [ ] [tool.tox.env.publish] -base_python = ["python39"] +base_python = ["python3.10"] dependency_groups = ["packaging"] commands = [ ["python", "-m", "build", "--outdir", "dist/"], @@ -231,7 +231,7 @@ commands = [ ] [tool.tox.env.graphs] -basepython = ["python3.9"] +basepython = ["python3.10"] deps = [ "graphviz==0.14.1", ] @@ -240,7 +240,7 @@ commands = [ ] [tool.tox.env.h2spec] -basepython = ["python3.9"] +basepython = ["python3.10"] deps = [ "twisted[tls]==20.3.0", ] diff --git a/src/h2/__init__.py b/src/h2/__init__.py index 7f0ccd774..0718c5704 100644 --- a/src/h2/__init__.py +++ b/src/h2/__init__.py @@ -3,4 +3,4 @@ """ from __future__ import annotations -__version__ = "4.3.0" +__version__ = "4.4.0+dev" From f3b8a301c58ba9e1b2c62741abbd1ed614f8e2a5 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Sat, 4 Oct 2025 17:22:30 +0200 Subject: [PATCH 34/60] update github actions --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f094f1d98..eaa106792 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,11 @@ jobs: max-parallel: 5 matrix: python-version: - - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" + - "3.14" - "pypy3.9" steps: From 0d72e3429a57840cf08898b14df1de9363ffb9f6 Mon Sep 17 00:00:00 2001 From: ChasingImpact Date: Tue, 7 Oct 2025 16:22:20 -0400 Subject: [PATCH 35/60] utilities/tests: align CONNECT validation with RFC 9113 s8.3 & RFC 8441 s4; add tests; changelog --- CHANGELOG.rst | 20 ++ src/h2/utilities.py | 32 ++- tests/test_connect_pseudo_headers.py | 99 +++++++++ tests/test_connect_roundtrip_and_edges.py | 234 ++++++++++++++++++++++ 4 files changed, 380 insertions(+), 5 deletions(-) create mode 100644 tests/test_connect_pseudo_headers.py create mode 100644 tests/test_connect_roundtrip_and_edges.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 06111c33d..fe4d288a8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,26 @@ Release History =============== +dev +--- + +**API Changes (Backward Incompatible)** + +- Support for Python 3.9 has been removed. + +**API Changes (Backward Compatible)** + +- Support for Python 3.14 has been added. +- Align CONNECT pseudo-header validation with RFC 9113 s8.3 and RFC 8441 s4. + Ordinary CONNECT now requires ``:method=CONNECT`` and ``:authority``, and + forbids ``:scheme``/``:path``. Extended CONNECT (e.g., WebSocket) requires + ``:scheme``, ``:path``, ``:authority`` plus ``:protocol``. (PR #1309) + + +**Bugfixes** + +- + 4.3.0 (2025-08-23) ------------------ diff --git a/src/h2/utilities.py b/src/h2/utilities.py index a7409b388..7f0541b23 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -385,18 +385,39 @@ def _check_pseudo_header_field_acceptability(pseudo_headers: set[bytes | str] | if invalid_response_headers: msg = f"Encountered request-only headers {invalid_response_headers}" raise ProtocolError(msg) + elif (not hdr_validation_flags.is_response_header and not hdr_validation_flags.is_trailer): - # This is a request, so we need to have seen :path, :method, and - # :scheme. - _assert_header_in_set(b":path", pseudo_headers) + # Request header block. _assert_header_in_set(b":method", pseudo_headers) - _assert_header_in_set(b":scheme", pseudo_headers) + + is_connect = (method == b"CONNECT") + is_extended_connect = is_connect and (b":protocol" in pseudo_headers) + + if is_connect and not is_extended_connect: + # Ordinary CONNECT (RFC 9113 s8.3): + # MUST NOT include :scheme or :path. + if b":scheme" in pseudo_headers or b":path" in pseudo_headers: + msg = "Ordinary CONNECT MUST NOT include :scheme or :path" + raise ProtocolError(msg) + # :authority presence is enforced elsewhere; no extra asserts here. + elif is_extended_connect: + # Extended CONNECT (RFC 8441 s4): require the regular tuple. + _assert_header_in_set(b":scheme", pseudo_headers) + _assert_header_in_set(b":path", pseudo_headers) + # :authority presence validated by host/authority checker. + else: + # Non-CONNECT requests require :scheme and :path (RFC 9113 s8.3). + _assert_header_in_set(b":scheme", pseudo_headers) + _assert_header_in_set(b":path", pseudo_headers) + invalid_request_headers = pseudo_headers & _RESPONSE_ONLY_HEADERS if invalid_request_headers: msg = f"Encountered response-only headers {invalid_request_headers}" raise ProtocolError(msg) - if method != b"CONNECT": + + # If not CONNECT, then :protocol is invalid. + if not is_connect: invalid_headers = pseudo_headers & _CONNECT_REQUEST_ONLY_HEADERS if invalid_headers: msg = f"Encountered connect-request-only headers {invalid_headers!r}" @@ -698,3 +719,4 @@ def _check_size_limit(self) -> None: if self._size_limit is not None: while len(self) > self._size_limit: self.popitem(last=False) + diff --git a/tests/test_connect_pseudo_headers.py b/tests/test_connect_pseudo_headers.py new file mode 100644 index 000000000..4ab60bfbb --- /dev/null +++ b/tests/test_connect_pseudo_headers.py @@ -0,0 +1,99 @@ +"""unit tests for ordinary vs extended CONNECT validation on the client side.""" + +from __future__ import annotations + +import pytest + +from h2.config import H2Configuration +from h2.connection import H2Connection +from h2.utilities import HeaderValidationFlags, validate_outbound_headers + + +def _new_conn() -> H2Connection: + c = H2Connection( + config=H2Configuration(client_side=True, header_encoding="utf-8") + ) + c.initiate_connection() + # settings ack frame: length=0, type=4, flags=1(ACK), stream=0 + c.receive_data(b"\x00\x00\x00\x04\x01\x00\x00\x00\x00") + return c + + +def _client_req_flags() -> HeaderValidationFlags: + # client, not trailers, not response, not push + return HeaderValidationFlags( + is_client=True, + is_trailer=False, + is_response_header=False, + is_push_promise=False, + ) + + +def test_ordinary_connect_allows_no_scheme_no_path_and_send_headers_ok() -> None: + # ---- bytes for validate_outbound_headers ---- + hdrs_bytes = [ + (b":method", b"CONNECT"), + (b":authority", b"example.com:443"), + ] + # should not raise + list(validate_outbound_headers(hdrs_bytes, _client_req_flags())) + + # ---- str is fine for send_headers due to header_encoding ---- + hdrs_str = [ + (":method", "CONNECT"), + (":authority", "example.com:443"), + ] + conn = _new_conn() + # should not raise + conn.send_headers(1, hdrs_str, end_stream=True) + + +def test_ordinary_connect_rejects_path_or_scheme() -> None: + bad1 = [ + (b":method", b"CONNECT"), + (b":authority", b"example.com:443"), + (b":path", b"/"), + ] + bad2 = [ + (b":method", b"CONNECT"), + (b":authority", b"example.com:443"), + (b":scheme", b"https"), + ] + with pytest.raises(Exception): + list(validate_outbound_headers(bad1, _client_req_flags())) + with pytest.raises(Exception): + list(validate_outbound_headers(bad2, _client_req_flags())) + + +def test_extended_connect_requires_regular_tuple_and_send_headers_ok() -> None: + hdrs_bytes = [ + (b":method", b"CONNECT"), + (b":protocol", b"websocket"), + (b":scheme", b"https"), + (b":path", b"/chat?room=1"), + (b":authority", b"ws.example.com"), + ] + # should not raise + list(validate_outbound_headers(hdrs_bytes, _client_req_flags())) + + hdrs_str = [ + (":method", "CONNECT"), + (":protocol", "websocket"), + (":scheme", "https"), + (":path", "/chat?room=1"), + (":authority", "ws.example.com"), + ] + conn = _new_conn() + # should not raise + conn.send_headers(3, hdrs_str, end_stream=True) + + +def test_non_connect_still_requires_scheme_and_path() -> None: + hdrs_bytes = [ + (b":method", b"GET"), + (b":authority", b"example.com"), + # omit :scheme and :path -> should raise + ] + with pytest.raises(Exception): + list(validate_outbound_headers(hdrs_bytes, _client_req_flags())) + diff --git a/tests/test_connect_roundtrip_and_edges.py b/tests/test_connect_roundtrip_and_edges.py new file mode 100644 index 000000000..9074d3c5f --- /dev/null +++ b/tests/test_connect_roundtrip_and_edges.py @@ -0,0 +1,234 @@ +"""round-trip and edge tests for ordinary vs extended CONNECT behavior.""" + +from __future__ import annotations + + +import pytest + +from h2.config import H2Configuration +from h2.connection import H2Connection +from h2.events import RequestReceived +from h2.utilities import HeaderValidationFlags, validate_outbound_headers + + + +def _new_conn(client_side: bool) -> H2Connection: + return H2Connection( + H2Configuration(client_side=client_side, header_encoding="utf-8") + ) + + +def _xfer_once(a: H2Connection, b: H2Connection): + """ + move bytes from a -> b one step and return the events produced on b. + """ + data = a.data_to_send() + if not data: + return [] + return b.receive_data(data) + + +def _shake(client: H2Connection, server: H2Connection) -> None: + client.initiate_connection() + server.initiate_connection() + _xfer_once(client, server) + _xfer_once(server, client) + + +def _drain_events( + src: H2Connection, dst: H2Connection, max_iters: int = 10 +) -> list: + """ + keep transferring until src has nothing more to send (or max_iters reached). + return the list of all events generated on dst. + """ + out = [] + for _ in range(max_iters): + evs = _xfer_once(src, dst) + if not evs: + break + out.extend(evs) + return out + + +def _validate_bytes(hdrs) -> None: + flags = HeaderValidationFlags( + is_client=True, + is_trailer=False, + is_response_header=False, + is_push_promise=False, + ) + # force bytes; validate_outbound_headers expects (bytes, bytes) tuples + hdrs_b = [ + ( + k if isinstance(k, bytes) else k.encode("ascii"), + v if isinstance(v, bytes) else v.encode("ascii"), + ) + for (k, v) in hdrs + ] + # exhaust generator to trigger validation side-effects + list(validate_outbound_headers(hdrs_b, flags)) # noqa: B018 (intentional) + + +def test_roundtrip_ordinary_connect_no_path_ok() -> None: + client = _new_conn(True) + server = _new_conn(False) + _shake(client, server) + + # ordinary CONNECT: only :method and :authority + hdrs = [ + (":method", "CONNECT"), + (":authority", "example.com:443"), + ] + _validate_bytes(hdrs) + + stream_id = 1 + client.send_headers(stream_id, hdrs, end_stream=True) + evs = _drain_events(client, server) + + req_evs = [e for e in evs if isinstance(e, RequestReceived)] + assert req_evs, "server should receive a RequestReceived" + ps = {k: v for k, v in req_evs[0].headers if k.startswith(":")} + assert ps[":method"] == "CONNECT" + assert ps[":authority"] == "example.com:443" + assert ":scheme" not in ps and ":path" not in ps + + +def test_roundtrip_extended_connect_websocket_ok() -> None: + client = _new_conn(True) + server = _new_conn(False) + _shake(client, server) + + # extended CONNECT for WebSocket (RFC 8441 §4) + hdrs = [ + (":method", "CONNECT"), + (":protocol", "websocket"), + (":scheme", "https"), + (":path", "/chat?room=1"), + (":authority", "ws.example.com"), + ] + _validate_bytes(hdrs) + + stream_id = 1 + client.send_headers(stream_id, hdrs, end_stream=True) + evs = _drain_events(client, server) + + req_evs = [e for e in evs if isinstance(e, RequestReceived)] + assert req_evs + ps = {k: v for k, v in req_evs[0].headers if k.startswith(":")} + assert ps[":method"] == "CONNECT" + assert ps[":protocol"] == "websocket" + assert ps[":scheme"] == "https" + assert ps[":path"] == "/chat?room=1" + assert ps[":authority"] == "ws.example.com" + + +def test_extended_connect_rejects_if_tuple_incomplete() -> None: + # :protocol present but missing :scheme/:path => should fail validation + hdrs = [ + (":method", "CONNECT"), + (":protocol", "websocket"), + (":authority", "ws.example.com"), + # no :scheme, no :path + ] + with pytest.raises(Exception): + _validate_bytes(hdrs) + + +def test_ordinary_connect_rejects_path_or_scheme() -> None: + bad1 = [ + (":method", "CONNECT"), + (":authority", "example.com:443"), + (":path", "/"), + ] + bad2 = [ + (":method", "CONNECT"), + (":authority", "example.com:443"), + (":scheme", "https"), + ] + with pytest.raises(Exception): + _validate_bytes(bad1) + with pytest.raises(Exception): + _validate_bytes(bad2) + + +def test_pseudo_headers_must_come_first() -> None: + # a regular header before a pseudo-header should be rejected + hdrs = [ + ("host", "example.com"), + (":method", "CONNECT"), + (":authority", "example.com:443"), + ] + with pytest.raises(Exception): + _validate_bytes(hdrs) + + +def test_duplicate_pseudo_header_rejected() -> None: + hdrs = [ + (":method", "CONNECT"), + (":method", "CONNECT"), + (":authority", "example.com:443"), + ] + with pytest.raises(Exception): + _validate_bytes(hdrs) + + +def test_large_header_block_continuation_ok() -> None: + client = _new_conn(True) + server = _new_conn(False) + _shake(client, server) + + # many regular headers to force CONTINUATION frames + big = [(f"x-h{i}", "y" * 200) for i in range(200)] + hdrs = [ + (":method", "CONNECT"), + (":authority", "example.com:443"), + ] + big + + _validate_bytes(hdrs) + client.send_headers(1, hdrs, end_stream=True) + + evs = _drain_events(client, server, max_iters=20) + req_evs = [e for e in evs if isinstance(e, RequestReceived)] + assert req_evs + got = dict(req_evs[0].headers) + assert "x-h0" in got and str(got["x-h0"]).startswith("y") + + +def test_te_header_rules() -> None: + # only "te: trailers" is legal in HTTP/2 requests. anything else must be rejected. + ok = [ + (":method", "CONNECT"), + (":authority", "example.com:443"), + ("te", "trailers"), + ] + _validate_bytes(ok) # should not raise + + bad = [ + (":method", "CONNECT"), + (":authority", "example.com:443"), + ("te", "gzip"), + ] + with pytest.raises(Exception): + _validate_bytes(bad) + + +def test_multiple_concurrent_connect_streams_ok() -> None: + client = _new_conn(True) + server = _new_conn(False) + _shake(client, server) + + s1 = 1 + s2 = 3 + hdrs = [ + (":method", "CONNECT"), + (":authority", "example.internal:3128"), + ] + _validate_bytes(hdrs) + client.send_headers(s1, hdrs, end_stream=True) + client.send_headers(s2, hdrs, end_stream=True) + + evs = _drain_events(client, server, max_iters=10) + reqs = [e for e in evs if isinstance(e, RequestReceived)] + assert len(reqs) == 2 + From fd104ad7d31b897a17c05768815de5ab2565f56c Mon Sep 17 00:00:00 2001 From: ChasingImpact Date: Thu, 9 Oct 2025 17:54:33 -0400 Subject: [PATCH 36/60] lint/types: utilities.py noqa+comma; stream.py coerce memoryview -> bytes; silence settings.py PLW1641 --- src/h2/settings.py | 2 +- src/h2/stream.py | 2 +- src/h2/utilities.py | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/h2/settings.py b/src/h2/settings.py index c1be953bd..659dac794 100644 --- a/src/h2/settings.py +++ b/src/h2/settings.py @@ -90,7 +90,7 @@ def __repr__(self) -> str: ) -class Settings(MutableMapping[Union[SettingCodes, int], int]): +class Settings(MutableMapping[Union[SettingCodes, int], int]): # noqa: PLW1641 """ An object that encapsulates HTTP/2 settings state. diff --git a/src/h2/stream.py b/src/h2/stream.py index d6f5845c3..1f38339d5 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -968,7 +968,7 @@ def send_data(self, self.state_machine.process_input(StreamInputs.SEND_DATA) df = DataFrame(self.stream_id) - df.data = data + df.data = data.tobytes() if isinstance(data, memoryview) else data if end_stream: self.state_machine.process_input(StreamInputs.SEND_END_STREAM) df.flags.add("END_STREAM") diff --git a/src/h2/utilities.py b/src/h2/utilities.py index 7f0541b23..9b492210e 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -361,9 +361,11 @@ def _reject_pseudo_header_fields(headers: Iterable[Header], ) -def _check_pseudo_header_field_acceptability(pseudo_headers: set[bytes | str] | set[bytes] | set[str], - method: bytes | None, - hdr_validation_flags: HeaderValidationFlags) -> None: +def _check_pseudo_header_field_acceptability( # noqa: C901 + pseudo_headers: set[bytes | str] | set[bytes] | set[str], + method: bytes | None, + hdr_validation_flags: HeaderValidationFlags, +) -> None: """ Given the set of pseudo-headers present in a header block and the validation flags, confirms that RFC 7540 allows them. @@ -385,7 +387,6 @@ def _check_pseudo_header_field_acceptability(pseudo_headers: set[bytes | str] | if invalid_response_headers: msg = f"Encountered request-only headers {invalid_response_headers}" raise ProtocolError(msg) - elif (not hdr_validation_flags.is_response_header and not hdr_validation_flags.is_trailer): # Request header block. From adcb88c3595912d5da98e147348e93870bd84541 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Mon, 13 Oct 2025 12:32:55 +0530 Subject: [PATCH 37/60] updates and linting --- .github/workflows/ci.yml | 2 +- CHANGELOG.rst | 2 ++ pyproject.toml | 24 +++++++++--------------- src/h2/connection.py | 4 ++-- src/h2/events.py | 6 +----- src/h2/settings.py | 7 +++++-- src/h2/stream.py | 11 +++++++---- 7 files changed, 27 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eaa106792..84463473d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - "3.12" - "3.13" - "3.14" - - "pypy3.9" + - "pypy3.11" steps: - uses: actions/checkout@v2 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dc194624a..5c1ad3533 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,10 +7,12 @@ dev **API Changes (Backward Incompatible)** - Support for Python 3.9 has been removed. +- Support for PyPy 3.9 has been removed. **API Changes (Backward Compatible)** - Support for Python 3.14 has been added. +- Support for PyPy 3.11 has been added. **Bugfixes** diff --git a/pyproject.toml b/pyproject.toml index 249dabb4b..661050a94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,14 +2,16 @@ # https://packaging.python.org/en/latest/specifications/pyproject-toml/ [build-system] -requires = ["setuptools"] +requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [project] name = "h2" description = "Pure-Python HTTP/2 protocol implementation" readme = { file = "README.rst", content-type = "text/x-rst" } -license = { file = "LICENSE" } + +license = "MIT" +license-files = [ "LICENSE" ] authors = [ { name = "Cory Benfield", email = "cory@lukasa.co.uk" } @@ -29,7 +31,6 @@ dynamic = ["version"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", @@ -72,9 +73,9 @@ linting = [ packaging = [ "check-manifest==0.50", "readme-renderer==44.0", - "build>=1.2.2,<2", - "twine>=6.1.0,<7", - "wheel>=0.45.0,<1", + "build>=1.3.0,<2", + "twine>=6.2.0,<7", + "wheel>=0.45.1,<1", ] docs = [ @@ -132,7 +133,6 @@ lint.ignore = [ "TD002", # readability "TD003", # readability "S101", # readability - "PD901", # readability "ERA001", # readability "ARG001", # readability "ARG002", # readability @@ -170,7 +170,7 @@ source = [ [tool.tox] min_version = "4.23.2" -env_list = [ "py310", "py311", "py312", "py313", "py314", "pypy3", "lint", "docs", "packaging" ] +env_list = [ "py310", "py311", "py312", "py313", "py314", "pypy311", "lint", "docs", "packaging" ] [tool.tox.gh-actions] python = """ @@ -179,7 +179,7 @@ python = """ 3.12: py312 3.13: py313 3.14: py314 - pypy3: pypy3 + pypy3.11: pypy311 """ [tool.tox.env_run_base] @@ -188,12 +188,6 @@ commands = [ ["python", "-bb", "-m", "pytest", "--cov-report=xml", "--cov-report=term", "--cov=h2", { replace = "posargs", extend = true }] ] -[tool.tox.env.pypy3] -# temporarily disable coverage testing on PyPy due to performance problems -commands = [ - ["pytest", { replace = "posargs", extend = true }] -] - [tool.tox.env.lint] dependency_groups = ["linting"] commands = [ diff --git a/src/h2/connection.py b/src/h2/connection.py index 313efc146..b65f075a3 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -8,7 +8,7 @@ import base64 from enum import Enum, IntEnum -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from hpack.exceptions import HPACKError, OversizedHeaderListError from hpack.hpack import Decoder, Encoder @@ -66,7 +66,7 @@ from .windows import WindowManager if TYPE_CHECKING: # pragma: no cover - from collections.abc import Iterable + from collections.abc import Callable, Iterable from hpack.struct import Header, HeaderWeaklyTyped diff --git a/src/h2/events.py b/src/h2/events.py index 6aab0713d..5bb892ebf 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -11,7 +11,6 @@ from __future__ import annotations import binascii -import sys from dataclasses import dataclass from typing import TYPE_CHECKING, Any @@ -24,10 +23,7 @@ from .errors import ErrorCodes -if sys.version_info < (3, 10): # pragma: no cover - kw_only: dict[str, bool] = {} -else: # pragma: no cover - kw_only = {"kw_only": True} +kw_only = {"kw_only": True} _LAZY_INIT: Any = object() diff --git a/src/h2/settings.py b/src/h2/settings.py index c1be953bd..aad7ce780 100644 --- a/src/h2/settings.py +++ b/src/h2/settings.py @@ -11,7 +11,6 @@ import collections import enum from collections.abc import Iterator, MutableMapping -from typing import Union from hyperframe.frame import SettingsFrame @@ -90,7 +89,7 @@ def __repr__(self) -> str: ) -class Settings(MutableMapping[Union[SettingCodes, int], int]): +class Settings(MutableMapping[SettingCodes | int, int]): """ An object that encapsulates HTTP/2 settings state. @@ -307,6 +306,10 @@ def __ne__(self, other: object) -> bool: return not self == other return NotImplemented + # be explicit that Settings is not providing a hash implementation + # see https://docs.python.org/3/reference/datamodel.html#object.__hash__ + __hash__ = MutableMapping.__hash__ + def _validate_setting(setting: SettingCodes | int, value: int) -> ErrorCodes: """ diff --git a/src/h2/stream.py b/src/h2/stream.py index d6f5845c3..7eb436159 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -7,7 +7,7 @@ from __future__ import annotations from enum import Enum, IntEnum -from typing import TYPE_CHECKING, Any, Union, cast +from typing import TYPE_CHECKING, Any, cast from hpack import HeaderTuple from hyperframe.frame import AltSvcFrame, ContinuationFrame, DataFrame, Frame, HeadersFrame, PushPromiseFrame, RstStreamFrame, WindowUpdateFrame @@ -968,7 +968,10 @@ def send_data(self, self.state_machine.process_input(StreamInputs.SEND_DATA) df = DataFrame(self.stream_id) - df.data = data + if isinstance(data, memoryview): + df.data = data.tobytes() + else: + df.data = data if end_stream: self.state_machine.process_input(StreamInputs.SEND_END_STREAM) df.flags.add("END_STREAM") @@ -1076,7 +1079,7 @@ def receive_headers(self, events = self.state_machine.process_input(input_) headers_event = cast( - "Union[RequestReceived, ResponseReceived, TrailersReceived, InformationalResponseReceived]", + "RequestReceived | ResponseReceived | TrailersReceived | InformationalResponseReceived", events[0], ) @@ -1086,7 +1089,7 @@ def receive_headers(self, ) # We ensured it's not an information response at the beginning of the method. cast( - "Union[RequestReceived, ResponseReceived, TrailersReceived]", + "RequestReceived | ResponseReceived | TrailersReceived", headers_event, ).stream_ended = cast("StreamEnded", es_events[0]) events += es_events From fd69c3b83552bea54bd1fd587fa2c4b1d3d75b99 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Tue, 14 Oct 2025 15:18:56 +0530 Subject: [PATCH 38/60] document API-incompatible change that slipped through the v4.3.0 changelog --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3720fc579..314a02295 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,9 @@ dev - Support for Python 3.9 has been removed. - Support for PyPy 3.9 has been removed. +- **backfill from v4.3.0** Convert emitted events into Python `dataclass`, which introduces new constructors with required arguments. + Instantiating these events without arguments, as previously commonly used API pattern, will no longer work. + **API Changes (Backward Compatible)** @@ -31,6 +34,8 @@ dev The main Python API is compatible, but some previously valid requests/response headers might now be blocked. Use the `validate_inbound_headers` config option if needed. Thanks to Sebastiano Sartor (sebsrt) for the report. +- Convert emitted events into Python `dataclass`, which introduces new constructors with required arguments. + Instantiating these events without arguments, as previously commonly used API pattern, will no longer work. **API Changes (Backward Compatible)** From 700990edc85222bac80c1a372aab4c1c205d4f32 Mon Sep 17 00:00:00 2001 From: Joaquin Borggio Date: Tue, 30 Sep 2025 15:07:48 -0400 Subject: [PATCH 39/60] fix: Raise more informative error instead of generic KeyError on end_stream --- src/h2/connection.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/h2/connection.py b/src/h2/connection.py index b65f075a3..841e46440 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -919,10 +919,14 @@ def end_stream(self, stream_id: int) -> None: :param stream_id: The ID of the stream to end. :type stream_id: ``int`` :returns: Nothing + :raises NoSuchStreamError: If the stream ID does not correspond to a + known stream and is higher than the current maximum stream ID. + :raises StreamClosedError: If the stream ID corresponds to a stream + that has been closed. """ self.config.logger.debug("End stream ID %d", stream_id) self.state_machine.process_input(ConnectionInputs.SEND_DATA) - frames = self.streams[stream_id].end_stream() + frames = self._get_stream_by_id(stream_id).end_stream() self._prepare_for_sending(frames) def increment_flow_control_window(self, increment: int, stream_id: int | None = None) -> None: From bcdf4192e6043b642de922eded3f95a5c225ac64 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Tue, 14 Oct 2025 15:29:53 +0530 Subject: [PATCH 40/60] add test for new Stream.end_stream exception behaviour --- CHANGELOG.rst | 2 +- tests/test_basic_logic.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 314a02295..aa5d9cf68 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,10 +8,10 @@ dev - Support for Python 3.9 has been removed. - Support for PyPy 3.9 has been removed. +- `Stream.end_stream()` now raises `NoSuchStreamError` or `StreamClosedError` exceptions, instead of a generic `KeyError`. - **backfill from v4.3.0** Convert emitted events into Python `dataclass`, which introduces new constructors with required arguments. Instantiating these events without arguments, as previously commonly used API pattern, will no longer work. - **API Changes (Backward Compatible)** - Support for Python 3.14 has been added. diff --git a/tests/test_basic_logic.py b/tests/test_basic_logic.py index 0ea7e47bc..782a52ac3 100644 --- a/tests/test_basic_logic.py +++ b/tests/test_basic_logic.py @@ -340,6 +340,23 @@ def test_end_stream_without_data(self, frame_factory) -> None: assert not events assert c.data_to_send() == f.serialize() + def test_end_stream_exceptions(self, frame_factory) -> None: + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(1, self.example_request_headers, end_stream=False) + c.clear_outbound_data_buffer() + + with pytest.raises(h2.exceptions.NoSuchStreamError): + c.end_stream(42) + + c.end_stream(1) + + c.send_headers(5, self.example_request_headers, end_stream=False) + + with pytest.raises(h2.exceptions.StreamClosedError): + c.end_stream(3) + + def test_cannot_send_headers_on_lower_stream_id(self) -> None: """ Once stream ID x has been used, cannot use stream ID y where y < x. From dd99650aefa4b9fe95f3844afdb46408dab613dc Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 24 Nov 2025 09:16:31 +0100 Subject: [PATCH 41/60] Fix typos discovered by codespell --- src/h2/connection.py | 2 +- src/h2/events.py | 2 +- src/h2/frame_buffer.py | 6 +++--- tests/test_flow_control_window.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/h2/connection.py b/src/h2/connection.py index 841e46440..e7c676c1e 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -780,7 +780,7 @@ def send_headers(self, on the stream given in ``priority_depends_on`` for priority purposes. See :meth:`prioritize ` for more about how this - field workds. Defaults to ``None``, which means that no priority + field works. Defaults to ``None``, which means that no priority information will be sent. :type priority_depends_on: ``bool`` or ``None`` diff --git a/src/h2/events.py b/src/h2/events.py index 5bb892ebf..e3accea76 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -510,7 +510,7 @@ class SettingsAcknowledged(Event): """ The SettingsAcknowledged event is fired whenever a settings ACK is received from the remote peer. The event carries on it the settings that were - acknowedged, in the same format as + acknowledged, in the same format as :class:`h2.events.RemoteSettingsChanged`. """ diff --git a/src/h2/frame_buffer.py b/src/h2/frame_buffer.py index e7b0a7126..2555cdce5 100644 --- a/src/h2/frame_buffer.py +++ b/src/h2/frame_buffer.py @@ -13,7 +13,7 @@ from .exceptions import FrameDataMissingError, FrameTooLargeError, ProtocolError # To avoid a DOS attack based on sending loads of continuation frames, we limit -# the maximum number we're perpared to receive. In this case, we'll set the +# the maximum number we're prepared to receive. In this case, we'll set the # limit to 64, which means the largest encoded header block we can receive by # default is 262144 bytes long, and the largest possible *at all* is 1073741760 # bytes long. @@ -25,7 +25,7 @@ class FrameBuffer: """ - A buffer data structure for HTTP/2 data that allows iteraton in terms of + A buffer data structure for HTTP/2 data that allows iteration in terms of H2 frames. """ @@ -94,7 +94,7 @@ def _update_header_buffer(self, f: Frame | None) -> Frame | None: # If this is the end of the header block, then we want to build a # mutant HEADERS frame that's massive. Use the original one we got, - # then set END_HEADERS and set its data appopriately. If it's not + # then set END_HEADERS and set its data appropriately. If it's not # the end of the block, lose the current frame: we can't yield it. if "END_HEADERS" in f.flags: f = self._headers_buffer[0] diff --git a/tests/test_flow_control_window.py b/tests/test_flow_control_window.py index 21cc7b8f0..4b26cec8c 100644 --- a/tests/test_flow_control_window.py +++ b/tests/test_flow_control_window.py @@ -730,7 +730,7 @@ def test_must_acknowledge_for_stream(self, frame_factory, stream_id) -> None: frame_factory.refresh_encoder() # Create a connection in a state that might actually accept - # data acknolwedgement. + # data acknowledgement. c = self._setup_connection_and_send_headers(frame_factory) data_frame = frame_factory.build_data_frame( b"some data", flags=["END_STREAM"], @@ -755,7 +755,7 @@ def test_cannot_acknowledge_less_than_zero(self, frame_factory, size) -> None: frame_factory.refresh_encoder() # Create a connection in a state that might actually accept - # data acknolwedgement. + # data acknowledgement. c = self._setup_connection_and_send_headers(frame_factory) data_frame = frame_factory.build_data_frame( b"some data", flags=["END_STREAM"], From 18fa3480a7a9009dd2db4a59269ff46a35b18ea5 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 22 Dec 2025 19:54:02 +0100 Subject: [PATCH 42/60] Update GitHub Actions (#1310) --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84463473d..c2011465f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,6 @@ jobs: tox: runs-on: ubuntu-latest strategy: - max-parallel: 5 matrix: python-version: - "3.10" @@ -21,8 +20,8 @@ jobs: - "pypy3.11" steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install tox From be8b4e3dab0c3325ec89f2b41edd69c7eee86b78 Mon Sep 17 00:00:00 2001 From: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:38:40 +0300 Subject: [PATCH 43/60] Fix substring check used instead of equality for cookie header name (#1313) * Fix substring check used instead of equality for header names The `in` operator on `bytes` performs substring search, not equality. `header[0] in b"cookie"` matches any header name that is a substring of "cookie" (e.g. b"co", b"ok", b"e"), not just b"cookie" itself. This means short header names that happen to be substrings of "cookie" get incorrectly promoted to NeverIndexedHeaderTuple when their value is under 20 bytes, potentially affecting HPACK compression behavior. Changed both occurrences to use `==` for exact comparison: - Line 91: cookie header check in _secure_headers - Line 350: :method pseudo-header check in _reject_pseudo_header_fields * Add regression tests for substring check fix - Add test data with header names that are substrings of 'cookie' but not equal to 'cookie' to verify they are not treated as sensitive - Add test for extract_method_header to verify substring names like ':me' do not falsely match ':method' --- src/h2/utilities.py | 4 ++-- tests/test_header_indexing.py | 37 +++++++++++++++++++++++++++++++++ tests/test_utility_functions.py | 13 ++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/h2/utilities.py b/src/h2/utilities.py index 9b492210e..7709aa602 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -88,7 +88,7 @@ def _secure_headers(headers: Iterable[Header], """ for header in headers: assert isinstance(header[0], bytes) - if header[0] in _SECURE_HEADERS or (header[0] in b"cookie" and len(header[1]) < 20): + if header[0] in _SECURE_HEADERS or (header[0] == b"cookie" and len(header[1]) < 20): yield NeverIndexedHeaderTuple(header[0], header[1]) else: yield header @@ -347,7 +347,7 @@ def _reject_pseudo_header_fields(headers: Iterable[Header], msg = f"Received custom pseudo-header field {header[0]!r}" raise ProtocolError(msg) - if header[0] in b":method": + if header[0] == b":method": method = header[1] else: diff --git a/tests/test_header_indexing.py b/tests/test_header_indexing.py index f50f23d8b..252083e63 100644 --- a/tests/test_header_indexing.py +++ b/tests/test_header_indexing.py @@ -418,6 +418,19 @@ class TestSecureHeaders: HeaderTuple(b"cookie", b"twenty byte cookie!!"), HeaderTuple(b"Cookie", b"twenty byte cookie!!"), ] + # Headers with names that are substrings of "cookie" but not equal to + # "cookie". Before the fix, the ``in`` operator was used instead of + # ``==``, which caused these to be incorrectly treated as sensitive. + non_cookie_substring_headers = [ + (b"cook", b"short"), + (b"okie", b"short"), + (b"oo", b"short"), + (b"cooki", b"short"), + (b"ookie", b"short"), + HeaderTuple(b"cook", b"short"), + HeaderTuple(b"okie", b"short"), + HeaderTuple(b"cooki", b"short"), + ] server_config = h2.config.H2Configuration(client_side=False) @@ -622,3 +635,27 @@ def test_long_cookie_headers_can_be_indexed_push(self, ) assert c.data_to_send() == expected_frame.serialize() + + @pytest.mark.parametrize( + "headers", [example_request_headers, bytes_example_request_headers], + ) + @pytest.mark.parametrize("header", non_cookie_substring_headers) + def test_non_cookie_substring_headers_can_be_indexed(self, + headers, + header, + frame_factory) -> None: + """ + Headers with names that are substrings of 'cookie' but not equal to + 'cookie' should not be treated as sensitive. + """ + send_headers = [*headers, header] + expected_headers = [*headers, HeaderTuple(header[0], header[1])] + + c = h2.connection.H2Connection() + c.initiate_connection() + + c.clear_outbound_data_buffer() + c.send_headers(1, send_headers) + + f = frame_factory.build_headers_frame(headers=expected_headers) + assert c.data_to_send() == f.serialize() \ No newline at end of file diff --git a/tests/test_utility_functions.py b/tests/test_utility_functions.py index 31634f40e..364fe9682 100644 --- a/tests/test_utility_functions.py +++ b/tests/test_utility_functions.py @@ -163,6 +163,19 @@ def test_extract_header_method(self) -> None: self.example_headers_with_bytes, ) == b"GET" + def test_extract_method_header_no_false_substring_match(self) -> None: + """ + Headers with names that are substrings of ':method' but not equal + to ':method' should not be matched. + """ + headers = [ + (b":authority", b"example.com"), + (b":path", b"/"), + (b":scheme", b"https"), + (b":me", b"GET"), + ] + assert extract_method_header(headers) is None + def test_size_limit_dict_limit() -> None: dct = SizeLimitDict(size_limit=2) From 73a232d9b1f00514994bf9af066adb9f13f3fc73 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Fri, 3 Apr 2026 14:37:44 +0200 Subject: [PATCH 44/60] project cleanup --- .github/workflows/ci.yml | 3 +-- CHANGELOG.rst | 2 +- pyproject.toml | 18 ++++++++---------- src/h2/connection.py | 3 +-- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2011465f..bbcbccd20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,6 @@ jobs: - "3.12" - "3.13" - "3.14" - - "pypy3.11" steps: - uses: actions/checkout@v6 @@ -39,7 +38,7 @@ jobs: - name: Test with tox run: | tox --parallel 0 - - uses: codecov/codecov-action@v5 + - uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index aa5d9cf68..5fada816d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,11 +15,11 @@ dev **API Changes (Backward Compatible)** - Support for Python 3.14 has been added. -- Support for PyPy 3.11 has been added. - Align CONNECT pseudo-header validation with RFC 9113 s8.3 and RFC 8441 s4. Ordinary CONNECT now requires ``:method=CONNECT`` and ``:authority``, and forbids ``:scheme``/``:path``. Extended CONNECT (e.g., WebSocket) requires ``:scheme``, ``:path``, ``:authority`` plus ``:protocol``. (PR #1309) +- Fix incorrect substring matching of secure header in ``cookie`` and ``:method``. **Bugfixes** diff --git a/pyproject.toml b/pyproject.toml index 661050a94..c1a15bf60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ # https://packaging.python.org/en/latest/specifications/pyproject-toml/ [build-system] -requires = ["setuptools>=77"] +requires = ["setuptools>=82", "wheel>=0.46.3"] build-backend = "setuptools.build_meta" [project] @@ -40,7 +40,6 @@ classifiers = [ "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", ] [project.urls] @@ -71,15 +70,15 @@ linting = [ ] packaging = [ - "check-manifest==0.50", + "check-manifest==0.51", "readme-renderer==44.0", - "build>=1.3.0,<2", + "build>=1.4.2,<2", "twine>=6.2.0,<7", - "wheel>=0.45.1,<1", + "wheel>=0.46.3,<1", ] docs = [ - "sphinx>=7.4.7,<9", + "sphinx>=9.1.0,<10", ] [tool.setuptools.packages.find] @@ -170,16 +169,15 @@ source = [ [tool.tox] min_version = "4.23.2" -env_list = [ "py310", "py311", "py312", "py313", "py314", "pypy311", "lint", "docs", "packaging" ] +env_list = [ "py310", "py311", "py312", "py313", "py314", "lint", "docs", "packaging" ] [tool.tox.gh-actions] python = """ - 3.10: py310, h2spec, lint, docs, packaging + 3.10: py310, h2spec, lint, packaging 3.11: py311 3.12: py312 - 3.13: py313 + 3.13: py313, docs 3.14: py314 - pypy3.11: pypy311 """ [tool.tox.env_run_base] diff --git a/src/h2/connection.py b/src/h2/connection.py index e7c676c1e..6c5122024 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -1901,8 +1901,7 @@ def _receive_goaway_frame(self, frame: GoAwayFrame) -> tuple[list[Frame], list[E new_event = ConnectionTerminated() new_event.error_code = _error_code_from_int(frame.error_code) new_event.last_stream_id = frame.last_stream_id - new_event.additional_data = (frame.additional_data - if frame.additional_data else None) + new_event.additional_data = frame.additional_data or None events.append(new_event) return [], events From b08b9d7ceb2d5f779d62d0a3877964d537fd373e Mon Sep 17 00:00:00 2001 From: vladyslav Date: Wed, 6 May 2026 13:25:11 +0300 Subject: [PATCH 45/60] Allow sending 0-byte DATA frames when flow-control window is negative --- CHANGELOG.rst | 2 +- src/h2/connection.py | 4 ++-- src/h2/stream.py | 2 +- tests/test_flow_control_window.py | 11 +++++++++++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5fada816d..62baf5cdd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,7 +23,7 @@ dev **Bugfixes** -- +- Fix to allow sending 0 bytes on a stream even if the flow control window is negative. 4.3.0 (2025-08-23) ------------------ diff --git a/src/h2/connection.py b/src/h2/connection.py index 6c5122024..835ec479a 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -888,7 +888,7 @@ def send_data(self, "Frame size on stream ID %d is %d", stream_id, frame_size, ) - if frame_size > self.local_flow_control_window(stream_id): + if frame_size > 0 and frame_size > self.local_flow_control_window(stream_id): msg = f"Cannot send {frame_size} bytes, flow control window is {self.local_flow_control_window(stream_id)}" raise FlowControlError(msg) if frame_size > self.max_outbound_frame_size: @@ -907,7 +907,7 @@ def send_data(self, "Outbound flow control window size is %d", self.outbound_flow_control_window, ) - assert self.outbound_flow_control_window >= 0 + assert self.outbound_flow_control_window >= 0 or frame_size == 0 def end_stream(self, stream_id: int) -> None: """ diff --git a/src/h2/stream.py b/src/h2/stream.py index 7eb436159..9b5bce789 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -981,7 +981,7 @@ def send_data(self, # Subtract flow_controlled_length to account for possible padding self.outbound_flow_control_window -= df.flow_controlled_length - assert self.outbound_flow_control_window >= 0 + assert self.outbound_flow_control_window >= 0 or df.flow_controlled_length == 0 return [df] diff --git a/tests/test_flow_control_window.py b/tests/test_flow_control_window.py index 4b26cec8c..aea53fab1 100644 --- a/tests/test_flow_control_window.py +++ b/tests/test_flow_control_window.py @@ -103,6 +103,17 @@ def test_flow_control_decreases_with_padded_data(self, frame_factory) -> None: self.DEFAULT_FLOW_WINDOW - len(b"some data") - 10 - 1 ) assert (c.remote_flow_control_window(1) == remaining_length) + + def test_can_send_zero_bytes_when_window_negative(self) -> None: + """ + Sending 0 bytes when the flow control window is negative should not + raise a FlowControlError, allowing empty END_STREAM frames to be sent. + """ + c = h2.connection.H2Connection() + c.send_headers(1, self.example_request_headers) + c.outbound_flow_control_window = -100 + c._get_stream_by_id(1).outbound_flow_control_window = -100 + c.send_data(1, b"", end_stream=True) def test_flow_control_is_limited_by_connection(self) -> None: """ From bdf0b958024efc312e3474dd1386f8a1f997c682 Mon Sep 17 00:00:00 2001 From: Harshal Parekh Date: Sun, 10 May 2026 02:52:34 -0500 Subject: [PATCH 46/60] Broaden receive data buffer types --- pyproject.toml | 5 +++-- src/h2/_typing.py | 21 +++++++++++++++++++++ src/h2/connection.py | 6 ++++-- src/h2/frame_buffer.py | 17 ++++++++++++----- tests/typing/strict_bytes.py | 17 +++++++++++++++++ 5 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 src/h2/_typing.py create mode 100644 tests/typing/strict_bytes.py diff --git a/pyproject.toml b/pyproject.toml index c1a15bf60..fbdffff92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ testing = [ linting = [ "ruff>=0.8.0,<1", - "mypy>=1.13.0,<2", + "mypy>=1.16.0,<2", "typing_extensions>=4.12.2", ] @@ -149,6 +149,7 @@ testpaths = [ "tests" ] [tool.coverage.run] branch = true source = [ "h2" ] +omit = [ "*/h2/_typing.py" ] [tool.coverage.report] fail_under = 100 @@ -190,7 +191,7 @@ commands = [ dependency_groups = ["linting"] commands = [ ["ruff", "check", "src/"], - ["mypy", "src/"], + ["mypy", "--strict-bytes", "src/", "tests/typing/strict_bytes.py"], ] [tool.tox.env.docs] diff --git a/src/h2/_typing.py b/src/h2/_typing.py new file mode 100644 index 000000000..64592fd73 --- /dev/null +++ b/src/h2/_typing.py @@ -0,0 +1,21 @@ +""" +h2/_typing +~~~~~~~~~~ + +Shared typing helpers. +""" +from __future__ import annotations + +from typing import Protocol + + +class Buffer(Protocol): + """ + An object implementing the PEP 688 buffer protocol. + """ + + def __buffer__(self, flags: int, /) -> memoryview: + """ + Return a memoryview over this object's bytes. + """ + ... diff --git a/src/h2/connection.py b/src/h2/connection.py index 835ec479a..0efadec20 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -70,6 +70,8 @@ from hpack.struct import Header, HeaderWeaklyTyped + from ._typing import Buffer + class ConnectionState(Enum): IDLE = 0 @@ -1496,12 +1498,12 @@ def _inbound_flow_control_change_from_settings(self, old_value: int | None, new_ for stream in self.streams.values(): stream._inbound_flow_control_change_from_settings(delta) - def receive_data(self, data: bytes) -> list[Event]: + def receive_data(self, data: Buffer) -> list[Event]: """ Pass some received HTTP/2 data to the connection for handling. :param data: The data received from the remote peer on the network. - :type data: ``bytes`` + :type data: An object implementing the buffer protocol. :returns: A list of events that the remote peer triggered by sending this data. """ diff --git a/src/h2/frame_buffer.py b/src/h2/frame_buffer.py index 2555cdce5..c2f8a5411 100644 --- a/src/h2/frame_buffer.py +++ b/src/h2/frame_buffer.py @@ -7,11 +7,16 @@ """ from __future__ import annotations +from typing import TYPE_CHECKING + from hyperframe.exceptions import InvalidDataError, InvalidFrameError from hyperframe.frame import ContinuationFrame, Frame, HeadersFrame, PushPromiseFrame from .exceptions import FrameDataMissingError, FrameTooLargeError, ProtocolError +if TYPE_CHECKING: # pragma: no cover + from ._typing import Buffer + # To avoid a DOS attack based on sending loads of continuation frames, we limit # the maximum number we're prepared to receive. In this case, we'll set the # limit to 64, which means the largest encoded header block we can receive by @@ -36,25 +41,27 @@ def __init__(self, server: bool = False) -> None: self._preamble_len = len(self._preamble) self._headers_buffer: list[HeadersFrame | ContinuationFrame | PushPromiseFrame] = [] - def add_data(self, data: bytes) -> None: + def add_data(self, data: Buffer) -> None: """ Add more data to the frame buffer. :param data: A bytestring containing the byte buffer. """ + data_view = memoryview(data) + if self._preamble_len: - data_len = len(data) + data_len = len(data_view) of_which_preamble = min(self._preamble_len, data_len) - if self._preamble[:of_which_preamble] != data[:of_which_preamble]: + if self._preamble[:of_which_preamble] != data_view[:of_which_preamble]: msg = "Invalid HTTP/2 preamble." raise ProtocolError(msg) - data = data[of_which_preamble:] + data_view = data_view[of_which_preamble:] self._preamble_len -= of_which_preamble self._preamble = self._preamble[of_which_preamble:] - self._data += data + self._data += data_view def _validate_frame_length(self, length: int) -> None: """ diff --git a/tests/typing/strict_bytes.py b/tests/typing/strict_bytes.py new file mode 100644 index 000000000..9bfc35f00 --- /dev/null +++ b/tests/typing/strict_bytes.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from h2.connection import H2Connection +from h2.frame_buffer import FrameBuffer + + +def receive_data_accepts_buffer_types( + connection: H2Connection, + frame_buffer: FrameBuffer, +) -> None: + bytearray_data = bytearray(b"") + memoryview_data = memoryview(b"") + + connection.receive_data(bytearray_data) + connection.receive_data(memoryview_data) + frame_buffer.add_data(bytearray_data) + frame_buffer.add_data(memoryview_data) From dc5da5c3329e293492c97ff2243e93ede84f2d20 Mon Sep 17 00:00:00 2001 From: Harshal Parekh Date: Mon, 11 May 2026 22:49:15 -0500 Subject: [PATCH 47/60] add changelog, update tests --- CHANGELOG.rst | 3 +++ docs/source/basic-usage.rst | 6 +++++- tests/test_basic_logic.py | 16 ++++++++++++++++ tests/typing/strict_bytes.py | 23 +++++++++++++++++------ 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 62baf5cdd..775c385e3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,9 @@ dev **API Changes (Backward Compatible)** - Support for Python 3.14 has been added. +- ``H2Connection.receive_data`` now accepts any byte-like object that + implements the buffer protocol, such as ``bytes``, ``bytearray``, and + ``memoryview``. Existing ``bytes`` callers are unaffected. - Align CONNECT pseudo-header validation with RFC 9113 s8.3 and RFC 8441 s4. Ordinary CONNECT now requires ``:method=CONNECT`` and ``:authority``, and forbids ``:scheme``/``:path``. Extended CONNECT (e.g., WebSocket) requires diff --git a/docs/source/basic-usage.rst b/docs/source/basic-usage.rst index 22cfd4e0c..dc69f67a1 100644 --- a/docs/source/basic-usage.rst +++ b/docs/source/basic-usage.rst @@ -311,6 +311,10 @@ socket, in a loop. We then passed that data to the connection object, which returned us a single event object: :class:`RemoteSettingsChanged `. +``receive_data`` accepts the ``bytes`` returned by ``recv`` as well as other +byte-like objects that implement the buffer protocol, such as ``bytearray`` and +``memoryview``. + But what we didn't see was anything else. So it seems like all ``curl`` did was change its settings, but nothing else. If you look at the other ``curl`` window, you'll notice that it hangs for a while and then eventually fails with @@ -750,4 +754,4 @@ it, there are a few directions you could investigate: .. _PyOpenSSL: http://pyopenssl.readthedocs.org/ .. _Eventlet example: https://github.com/python-hyper/h2/blob/master/examples/eventlet/eventlet-server.py .. _curl: https://curl.se/docs/http2.html -.. _httpx: https://www.python-httpx.org/ \ No newline at end of file +.. _httpx: https://www.python-httpx.org/ diff --git a/tests/test_basic_logic.py b/tests/test_basic_logic.py index 782a52ac3..1df989e06 100644 --- a/tests/test_basic_logic.py +++ b/tests/test_basic_logic.py @@ -1003,6 +1003,22 @@ def test_ignores_preamble(self) -> None: assert not events assert not c.data_to_send() + @pytest.mark.parametrize("data_wrapper", [bytearray, memoryview]) + def test_receive_data_accepts_buffer_types( + self, + data_wrapper, + frame_factory, + ) -> None: + """ + ``receive_data`` accepts byte-like buffers and handles their contents. + """ + c = h2.connection.H2Connection(config=self.server_config) + + events = c.receive_data(data_wrapper(frame_factory.preamble())) + + assert not events + assert not c.data_to_send() + @pytest.mark.parametrize("chunk_size", range(1, 24)) def test_drip_feed_preamble(self, chunk_size) -> None: """ diff --git a/tests/typing/strict_bytes.py b/tests/typing/strict_bytes.py index 9bfc35f00..342e35f90 100644 --- a/tests/typing/strict_bytes.py +++ b/tests/typing/strict_bytes.py @@ -1,17 +1,28 @@ from __future__ import annotations +from hyperframe.frame import Frame +from typing_extensions import assert_type + from h2.connection import H2Connection +from h2.events import Event from h2.frame_buffer import FrameBuffer -def receive_data_accepts_buffer_types( - connection: H2Connection, - frame_buffer: FrameBuffer, -) -> None: +def receive_data_accepts_buffer_types() -> None: + connection = H2Connection() + frame_buffer = FrameBuffer() bytearray_data = bytearray(b"") memoryview_data = memoryview(b"") - connection.receive_data(bytearray_data) - connection.receive_data(memoryview_data) + bytearray_events = connection.receive_data(bytearray_data) + memoryview_events = connection.receive_data(memoryview_data) frame_buffer.add_data(bytearray_data) frame_buffer.add_data(memoryview_data) + buffered_frames = list(frame_buffer) + + assert_type(bytearray_events, list[Event]) + assert_type(memoryview_events, list[Event]) + assert_type(buffered_frames, list[Frame]) + assert bytearray_events == [] + assert memoryview_events == [] + assert buffered_frames == [] From 1cd9ce0c1c9862df948318b12c3b75d72537abd4 Mon Sep 17 00:00:00 2001 From: Miro Date: Sat, 23 May 2026 19:41:27 +0200 Subject: [PATCH 48/60] Reject client enable-push settings (#1318) Co-authored-by: Miro <200482516+Mirochill@users.noreply.github.com> --- CHANGELOG.rst | 1 + src/h2/connection.py | 2 ++ src/h2/settings.py | 33 +++++++++++++++++++++++++-- tests/test_invalid_frame_sequences.py | 18 +++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 775c385e3..16a6688e1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,7 @@ dev **Bugfixes** - Fix to allow sending 0 bytes on a stream even if the flow control window is negative. +- Reject non-zero ``SETTINGS_ENABLE_PUSH`` values received from servers. 4.3.0 (2025-08-23) ------------------ diff --git a/src/h2/connection.py b/src/h2/connection.py index 0efadec20..e5059a21c 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -1777,6 +1777,8 @@ def _receive_settings_frame(self, frame: SettingsFrame) -> tuple[list[Frame], li return [], events # Add the new settings. + for setting, value in frame.settings.items(): + self.remote_settings.validate_received_setting(setting, value) self.remote_settings.update(frame.settings) events.append( RemoteSettingsChanged.from_settings( diff --git a/src/h2/settings.py b/src/h2/settings.py index aad7ce780..cc9951472 100644 --- a/src/h2/settings.py +++ b/src/h2/settings.py @@ -126,6 +126,8 @@ class Settings(MutableMapping[SettingCodes | int, int]): """ def __init__(self, client: bool = True, initial_values: dict[SettingCodes, int] | None = None) -> None: + self._client = client + # Backing object for the settings. This is a dictionary of # (setting: [list of values]), where the first value in the list is the # current value of the setting. Strictly this doesn't use lists but @@ -287,6 +289,23 @@ def __setitem__(self, key: SettingCodes | int, value: int) -> None: items.append(value) + def validate_received_setting(self, setting: SettingCodes | int, value: int) -> None: + """ + Validate a setting received from the peer that owns this Settings + object. + + Servers may advertise ``ENABLE_PUSH`` only as ``0`` in received + SETTINGS frames. + """ + invalid = _validate_setting(setting, value, client=self._client) + + if invalid != ErrorCodes.NO_ERROR: + msg = f"Setting {setting} has invalid value {value}" + raise InvalidSettingsValueError( + msg, + error_code=invalid, + ) + def __delitem__(self, key: SettingCodes | int) -> None: del self._settings[key] @@ -311,13 +330,23 @@ def __ne__(self, other: object) -> bool: __hash__ = MutableMapping.__hash__ -def _validate_setting(setting: SettingCodes | int, value: int) -> ErrorCodes: +def _validate_setting( + setting: SettingCodes | int, + value: int, + *, + client: bool | None = None, +) -> ErrorCodes: """ Confirms that a specific setting has a well-formed value. If the setting is invalid, returns an error code. Otherwise, returns 0 (NO_ERROR). + + If ``client`` is set, the setting originated from a peer with that role. """ if setting == SettingCodes.ENABLE_PUSH: - if value not in (0, 1): + # RFC 9113 section 6.5.2: "A client MUST treat receipt of a + # SETTINGS frame with SETTINGS_ENABLE_PUSH set to 1 as a connection + # error (Section 5.4.1) of type PROTOCOL_ERROR." + if value not in (0, 1) or (client is False and value != 0): return ErrorCodes.PROTOCOL_ERROR elif setting == SettingCodes.INITIAL_WINDOW_SIZE: if not 0 <= value <= 2147483647: # 2^31 - 1 diff --git a/tests/test_invalid_frame_sequences.py b/tests/test_invalid_frame_sequences.py index f78e9f14c..453750a03 100644 --- a/tests/test_invalid_frame_sequences.py +++ b/tests/test_invalid_frame_sequences.py @@ -10,6 +10,7 @@ import h2.errors import h2.events import h2.exceptions +import h2.settings class TestInvalidFrameSequences: @@ -266,6 +267,23 @@ def test_reject_invalid_settings_values(self, frame_factory, settings) -> None: h2.errors.ErrorCodes.PROTOCOL_ERROR ) + def test_reject_server_enable_push_updates(self, frame_factory) -> None: + """ + Clients reject servers that advertise non-zero SETTINGS_ENABLE_PUSH + values in received SETTINGS frames. + """ + c = h2.connection.H2Connection(config=self.client_config) + c.initiate_connection() + + f = frame_factory.build_settings_frame( + settings={h2.settings.SettingCodes.ENABLE_PUSH: 1}, + ) + + with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e: + c.receive_data(f.serialize()) + + assert e.value.error_code == h2.errors.ErrorCodes.PROTOCOL_ERROR + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) def test_invalid_frame_headers_are_protocol_errors(self, frame_factory, request_headers) -> None: """ From aba9ad6c369eb817df081f1424a3e712a5a3e877 Mon Sep 17 00:00:00 2001 From: Harshal Parekh Date: Thu, 23 Jul 2026 13:26:16 -0500 Subject: [PATCH 49/60] fix: reject conflicting content-length headers (#1317) --- CHANGELOG.rst | 2 + src/h2/stream.py | 12 +++- tests/test_invalid_content_lengths.py | 82 ++++++++++++++++++++++++++- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 16a6688e1..f61b6c8a2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,8 @@ dev - Support for Python 3.9 has been removed. - Support for PyPy 3.9 has been removed. - `Stream.end_stream()` now raises `NoSuchStreamError` or `StreamClosedError` exceptions, instead of a generic `KeyError`. +- Duplicate ``content-length`` headers with different values now raise ``ProtocolError``. + Previously, the first ``content-length`` header was accepted and later conflicting values were ignored. - **backfill from v4.3.0** Convert emitted events into Python `dataclass`, which introduces new constructors with required arguments. Instantiating these events without arguments, as previously commonly used API pattern, will no longer work. diff --git a/src/h2/stream.py b/src/h2/stream.py index 9b5bce789..f8dc2bf2e 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -1364,15 +1364,23 @@ def _initialize_content_length(self, headers: Iterable[Header]) -> None: self._expected_content_length = 0 return + content_length = None + for n, v in headers: if n == b"content-length": try: - self._expected_content_length = int(v, 10) + parsed_content_length = int(v, 10) except ValueError as err: msg = f"Invalid content-length header: {v!r}" raise ProtocolError(msg) from err - return + if content_length is None: + content_length = parsed_content_length + elif parsed_content_length != content_length: + msg = f"Conflicting content-length headers: {content_length} and {parsed_content_length}" + raise ProtocolError(msg) + + self._expected_content_length = content_length def _track_content_length(self, length: int, end_stream: bool) -> None: """ diff --git a/tests/test_invalid_content_lengths.py b/tests/test_invalid_content_lengths.py index 39401ea2b..4242c94e4 100644 --- a/tests/test_invalid_content_lengths.py +++ b/tests/test_invalid_content_lengths.py @@ -19,18 +19,24 @@ class TestInvalidContentLengths: peer is not valid. """ - example_request_headers = [ + example_request_headers_without_content_length = [ (":authority", "example.com"), (":path", "/"), (":scheme", "https"), (":method", "POST"), + ] + example_request_headers = [ + *example_request_headers_without_content_length, ("content-length", "15"), ] - example_request_headers_bytes = [ + example_request_headers_bytes_without_content_length = [ (b":authority", b"example.com"), (b":path", b"/"), (b":scheme", b"https"), (b":method", b"POST"), + ] + example_request_headers_bytes = [ + *example_request_headers_bytes_without_content_length, (b"content-length", b"15"), ] example_response_headers = [ @@ -39,6 +45,78 @@ class TestInvalidContentLengths: ] server_config = h2.config.H2Configuration(client_side=False) + @pytest.mark.parametrize( + "request_headers", + [ + example_request_headers_without_content_length, + example_request_headers_bytes_without_content_length, + ], + ) + def test_duplicate_matching_content_lengths(self, frame_factory, request_headers) -> None: + """ + Remote peers sending duplicate matching content-length fields are + accepted. + """ + c = h2.connection.H2Connection(config=self.server_config) + c.initiate_connection() + c.receive_data(frame_factory.preamble()) + c.clear_outbound_data_buffer() + + headers = frame_factory.build_headers_frame( + headers=[ + *request_headers, + ("content-length", "15"), + ("content-length", "15"), + ], + ) + data = frame_factory.build_data_frame( + data=b"\x01"*15, + flags=["END_STREAM"], + ) + + events = c.receive_data(headers.serialize() + data.serialize()) + + assert isinstance(events[0], h2.events.RequestReceived) + assert isinstance(events[1], h2.events.DataReceived) + assert isinstance(events[2], h2.events.StreamEnded) + assert c.data_to_send() == b"" + + @pytest.mark.parametrize( + "request_headers", + [ + example_request_headers_without_content_length, + example_request_headers_bytes_without_content_length, + ], + ) + def test_duplicate_conflicting_content_lengths(self, frame_factory, request_headers) -> None: + """ + Remote peers sending duplicate conflicting content-length fields cause + Protocol Errors. + """ + c = h2.connection.H2Connection(config=self.server_config) + c.initiate_connection() + c.receive_data(frame_factory.preamble()) + c.clear_outbound_data_buffer() + + headers = frame_factory.build_headers_frame( + headers=[ + *request_headers, + ("content-length", "15"), + ("content-length", "16"), + ], + ) + with pytest.raises( + h2.exceptions.ProtocolError, + match="Conflicting content-length headers: 15 and 16", + ): + c.receive_data(headers.serialize()) + + expected_frame = frame_factory.build_goaway_frame( + last_stream_id=1, + error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR, + ) + assert c.data_to_send() == expected_frame.serialize() + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) def test_too_much_data(self, frame_factory, request_headers) -> None: """ From efc8fea65c7359795fb469fd53390cbaf35eff0b Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Thu, 23 Jul 2026 20:21:09 +0200 Subject: [PATCH 50/60] cleanup double-dependency --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fbdffff92..8b55a5c75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,6 @@ linting = [ packaging = [ "check-manifest==0.51", - "readme-renderer==44.0", "build>=1.4.2,<2", "twine>=6.2.0,<7", "wheel>=0.46.3,<1", From c40145f69c5473850849fe96301ac0416b1afea6 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Thu, 23 Jul 2026 20:50:44 +0200 Subject: [PATCH 51/60] parse `content-length` headers according to RFC9110 grammar for numbers (1*DIGIT) https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length --- CHANGELOG.rst | 1 + src/h2/stream.py | 5 ++++ tests/test_invalid_content_lengths.py | 34 +++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f61b6c8a2..73c024c10 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,7 @@ dev - `Stream.end_stream()` now raises `NoSuchStreamError` or `StreamClosedError` exceptions, instead of a generic `KeyError`. - Duplicate ``content-length`` headers with different values now raise ``ProtocolError``. Previously, the first ``content-length`` header was accepted and later conflicting values were ignored. +- Parse ``content-length`` headers according to RFC9110 grammar for numbers (1*DIGIT) - **backfill from v4.3.0** Convert emitted events into Python `dataclass`, which introduces new constructors with required arguments. Instantiating these events without arguments, as previously commonly used API pattern, will no longer work. diff --git a/src/h2/stream.py b/src/h2/stream.py index f8dc2bf2e..249f73e0c 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -1368,6 +1368,11 @@ def _initialize_content_length(self, headers: Iterable[Header]) -> None: for n, v in headers: if n == b"content-length": + if not v.isdigit(): + # https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length + # RFC grammar for content-length is 1*DIGIT, so any non-digit value is invalid. + msg = f"Invalid content-length header: {v!r}" + raise ProtocolError(msg) try: parsed_content_length = int(v, 10) except ValueError as err: diff --git a/tests/test_invalid_content_lengths.py b/tests/test_invalid_content_lengths.py index 4242c94e4..3927fb5e2 100644 --- a/tests/test_invalid_content_lengths.py +++ b/tests/test_invalid_content_lengths.py @@ -81,6 +81,40 @@ def test_duplicate_matching_content_lengths(self, frame_factory, request_headers assert isinstance(events[2], h2.events.StreamEnded) assert c.data_to_send() == b"" + @pytest.mark.parametrize( + "value", [ + "15a", "15.0", "-15", "+15", " 15", "15 ", "1e2", "0xF", "15_000", "15,000", "NaN", "\x00", "0b1111", "0o17", + ("1" * 5000), # https://docs.python.org/3/library/stdtypes.html#int-max-str-digits + ], + ) + def test_content_length_with_non_digit_value(self, frame_factory, value) -> None: + """ + Remote peers sending content-length with non-digit value causes Protocol + Errors. + """ + c = h2.connection.H2Connection(config=self.server_config) + c.initiate_connection() + c.receive_data(frame_factory.preamble()) + c.clear_outbound_data_buffer() + + headers = frame_factory.build_headers_frame( + headers=[ + *self.example_request_headers_bytes_without_content_length, + ("content-length", value), + ], + ) + with pytest.raises( + h2.exceptions.ProtocolError, + match=f"Invalid content-length header", + ): + c.receive_data(headers.serialize()) + + expected_frame = frame_factory.build_goaway_frame( + last_stream_id=1, + error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR, + ) + assert c.data_to_send() == expected_frame.serialize() + @pytest.mark.parametrize( "request_headers", [ From b45207cedf9fabe2c77bb3c1c10f403a610599a0 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Thu, 23 Jul 2026 20:50:52 +0200 Subject: [PATCH 52/60] dependencies and packaging++ --- .readthedocs.yaml | 2 +- MANIFEST.in | 1 + pyproject.toml | 4 +- uv.lock | 1441 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1445 insertions(+), 3 deletions(-) create mode 100644 uv.lock diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1092bf085..322b844b3 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,7 +6,7 @@ version: 2 build: os: ubuntu-24.04 tools: - python: "3.13" + python: "3.14" jobs: install: - pip install -U pip diff --git a/MANIFEST.in b/MANIFEST.in index ff2f15801..0e73eee4f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -12,3 +12,4 @@ include README.rst LICENSE CHANGELOG.rst pyproject.toml global-exclude *.pyc *.pyo *.swo *.swp *.map *.yml *.DS_Store exclude .readthedocs.yaml +exclude uv.lock diff --git a/pyproject.toml b/pyproject.toml index 8b55a5c75..3e9c107cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ maintainers = [ requires-python = ">=3.10" dependencies = [ "hyperframe>=6.1,<7", - "hpack>=4.1,<5", + "hpack>=4.2,<5", ] dynamic = ["version"] @@ -77,7 +77,7 @@ packaging = [ ] docs = [ - "sphinx>=9.1.0,<10", + 'sphinx>=9.1.0,<10 ; python_version >= "3.12"', ] [tool.setuptools.packages.find] diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..672688a82 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1441 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version < '3.12'", +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "build" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "check-manifest" +version = "0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "build" }, + { name = "setuptools" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/e3/8ce797dfdf12d447683490107c4dcd97bb2535d7a2b031bf3f3e4c441c3d/check_manifest-0.51.tar.gz", hash = "sha256:9801c7637675755a563f33e3c48ee59a59b37a7677297c05c910c16c5b9b6d67", size = 36302, upload-time = "2025-10-15T11:15:48.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c1/df01ef6ba1c8a2bfc201be45d0889b06f165008b240e8d1657aa665421a8/check_manifest-0.51-py3-none-any.whl", hash = "sha256:f5f35ed561012fc2115bb070e42a748ac2e034cf8904ab4dfaae893859085ca4", size = 20500, upload-time = "2025-10-15T11:15:46.058Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/060ce69008ac97bbc01b1411b3e55b61f6f015659400b46749b662107831/coverage-7.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b5bd92ff1ec22e535eab0de75fa6db021992791f461a2aceb7822c625a1187d", size = 221284, upload-time = "2026-07-15T18:53:29.52Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a3/d936e8b53edd9684100a6aefaf3fcabaa54728fe33324436c8d279c047aa/coverage-7.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44826758cfe73fcd0e6af5deb4ba6d5417cc1d13df3acb35c93484a11160f846", size = 221799, upload-time = "2026-07-15T18:53:31.708Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a3/ca234b06aec7ee28226f11d39a696b4481fe5eddfce8e03bf39979bb8ffb/coverage-7.15.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09f5c6ec5901f667bd97dd140b5b9a2586b10efec66f46fb1e6d8135f8b95bdf", size = 248544, upload-time = "2026-07-15T18:53:33.212Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/dda79527bb7573ba91828b2fb91b3105d87378d6a2749ca0c0924ce0addd/coverage-7.15.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d16e3a7104ea84f03e614611b3edbf6fb6892554b3ab0fe7fbb3f2b2ef04376", size = 250374, upload-time = "2026-07-15T18:53:34.683Z" }, + { url = "https://files.pythonhosted.org/packages/67/c6/c33755a34572f81f49a8c0cdf6b622f35ccb3238b136e1909daf0cdd4319/coverage-7.15.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d46e62cb35d91e6e2589fda6d28074426b0e276422b5d2ebef2c6b11dc60dbfd", size = 252239, upload-time = "2026-07-15T18:53:36.205Z" }, + { url = "https://files.pythonhosted.org/packages/b9/6f/dc341741b375be53a5baeee5b4bf0f0e525d38caed428f7932d23bb7bcb1/coverage-7.15.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dfd3db045e95960ae3683059571e597fda7cc610106a8916f77c5839048c1deb", size = 254150, upload-time = "2026-07-15T18:53:37.863Z" }, + { url = "https://files.pythonhosted.org/packages/e9/8d/966a18a5b195cb4e77b14c53f5f3dce22b5da05e6de7fafd1e08f2d2067a/coverage-7.15.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:affd532502d34c0472d0cdb181325c89f1d2c44992fef0c17e88e7b1576259a1", size = 249234, upload-time = "2026-07-15T18:53:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/8b2e367496ab48484d48e79984fec76cdc1b7cb5d3a00ee799a5602e3ec9/coverage-7.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d17d7512151fedfcc64c1821a8977fc9be0dbf495754669afcab7b57abc98ae9", size = 250276, upload-time = "2026-07-15T18:53:41.027Z" }, + { url = "https://files.pythonhosted.org/packages/63/92/1199318a200eb6c8c6ce0192c892c8710ac791abbe0f35099294620bbfda/coverage-7.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e26ff680768b8095e8874aabe0e9d3a47a2a9f176a8340d05f8604c56457c23a", size = 248283, upload-time = "2026-07-15T18:53:42.557Z" }, + { url = "https://files.pythonhosted.org/packages/56/da/be284a55c5619bda891a89c27dfd59324a2c6a14d755cf6aac6960ceebeb/coverage-7.15.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7e8f27131dc7cd53de2c137dd207b3720919320b3c20d499dc30aa9ee6173287", size = 252093, upload-time = "2026-07-15T18:53:44.271Z" }, + { url = "https://files.pythonhosted.org/packages/d4/53/ee112da833ddd77b73c6d781a98029b45b584b136615b4900ed0569f887e/coverage-7.15.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:728a33676d4c3f0db977990a4bd421dcaa3be3e53b5b6273036fff6666008e89", size = 248552, upload-time = "2026-07-15T18:53:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/82/6a/802cfc802e9113494c80bf3f284cd4d72faeb1f24e244f61046af364f2ca/coverage-7.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29c052f7c83ccfcc5c577eaae025d2e4a9bb80daf03c0ac31c996e83b000ce88", size = 249154, upload-time = "2026-07-15T18:53:47.256Z" }, + { url = "https://files.pythonhosted.org/packages/2c/65/529808e91d651147edae408fd9e894abc3b8cad7f3e594bbc36719a3e13a/coverage-7.15.2-cp310-cp310-win32.whl", hash = "sha256:1268ac8fb9ddcd783d3948dbabaf80a5d53bfdaa0575e873e2139a692f797443", size = 223334, upload-time = "2026-07-15T18:53:48.768Z" }, + { url = "https://files.pythonhosted.org/packages/68/0f/0e1829d7001130876dfbc0b4e1c737ea7c155b809e3e4a98a0aa268e2369/coverage-7.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:9f4432898c4bf2fba0435bbe35dd4437d7264565e5a88a21f5b49d8662a6b629", size = 223959, upload-time = "2026-07-15T18:53:50.429Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3a/54536704f507d4573bf9161c4d0dd3dd59b6d85e48c664e901b6844d8e33/coverage-7.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036", size = 221414, upload-time = "2026-07-15T18:53:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d9/8ba925d29743e3577b21e4d8c11a702b76bc93c41e7fdfd1177af63d4b8d/coverage-7.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660", size = 221913, upload-time = "2026-07-15T18:53:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/a855f3aa0187f2b431ade4e4791b77b56282cfb5d201c83ec26a31b5b36a/coverage-7.15.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589", size = 252332, upload-time = "2026-07-15T18:53:55.467Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d3/13ac97b4370640ba3452fc8559b06cc2f479ce3ba4a0b632a73e44c38a7d/coverage-7.15.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee", size = 254243, upload-time = "2026-07-15T18:53:57.055Z" }, + { url = "https://files.pythonhosted.org/packages/88/83/5eca144942d8d0659d3f55176517f4a59cdc65eefd17146a0770935a3ebd/coverage-7.15.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0", size = 256352, upload-time = "2026-07-15T18:53:58.83Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ba/d3db2e01a50fc88cdb4c0f19542bcf6f61489e34dc9aa3538413e2459a38/coverage-7.15.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487", size = 258313, upload-time = "2026-07-15T18:54:00.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/b3/aba83416e9177df28e5186d856c19158c59fc0e7e814aaa61a4a2354ad1b/coverage-7.15.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f", size = 252449, upload-time = "2026-07-15T18:54:02.456Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a5/4b00ecac0194431ab451b0f6710f8e2517d04cef60f821b14dec4637d575/coverage-7.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1", size = 254043, upload-time = "2026-07-15T18:54:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/75/b6/cfa209b4313ee7f1b34da47efcd789ea51c024ad35af390e00f5a3c10a2e/coverage-7.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5", size = 252107, upload-time = "2026-07-15T18:54:06.745Z" }, + { url = "https://files.pythonhosted.org/packages/36/67/e8cac5a6954038c98d7fe7eb9802afe7ab3ecb637bb7cc00e69b4148b56d/coverage-7.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0", size = 255873, upload-time = "2026-07-15T18:54:08.48Z" }, + { url = "https://files.pythonhosted.org/packages/2c/92/395cca9f330a86c3fe3471d73e2c102116c4c58fdc619dbbc125c6e93a54/coverage-7.15.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad", size = 251826, upload-time = "2026-07-15T18:54:10.083Z" }, + { url = "https://files.pythonhosted.org/packages/51/60/3e91b20295439652424f426b7086ec5bf4fbe3f604c73eda22b986c4fd6b/coverage-7.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db", size = 252735, upload-time = "2026-07-15T18:54:11.878Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/8c07839005e5e3c6b3877d3a6e2a80ce766589f31dd2b6882b78d59a7b8c/coverage-7.15.2-cp311-cp311-win32.whl", hash = "sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9", size = 223500, upload-time = "2026-07-15T18:54:13.525Z" }, + { url = "https://files.pythonhosted.org/packages/2e/98/59d83c257cd59f0fbaf9d9ddb26b744a576760dfd1ae16e516408894a02b/coverage-7.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688", size = 223973, upload-time = "2026-07-15T18:54:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/ea/09/2d285c8bef5c4f695d120c1c96dc11715638aa8e134069f210bb6a62a9fe/coverage-7.15.2-cp311-cp311-win_arm64.whl", hash = "sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934", size = 223519, upload-time = "2026-07-15T18:54:16.803Z" }, + { url = "https://files.pythonhosted.org/packages/6a/50/eb5bf42e531611a9f8d272556b1ed4de503f84a91413584094487cf69f8f/coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9", size = 221587, upload-time = "2026-07-15T18:54:18.439Z" }, + { url = "https://files.pythonhosted.org/packages/06/d1/da99af464c335d4e023a6efcd7ec30f63b88a43c93745154ab74ffb31cea/coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73", size = 221943, upload-time = "2026-07-15T18:54:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8a/13c42723d61ca447eafa18732e8141dd6a63f2732e1c7e1502c182dd88d7/coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d", size = 253450, upload-time = "2026-07-15T18:54:21.765Z" }, + { url = "https://files.pythonhosted.org/packages/d7/29/99021303f98fbdcb63504b4d07bea4cc025b9b2dd907c4f07c85d50a0dab/coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b", size = 256187, upload-time = "2026-07-15T18:54:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a8/fd503715ed6ca9c5d742923aa5209257340b367a867b2ced0c7d4ba8a0b9/coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296", size = 257301, upload-time = "2026-07-15T18:54:25.183Z" }, + { url = "https://files.pythonhosted.org/packages/da/40/3f4b8fb409810036ebc2857d36adc0498c6e957b5df0290c5036b2e143f1/coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6", size = 259562, upload-time = "2026-07-15T18:54:27.204Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8a/9bdffbef47db77cce3d6b02a28f7e919b19f0106c4b080c2c2246040f885/coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098", size = 253841, upload-time = "2026-07-15T18:54:29.134Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1e/9031efde019d31a06646261fce6dfc5c3c74e951e27a71e5c9a424563178/coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a", size = 255221, upload-time = "2026-07-15T18:54:31.142Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/787acde872389fc84a9ef9d8cd1ccc658e391ab4cb5b28092a714426a394/coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b", size = 253366, upload-time = "2026-07-15T18:54:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9b/6f57bc4b93c842eef1695f8cdaf2318e35e7ba54f5ba80d84be213ab7858/coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2", size = 257434, upload-time = "2026-07-15T18:54:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/b3186a21b2acc83e451118978905c81c7072c3333707804db09a78c096a2/coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440", size = 252935, upload-time = "2026-07-15T18:54:36.548Z" }, + { url = "https://files.pythonhosted.org/packages/20/c2/c9f3376b2e717ea69ed7a6e9a5fcab968fb0b290db6cf4bd9a1fc7541b75/coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e", size = 254807, upload-time = "2026-07-15T18:54:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e1/dfc15401f4a8aaeb486e1ba3e9e3c40522a6e38bd0ecf0b3f29cb8082957/coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd", size = 223641, upload-time = "2026-07-15T18:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/91/40/81b6d809d320cd366ec5bdf8176575e897dcb8efe7fb4b489ef9e93e4d13/coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40", size = 224172, upload-time = "2026-07-15T18:54:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/ef/28/9f14ec438149f7de557f45518f09b4a7917b795cc37083aa7db482693f8c/coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3", size = 223556, upload-time = "2026-07-15T18:54:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606, upload-time = "2026-07-15T18:54:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982, upload-time = "2026-07-15T18:54:47.341Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972, upload-time = "2026-07-15T18:54:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569, upload-time = "2026-07-15T18:54:51.098Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806, upload-time = "2026-07-15T18:54:53.145Z" }, + { url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936, upload-time = "2026-07-15T18:54:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178, upload-time = "2026-07-15T18:54:57.527Z" }, + { url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934, upload-time = "2026-07-15T18:54:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898, upload-time = "2026-07-15T18:55:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056, upload-time = "2026-07-15T18:55:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718, upload-time = "2026-07-15T18:55:05.029Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490, upload-time = "2026-07-15T18:55:06.889Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647, upload-time = "2026-07-15T18:55:08.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190, upload-time = "2026-07-15T18:55:10.789Z" }, + { url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583, upload-time = "2026-07-15T18:55:12.678Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" }, + { url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" }, + { url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, + { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +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 = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "h2" +source = { editable = "." } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] + +[package.dev-dependencies] +dev = [ + { name = "build" }, + { name = "check-manifest" }, + { name = "hypothesis" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "sphinx", marker = "python_full_version >= '3.12'" }, + { name = "twine" }, + { name = "typing-extensions" }, + { name = "wheel" }, +] +docs = [ + { name = "sphinx", marker = "python_full_version >= '3.12'" }, +] +linting = [ + { name = "mypy" }, + { name = "ruff" }, + { name = "typing-extensions" }, +] +packaging = [ + { name = "build" }, + { name = "check-manifest" }, + { name = "twine" }, + { name = "wheel" }, +] +testing = [ + { name = "hypothesis" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, +] + +[package.metadata] +requires-dist = [ + { name = "hpack", specifier = ">=4.2,<5" }, + { name = "hyperframe", specifier = ">=6.1,<7" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "build", specifier = ">=1.4.2,<2" }, + { name = "check-manifest", specifier = "==0.51" }, + { name = "hypothesis", specifier = ">=6.119.4,<7" }, + { name = "mypy", specifier = ">=1.16.0,<2" }, + { name = "pytest", specifier = ">=8.3.3,<9" }, + { name = "pytest-cov", specifier = ">=6.0.0,<7" }, + { name = "pytest-xdist", specifier = ">=3.6.1,<4" }, + { name = "ruff", specifier = ">=0.8.0,<1" }, + { name = "sphinx", marker = "python_full_version >= '3.12'", specifier = ">=9.1.0,<10" }, + { name = "twine", specifier = ">=6.2.0,<7" }, + { name = "typing-extensions", specifier = ">=4.12.2" }, + { name = "wheel", specifier = ">=0.46.3,<1" }, +] +docs = [{ name = "sphinx", marker = "python_full_version >= '3.12'", specifier = ">=9.1.0,<10" }] +linting = [ + { name = "mypy", specifier = ">=1.16.0,<2" }, + { name = "ruff", specifier = ">=0.8.0,<1" }, + { name = "typing-extensions", specifier = ">=4.12.2" }, +] +packaging = [ + { name = "build", specifier = ">=1.4.2,<2" }, + { name = "check-manifest", specifier = "==0.51" }, + { name = "twine", specifier = ">=6.2.0,<7" }, + { name = "wheel", specifier = ">=0.46.3,<1" }, +] +testing = [ + { name = "hypothesis", specifier = ">=6.119.4,<7" }, + { name = "pytest", specifier = ">=8.3.3,<9" }, + { name = "pytest-cov", specifier = ">=6.0.0,<7" }, + { name = "pytest-xdist", specifier = ">=3.6.1,<4" }, +] + +[[package]] +name = "hpack" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "hypothesis" +version = "6.161.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/96/35022710908b82d20af0c57ab8be4d4a3e4045a74d3ae9c806eb4443297c/hypothesis-6.161.0.tar.gz", hash = "sha256:c357150f826fc7492304621d535a23e8f1b7440a3b10a337c23bea52102e2e7f", size = 485855, upload-time = "2026-07-23T07:17:40.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/b9/c8b80fb7517e6f3039d0ef9a5df6aaee53667935e62c6c9d9d635436708d/hypothesis-6.161.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c9f877e288dfb46207b5c3bfcc8ab28e2613e529be8621816423960403377286", size = 766230, upload-time = "2026-07-23T07:16:54.313Z" }, + { url = "https://files.pythonhosted.org/packages/90/16/e5c1287fee682f7c1e9afccc91c07ae36a6855a5863d0b3c15d7bfa0b322/hypothesis-6.161.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b7b6980265cb04605b2b42132dc8ef5735917fc482869298611a49d2e06dc322", size = 761883, upload-time = "2026-07-23T07:17:05.884Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b8/d9792e24e53f82bb1455935f79cf3b56ccf556fac325c8a90f7968180706/hypothesis-6.161.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f10253cac459922cad3bc09e397718188e57dffefe810e5db1d444e7113d7fb5", size = 1091083, upload-time = "2026-07-23T07:17:10.987Z" }, + { url = "https://files.pythonhosted.org/packages/f7/15/33cba9c6bee8a80ab18f48e40669038275bf4b82e8dfb0fa9fe716925265/hypothesis-6.161.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:232ab78dd8cb0a891914d20697e0fd340ca1e6d4d8d5855df5e433d8161173e2", size = 1140530, upload-time = "2026-07-23T07:16:14.195Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f6/30c421822cd65b8edd56b2b90a5e1acf4a624d5619067957668027ab7e46/hypothesis-6.161.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:264336ca1e9f31edd24a8885c4020db8e18986c51a255613db28c076ac4289a8", size = 1132680, upload-time = "2026-07-23T07:16:49.73Z" }, + { url = "https://files.pythonhosted.org/packages/e9/db/d18e45339b2ffda57a52395e03df6166bc4e428bc90e878dd3f20a7423c0/hypothesis-6.161.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:323aac4347e6ffa86929407b7f386cbf54e1c66faf923ffd6b4b86c21815d117", size = 1264892, upload-time = "2026-07-23T07:16:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/f4b7600272fbc9a2b28c95b96059d89cf5093ae705e52360a818df8154a5/hypothesis-6.161.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5dc83c8e83b9d133babcf9468703cece0ddb25413983561f2516927edddcc52d", size = 1307563, upload-time = "2026-07-23T07:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/d6/28/fa4f2d50c7434076ec7653a8372750531576e4d11cd5f3316ad83e12a553/hypothesis-6.161.0-cp310-abi3-win32.whl", hash = "sha256:75a3036121e6ae2cf55b7433f1953834cc9eca97c2e4e4be3369fe080c86b237", size = 652098, upload-time = "2026-07-23T07:16:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/93/5c/6811eee772a5cc33f9bf863326983f493977e9aee9535c8dbb6c172575d3/hypothesis-6.161.0-cp310-abi3-win_amd64.whl", hash = "sha256:e3f5b2527789a748b54d6ef46b2b042f3225164d54c24ba74a137ddb10a39407", size = 658272, upload-time = "2026-07-23T07:16:51.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/97/49216c1087962033451cc6e3093deb765b0d14eed7dda2980f6d8dfc9062/hypothesis-6.161.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:38eea270b81398c9e2c7eed028f53ba51dc7005eb97fa681c7c90007ba029423", size = 766930, upload-time = "2026-07-23T07:17:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6d/766a280bea353045ae7311ba847a50ebb939155e2a990fd08040be2b6b5b/hypothesis-6.161.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6b7183980c7729d7cf084ab26c127e4c591876536278ecb84ed2449d4d93f4e", size = 762699, upload-time = "2026-07-23T07:17:38.977Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/f28648668b5ce18bba7ae846c629c54427aa622a76280309c3bc3dca4f2d/hypothesis-6.161.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e60bbe528d6005373146808bd0b5e618bf1a20e912c0568ae56802e8c455fc", size = 1091551, upload-time = "2026-07-23T07:16:28.499Z" }, + { url = "https://files.pythonhosted.org/packages/52/e3/3ae24ad1056e1c992dade22e9c784c93d57ea0dc3ff9fa6a48683548489d/hypothesis-6.161.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3167e3153a9b8a8ad43284a22cbe1e3c1819d6d944f727f9810bb115a9c2ade", size = 1141106, upload-time = "2026-07-23T07:17:19.854Z" }, + { url = "https://files.pythonhosted.org/packages/c4/37/fa3a21edcd7c4a104d6782ee98135af8ef86ae42d39ea9eb55072f84b668/hypothesis-6.161.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1489ef3b86688fc0051d0c86db238ff8f3732bd353dc4b6b28a81c3897bd756e", size = 1265529, upload-time = "2026-07-23T07:16:44.815Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2e/f377a5ea8aba231213da1b26f333a6af29c43fcdac7dda790304dd9c3ffc/hypothesis-6.161.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28a450d338067845870b03a8c61ba6d83cccf5d1ed360a6b1cfebb4f42818791", size = 1307881, upload-time = "2026-07-23T07:16:18.438Z" }, + { url = "https://files.pythonhosted.org/packages/8f/73/276defee614d45462a1512888283d4a9bdf852e3f9d74f564bd8d6cecd09/hypothesis-6.161.0-cp310-cp310-win_amd64.whl", hash = "sha256:170fc6fe2157c8e813818a08709d78c79ffa9171b015389c50f5538eab3de1bb", size = 658159, upload-time = "2026-07-23T07:16:38.473Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1f/07054e18c7696fe5aa127952e1ff2b74c7917100e0998d77405f9aea7bbf/hypothesis-6.161.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2658a95ac7cf1943b9397725d58481373a1709e79b6867628108f695b202ff3f", size = 766737, upload-time = "2026-07-23T07:16:27.055Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/6a04fbda729f5889b486aa3b20912ee6c7391c8db0a2926346a1b3ad0834/hypothesis-6.161.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:584906b4f8f9504d7c9d6fd3c42bf991fa42ab64c3a4490a84d6e4acf69fe7fe", size = 762514, upload-time = "2026-07-23T07:17:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/12/609a956b716ab20cb81263d8e0cecfe442fb46e4d54ae9b82b453f2465fe/hypothesis-6.161.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87a996d0a1c865173ed67a0b3537efbe6736bcf9f58ac28826712a48ed8d2d23", size = 1091414, upload-time = "2026-07-23T07:16:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/fc/99/093bc8aca6dddc05e88dd0baa64c5586e031737d798c66e770fbeb034510/hypothesis-6.161.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0250e209ed2401cf6c80c956c5c1906097b537d05fa748f03e9c53060a837d3f", size = 1140888, upload-time = "2026-07-23T07:17:07.579Z" }, + { url = "https://files.pythonhosted.org/packages/65/fc/f681828dc1ca13243622eb6ddc3f7370efb1cb7931ea7505f8ade4d37ba6/hypothesis-6.161.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:210024a6e84c361803545ca055218bcae06e17fcb53d5956db7db6a1e14d7769", size = 1265245, upload-time = "2026-07-23T07:16:56.103Z" }, + { url = "https://files.pythonhosted.org/packages/53/d4/98deace31c31369196ece4d6f32bb8c1820bf5cf5584721bda791bffa0cc/hypothesis-6.161.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c3e39be8141496a73115f1ea2c8fd7146f78d26f148e884f3d4a68dec06418a0", size = 1307841, upload-time = "2026-07-23T07:16:59.457Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5d/d9bbe1fc769e46b21d368497d4739375fcb17270eba611bac1117473e337/hypothesis-6.161.0-cp311-cp311-win_amd64.whl", hash = "sha256:e234937af9de105e28dc7ffdac5d7932265abb5881f0264aff01d3515baee732", size = 657953, upload-time = "2026-07-23T07:17:18.224Z" }, + { url = "https://files.pythonhosted.org/packages/01/f5/b01692ea422f9995260435a5c2a425dd558ceb0b6544cf4037d312b52927/hypothesis-6.161.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8cf6149e5bcb1deaa3d029280c2c9a47fede2186ea58d8dc3e71864428b748b5", size = 767859, upload-time = "2026-07-23T07:16:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ea/147f96f352a1c62f4fa4d46ec2d8b103d39cafce236826531aba9dfbf6fc/hypothesis-6.161.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:439b9ccefc2b87b9752752d2ec6c9d40f92de2acaf07f4da94c2ebcbde4eb660", size = 759491, upload-time = "2026-07-23T07:17:37.046Z" }, + { url = "https://files.pythonhosted.org/packages/44/05/c1ddd72ca9af054332a05bdb19b666b1dbb4ff904cac2b6e04bc483518fa/hypothesis-6.161.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35c577d4b635b914e2cdd59d144448e0de82e47f8422d8373cbb48daa5571686", size = 1089838, upload-time = "2026-07-23T07:16:52.774Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/0d9adc2ca7fe226e4f81e0e9ff88ab9a2025395a705e55e8447e8833b2e2/hypothesis-6.161.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:894af9777f2fd51bca9625fd07573456c1fa67b6bed3f6aa1659cb60255594e0", size = 1139915, upload-time = "2026-07-23T07:16:11.769Z" }, + { url = "https://files.pythonhosted.org/packages/50/48/c557ee9899ab58e6d373712dcfe019b4eb65035f5bbffcb7624201984bd4/hypothesis-6.161.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c9f15d18d3041216d80d2a3203c1b8881efb7c20296f068651e2ad34f3852392", size = 1262692, upload-time = "2026-07-23T07:17:09.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/37/8eede820af48d8f7a73c0741c659b4839ffe9825b020affe43d52f58acb5/hypothesis-6.161.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f5588bde17a517c943b5ef6172cacfce6ef2d542c9249cc1bd86ce3c4c07161b", size = 1306904, upload-time = "2026-07-23T07:16:32.698Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6b/16282f58b92b6698dbed7b23d7015fb9f7d5dfb78e7ba2e4b44c88116bca/hypothesis-6.161.0-cp312-cp312-win_amd64.whl", hash = "sha256:c7994d32bcca19b7cbf3c087172245fe9f4d55b21bccef81693efd5a0637d4d9", size = 655392, upload-time = "2026-07-23T07:16:16.988Z" }, + { url = "https://files.pythonhosted.org/packages/27/13/50b3fabaa9a52f82905d6bf70b0027cbc61972f0b60dcf68506e4a85674f/hypothesis-6.161.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5217e3df508ab71303bf1288b412548e96483eef195f6008b6472770e4fe4ed", size = 767734, upload-time = "2026-07-23T07:16:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/72/0c/0176f7722896dffef2aa677699df75cd2a53ed00d4dc6b2959c40c1c8389/hypothesis-6.161.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad4899908abec1888d0d16eb70acd54c9d8198b58410ec26a346ba35d384fc0d", size = 759394, upload-time = "2026-07-23T07:16:57.551Z" }, + { url = "https://files.pythonhosted.org/packages/97/1d/5ade6e0c80ce8160bbcd55c300d95a5450cdb82fa04fdcdd8a33f6198441/hypothesis-6.161.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fee553e5150af6d66ee058f3ccbc3b5b83e6df62139e8935abd0510254a2d4a7", size = 1089752, upload-time = "2026-07-23T07:16:34.18Z" }, + { url = "https://files.pythonhosted.org/packages/99/02/14c6d54e60159ba9991a52b14ea5a9b6935d4878ffe9d0a8fabd2d166767/hypothesis-6.161.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f8110cd815f3a79e2351700654c21771eec47d98a78db56cc81879d41f08ed1", size = 1139731, upload-time = "2026-07-23T07:17:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/36/98/c099c382b0fbf6dfd209d35961e6ee9739ad1d787288a40eb364b278217a/hypothesis-6.161.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16aacb26a277d25da7466f0588c2687811334455753d513c55d8ca4dbbc5174e", size = 1262736, upload-time = "2026-07-23T07:16:07.875Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/6b1fbde6e2a1c9bd54acb1e5d8fa866c6fef59a829a67ca310f5d12a8fbe/hypothesis-6.161.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eb24cf5f7f301ad3db14caa4462ebc2e693fe38815d793ff6dbc116820b18dff", size = 1306628, upload-time = "2026-07-23T07:16:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c2/033da1694f956f0c566b12a1f0667138ad06a3b0a67837f17c6873cc2513/hypothesis-6.161.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4f715f5598444a0d569aa8a3e74ebc14a46c67a873193db38c8542b2838e91f", size = 655355, upload-time = "2026-07-23T07:16:48.054Z" }, + { url = "https://files.pythonhosted.org/packages/2b/26/29582b8ba467eedf270515422f41cb564d06b4eef38bdf06e236cc841546/hypothesis-6.161.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:a046954e17a1edd20b6c95e9d29f1df7bc20ccab94c01aa8b4177552896230fb", size = 767928, upload-time = "2026-07-23T07:17:12.935Z" }, + { url = "https://files.pythonhosted.org/packages/f7/25/bb7cfd851f6b0f4b0130485785a3447621523116b89f8d82042fb9897752/hypothesis-6.161.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2d503b0fdb916371d536b33fad0c4f909846af2fc4273d3049ca6fe661aa81ff", size = 759542, upload-time = "2026-07-23T07:16:41.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ab/bc31d4aa5840c2438e011a615095b0e2fae5de94c3abfc18a01686825ea7/hypothesis-6.161.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df011a94870dc3e1b5fb4fb8d2c68dd641b412a3b73050288d85af1467c9a689", size = 1090304, upload-time = "2026-07-23T07:16:19.817Z" }, + { url = "https://files.pythonhosted.org/packages/51/ee/6304f6184aee6a1b91fff746a767b4f3aab58c29e48e64cc16d56fc6dedc/hypothesis-6.161.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4678e3988503b3bd5be0ed995f84cc15ac4f99c168bade32456a08c04868f3f3", size = 1139915, upload-time = "2026-07-23T07:17:14.565Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6d/23efce26bf7f1773346732c58a23cbe33ed4f171da1bb3aa11bf349fdb4c/hypothesis-6.161.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74c6e5b5623f37eb6af8be6f861d138fac3ee3528ee30c3b48ff11c39f7be4b7", size = 1263070, upload-time = "2026-07-23T07:16:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a2/e0b4bf410630f16661eea6fdf9c3970e47c749a837de12098d63c81bb01b/hypothesis-6.161.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b841c25267ab360812a523d1861e0b0ed0f5cc4e6d7bcecc9d9eddd3f835aa0f", size = 1306929, upload-time = "2026-07-23T07:16:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/03/13/58047eb148a31ae7cce26ec0b2f0e980c46874c6673c1110c53684ba181a/hypothesis-6.161.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:a0f3830c1e816e34bd8cd940244c8c877dedc2ecfea771d2ecea252bc35eb21d", size = 599455, upload-time = "2026-07-23T07:16:25.552Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0e/a206013edd7dfd44b9a81ae1946a1ea30d878974850d60a61fa128cc170d/hypothesis-6.161.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1d38f05acb9c25181157f1756f5faaa1759b4641ff6b32cb1d2ab1d55d6af2d", size = 655306, upload-time = "2026-07-23T07:17:30.273Z" }, + { url = "https://files.pythonhosted.org/packages/e2/74/32d224a0ccf4ca9af6acc1d805047e12cd13105e0769d54ac00a86f25850/hypothesis-6.161.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:81570959521eebd0172ea9132ffab71b90050e7cd44d045da67210aa9a594376", size = 766503, upload-time = "2026-07-23T07:16:10.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5d/8b61c3490fd8195a25fdc37e951ccba3ae4df2c74839ffe6a6d5720fdb79/hypothesis-6.161.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:46a6039181337b85a995666e9bf87cc2390282720ba345479bb9be9c867914da", size = 758013, upload-time = "2026-07-23T07:17:26.71Z" }, + { url = "https://files.pythonhosted.org/packages/d9/11/ac4ab15ec4586a23bb4e2acdcbed814517e341f2940eeb74fbf428dac243/hypothesis-6.161.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f2d4141c952f522d6aae0e493d170f9b0127c001319bd312ee9cfba7ed419d4", size = 1088871, upload-time = "2026-07-23T07:17:02.884Z" }, + { url = "https://files.pythonhosted.org/packages/27/13/fd83965bcdd44dc5002c67fe8e8e2e974ed45c82dc6864e104c1c70093d5/hypothesis-6.161.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ed3e1b9c036954bfabe64899072a18e6b5113703d32a96645c6656a8cfc43e", size = 1138801, upload-time = "2026-07-23T07:17:35.354Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e7/a4ad5f3b0b805fd2e583d193e2a762cd413465b524ef07829452521dca6d/hypothesis-6.161.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:aba1508da6c317819305e875bf6c4d4dd0922193c76416d4cc907447ebe08fea", size = 1261305, upload-time = "2026-07-23T07:17:23.201Z" }, + { url = "https://files.pythonhosted.org/packages/38/54/35e1b62ece96c24921e9a1e811179d54a6429d9611a2cd49504f5b95382e/hypothesis-6.161.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:981acb3efa88df363a0d31e0f4bacd36ef739104eee789a6b12c7f7200a457fc", size = 1305689, upload-time = "2026-07-23T07:17:21.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/87/6491e9a36d8e8df67a3b9c3eeb5a85c12c6b0d5302b5ce395b5427698b52/hypothesis-6.161.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e71e229f2dec694685245b8e55e908855b475c17ab8337bcf848768b4c32aa97", size = 655436, upload-time = "2026-07-23T07:17:04.424Z" }, + { url = "https://files.pythonhosted.org/packages/54/e2/0782e45562fb091cd75bd12f69932c8aa55a9e3bb699599f43e5258d013c/hypothesis-6.161.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:33a19886dc05b489f0ab632c89b8ca9dc6f89a0b380b6405671cecb2b0d5b5c4", size = 767674, upload-time = "2026-07-23T07:17:16.37Z" }, + { url = "https://files.pythonhosted.org/packages/03/46/eaccb5375ed83396be3153857f9de808b5d5ecc001fab8e30bf8e30d33d4/hypothesis-6.161.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b4fd38d0e757ae290583774b3695ab0d7f0da80e924c6473de84483449c6da8d", size = 763579, upload-time = "2026-07-23T07:16:46.309Z" }, + { url = "https://files.pythonhosted.org/packages/99/48/31ca6b9414cf30388ba825c740b13ab74f6afcf856a194a9256f7f1ca38d/hypothesis-6.161.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59dc6871e2fbbfd2d28e7e6f33d19bd8513a3b7510bd0b224a59bbebdd5cc1b3", size = 1092394, upload-time = "2026-07-23T07:17:01.136Z" }, + { url = "https://files.pythonhosted.org/packages/75/1d/709c03af162418c0b3e0cf624549b13ecacd8df0f2ca09d53214702cb1f8/hypothesis-6.161.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55ff16ec4b4e98bfd97e9a2a44905eaf7a3ec8f3f157d8c7eac2385a88059a29", size = 1142170, upload-time = "2026-07-23T07:17:32.044Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ae/63458a5f80db8433beb7556482e3de1a6789b6c469a5f2f99c21aaa7fdef/hypothesis-6.161.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:77ae374d9ed7046b15443053b11f5b05e185bca24b3b849c3f473a9e4cc85451", size = 659069, upload-time = "2026-07-23T07:16:12.922Z" }, +] + +[[package]] +name = "id" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "librt" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/2f/ec5241c38e7fa0fe6c26bfc450e78b9489a6c3c08b394b85d2c10e506975/librt-0.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5", size = 148654, upload-time = "2026-07-08T12:24:30.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1a/d651e18d3ee7aa2879322368c4f278bb7ecaa6b90caadfdec4ebfa8389f3/librt-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547", size = 153537, upload-time = "2026-07-08T12:24:31.773Z" }, + { url = "https://files.pythonhosted.org/packages/45/18/10bff2122577246009d9619b6569596daf69b7648812f997ca9ca0426f60/librt-0.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2", size = 494336, upload-time = "2026-07-08T12:24:33.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/87dfee871b852970f137fdeae8e2ca356c5ab38e6f21d2a3299535fc3159/librt-0.13.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929", size = 485393, upload-time = "2026-07-08T12:24:34.324Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d5/625447a8c0441ff5f15f4ac5e1d323fb9d4d256ebfde7a3c8e003f646057/librt-0.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a", size = 515382, upload-time = "2026-07-08T12:24:35.575Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d8/1c8c49ea04235960426444deece9092a6b3a9587a850a81bae2335317411/librt-0.13.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac", size = 509483, upload-time = "2026-07-08T12:24:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/6f/65/f1760fc48050e215201a03506c32b7270159088d01f64557b53e39e74a45/librt-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7", size = 532503, upload-time = "2026-07-08T12:24:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/18/1b/793e281dcf494879eff99f642b63ebc9c7c58694a1c2d1e93362a22c7041/librt-0.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40", size = 537027, upload-time = "2026-07-08T12:24:39.34Z" }, + { url = "https://files.pythonhosted.org/packages/69/45/0801bbb40c9eea795d3dd3ce91c4c5f3fe7d42d23ec4be3e8cb283bcc754/librt-0.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a", size = 517100, upload-time = "2026-07-08T12:24:40.907Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6c/eb5f514f8e29d4924bc0ff4601dd7b4175557e182e7c0721e84cffa39b8a/librt-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde", size = 558653, upload-time = "2026-07-08T12:24:42.359Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bf/f140100d1b59fe87ff40b5ecbb4e27924335b189a784e230ee465452f6c2/librt-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8", size = 104402, upload-time = "2026-07-08T12:24:43.668Z" }, + { url = "https://files.pythonhosted.org/packages/22/7c/57e40fef7cfb61869341cb28bdcefe8a950bebcbecca74a397bae14dce4a/librt-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc", size = 125002, upload-time = "2026-07-08T12:24:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/a6498964cfeec270c468cffdc118f69c29b412593610d55fa1327ca51ff4/librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082", size = 148029, upload-time = "2026-07-08T12:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/dc86d1bffd8e0c2818bace29d9f7783cfbb8e0673bf3673b5bbd5bbe0420/librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14", size = 153036, upload-time = "2026-07-08T12:24:47.257Z" }, + { url = "https://files.pythonhosted.org/packages/29/3f/b923826660f02f286186cd9303d52bb05ced0a13708edc104dc8480920e3/librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79", size = 493062, upload-time = "2026-07-08T12:24:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/6c0980a9c9b1302cb68d108906697b89eceb55889bb1dcf77c109aa56ca5/librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176", size = 485510, upload-time = "2026-07-08T12:24:49.727Z" }, + { url = "https://files.pythonhosted.org/packages/32/81/795ae3b9df5dd94079fb807e38191855e023e8c6249014ae6bc3f0d9a490/librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89", size = 515909, upload-time = "2026-07-08T12:24:51.135Z" }, + { url = "https://files.pythonhosted.org/packages/20/e5/182de15abce8907108a6fdb41487de65beb5099b74dc5841b19b099168db/librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f", size = 508620, upload-time = "2026-07-08T12:24:52.358Z" }, + { url = "https://files.pythonhosted.org/packages/32/03/33978d32db76e1f66377e8f78e42a2ca3c162143331677d1f50bbad36cfb/librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d", size = 530363, upload-time = "2026-07-08T12:24:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f5/b291fbd2d00f7d8287bcbf67b5aa0c6afed4bc26cef23e079629c47a2c04/librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd", size = 534209, upload-time = "2026-07-08T12:24:55.138Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/6f41f17939d191bc21609f220da8509316bc62797f078545fe83be522e78/librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588", size = 514254, upload-time = "2026-07-08T12:24:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/af/c2/2e4befa5410a7443019c14abccc94ff619797171f6b72013635fb87f31d7/librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1", size = 557611, upload-time = "2026-07-08T12:24:57.561Z" }, + { url = "https://files.pythonhosted.org/packages/ab/54/8b69f81448417adbc040a2185f4e2eece1e1994b7dcfaeed4662b30f98a5/librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21", size = 104906, upload-time = "2026-07-08T12:24:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/76/5a/f4aaf37b50f2fde12c8c663b83fdd499cdc24f957f19543d7414bfcc9e25/librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b", size = 125852, upload-time = "2026-07-08T12:25:00.065Z" }, + { url = "https://files.pythonhosted.org/packages/f2/99/bf1820e6feeabc2f218c24450ec0c995d6a91e8ba0fd3caf042c9e8adb2a/librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c", size = 111832, upload-time = "2026-07-08T12:25:01.148Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" }, + { url = "https://files.pythonhosted.org/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" }, + { url = "https://files.pythonhosted.org/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" }, + { url = "https://files.pythonhosted.org/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" }, + { url = "https://files.pythonhosted.org/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" }, + { url = "https://files.pythonhosted.org/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" }, + { url = "https://files.pythonhosted.org/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" }, + { url = "https://files.pythonhosted.org/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" }, + { url = "https://files.pythonhosted.org/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" }, + { url = "https://files.pythonhosted.org/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" }, + { url = "https://files.pythonhosted.org/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" }, + { url = "https://files.pythonhosted.org/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" }, + { url = "https://files.pythonhosted.org/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, +] + +[[package]] +name = "mypy" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +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/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/97/ce2502df2cecf2ef997b6c6527c4a223b92feb9e7b790cdc8dcd683f3a8a/mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4", size = 14457059, upload-time = "2026-04-21T17:06:14.935Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/417ee60b822cc80c0f3dc9f495ad7fd8dbb8d8b2cf4baf22d4046d25d01d/mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997", size = 13346816, upload-time = "2026-04-21T17:10:41.433Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/e20951978702df58379d0bcc2e8f7ccdca4e78cd7dc66dd3ddbf9b29d517/mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14", size = 13772593, upload-time = "2026-04-21T17:08:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/63/a5/5441a13259ec516c56fd5de0fd96a69a9590ae6c5e5d3e5174aa84b97973/mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99", size = 14656635, upload-time = "2026-04-21T17:09:54.042Z" }, + { url = "https://files.pythonhosted.org/packages/3b/51/b89c69157c5e1f19fd125a65d991166a26906e7902f026f00feebbcfa2b9/mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c", size = 14943278, upload-time = "2026-04-21T17:09:15.599Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/6b0eeecfe96d7cce1d71c66b8e03cb304aa70ec11f1955dc1d6b46aca3c3/mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd", size = 10851915, upload-time = "2026-04-21T17:06:03.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/6593dc88545d75fb96416184be5392da5e2a8e8c2802a8597913e16ae25c/mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2", size = 9786676, upload-time = "2026-04-21T17:07:02.035Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/9ebeae211caccbdaddde7ed5e31dfcf57faac66be9b11deb1dc6526c8078/mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c", size = 14371307, upload-time = "2026-04-21T17:08:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/d7/93473d34b61f04fac1aecc01368485c89c5c4af7a4b9a0cab5d77d04b63f/mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3", size = 13258917, upload-time = "2026-04-21T17:05:50.978Z" }, + { url = "https://files.pythonhosted.org/packages/e2/30/3dd903e8bafb7b5f7bf87fcd58f8382086dea2aa19f0a7b357f21f63071b/mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254", size = 13700516, upload-time = "2026-04-21T17:11:33.161Z" }, + { url = "https://files.pythonhosted.org/packages/07/05/c61a140aba4c729ac7bc99ae26fc627c78a6e08f5b9dd319244ea71a3d7e/mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98", size = 14562889, upload-time = "2026-04-21T17:05:27.674Z" }, + { url = "https://files.pythonhosted.org/packages/fd/87/da78243742ffa8a36d98c3010f0d829f93d5da4e6786f1a1a6f2ad616502/mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac", size = 14803844, upload-time = "2026-04-21T17:10:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/37/52/10a1ddf91b40f843943a3c6db51e2df59c9e237f29d355e95eaab427461f/mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67", size = 10846300, upload-time = "2026-04-21T17:12:23.886Z" }, + { url = "https://files.pythonhosted.org/packages/20/02/f9a4415b664c53bd34d6709be59da303abcae986dc4ac847b402edb6fa1e/mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100", size = 9779498, upload-time = "2026-04-21T17:09:23.695Z" }, + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nh3" +version = "0.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/1b/ef84624f14954d270f74060a19fc550dd4f06656399447569afb584d8c06/nh3-0.3.6.tar.gz", hash = "sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7", size = 24684, upload-time = "2026-06-22T00:47:02.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/3e/6506aa4f23dc7b7993a2d0a45dca3ce864ec48380adfe15a173e643c63e8/nh3-0.3.6-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2", size = 1421679, upload-time = "2026-06-22T00:46:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e1/e96e7864a7a53bd6b6fab7e9632467382a2a2c1f3fed951918ad131542fb/nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d", size = 792570, upload-time = "2026-06-22T00:46:22.179Z" }, + { url = "https://files.pythonhosted.org/packages/59/62/5b6108bedaef2b2637fed04c87bdbcb5967b9961758b41f0e466ef22a022/nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e", size = 842243, upload-time = "2026-06-22T00:46:23.801Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4a/526f199626bfcb496bc01a268051b44737962005553b158e985ed7e64865/nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917", size = 1001468, upload-time = "2026-06-22T00:46:25.481Z" }, + { url = "https://files.pythonhosted.org/packages/49/09/0d8e3101636d9ad88cdefb2914e764cb8e876ebdbb4286bfc251277d9c67/nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4", size = 1082933, upload-time = "2026-06-22T00:46:27.15Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/ea83abe738a3fbaa203dfdb836ca7cbab0e7e9609faaee4fe1d4652599c0/nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9", size = 1043120, upload-time = "2026-06-22T00:46:28.89Z" }, + { url = "https://files.pythonhosted.org/packages/66/69/0654482b8635012fbae67826bd6c381abb05d841ac7388b9b4666300fdad/nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6", size = 1023824, upload-time = "2026-06-22T00:46:30.453Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/1f7285ffadc8307c4dbeb08d21b920536d5117785056d1079e998c4dfa44/nh3-0.3.6-cp314-cp314t-win32.whl", hash = "sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796", size = 599253, upload-time = "2026-06-22T00:46:32.072Z" }, + { url = "https://files.pythonhosted.org/packages/36/ea/5542f3c45da4c00290d9d67a65e996702e23e613c4b627de3e09cb9fe357/nh3-0.3.6-cp314-cp314t-win_amd64.whl", hash = "sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6", size = 612553, upload-time = "2026-06-22T00:46:33.53Z" }, + { url = "https://files.pythonhosted.org/packages/66/35/26bd47e6af5915a628281dccdac354ddf4e32f7397047894270acd8c9870/nh3-0.3.6-cp314-cp314t-win_arm64.whl", hash = "sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41", size = 595151, upload-time = "2026-06-22T00:46:34.878Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25", size = 1443564, upload-time = "2026-06-22T00:46:36.66Z" }, + { url = "https://files.pythonhosted.org/packages/41/21/e1084ab18eb589506335c7c7576f2d4643e9a0c0e33983ef0e549a256b96/nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69", size = 838002, upload-time = "2026-06-22T00:46:38.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/94/f48d08e6f72a406300fa11d8acd929fea1a80d4bf750fa292cb10785f126/nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e", size = 823045, upload-time = "2026-06-22T00:46:39.495Z" }, + { url = "https://files.pythonhosted.org/packages/25/bb/431615ba1d1d3eb63cde0f974f2114edf863a8a3f6049a12fed23fc241d3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae", size = 1093171, upload-time = "2026-06-22T00:46:41.21Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/a0d80182a18919665fefd19c1c06f1d1df1c9a6455d0252de40c034a0bc3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d", size = 1049217, upload-time = "2026-06-22T00:46:42.804Z" }, + { url = "https://files.pythonhosted.org/packages/0a/13/6f1e302ca674ac74362e150848ad56a1be5145391204f74facdb8e94df12/nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf", size = 917372, upload-time = "2026-06-22T00:46:44.495Z" }, + { url = "https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e", size = 806699, upload-time = "2026-06-22T00:46:45.99Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a6/bfaa00046e58603507dcfc266c4778e3ab7adf68a5dedd73b6274b8d9314/nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec", size = 835165, upload-time = "2026-06-22T00:46:47.617Z" }, + { url = "https://files.pythonhosted.org/packages/30/a8/fb2c38845efb703a9173bffdfc745fc64d2b0e55cfc73a3647d2f028250c/nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0", size = 858282, upload-time = "2026-06-22T00:46:49.276Z" }, + { url = "https://files.pythonhosted.org/packages/68/17/06e72a18ee9b572914447338237ca7eb164c0df901f141bc10d1282247a2/nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78", size = 1014328, upload-time = "2026-06-22T00:46:51.026Z" }, + { url = "https://files.pythonhosted.org/packages/11/f9/3966c61455668c08853bf5e33b4bed93c421f3194ce4de896dc248d6f6ce/nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438", size = 1098207, upload-time = "2026-06-22T00:46:52.674Z" }, + { url = "https://files.pythonhosted.org/packages/19/d3/479cb4ae440424825735d60525b53e3c77fd60fd6e6afc0e984f00eb0178/nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56", size = 1056961, upload-time = "2026-06-22T00:46:54.335Z" }, + { url = "https://files.pythonhosted.org/packages/17/0c/6cdb5ee1e127be50dc8391e54bddc1f64e87bf4bfad0c55633320e2e02db/nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f", size = 1033829, upload-time = "2026-06-22T00:46:56.258Z" }, + { url = "https://files.pythonhosted.org/packages/e9/55/9de666ad975d6ccd77d799ea0add55ee2347aa81286ce21b2a97c070746b/nh3-0.3.6-cp38-abi3-win32.whl", hash = "sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da", size = 609081, upload-time = "2026-06-22T00:46:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl", hash = "sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10", size = 624461, upload-time = "2026-06-22T00:46:59.163Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e5/7cafee2f0413ca4cb0ef3bd111e94d408a48810008b283ad8aee00dd1809/nh3-0.3.6-cp38-abi3-win_arm64.whl", hash = "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21", size = 603060, upload-time = "2026-06-22T00:47:00.596Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.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/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/4c/f883ab8f0daad69f47efdf95f55a66b51a8b939c430dadce0611508d9e99/pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2", size = 70398, upload-time = "2025-09-06T15:40:14.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/b4/bb7263e12aade3842b938bc5c6958cae79c5ee18992f9b9349019579da0f/pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749", size = 25115, upload-time = "2025-09-06T15:40:12.44Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "readme-renderer" +version = "45.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz", hash = "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", size = 36172, upload-time = "2026-06-09T21:05:17.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl", hash = "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f", size = 14134, upload-time = "2026-06-09T21:05:15.85Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +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/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]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "twine" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "wheel" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +] From dfafda3b0cd96455b45d1785ef1ebc6968bba5cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:10:25 +0200 Subject: [PATCH 53/60] Bump pytest from 8.4.2 to 9.0.3 (#1320) Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.2 to 9.0.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.4.2...9.0.3) --- updated-dependencies: - dependency-name: pytest dependency-version: 9.0.3 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 50 +++++++++++++++++++++++++------------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3e9c107cf..c5ef8272b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ dev = [ ] testing = [ - "pytest>=8.3.3,<9", + "pytest>=8.3.3,<10", "pytest-cov>=6.0.0,<7", "pytest-xdist>=3.6.1,<4", "hypothesis>=6.119.4,<7", diff --git a/uv.lock b/uv.lock index 672688a82..9d8ee7035 100644 --- a/uv.lock +++ b/uv.lock @@ -384,7 +384,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] 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 = [ @@ -456,7 +456,7 @@ dev = [ { name = "check-manifest", specifier = "==0.51" }, { name = "hypothesis", specifier = ">=6.119.4,<7" }, { name = "mypy", specifier = ">=1.16.0,<2" }, - { name = "pytest", specifier = ">=8.3.3,<9" }, + { name = "pytest", specifier = ">=8.3.3,<10" }, { name = "pytest-cov", specifier = ">=6.0.0,<7" }, { name = "pytest-xdist", specifier = ">=3.6.1,<4" }, { name = "ruff", specifier = ">=0.8.0,<1" }, @@ -479,7 +479,7 @@ packaging = [ ] testing = [ { name = "hypothesis", specifier = ">=6.119.4,<7" }, - { name = "pytest", specifier = ">=8.3.3,<9" }, + { name = "pytest", specifier = ">=8.3.3,<10" }, { name = "pytest-cov", specifier = ">=6.0.0,<7" }, { name = "pytest-xdist", specifier = ">=3.6.1,<4" }, ] @@ -606,7 +606,7 @@ name = "importlib-metadata" version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } wheels = [ @@ -672,7 +672,7 @@ name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe" }, + { name = "markupsafe", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ @@ -1056,7 +1056,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1067,9 +1067,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -1250,23 +1250,23 @@ name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ From 6cce763997eca5b826f3e435a611b7a7fc73f633 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Thu, 23 Jul 2026 21:11:48 +0200 Subject: [PATCH 54/60] v4.4.0 --- CHANGELOG.rst | 4 ++-- src/h2/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 73c024c10..a691b73d7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,8 @@ Release History =============== -dev ---- +4.4.0 (2026-07-23) +------------------ **API Changes (Backward Incompatible)** diff --git a/src/h2/__init__.py b/src/h2/__init__.py index 0718c5704..2faf308ed 100644 --- a/src/h2/__init__.py +++ b/src/h2/__init__.py @@ -3,4 +3,4 @@ """ from __future__ import annotations -__version__ = "4.4.0+dev" +__version__ = "4.4.0" From 9a7ff7430df669fa8e90b6121f3cc1ed64d1115a Mon Sep 17 00:00:00 2001 From: Utkarsh Tiwari <65409319+utkarshalpha@users.noreply.github.com> Date: Wed, 29 Jul 2026 01:14:35 +0530 Subject: [PATCH 55/60] performance: remove consumed frames in place from data buffer (#1321) Deleting the consumed prefix preserves the bytearray's amortized left-delete behavior instead of copying the entire remaining buffer after every frame. Closes #474 --- src/h2/frame_buffer.py | 9 ++++++++- tests/test_basic_logic.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/h2/frame_buffer.py b/src/h2/frame_buffer.py index c2f8a5411..cab6f9056 100644 --- a/src/h2/frame_buffer.py +++ b/src/h2/frame_buffer.py @@ -155,7 +155,14 @@ def __next__(self) -> Frame: # At this point, as we know we'll use or discard the entire frame, we # can update the data. - self._data = self._data[9+length:] + # Deleting the consumed prefix mutates the bytearray in place instead + # of copying the remaining bytes into a new object, as slicing would. + # ``del s[i:j]`` is documented for mutable sequences in + # https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types + # and CPython's bytearray tracks an internal offset (``ob_start`` in + # Objects/bytearrayobject.c) that makes repeated deletes from the + # front amortized O(1) per byte rather than O(len) per frame. + del self._data[:9+length] # Pass the frame through the header buffer. new_frame = self._update_header_buffer(f) diff --git a/tests/test_basic_logic.py b/tests/test_basic_logic.py index 1df989e06..d1bc0f1eb 100644 --- a/tests/test_basic_logic.py +++ b/tests/test_basic_logic.py @@ -23,6 +23,24 @@ from . import helpers +class TestFrameBuffer: + def test_consumed_frames_are_removed_in_place(self) -> None: + frame = hyperframe.frame.SettingsFrame(0).serialize() + buffer = h2.frame_buffer.FrameBuffer() + buffer.max_frame_size = 65535 + buffer.add_data(frame * 2) + data = buffer._data + + next(buffer) + + # ``is`` checks object identity (CPython compares ``id(...)`` of both + # operands): the buffer must still be the very same bytearray object, + # proving the consumed frame was deleted in place rather than the + # buffer being replaced by a sliced copy. + assert buffer._data is data + assert buffer._data == frame + + class TestBasicClient: """ Basic client-side tests. From 439b970d0fa19891fa81068907de97dfa3a07c3a Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Thu, 23 Jul 2026 21:21:25 +0200 Subject: [PATCH 56/60] prepare for next release cycle --- CHANGELOG.rst | 15 +++++++++++++++ pyproject.toml | 1 + src/h2/__init__.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a691b73d7..0520eefcc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,21 @@ Release History =============== +dev +--- + +**API Changes (Backward Incompatible)** + +- + +**API Changes (Backward Compatible)** + +- + +**Bugfixes** + +- + 4.4.0 (2026-07-23) ------------------ diff --git a/pyproject.toml b/pyproject.toml index c5ef8272b..9ea90beeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,6 +118,7 @@ lint.ignore = [ "D400", # docs readability "D401", # docs readability "D415", # docs readability + "PLR0917", # readability "PLR2004", # readability "SIM108", # readability "RUF012", # readability diff --git a/src/h2/__init__.py b/src/h2/__init__.py index 2faf308ed..0e3f5d2ac 100644 --- a/src/h2/__init__.py +++ b/src/h2/__init__.py @@ -3,4 +3,4 @@ """ from __future__ import annotations -__version__ = "4.4.0" +__version__ = "4.5.0+dev" From 04d3b87cbc1db020d28c7cfb44fe194558efbdde Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Tue, 28 Jul 2026 21:46:14 +0200 Subject: [PATCH 57/60] update changelog --- CHANGELOG.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0520eefcc..64dc68aa1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,7 +10,7 @@ dev **API Changes (Backward Compatible)** -- +- Performance improvement: remove consumed frames in-place from data buffer. **Bugfixes** @@ -25,8 +25,8 @@ dev - Support for PyPy 3.9 has been removed. - `Stream.end_stream()` now raises `NoSuchStreamError` or `StreamClosedError` exceptions, instead of a generic `KeyError`. - Duplicate ``content-length`` headers with different values now raise ``ProtocolError``. - Previously, the first ``content-length`` header was accepted and later conflicting values were ignored. -- Parse ``content-length`` headers according to RFC9110 grammar for numbers (1*DIGIT) + Previously, the first ``content-length`` header was accepted and later conflicting values were ignored. Thanks to Harshal Parekh for the report. +- Parse ``content-length`` headers according to RFC9110 grammar for numbers (1*DIGIT). Thanks to Arkadiusz Marta for the report. - **backfill from v4.3.0** Convert emitted events into Python `dataclass`, which introduces new constructors with required arguments. Instantiating these events without arguments, as previously commonly used API pattern, will no longer work. From 292a40829feefda98c8509dcdbbb4a57af9bd6a6 Mon Sep 17 00:00:00 2001 From: SunandM Date: Mon, 3 Aug 2026 13:28:23 +0200 Subject: [PATCH 58/60] reject duplicate Host headers in request headers Duplicate Host headers are now rejected with a ProtocolError. This addresses a request smuggling primitive where a consumer downgrading HTTP/2 to HTTP/1.1 would emit two Host header lines, causing backend divergence (CWE-444). The _validate_host_authority_header() function now counts Host headers and raises ProtocolError if more than one is present, covering both the receive path (_check_host_authority_header) and the send path (_check_sent_host_authority_header). --- src/h2/utilities.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/h2/utilities.py b/src/h2/utilities.py index 7709aa602..c4e62f2f3 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -449,6 +449,9 @@ def _validate_host_authority_header(headers: Iterable[Header]) -> Generator[Head if header[0] == b":authority": authority_header_val = header[1] elif header[0] == b"host": + if host_header_val is not None: + msg = "Request header block has multiple Host headers." + raise ProtocolError(msg) host_header_val = header[1] yield header From 92b925ed1b1817c82db32893503f74f47fcf4452 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Mon, 3 Aug 2026 13:38:41 +0200 Subject: [PATCH 59/60] add test for duplicate host headers --- tests/test_invalid_headers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_invalid_headers.py b/tests/test_invalid_headers.py index 96876c6f5..2a68c6fc6 100644 --- a/tests/test_invalid_headers.py +++ b/tests/test_invalid_headers.py @@ -43,6 +43,7 @@ class TestInvalidFrameSequences: [*base_request_headers, ("transfer-encoding", "gzip")], [*base_request_headers, ("upgrade", "super-protocol/1.1")], [*base_request_headers, ("te", "chunked")], + [*base_request_headers, ("host", "example.com"), ("host", "evil.example.com")], [*base_request_headers, ("host", "notexample.com")], [*base_request_headers, (" name", "name with leading space")], [*base_request_headers, ("name ", "name with trailing space")], From bc239af1d1b85bc70482804f30a0e0e587d90a08 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Mon, 3 Aug 2026 13:30:53 +0200 Subject: [PATCH 60/60] v4.4.1 --- CHANGELOG.rst | 15 ++++----------- src/h2/__init__.py | 2 +- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 64dc68aa1..44134f898 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,20 +1,13 @@ Release History =============== -dev ---- - -**API Changes (Backward Incompatible)** - -- - -**API Changes (Backward Compatible)** - -- Performance improvement: remove consumed frames in-place from data buffer. +4.4.1 (2026-08-03) +------------------ **Bugfixes** -- +- Performance improvement: remove consumed frames in-place from data buffer. +- Reject duplicate Host headers in request headers. Thanks to Sunand Mohan for the report. 4.4.0 (2026-07-23) ------------------ diff --git a/src/h2/__init__.py b/src/h2/__init__.py index 0e3f5d2ac..c2c66659a 100644 --- a/src/h2/__init__.py +++ b/src/h2/__init__.py @@ -3,4 +3,4 @@ """ from __future__ import annotations -__version__ = "4.5.0+dev" +__version__ = "4.4.1"