diff --git a/CHANGELOG.md b/CHANGELOG.md index b6e9852..8662331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [3.14.1] - 2026-06-01 + +* Feature: Enhance online documentation +* Feature: Chain nav to browser history state +* Feature: Add packet_hash to bot kwargs +* Bug: Fix amp/ma units for HA integration of LPP sensors +* Bug: Don't display blocked contacts on the map +* Bug: Don't trim trailing space from repeater console commands +* Bug: Make the trace pane not unusable with a bunch of hops or a bunch of recents +* Misc: Dependency bumps + test updates + ## [3.14.0] - 2026-05-13 * Feature: Support active/intervalized contact telemetry gathering + HA forwarding diff --git a/app/AGENTS.md b/app/AGENTS.md index c1961b4..1a6e70a 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -137,6 +137,7 @@ app/ - Non-final DM attempts use the contact's effective route (`override > direct > flood`). The final retry is intentionally sent as flood even when a routing override exists. - DM ACK state is terminal on first ACK. Retry attempts may register multiple expected ACK codes for the same message, but sibling pending codes are cleared once one ACK wins so a DM should not accrue multiple delivery confirmations from retries. - ACKs are delivery state, not routing state. Bundled ACKs inside PATH packets still satisfy pending DM sends, but ACK history does not feed contact route learning. +- DM ACKs are matched from two independent radio emissions, so confirmation does not depend on the radio surfacing a host control frame: (1) the `EventType.ACK`/`SEND_CONFIRMED` host frame via `event_handlers.on_ack`, and (2) the raw RF packet itself via `packet_processor.process_raw_packet`. The packet processor extracts ACK codes both from PATH-return packets (flood replies, ACK embedded in `extra`) and from standalone `PayloadType.ACK` packets (direct replies, 4-byte cleartext payload), feeding both into `apply_dm_ack_code`. This matters for companion firmwares (e.g. pyMC over TCP) that do not reliably emit a separate host ACK frame for direct-routed replies. ### Echo/repeat dedup diff --git a/app/fanout/bot.py b/app/fanout/bot.py index 727453f..bb22412 100644 --- a/app/fanout/bot.py +++ b/app/fanout/bot.py @@ -136,6 +136,7 @@ class BotModule(FanoutModule): if path_value is None and paths and isinstance(paths, list) and len(paths) > 0: path_value = paths[0].get("path") if isinstance(paths[0], dict) else None path_bytes_per_hop = _derive_path_bytes_per_hop(paths, path_value) + packet_hash = data.get("packet_hash") # Wait for message to settle (allows retransmissions to be deduped) await asyncio.sleep(2) @@ -161,6 +162,7 @@ class BotModule(FanoutModule): path_value, is_outgoing, path_bytes_per_hop, + packet_hash, ), timeout=BOT_EXECUTION_TIMEOUT, ) diff --git a/app/fanout/bot_exec.py b/app/fanout/bot_exec.py index cc6957b..e8b4e19 100644 --- a/app/fanout/bot_exec.py +++ b/app/fanout/bot_exec.py @@ -71,14 +71,14 @@ def _analyze_bot_signature(bot_func_or_sig) -> BotCallPlan: has_varargs = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in param_values) has_kwargs = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in param_values) explicit_optional_names = tuple( - name for name in ("is_outgoing", "path_bytes_per_hop") if name in params + name for name in ("is_outgoing", "path_bytes_per_hop", "packet_hash") if name in params ) unsupported_required_kwonly = [ p.name for p in param_values if p.kind == inspect.Parameter.KEYWORD_ONLY and p.default is inspect.Parameter.empty - and p.name not in {"is_outgoing", "path_bytes_per_hop"} + and p.name not in {"is_outgoing", "path_bytes_per_hop", "packet_hash"} ] if unsupported_required_kwonly: raise ValueError( @@ -104,6 +104,8 @@ def _analyze_bot_signature(bot_func_or_sig) -> BotCallPlan: keyword_args["is_outgoing"] = False if has_kwargs or "path_bytes_per_hop" in params: keyword_args["path_bytes_per_hop"] = 1 + if has_kwargs or "packet_hash" in params: + keyword_args["packet_hash"] = "" candidate_specs.append(("keyword", [], keyword_args)) if not has_kwargs and explicit_optional_names: @@ -112,8 +114,12 @@ def _analyze_bot_signature(bot_func_or_sig) -> BotCallPlan: kwargs["is_outgoing"] = False if has_kwargs or "path_bytes_per_hop" in params: kwargs["path_bytes_per_hop"] = 1 + if has_kwargs or "packet_hash" in params: + kwargs["packet_hash"] = "" candidate_specs.append(("mixed_keyword", base_args, kwargs)) + if has_varargs or positional_capacity >= 11: + candidate_specs.append(("positional_11", base_args + [False, 1, ""], {})) if has_varargs or positional_capacity >= 10: candidate_specs.append(("positional_10", base_args + [False, 1], {})) if has_varargs or positional_capacity >= 9: @@ -134,6 +140,7 @@ def _analyze_bot_signature(bot_func_or_sig) -> BotCallPlan: "Bot function signature is not supported. Use the default bot template as a reference. " "Supported trailing parameters are: path; path + is_outgoing; " "path + path_bytes_per_hop; path + is_outgoing + path_bytes_per_hop; " + "path + is_outgoing + path_bytes_per_hop + packet_hash; " "or use **kwargs for forward compatibility." ) @@ -150,12 +157,13 @@ def execute_bot_code( path: str | None, is_outgoing: bool = False, path_bytes_per_hop: int | None = None, + packet_hash: str | None = None, ) -> str | list[str] | None: """ Execute user-provided bot code with message context. The code should define a function: - `bot(sender_name, sender_key, message_text, is_dm, channel_key, channel_name, sender_timestamp, path, is_outgoing, path_bytes_per_hop)` + `bot(sender_name, sender_key, message_text, is_dm, channel_key, channel_name, sender_timestamp, path, is_outgoing, path_bytes_per_hop, packet_hash)` or use named parameters / `**kwargs`. that returns either None (no response), a string (single response message), or a list of strings (multiple messages sent in order). @@ -175,6 +183,7 @@ def execute_bot_code( path: Hex-encoded routing path (may be None) is_outgoing: True if this is our own outgoing message path_bytes_per_hop: Number of bytes per routing hop (1, 2, or 3), if known + packet_hash: MeshCore packet hash (first 16 hex chars of SHA256, uppercase), if known Returns: Response string, list of strings, or None. @@ -211,7 +220,21 @@ def execute_bot_code( try: # Call the bot function with appropriate signature - if call_plan.call_style == "positional_10": + if call_plan.call_style == "positional_11": + result = bot_func( + sender_name, + sender_key, + message_text, + is_dm, + channel_key, + channel_name, + sender_timestamp, + path, + is_outgoing, + path_bytes_per_hop, + packet_hash, + ) + elif call_plan.call_style == "positional_10": result = bot_func( sender_name, sender_key, @@ -258,6 +281,8 @@ def execute_bot_code( keyword_args["is_outgoing"] = is_outgoing if "path_bytes_per_hop" in call_plan.keyword_args: keyword_args["path_bytes_per_hop"] = path_bytes_per_hop + if "packet_hash" in call_plan.keyword_args: + keyword_args["packet_hash"] = packet_hash result = bot_func(**keyword_args) else: result = bot_func( diff --git a/app/fanout/community_mqtt.py b/app/fanout/community_mqtt.py index 8407428..42d1943 100644 --- a/app/fanout/community_mqtt.py +++ b/app/fanout/community_mqtt.py @@ -11,19 +11,18 @@ from __future__ import annotations import asyncio import base64 -import hashlib import json import logging import ssl import time -from datetime import datetime +from datetime import UTC, datetime from typing import Any, Protocol import aiomqtt from app.fanout.mqtt_base import BaseMqttPublisher from app.keystore import ed25519_sign_expanded -from app.path_utils import parse_packet_envelope, split_path_hex +from app.path_utils import calculate_packet_hash, parse_packet_envelope, split_path_hex from app.version_info import get_app_build_info logger = logging.getLogger(__name__) @@ -46,6 +45,12 @@ _STATS_MIN_CACHE_SECS = 60 # Don't re-fetch stats within 60s _ROUTE_MAP = {0: "F", 1: "F", 2: "D", 3: "T"} +def _format_utc_timestamp(dt: datetime | None = None) -> str: + """Return an ISO-8601 UTC timestamp accepted by community observers.""" + current = dt.astimezone(UTC) if dt is not None else datetime.now(UTC) + return current.isoformat().replace("+00:00", "Z") + + class CommunityMqttSettings(Protocol): """Attributes expected on the settings object for the community MQTT publisher.""" @@ -110,34 +115,6 @@ def _generate_jwt_token( return f"{header_b64}.{payload_b64}.{signature.hex()}" -def _calculate_packet_hash(raw_bytes: bytes) -> str: - """Calculate packet hash matching MeshCore's Packet::calculatePacketHash(). - - Parses the packet structure to extract payload type and payload data, - then hashes: payload_type(1 byte) [+ path_len(2 bytes LE) for TRACE] + payload_data. - Returns first 16 hex characters (uppercase). - """ - if not raw_bytes: - return "0" * 16 - - try: - envelope = parse_packet_envelope(raw_bytes) - if envelope is None: - return "0" * 16 - - # Hash: payload_type(1 byte) [+ path_byte as uint16_t LE for TRACE] + payload_data - # IMPORTANT: TRACE hash uses the raw wire byte (not decoded hop count) to match firmware. - hash_obj = hashlib.sha256() - hash_obj.update(bytes([envelope.payload_type])) - if envelope.payload_type == 9: # PAYLOAD_TYPE_TRACE - hash_obj.update(envelope.path_byte.to_bytes(2, byteorder="little")) - hash_obj.update(envelope.payload) - - return hash_obj.hexdigest()[:16].upper() - except Exception: - return "0" * 16 - - def _decode_packet_fields(raw_bytes: bytes) -> tuple[str, str, str, list[str], int | None]: """Decode packet fields used by the community uploader payload format. @@ -181,9 +158,9 @@ def _format_raw_packet(data: dict[str, Any], device_name: str, public_key_hex: s if route == "U": return None - # Reference format uses local "now" timestamp and derived time/date fields. - current_time = datetime.now() - ts_str = current_time.isoformat() + # Community observers clamp zone-less local timestamps; publish explicit UTC. + current_time = datetime.now(UTC) + ts_str = _format_utc_timestamp(current_time) # Keep numeric telemetry numeric so downstream analyzers can ingest it. # Preserve the existing "Unknown" fallback for missing values. @@ -192,7 +169,7 @@ def _format_raw_packet(data: dict[str, Any], device_name: str, public_key_hex: s snr: float | str = float(snr_val) if snr_val is not None else "Unknown" rssi: int | str = int(rssi_val) if rssi_val is not None else "Unknown" - packet_hash = _calculate_packet_hash(raw_bytes) + packet_hash = calculate_packet_hash(raw_bytes) packet = { "origin": device_name or "MeshCore Device", @@ -343,7 +320,7 @@ class CommunityMqttPublisher(BaseMqttPublisher): offline_payload = json.dumps( { "status": "offline", - "timestamp": datetime.now().isoformat(), + "timestamp": _format_utc_timestamp(), "origin": device_name or "MeshCore Device", "origin_id": pubkey_hex, } @@ -507,7 +484,7 @@ class CommunityMqttPublisher(BaseMqttPublisher): status_topic = _build_status_topic(settings, pubkey_hex) payload: dict[str, Any] = { "status": "online", - "timestamp": datetime.now().isoformat(), + "timestamp": _format_utc_timestamp(), "origin": device_name or "MeshCore Device", "origin_id": pubkey_hex, "model": device_info.get("model", "unknown"), diff --git a/app/packet_processor.py b/app/packet_processor.py index 8c84d16..e5ba30d 100644 --- a/app/packet_processor.py +++ b/app/packet_processor.py @@ -35,6 +35,7 @@ from app.models import ( RawPacketBroadcast, RawPacketDecryptedInfo, ) +from app.path_utils import calculate_packet_hash from app.repository import ( ChannelRepository, ContactAdvertPathRepository, @@ -73,6 +74,7 @@ async def create_message_from_decrypted( snr: float | None = None, channel_name: str | None = None, realtime: bool = True, + packet_hash: str | None = None, ) -> int | None: """Store a decrypted channel message via the shared message service.""" return await _create_message_from_decrypted( @@ -89,6 +91,7 @@ async def create_message_from_decrypted( channel_name=channel_name, realtime=realtime, broadcast_fn=broadcast_event, + packet_hash=packet_hash, ) @@ -104,6 +107,7 @@ async def create_dm_message_from_decrypted( snr: float | None = None, outgoing: bool = False, realtime: bool = True, + packet_hash: str | None = None, ) -> int | None: """Store a decrypted direct message via the shared message service.""" return await _create_dm_message_from_decrypted( @@ -119,6 +123,7 @@ async def create_dm_message_from_decrypted( outgoing=outgoing, realtime=realtime, broadcast_fn=broadcast_event, + packet_hash=packet_hash, ) @@ -323,13 +328,16 @@ async def process_raw_packet( "sender": None, } + # Compute packet hash once for threading into message broadcasts (used by bot fanout). + pkt_hash = calculate_packet_hash(raw_bytes) + # Process packets based on payload type # For GROUP_TEXT, we always try to decrypt even for duplicate packets - the message # deduplication in create_message_from_decrypted handles adding paths to existing messages. # This is more reliable than trying to look up the message via raw packet linking. if payload_type == PayloadType.GROUP_TEXT: decrypt_result = await _process_group_text( - raw_bytes, packet_id, ts, packet_info, rssi=rssi, snr=snr + raw_bytes, packet_id, ts, packet_info, rssi=rssi, snr=snr, packet_hash=pkt_hash ) if decrypt_result: result.update(decrypt_result) @@ -342,7 +350,7 @@ async def process_raw_packet( elif payload_type == PayloadType.TEXT_MESSAGE: # Try to decrypt direct messages using stored private key and known contacts decrypt_result = await _process_direct_message( - raw_bytes, packet_id, ts, packet_info, rssi=rssi, snr=snr + raw_bytes, packet_id, ts, packet_info, rssi=rssi, snr=snr, packet_hash=pkt_hash ) if decrypt_result: result.update(decrypt_result) @@ -350,6 +358,23 @@ async def process_raw_packet( elif payload_type == PayloadType.PATH: await _process_path_packet(raw_bytes, ts, packet_info) + elif payload_type == PayloadType.ACK: + # Standalone ACK packets carry the 4-byte ack code in cleartext (the + # firmware just memcpy's the uint32 into the payload). A contact answers + # a *direct*-routed DM with one of these, whereas a *flood*-routed DM is + # answered with a PATH-return that has the ACK embedded (handled above in + # _process_path_packet). We match directly from the raw RF packet so DM + # delivery confirmation does not depend on the radio also surfacing a + # separate EventType.ACK host control frame, which some companion + # firmwares (e.g. pyMC over TCP) do not reliably emit for direct ACKs. + if packet_info is not None and len(packet_info.payload) >= 4: + ack_code = packet_info.payload[:4].hex() + matched = await apply_dm_ack_code(ack_code, broadcast_fn=broadcast_event) + if matched: + logger.info("Applied standalone ACK %s from raw packet", ack_code) + else: + logger.debug("Buffered/ignored standalone ACK %s from raw packet", ack_code) + # Always broadcast raw packet for the packet feed UI (even duplicates) # This enables the frontend cracker to see all incoming packets in real-time broadcast_payload = RawPacketBroadcast( @@ -384,6 +409,7 @@ async def _process_group_text( packet_info: PacketInfo | None, rssi: int | None = None, snr: float | None = None, + packet_hash: str | None = None, ) -> dict | None: """ Process a GroupText (channel message) packet. @@ -422,6 +448,7 @@ async def _process_group_text( path_len=packet_info.path_length if packet_info else None, rssi=rssi, snr=snr, + packet_hash=packet_hash, ) return { @@ -567,6 +594,7 @@ async def _process_direct_message( packet_info: PacketInfo | None, rssi: int | None = None, snr: float | None = None, + packet_hash: str | None = None, ) -> dict | None: """ Process a TEXT_MESSAGE (direct message) packet. @@ -690,6 +718,7 @@ async def _process_direct_message( rssi=rssi, snr=snr, outgoing=effective_outgoing, + packet_hash=packet_hash, ) return { diff --git a/app/path_utils.py b/app/path_utils.py index 99b87f2..1996889 100644 --- a/app/path_utils.py +++ b/app/path_utils.py @@ -9,6 +9,7 @@ The path_len wire byte is packed as [hash_mode:2][hop_count:6]: Mode 3 (hash_size=4) is reserved and rejected. """ +import hashlib from collections.abc import Iterable from dataclasses import dataclass @@ -289,3 +290,30 @@ def bucket_path_hash_widths(rows: Iterable) -> dict[str, int | float]: "double_byte_pct": (double_byte / total) * 100, "triple_byte_pct": (triple_byte / total) * 100, } + + +def calculate_packet_hash(raw_bytes: bytes) -> str: + """Calculate packet hash matching MeshCore's Packet::calculatePacketHash(). + + Parses the packet structure to extract payload type and payload data, + then hashes: payload_type(1 byte) [+ path_len(2 bytes LE) for TRACE] + payload_data. + Returns first 16 hex characters (uppercase). + """ + if not raw_bytes: + return "0" * 16 + + try: + envelope = parse_packet_envelope(raw_bytes) + if envelope is None: + return "0" * 16 + + hash_obj = hashlib.sha256() + hash_obj.update(bytes([envelope.payload_type])) + # TRACE hash uses the raw wire byte (not decoded hop count) to match firmware. + if envelope.payload_type == 9: # PAYLOAD_TYPE_TRACE + hash_obj.update(envelope.path_byte.to_bytes(2, byteorder="little")) + hash_obj.update(envelope.payload) + + return hash_obj.hexdigest()[:16].upper() + except Exception: + return "0" * 16 diff --git a/app/services/dm_ingest.py b/app/services/dm_ingest.py index 4256801..318aafc 100644 --- a/app/services/dm_ingest.py +++ b/app/services/dm_ingest.py @@ -154,6 +154,7 @@ async def _store_direct_message( update_last_contacted_key: str | None, best_effort_content_dedup: bool, linked_packet_dedup: bool, + packet_hash: str | None = None, message_repository=MessageRepository, contact_repository=ContactRepository, raw_packet_repository=RawPacketRepository, @@ -248,7 +249,9 @@ async def _store_direct_message( sender_name=sender_name, packet_id=packet_id, ) - broadcast_message(message=message, broadcast_fn=broadcast_fn, realtime=realtime) + broadcast_message( + message=message, broadcast_fn=broadcast_fn, realtime=realtime, packet_hash=packet_hash + ) if update_last_contacted_key: await contact_repository.update_last_contacted(update_last_contacted_key, received_at) @@ -279,6 +282,7 @@ async def ingest_decrypted_direct_message( outgoing: bool = False, realtime: bool = True, broadcast_fn: BroadcastFn, + packet_hash: str | None = None, contact_repository=ContactRepository, ) -> Message | None: conversation_key = their_public_key.lower() @@ -338,6 +342,7 @@ async def ingest_decrypted_direct_message( update_last_contacted_key=conversation_key, best_effort_content_dedup=outgoing, linked_packet_dedup=True, + packet_hash=packet_hash, ) if message is None: return None diff --git a/app/services/messages.py b/app/services/messages.py index f6445b3..0f1d5f4 100644 --- a/app/services/messages.py +++ b/app/services/messages.py @@ -95,9 +95,12 @@ def broadcast_message( message: Message, broadcast_fn: BroadcastFn, realtime: bool | None = None, + packet_hash: str | None = None, ) -> None: """Broadcast a message payload, preserving the caller's broadcast signature.""" payload = message.model_dump() + if packet_hash is not None: + payload["packet_hash"] = packet_hash if realtime is None: broadcast_fn("message", payload) else: @@ -272,6 +275,7 @@ async def create_message_from_decrypted( channel_name: str | None = None, realtime: bool = True, broadcast_fn: BroadcastFn, + packet_hash: str | None = None, ) -> int | None: """Store and broadcast a decrypted channel message.""" received = received_at or int(time.time()) @@ -340,6 +344,7 @@ async def create_message_from_decrypted( ), broadcast_fn=broadcast_fn, realtime=realtime, + packet_hash=packet_hash, ) return msg_id @@ -359,6 +364,7 @@ async def create_dm_message_from_decrypted( outgoing: bool = False, realtime: bool = True, broadcast_fn: BroadcastFn, + packet_hash: str | None = None, ) -> int | None: """Store and broadcast a decrypted direct message.""" from app.services.dm_ingest import ingest_decrypted_direct_message @@ -375,6 +381,7 @@ async def create_dm_message_from_decrypted( outgoing=outgoing, realtime=realtime, broadcast_fn=broadcast_fn, + packet_hash=packet_hash, ) return message.id if message is not None else None diff --git a/frontend/package.json b/frontend/package.json index 17ca4d9..61bdeaa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "remoteterm-meshcore-frontend", "private": true, - "version": "3.14.0", + "version": "3.14.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/TracePane.tsx b/frontend/src/components/TracePane.tsx index e1cf4c5..105e95d 100644 --- a/frontend/src/components/TracePane.tsx +++ b/frontend/src/components/TracePane.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; -import { ArrowDown, ArrowUp, Plus, X } from 'lucide-react'; +import { ArrowDown, ArrowUp, ChevronDown, ChevronRight, Plus, X } from 'lucide-react'; import type { Contact, @@ -199,6 +199,7 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps) const [customHopHexDraft, setCustomHopHexDraft] = useState(''); const [customHopError, setCustomHopError] = useState(null); const [recentTraces, setRecentTraces] = useState(loadRecentTraces); + const [recentTracesOpen, setRecentTracesOpen] = useState(false); const activeRunTokenRef = useRef(0); const repeaters = useMemo(() => { @@ -562,7 +563,7 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps)
-
+

Trace Path

@@ -571,35 +572,46 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps)

{recentTraces.length > 0 && (
-
- Rerun a recent trace: -
-
- {recentTraces.map((trace, i) => { - const label = trace.hops - .map((h) => { - if (h.kind === 'repeater' && h.publicKey) { - const shortKey = h.publicKey.slice(0, 12); - return h.displayName !== shortKey - ? `${h.displayName} (${shortKey})` - : shortKey; - } - return h.displayName; - }) - .join(' → '); - return ( - - ); - })} -
+ + {recentTracesOpen && ( +
+ {recentTraces.map((trace, i) => { + const label = trace.hops + .map((h) => { + if (h.kind === 'repeater' && h.publicKey) { + const shortKey = h.publicKey.slice(0, 12); + return h.displayName !== shortKey + ? `${h.displayName} (${shortKey})` + : shortKey; + } + return h.displayName; + }) + .join(' → '); + return ( + + ); + })} +
+ )}
)}
diff --git a/frontend/src/components/repeater/RepeaterConsolePane.tsx b/frontend/src/components/repeater/RepeaterConsolePane.tsx index 027a69e..cd7d7fd 100644 --- a/frontend/src/components/repeater/RepeaterConsolePane.tsx +++ b/frontend/src/components/repeater/RepeaterConsolePane.tsx @@ -34,7 +34,7 @@ export function ConsolePane({ const handleSubmit = useCallback( async (e: FormEvent) => { e.preventDefault(); - const trimmed = input.trim(); + const trimmed = input.trimStart(); if (!trimmed || loading) return; setInput(''); await onSend(trimmed); @@ -80,7 +80,7 @@ export function ConsolePane({ disabled={loading} className="flex-1 font-mono text-sm" /> - diff --git a/frontend/src/components/repeater/RepeaterNeighborsPane.tsx b/frontend/src/components/repeater/RepeaterNeighborsPane.tsx index d6ae6ba..fbd07a9 100644 --- a/frontend/src/components/repeater/RepeaterNeighborsPane.tsx +++ b/frontend/src/components/repeater/RepeaterNeighborsPane.tsx @@ -149,7 +149,14 @@ export function NeighborsPane({ n.snr >= 6 ? 'text-success' : n.snr >= 0 ? 'text-warning' : 'text-destructive'; return ( - {n.name || n.pubkey_prefix} + + {n.name || n.pubkey_prefix} + {n.name && ( + + {n.pubkey_prefix.substring(0, 6)} + + )} + {snrStr} dB {hasDistances && ( diff --git a/frontend/src/components/repeater/RepeaterTelemetryHistoryPane.tsx b/frontend/src/components/repeater/RepeaterTelemetryHistoryPane.tsx index dd02aec..4f43f84 100644 --- a/frontend/src/components/repeater/RepeaterTelemetryHistoryPane.tsx +++ b/frontend/src/components/repeater/RepeaterTelemetryHistoryPane.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { AreaChart, Area, @@ -7,6 +7,7 @@ import { CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, + Brush, } from 'recharts'; import { cn } from '@/lib/utils'; import { Button } from '../ui/button'; @@ -84,6 +85,31 @@ function formatUptime(seconds: number): string { return `${(seconds / 86400).toFixed(1)}d`; } +/** Collect all numeric values for the given keys across a set of chart points. */ +function collectValues(data: Array>, keys: string[]): number[] { + const out: number[] = []; + for (const d of data) { + for (const k of keys) { + const v = d[k]; + if (typeof v === 'number' && Number.isFinite(v)) out.push(v); + } + } + return out; +} + +/** Bound a Y axis to the data range padded by 10% on each side. + * Returns undefined (recharts auto-domain) when there is nothing to plot. */ +function paddedDomain(values: number[]): [number, number] | undefined { + if (values.length === 0) return undefined; + const lo = Math.min(...values); + const hi = Math.max(...values); + const span = hi - lo; + // Flat series (single value / no spread): pad relative to magnitude so the + // line doesn't sit on a degenerate zero-height axis. + const pad = span === 0 ? Math.abs(lo) * 0.1 || 1 : span * 0.1; + return [lo - pad, hi + pad]; +} + interface TelemetryHistoryPaneProps { entries: TelemetryHistoryEntry[]; publicKey: string; @@ -102,6 +128,12 @@ export function TelemetryHistoryPane({ const { distanceUnit } = useDistanceUnit(); const [metric, setMetric] = useState('battery_volts'); const [toggling, setToggling] = useState(false); + const [brushRange, setBrushRange] = useState<{ start: number; end: number } | null>(null); + + // Reset the zoom window when switching to a different repeater. + useEffect(() => { + setBrushRange(null); + }, [publicKey]); const isTracked = trackedTelemetryRepeaters.includes(publicKey); const slotsFull = trackedTelemetryRepeaters.length >= MAX_TRACKED && !isTracked; @@ -143,25 +175,51 @@ export function TelemetryHistoryPane({ const activeMetric = allMetricKeys.includes(metric) ? metric : 'battery_volts'; const isBuiltin = BUILTIN_METRICS.includes(activeMetric as BuiltinMetric); - const activeConfig: MetricConfig = isBuiltin - ? BUILTIN_METRIC_CONFIG[activeMetric as BuiltinMetric] - : (lppMetrics.find((m) => m.key === activeMetric)?.config ?? { - label: activeMetric, - unit: '', - color: '#888', - }); + const activeConfig: MetricConfig = useMemo( + () => + isBuiltin + ? BUILTIN_METRIC_CONFIG[activeMetric as BuiltinMetric] + : (lppMetrics.find((m) => m.key === activeMetric)?.config ?? { + label: activeMetric, + unit: '', + color: '#888', + }), + [isBuiltin, activeMetric, lppMetrics] + ); const chartData = useMemo(() => { - return entries.map((e) => { + // Sort chronologically so per-sample deltas compare against the true + // predecessor (entries are not guaranteed ordered by the API). + const ordered = [...entries].sort((a, b) => a.timestamp - b.timestamp); + let prevRecv: number | undefined; + let prevSent: number | undefined; + return ordered.map((e) => { const d = e.data; const recvErrors = d.recv_errors ?? undefined; const packetsReceived = d.packets_received; + const packetsSent = d.packets_sent; + // Per-sample deltas off the cumulative lifetime counters. A drop + // (counter < previous) means the repeater rebooted and reset its + // counters, so we emit no delta for that sample rather than a large + // negative spike. The first sample has no predecessor, so no delta. + const recvDelta = + prevRecv != null && packetsReceived != null && packetsReceived >= prevRecv + ? packetsReceived - prevRecv + : undefined; + const sentDelta = + prevSent != null && packetsSent != null && packetsSent >= prevSent + ? packetsSent - prevSent + : undefined; + if (packetsReceived != null) prevRecv = packetsReceived; + if (packetsSent != null) prevSent = packetsSent; const point: Record = { timestamp: e.timestamp, battery_volts: d.battery_volts, noise_floor_dbm: d.noise_floor_dbm, packets_received: packetsReceived, - packets_sent: d.packets_sent, + packets_sent: packetsSent, + packets_received_delta: recvDelta, + packets_sent_delta: sentDelta, recv_errors: recvErrors, recv_error_pct: recvErrors != null && packetsReceived != null && packetsReceived + recvErrors > 0 @@ -179,35 +237,156 @@ export function TelemetryHistoryPane({ }); }, [entries, distanceUnit]); - const dataKeys = - activeMetric === 'packets' - ? ['packets_received', 'packets_sent'] - : activeMetric === 'recv_errors' - ? ['recv_errors', 'recv_error_pct'] - : [activeMetric]; + // Series descriptors drive axes, colors, labels, and tooltip formatting. + // Cumulative counters render as filled areas on the left axis; derived + // per-sample deltas render as gapped lines on a secondary right axis. + const series = useMemo(() => { + if (activeMetric === 'packets') { + return [ + { + key: 'packets_received', + color: '#0ea5e9', + axis: 'left' as const, + line: false, + label: 'Received', + }, + { + key: 'packets_sent', + color: '#f43f5e', + axis: 'left' as const, + line: false, + label: 'Sent', + }, + { + key: 'packets_received_delta', + color: '#14b8a6', + axis: 'right' as const, + line: true, + label: 'Received Δ', + }, + { + key: 'packets_sent_delta', + color: '#f59e0b', + axis: 'right' as const, + line: true, + label: 'Sent Δ', + }, + ]; + } + if (activeMetric === 'recv_errors') { + return [ + { + key: 'recv_errors', + color: '#ef4444', + axis: 'left' as const, + line: false, + label: 'RX Errors', + }, + { + key: 'recv_error_pct', + color: '#f59e0b', + axis: 'right' as const, + line: false, + label: 'Error Rate', + }, + ]; + } + return [ + { + key: activeMetric, + color: activeConfig.color, + axis: 'left' as const, + line: false, + label: activeConfig.label, + }, + ]; + }, [activeMetric, activeConfig]); - const yDomain = useMemo<[number, number] | undefined>(() => { - if (activeMetric !== 'battery_volts' || chartData.length === 0) return undefined; - const values = chartData.map((d) => d.battery_volts).filter((v) => v != null) as number[]; - if (values.length === 0) return [3, 5]; - const lo = Math.min(...values); - const hi = Math.max(...values); - return [Math.min(3, Math.floor(lo) - 1), Math.max(5, Math.ceil(hi) + 1)]; - }, [activeMetric, chartData]); + const leftKeys = useMemo( + () => series.filter((s) => s.axis === 'left').map((s) => s.key), + [series] + ); + const rightKeys = useMemo( + () => series.filter((s) => s.axis === 'right').map((s) => s.key), + [series] + ); - const yDomainPct = useMemo<[number, number]>(() => { - const MIN_SPAN = 5; - const values = chartData.map((d) => d.recv_error_pct).filter((v) => v != null) as number[]; - if (values.length === 0) return [0, MIN_SPAN]; - const lo = Math.min(...values); - const hi = Math.max(...values); - const span = hi - lo; - if (span >= MIN_SPAN) - return [Math.max(0, Math.floor(lo - span * 0.1)), Math.ceil(hi + span * 0.1)]; - const pad = (MIN_SPAN - span) / 2; - const bottom = Math.max(0, Math.floor(lo - pad)); - return [bottom, Math.ceil(bottom + MIN_SPAN)]; - }, [chartData]); + // Brush-controlled viewport. Indices are clamped to the current data length + // so a stale range from a previous repeater can never index out of bounds. + const lastIndex = Math.max(0, chartData.length - 1); + const brushStart = brushRange ? Math.min(brushRange.start, lastIndex) : 0; + const brushEnd = brushRange ? Math.min(brushRange.end, lastIndex) : lastIndex; + + const visibleData = useMemo( + () => chartData.slice(brushStart, brushEnd + 1), + [chartData, brushStart, brushEnd] + ); + + // Y extents bound to the visible window so zooming re-tightens the axis. + const leftDomain = useMemo( + () => paddedDomain(collectValues(visibleData, leftKeys)), + [visibleData, leftKeys] + ); + const rightDomain = useMemo( + () => (rightKeys.length ? paddedDomain(collectValues(visibleData, rightKeys)) : undefined), + [visibleData, rightKeys] + ); + + const handleBrushChange = (range: { startIndex?: number; endIndex?: number }) => { + if (typeof range.startIndex === 'number' && typeof range.endIndex === 'number') { + setBrushRange({ start: range.startIndex, end: range.endIndex }); + } + }; + + const formatSeriesValue = (key: string, value: number): string => { + if (key === 'recv_error_pct') return `${value}%`; + if (activeMetric === 'uptime_seconds') return formatUptime(value); + const suffix = + activeConfig.unit && activeMetric !== 'packets' && activeMetric !== 'recv_errors' + ? ` ${activeConfig.unit}` + : ''; + return `${value}${suffix}`; + }; + + // Custom tooltip so each row carries a color swatch matching its line — + // essential for the multi-series packets view where four values overlap. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const renderTooltip = ({ active, payload, label }: any) => { + if (!active || !Array.isArray(payload) || payload.length === 0) return null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rows = (payload as any[]).filter((p) => p.value != null); + if (rows.length === 0) return null; + return ( +
+
+ {formatTime(Number(label))} +
+ {rows.map((p) => { + const key = String(p.dataKey ?? p.name); + const s = series.find((x) => x.key === key); + const color = s?.color ?? (p.color as string); + const numVal = typeof p.value === 'number' ? p.value : Number(p.value); + return ( +
+ + {s?.label ?? key}: + + {formatSeriesValue(key, numVal)} + +
+ ); + })} +
+ ); + }; const handleToggle = async () => { setToggling(true); @@ -329,12 +508,12 @@ export function TelemetryHistoryPane({ No history yet. Fetch status above to record data points.

) : ( - + - {activeMetric === 'recv_errors' && ( + {rightKeys.length > 0 && ( `${v}%`} + tickFormatter={(v) => (activeMetric === 'recv_errors' ? `${v}%` : `${v}`)} /> )} formatTime(Number(ts))} - // eslint-disable-next-line @typescript-eslint/no-explicit-any - formatter={(value: any, name: any) => { - const numVal = typeof value === 'number' ? value : Number(value); - if (activeMetric === 'recv_errors') { - if (name === 'recv_error_pct') return [`${numVal}%`, 'Error Rate']; - return [`${value}`, 'RX Errors']; - } - const display = - activeMetric === 'uptime_seconds' ? formatUptime(numVal) : `${value}`; - const suffix = - activeMetric === 'uptime_seconds' - ? '' - : activeConfig.unit - ? ` ${activeConfig.unit}` - : ''; - const label = - activeMetric === 'packets' - ? name === 'packets_received' - ? 'Received' - : 'Sent' - : activeConfig.label; - return [`${display}${suffix}`, label]; - }} + content={renderTooltip} /> - {dataKeys.map((key, i) => { - const color = - activeMetric === 'packets' - ? i === 0 - ? '#0ea5e9' - : '#f43f5e' - : activeMetric === 'recv_errors' - ? i === 0 - ? '#ef4444' - : '#f59e0b' - : activeConfig.color; - return ( - - ); - })} + {series.map((s) => ( + + ))} + {chartData.length > 2 && ( + formatTime(Number(ts))} + startIndex={brushStart} + endIndex={brushEnd} + onChange={handleBrushChange} + /> + )} )} diff --git a/frontend/src/test/appStartupHash.test.tsx b/frontend/src/test/appStartupHash.test.tsx index 9288273..627dbe5 100644 --- a/frontend/src/test/appStartupHash.test.tsx +++ b/frontend/src/test/appStartupHash.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { configure, render, screen, waitFor } from '@testing-library/react'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; const mocks = vi.hoisted(() => ({ api: { @@ -151,6 +151,24 @@ const publicChannel = { }; describe('App startup hash resolution', () => { + // App startup fans out several async fetches (config, settings, channels, + // contacts, unreads) that must all settle before conversation selection + // renders. This suite passes 20/20 in isolation, but under the full parallel + // suite (~69 files) CPU contention can stretch that startup well past RTL's + // 1000ms default — and even past vitest's 5000ms test timeout — for any + // waitFor in this file. It's starvation, not a hang, so give this suite + // generous headroom on both timeouts: a healthy render still settles in + // ~100ms (nothing is slowed), only a starved run waits longer. Scoped to this + // file and restored afterward so other test files are unaffected. + beforeAll(() => { + vi.setConfig({ testTimeout: 15000 }); + configure({ asyncUtilTimeout: 10000 }); + }); + afterAll(() => { + vi.resetConfig(); + configure({ asyncUtilTimeout: 1000 }); + }); + beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); diff --git a/pyproject.toml b/pyproject.toml index 70fbb73..8b4dcc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "remoteterm-meshcore" -version = "3.14.0" +version = "3.14.1" description = "RemoteTerm - Web interface for MeshCore radio mesh networks" readme = "README.md" requires-python = ">=3.11" diff --git a/tests/test_community_mqtt.py b/tests/test_community_mqtt.py index be3c72b..eb4b7b3 100644 --- a/tests/test_community_mqtt.py +++ b/tests/test_community_mqtt.py @@ -3,6 +3,7 @@ import json import time from contextlib import asynccontextmanager +from datetime import UTC, datetime from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch @@ -17,7 +18,6 @@ from app.fanout.community_mqtt import ( _base64url_encode, _build_radio_info, _build_status_topic, - _calculate_packet_hash, _decode_packet_fields, _format_raw_packet, _generate_jwt_token, @@ -29,6 +29,7 @@ from app.fanout.mqtt_community import ( _render_packet_topic, ) from app.keystore import ed25519_sign_expanded +from app.path_utils import calculate_packet_hash as _calculate_packet_hash def _make_test_keys() -> tuple[bytes, bytes]: @@ -76,6 +77,13 @@ def _make_community_settings(**overrides) -> SimpleNamespace: return SimpleNamespace(**defaults) +def _assert_utc_z_timestamp(value: str) -> None: + assert value.endswith("Z") + assert "+00:00" not in value + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + assert parsed.tzinfo == UTC + + class TestBase64UrlEncode: def test_encodes_without_padding(self): result = _base64url_encode(b"\x00\x01\x02") @@ -219,12 +227,11 @@ class TestPacketFormatConversion: assert result["direction"] == "rx" assert result["len"] == "3" - def test_timestamp_is_iso8601(self): + def test_timestamp_is_utc_z_iso8601(self): data = {"timestamp": 1700000000, "data": "0100AA", "snr": None, "rssi": None} result = _format_raw_packet(data, "Node", "AA" * 32) assert result is not None - assert result["timestamp"] - assert "T" in result["timestamp"] + _assert_utc_z_timestamp(result["timestamp"]) def test_snr_rssi_unknown_when_none(self): data = {"timestamp": 0, "data": "0100AA", "snr": None, "rssi": None} @@ -734,7 +741,7 @@ class TestLwtAndStatusPublish: assert payload["status"] == "offline" assert payload["origin"] == "TestNode" assert payload["origin_id"] == pubkey_hex - assert "timestamp" in payload + _assert_utc_z_timestamp(payload["timestamp"]) assert "client" not in payload assert kwargs["transport"] == "websockets" assert kwargs["websocket_path"] == "/" @@ -884,7 +891,7 @@ class TestLwtAndStatusPublish: assert payload["origin"] == "TestNode" assert payload["origin_id"] == pubkey_hex assert "client" not in payload - assert "timestamp" in payload + _assert_utc_z_timestamp(payload["timestamp"]) assert payload["model"] == "T-Deck" assert payload["firmware_version"] == "v2.2.2 (Build: 2025-01-15)" assert payload["radio"] == "915.0,250.0,10,8" @@ -1393,6 +1400,7 @@ class TestPublishStatus: assert payload["origin"] == "TestNode" assert payload["origin_id"] == pubkey_hex assert "client" not in payload + _assert_utc_z_timestamp(payload["timestamp"]) assert payload["model"] == "T-Deck" assert payload["firmware_version"] == "v2.2.2 (Build: 2025-01-15)" assert payload["radio"] == "915.0,250.0,10,8" diff --git a/tests/test_fanout_hitlist.py b/tests/test_fanout_hitlist.py index 640352b..722b047 100644 --- a/tests/test_fanout_hitlist.py +++ b/tests/test_fanout_hitlist.py @@ -37,6 +37,7 @@ class TestBotModuleParameterExtraction: path, is_outgoing, path_bytes_per_hop, + packet_hash, ): captured["is_outgoing"] = is_outgoing captured["is_dm"] = is_dm @@ -86,6 +87,7 @@ class TestBotModuleParameterExtraction: path, is_outgoing, path_bytes_per_hop, + packet_hash, ): captured["is_outgoing"] = is_outgoing return None @@ -132,6 +134,7 @@ class TestBotModuleParameterExtraction: path, is_outgoing, path_bytes_per_hop, + packet_hash, ): captured["path"] = path captured["path_bytes_per_hop"] = path_bytes_per_hop @@ -180,6 +183,7 @@ class TestBotModuleParameterExtraction: path, is_outgoing, path_bytes_per_hop, + packet_hash, ): captured["message_text"] = message_text captured["sender_name"] = sender_name @@ -228,6 +232,7 @@ class TestBotModuleParameterExtraction: path, is_outgoing, path_bytes_per_hop, + packet_hash, ): captured["channel_name"] = channel_name return None @@ -275,6 +280,7 @@ class TestBotModuleParameterExtraction: path, is_outgoing, path_bytes_per_hop, + packet_hash, ): captured["sender_name"] = sender_name captured["sender_key"] = sender_key diff --git a/tests/test_packet_pipeline.py b/tests/test_packet_pipeline.py index c5e0910..3e58f8c 100644 --- a/tests/test_packet_pipeline.py +++ b/tests/test_packet_pipeline.py @@ -74,6 +74,12 @@ def _build_path_packet( return header + payload +def _build_ack_packet(code: bytes, *, route_type: RouteType = RouteType.DIRECT) -> bytes: + """Build a standalone ACK packet whose cleartext payload is the 4-byte code.""" + header = bytes([(PayloadType.ACK << 2) | route_type, 0x00]) + return header + code + + class TestChannelMessagePipeline: """Test channel message flow: packet → decrypt → store → broadcast.""" @@ -763,6 +769,93 @@ class TestAckPipeline: assert "ack_count" in broadcast["data"] assert broadcast["data"]["ack_count"] == 1 + @pytest.mark.asyncio + async def test_standalone_ack_packet_marks_message_acked(self, test_db, captured_broadcasts): + """A standalone ACK RF packet satisfies a pending DM ACK from the raw feed. + + Direct-routed DMs are answered with a standalone PAYLOAD_TYPE_ACK packet + (vs. the PATH-embedded ACK used for flood). We must match it straight from + the raw packet so delivery confirmation does not depend on the radio also + emitting a separate EventType.ACK host control frame. + """ + from app.packet_processor import process_raw_packet + from app.services import dm_ack_tracker + + code = bytes.fromhex("01020304") + raw_packet = _build_ack_packet(code) + + message_id = await MessageRepository.create( + msg_type="PRIV", + text="waiting for direct ack", + conversation_key=PATH_TEST_CONTACT_PUB.hex(), + sender_timestamp=1700000000, + received_at=1700000000, + outgoing=True, + ) + + prev_pending = dm_ack_tracker._pending_acks.copy() + prev_buffered = dm_ack_tracker._buffered_acks.copy() + dm_ack_tracker._pending_acks.clear() + dm_ack_tracker._buffered_acks.clear() + dm_ack_tracker.track_pending_ack(code.hex(), message_id, 30000) + + broadcasts, mock_broadcast = captured_broadcasts + try: + with patch("app.packet_processor.broadcast_event", mock_broadcast): + result = await process_raw_packet(raw_packet, timestamp=1700000300) + finally: + dm_ack_tracker._pending_acks.clear() + dm_ack_tracker._pending_acks.update(prev_pending) + dm_ack_tracker._buffered_acks.clear() + dm_ack_tracker._buffered_acks.update(prev_buffered) + + assert result["payload_type"] == "ACK" + + messages = await MessageRepository.get_all( + msg_type="PRIV", + conversation_key=PATH_TEST_CONTACT_PUB.hex(), + limit=10, + ) + assert len(messages) == 1 + assert messages[0].acked == 1 + + ack_broadcasts = [b for b in broadcasts if b["type"] == "message_acked"] + assert len(ack_broadcasts) == 1 + assert ack_broadcasts[0]["data"] == {"message_id": message_id, "ack_count": 1} + + @pytest.mark.asyncio + async def test_standalone_ack_packet_with_no_pending_is_buffered( + self, test_db, captured_broadcasts + ): + """An unmatched standalone ACK is buffered (for late registration), not dropped.""" + from app.packet_processor import process_raw_packet + from app.services import dm_ack_tracker + + code = bytes.fromhex("aabbccdd") + raw_packet = _build_ack_packet(code) + + prev_pending = dm_ack_tracker._pending_acks.copy() + prev_buffered = dm_ack_tracker._buffered_acks.copy() + dm_ack_tracker._pending_acks.clear() + dm_ack_tracker._buffered_acks.clear() + + broadcasts, mock_broadcast = captured_broadcasts + try: + with patch("app.packet_processor.broadcast_event", mock_broadcast): + await process_raw_packet(raw_packet, timestamp=1700000300) + buffered_after = dm_ack_tracker._buffered_acks.copy() + finally: + dm_ack_tracker._pending_acks.clear() + dm_ack_tracker._pending_acks.update(prev_pending) + dm_ack_tracker._buffered_acks.clear() + dm_ack_tracker._buffered_acks.update(prev_buffered) + + # Code is buffered so a slightly-later send registration still matches it. + assert code.hex() in buffered_after + # No message exists, so nothing should be marked acked / broadcast. + ack_broadcasts = [b for b in broadcasts if b["type"] == "message_acked"] + assert ack_broadcasts == [] + class TestCreateMessageFromDecrypted: """Test the shared message creation function used by both real-time and historical decryption.""" diff --git a/uv.lock b/uv.lock index 2d2e503..e55b19c 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.5" +version = "3.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -22,95 +22,111 @@ dependencies = [ { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674, upload-time = "2026-06-01T19:41:02.763Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, - { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, - { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, - { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, - { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, - { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, - { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, - { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, - { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, - { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, - { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, - { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, - { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, - { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, - { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, - { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, - { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, - { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, - { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, - { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, - { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, - { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, - { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, - { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, - { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, - { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, - { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, - { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, - { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, - { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, - { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, - { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, - { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, - { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, - { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, - { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, - { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, - { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, + { url = "https://files.pythonhosted.org/packages/67/47/7727bfe8db93f8835a001bd4359d8480cc68d1259b8bce334668f8be97bd/aiohttp-3.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:54bf3522d6f7351e55f89a62d5c2bf138ad557b031670266c5df604ae88e0b5a", size = 759147, upload-time = "2026-06-01T19:37:12.918Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f2/cd3fedff6fade73d71df9ec908c210cec518ef90fd00289250684b90aecf/aiohttp-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0746d9fb0ac4fdef643a84494efe3f06d50335dd8c7a530228b86448aae0a803", size = 513705, upload-time = "2026-06-01T19:37:14.633Z" }, + { url = "https://files.pythonhosted.org/packages/5a/fe/49746b6b610144a06323bebd8e1211a390310d8c69b98dd6d52df341bc3e/aiohttp-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f3a96b6d39a4872222beee72e1df41d2ff886ae96152cf3e757ef8c5673ef0e", size = 509627, upload-time = "2026-06-01T19:37:16.385Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3f/28f2f6cf3d5c0e7b01b27140d0e7873fd11fb341169ad3ce78ad04aba628/aiohttp-3.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d336820adbb914debbc90a1d8c1bfc4bea55996aecf64866a989d35d1f9fd903", size = 1769293, upload-time = "2026-06-01T19:37:18.067Z" }, + { url = "https://files.pythonhosted.org/packages/97/6f/2e5f1b525d5474b12b3c60abf733a755845f3bceff21542081ada515f837/aiohttp-3.14.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:71b2604c9bfc1b115547d63a094d5244b3f02799833513a99a68aaa7b167c4cb", size = 1732363, upload-time = "2026-06-01T19:37:20.138Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ce/596120faa85ca7b19cd061e3f2f3be23aa8f11a0aedf9191db9e0da1bd76/aiohttp-3.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:610d68800435903e303ca0542b9d3e4eb72a12ff33a6d471a070c1d81eebd3c2", size = 1840375, upload-time = "2026-06-01T19:37:22.104Z" }, + { url = "https://files.pythonhosted.org/packages/72/3c/a7ffe05a757a4a7867643da69357ec41f506879fbd1b231d2ed90af246b2/aiohttp-3.14.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:514db9a79337068981ee2137310283a07b4b885c584991097a91a4da419bcb81", size = 1921484, upload-time = "2026-06-01T19:37:24.068Z" }, + { url = "https://files.pythonhosted.org/packages/93/fa/2c861170bbd4a491de93a69e081db1d971092569e0d593a98ef62c384dc1/aiohttp-3.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c452d17eeb95d563fc8b936f3050301dbd1d268126c4632d8b70ede9696202ee", size = 1774153, upload-time = "2026-06-01T19:37:26.256Z" }, + { url = "https://files.pythonhosted.org/packages/9d/da/1d2f5a165f47ec9b1f69d37b8b977fdc4d501aa72ffb7930db27bb9e49ea/aiohttp-3.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed94a81506e3d1bdbad5108f497a58f2a2354aedb4ca314d5326f07d1fd1ac2d", size = 1632569, upload-time = "2026-06-01T19:37:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/46/1d/7a6e295c4257252f70f69e90864fdad74b6a1293054fb3f9e65a15de6d63/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1394dce36e0f0d260ac0b555a654de19cb989f3c1b8bdd24f505314dfea18a00", size = 1740325, upload-time = "2026-06-01T19:37:30.08Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/e1899b1ca3ec62f1eab2a5cbde14039b97493f7f53eb88d9b668562ffa8d/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d1467d1e7b48a73ca7237e0ee4335f3d02b923dbc27b82fd254bc301c97d4026", size = 1748691, upload-time = "2026-06-01T19:37:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/ec/54/4e6b61c1fe7d3433f82bcc6bd7e4d7c683a742a10c9b12a025fd3695c047/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5f3532125233c261cf61f32df4059cfcf482eb793c7d3db8452e3142028b86", size = 1814477, upload-time = "2026-06-01T19:37:34.173Z" }, + { url = "https://files.pythonhosted.org/packages/9c/38/86fd51be2e08d8e45c83d879d255f10391903cd9fe2a16512f7591a15873/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3ea81eb518a2ecb319d8ec6d1424a37c773f6634bd87d6985eb606b2faac419f", size = 1623393, upload-time = "2026-06-01T19:37:36.281Z" }, + { url = "https://files.pythonhosted.org/packages/78/49/466e947a42a88ee23c486d036e7e5d1b097f1bafd8084ad9c9a0a92f0f43/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:32e735c3182de7b64f6941a4ede48b38c7f47d9437bd615dd30b5bda8fa1bc93", size = 1824097, upload-time = "2026-06-01T19:37:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/f3/89/35f3410bc284682338a1be6b6ea0c5abfa05f063942cfaa9256608440434/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c21ca9a1c63d4509158f478aeb9d02914dcc52adc68d1bc9dee2452284ee5996", size = 1764790, upload-time = "2026-06-01T19:37:40.755Z" }, + { url = "https://files.pythonhosted.org/packages/42/80/2d4291bd5724d3d17e5951aff5a3e02281483fb47295f0788276ee66cd73/aiohttp-3.14.0-cp311-cp311-win32.whl", hash = "sha256:19ca5fc84130675ba11c6ca5c7da5cb65f7bf8a32cdd2b616bf49cd334688aae", size = 454176, upload-time = "2026-06-01T19:37:42.837Z" }, + { url = "https://files.pythonhosted.org/packages/59/ed/41d0ad4f6ececffc32bdf1f7b494e5498f7ca5c849ea2e3cc9bbd1668251/aiohttp-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d488e6e9d3bb8ba5ae7066d5be885ae9670eba021b8c6ccb9a3a568e6b19d6e5", size = 479334, upload-time = "2026-06-01T19:37:44.776Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/c0b5e305c770053f8c3d069bb52b8196917ba91949d1962d52eb307fb0d2/aiohttp-3.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:8b93618102caf12801638a01a2b478a55410ddd71bd41cfaf6f707953a49ac43", size = 450262, upload-time = "2026-06-01T19:37:46.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/97/2b6889bfb6b6847520d50d95eb8c4307a45e28aaca39faf4a9454b3d1b2f/aiohttp-3.14.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b29518c9c2ec7e373e68259206a137c7f4f5439c58baaec4b5ab3ab799850a4e", size = 750194, upload-time = "2026-06-01T19:37:48.164Z" }, + { url = "https://files.pythonhosted.org/packages/21/e2/62634b7fff918ed98c3c6b2f0e70d520f7f28846cb412d451b04354c6459/aiohttp-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbec68ce61b64cb73cab4d33df9433427b1713c8bcccb181dce695c1b6f8e87c", size = 506966, upload-time = "2026-06-01T19:37:50.014Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fb/5ce075150828c797a5106f1c2fb26034e709d4289b9d2bf8b07f1e59fac6/aiohttp-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cdf534aa455593e589302990c5097aa5c92c06c4262a20da22934f9186a5fff", size = 507527, upload-time = "2026-06-01T19:37:51.96Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/405a0ae4e6b081754a3609c1c97c63a950e000a2def16046f1e736933a0e/aiohttp-3.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb6c657104393b5fbff01a5f59b2023db74058a8077d94475d6c25d03882a108", size = 1762420, upload-time = "2026-06-01T19:37:53.839Z" }, + { url = "https://files.pythonhosted.org/packages/ae/1d/e05a7c896b15a6bc6fb8fc5319eb437861c2c49c34559ef928add6590315/aiohttp-3.14.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:46fbbec4e4fab7428d4396a3823f9320e4560aa3113b89eeebce712c27c9ed5a", size = 1733672, upload-time = "2026-06-01T19:37:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/cc/22/a72f7c459e195fa41bf4f7abd1f925b91fe91f8097e51c654229ba144a33/aiohttp-3.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c2c7e05dd5335b298085abf45ddf98673934c3ee1c083d0b9ea13d4186ad500", size = 1805064, upload-time = "2026-06-01T19:37:57.931Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/e85bdaba0be59ca4838005ebfef4048fcdd5f35a02b07057a9a123394440/aiohttp-3.14.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c7139100fbaae76515b73051d8f0aa3a3ff02e415eec8a8eee8e2223d9ba955", size = 1902125, upload-time = "2026-06-01T19:38:00.225Z" }, + { url = "https://files.pythonhosted.org/packages/19/d8/51de5c6b971c27bb1ef620293b8d1ca611ec78736b34b3f6ccf68e4c8785/aiohttp-3.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d6f9286a629ce52728430afe18f8ed2b6c39a1fddb3802d7244b9983910ad2", size = 1783112, upload-time = "2026-06-01T19:38:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b4402bfde77e43dfb1b6ccff83c7b7ab63ed06b50c4754f0c5423fb374fe/aiohttp-3.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3c3e12cdaeb92d7dcf13db00e9f6b1956b910e47256e696df1cfa946d02159", size = 1586356, upload-time = "2026-06-01T19:38:04.637Z" }, + { url = "https://files.pythonhosted.org/packages/bc/05/750a3265ca4dc54a460bd0cb1121a8f2ce9171fce4a135fb47ea7fd594d2/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d6a998191f5ebe3b8c28463ff72bc030250008b3193c402464efadd08b5ca02", size = 1723119, upload-time = "2026-06-01T19:38:06.713Z" }, + { url = "https://files.pythonhosted.org/packages/37/01/8c0812c50b3b1b1c37b323bf170d6be8847a8f234060485b7d1e71953f60/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0fc2b75ae8d169d853be2862d960be8550da6c5c65711d5476407eb3fdb006bd", size = 1757216, upload-time = "2026-06-01T19:38:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/2a/50fb98028a26887cbe48dcc1df92a90825615bc73b5584301304090cded8/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:16eee56bcc72d04600bc56c1759982c2385ec0b41d3fd3521f836bf64a0957ef", size = 1770500, upload-time = "2026-06-01T19:38:11.111Z" }, + { url = "https://files.pythonhosted.org/packages/bd/32/0ffd598a2fa2b9a423daf242e700cfdabda35d6e602394ad9ae58972c1c7/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5a2e7ca615c3ddc15b82687e05a624e5f5cba3f1d6c20cb81172d70ea498451e", size = 1576224, upload-time = "2026-06-01T19:38:13.391Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f9/b9fc381dd9b66afb33f2634c40e229d106467be0afcabe79648631ab6712/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0b7b8bbbec3ce9467ee0ebe334622fd90624f593edd3136c567811453fc4fae", size = 1794252, upload-time = "2026-06-01T19:38:15.498Z" }, + { url = "https://files.pythonhosted.org/packages/a8/fb/05d9214c975f23225a8cd5c439325e338c7c377b315480ef3871db51f54e/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ba10966d4f03dd96a14365be4b8e37c327c76f11c3ca867116966cdd9f98066", size = 1760193, upload-time = "2026-06-01T19:38:17.624Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4b/02992fc4fb9e1b6673ee3f888a8e587a6447afda1f6f4aca776c148c2876/aiohttp-3.14.0-cp312-cp312-win32.whl", hash = "sha256:101df7779c80c0636014a6b2c6642acd3efb5b355d48347c9d7dfb720aee9430", size = 448650, upload-time = "2026-06-01T19:38:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/246532214c3abda518477cbaaf16d420295ad8effa5233844cbb38f299ab/aiohttp-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:b0a5747586d4467efd1f932710b269131c9717a872dce082cd92a00c1c13123a", size = 476145, upload-time = "2026-06-01T19:38:21.505Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c3/63f8c20090048915711598b0adf475b149216d736157961de06480a45b15/aiohttp-3.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:5f1c5be60add78fabb4aacd13c5a348ae79d2fcbfc7fa78da8f1eb192273b370", size = 444250, upload-time = "2026-06-01T19:38:24.027Z" }, + { url = "https://files.pythonhosted.org/packages/21/61/d11f7d9a3144bffe825247d6367cd93053666da50b94707c9129c78868d5/aiohttp-3.14.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:25400d710641a8040bf022a8a99f579e581ffa1c5bd42c33255d7d6f3957c127", size = 502399, upload-time = "2026-06-01T19:38:25.955Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9b/a7e317625d36356844f8bb022cabd305b541f968856cc3c2e0b58e53ee6e/aiohttp-3.14.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c5492b9929826e07cc3fcb9739ae87aab05dff6b5e67a9b73fd1700c6d008981", size = 510068, upload-time = "2026-06-01T19:38:27.828Z" }, + { url = "https://files.pythonhosted.org/packages/11/41/cc2d2cfbfbdc3126ba258f3cd27d1ac8a33492ae3c35a4583ee21f0ba7f1/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3366751d68d237c621264233a32f3078bbc21b7904ab90a77e03d21390c742c6", size = 481670, upload-time = "2026-06-01T19:38:29.836Z" }, + { url = "https://files.pythonhosted.org/packages/3c/07/381f4023c3b08cb616e520f566d8c58957abad54e56441d41fe67cfb0195/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:57ea07d28695a7a40304d42251892a8df765e5588c10ee32afeddcd5df33c0a2", size = 487591, upload-time = "2026-06-01T19:38:31.704Z" }, + { url = "https://files.pythonhosted.org/packages/fb/4d/4506fdb7a022bdf70011a3bbb4ca00c5c570026ef6a3c5bd7bc70c39089c/aiohttp-3.14.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:076cb014191ae2e65d949e1ad01f1dcfe33e32789b5172510f3e79c79fc04d50", size = 496503, upload-time = "2026-06-01T19:38:33.6Z" }, + { url = "https://files.pythonhosted.org/packages/ef/7d/c814111e04894a45d9e2defc94443879a6f118d9633d5fedfe6e2e8af5f0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f3fc37054564dee64a855b5b092d87ec35dcddfaabf7dacb1c8a2b1f83dc0a9", size = 745870, upload-time = "2026-06-01T19:38:36.013Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ee/80eee0efddfe187e7cd05027086b7ce1c0e492e82a4eda58f5c5543a44a0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8fcaef74d2ab0f607d7ff85a0d15e21bb5a258c4a58df1908396eb50d7f4ed3c", size = 505588, upload-time = "2026-06-01T19:38:38.282Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f8/0f28f04eef75d52fc9c715dde7ce9c0abb810fd20cfeb0fea7afd2ab1e98/aiohttp-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4c01b0bfc6209590960e68eac083cd22d5d87c21f974dd6208cafa5d3542bc8", size = 504492, upload-time = "2026-06-01T19:38:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/ff/db/44c755232085545065c94378dfce38641b1aee647f4939fcd32f5b32e719/aiohttp-3.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f12eb7896e81caf403a2b18c9406426f1207361e7239c057ab29c076d4257e83", size = 1752111, upload-time = "2026-06-01T19:38:42.682Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6a/42e030a46743841414402a3b00cd3d78419055e86c66fb5822c14b5abfc6/aiohttp-3.14.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c79a044cacf360ec46738d863d2f41c9300d2a06ef4a7402ea0df306a350e61", size = 1729674, upload-time = "2026-06-01T19:38:44.79Z" }, + { url = "https://files.pythonhosted.org/packages/34/26/3199beb415202e3108e7b83ecebe10914d806d33fb9860c3e4aa60a19be3/aiohttp-3.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85e0675f47be4eff0636bf88c02140ea89168ae0df3ff1f3f464e9de9610d277", size = 1798808, upload-time = "2026-06-01T19:38:47.01Z" }, + { url = "https://files.pythonhosted.org/packages/bd/94/b9b6fcf0ee17c21d0d19fb8c22bf83ad18f82e702a9c3bd901a868f5e446/aiohttp-3.14.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b33e751cab03fdc960095b1e326cb5a03f5ee577d6ded59f3d1c100f8668882", size = 1891921, upload-time = "2026-06-01T19:38:49.233Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a3/3800dbd095cb2bb165a7ea5d94d790914677e27f45638c7d80e3f34c8945/aiohttp-3.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9224c6dd7f5c749aba4f61315a894601448b28d94d12f4dea0903e26d2096", size = 1777241, upload-time = "2026-06-01T19:38:52.04Z" }, + { url = "https://files.pythonhosted.org/packages/21/2a/45be91ad1b860508557448d4cc2e165a2ee68dd865657b73bf66cc5a00fb/aiohttp-3.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6281aecdf2732940f4fe06bd6adec5ae4d59b78b080b8e3a6b81467301010988", size = 1579554, upload-time = "2026-06-01T19:38:54.508Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/dc94df99ed1511fdf28314f722643ed334112643cab00223577085e788c4/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23e8314e7aed8576fbe33314d218bd81447a3adbc91dc36f1163bf583cd3084c", size = 1714864, upload-time = "2026-06-01T19:38:56.788Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e4/1f1c8acbb3acd5c8f795473b92c9c3d44eb60a5692c6104256c8a1c83a0c/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3b54fbff46127aeafdd764cecd0d99fa2f24a0e37ea5c18a7c3a4ac450df1db3", size = 1749803, upload-time = "2026-06-01T19:38:59.367Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c8/c45ea6e7ed84cebba939b9c334498a045ba19d79c61b0110df5f21580de3/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b27d89af91a555f58e08e4902dbcbc48862fd40095720ca705990476bd93b7ac", size = 1765023, upload-time = "2026-06-01T19:39:01.651Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a1/a932941784432962fe390e1066823aaef64b4e5ac9fa595df57b5fe472a9/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:25d2326a4967bf705a9f9913a13005e93b6020ad8a9f6bd6bd78850d5171332e", size = 1571671, upload-time = "2026-06-01T19:39:04.044Z" }, + { url = "https://files.pythonhosted.org/packages/b0/01/e1280feac522597a4d46eb67a0cdfa053cfae263033030b761ab146f29fb/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a1d209375c503472b3c0a340cdf3c55fcd82e84b46dda7caeaced59faba373ec", size = 1789904, upload-time = "2026-06-01T19:39:06.294Z" }, + { url = "https://files.pythonhosted.org/packages/fa/10/ab28818262f4d26bdb47ed5f1fc7999b69e2fc6e0370b02d0f49011f45ea/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:666c7c5036df57b693026398b69b41874a1931ac5b3485fd910e57bfac253869", size = 1754516, upload-time = "2026-06-01T19:39:08.788Z" }, + { url = "https://files.pythonhosted.org/packages/af/cc/c122eabd7a1b7e0c9bbdd6be60e4715905b858399145d9df872bb94f1427/aiohttp-3.14.0-cp313-cp313-win32.whl", hash = "sha256:23f094a1ef64823fd35854ddf5c7a80a078162f37f9d2f7c6142b51a6affa456", size = 448656, upload-time = "2026-06-01T19:39:11.171Z" }, + { url = "https://files.pythonhosted.org/packages/41/a5/bab07d79848a00eedd8ed979ccb302aaea3ac6eb9fa16bd0ed87135869b4/aiohttp-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e03abdaa17d553f17e1d1d06bb266b3970106c78051d06795723e748d8e49d11", size = 475803, upload-time = "2026-06-01T19:39:13.439Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/f03ade8566c153666a3871afccbedf6d99911da006325e1fc6cf72a2de99/aiohttp-3.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:acdb400538cf4769543548bb5d1eb23d39bed4f96554a6078cb728c7cb2c268b", size = 443889, upload-time = "2026-06-01T19:39:15.945Z" }, + { url = "https://files.pythonhosted.org/packages/28/03/5f36ab196a88ba5e9648ae5643e6531e67a3a8c0e96f9c6510ff41540fec/aiohttp-3.14.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:363ef9e91014e7891679bfb2ac0a7c6ea93435dbbfd10ecf41b9f06fcf506c5f", size = 503330, upload-time = "2026-06-01T19:39:18.195Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ce/8b49ec2f30f68e02f314f4832186cd45e583360a5a386058be36855d23b6/aiohttp-3.14.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:884a4edbdad77be9d0ef36142c8b504351b170df0bf62b51e784fadabf311c42", size = 509822, upload-time = "2026-06-01T19:39:20.396Z" }, + { url = "https://files.pythonhosted.org/packages/1a/fe/6edbf5d39bf29322b6816365b17ed8ede4dace164a3aea1abcd30110eb78/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:70ea956f6cc4a37620966b56c2e205d88ca3e6d85ec063277e414b1035cddad3", size = 483329, upload-time = "2026-06-01T19:39:22.607Z" }, + { url = "https://files.pythonhosted.org/packages/1b/5a/fae531bdbc6456fb6241f46b7b81e4d8a0dd3fc09118a0055dc7141ac1ec/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:ea3b9806c89f61da22fddf1f12dd524fb368e5e28f1261fbdafe5c3cd8ce893b", size = 489502, upload-time = "2026-06-01T19:39:24.881Z" }, + { url = "https://files.pythonhosted.org/packages/36/f4/48a7b0414db7fed77a03d5dde34508c026afd83510ab6bca08c313855776/aiohttp-3.14.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a071be341c2bd9b0188e62d173509f024e0a35b1c342c53c50f8daaeda8c3bd8", size = 497357, upload-time = "2026-06-01T19:39:27.197Z" }, + { url = "https://files.pythonhosted.org/packages/75/75/e85a13a370acc007fca5feb1fd1b88ac2d8426e6dadd625479b7cadd55a3/aiohttp-3.14.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:198cfe61bf253b19da1fb3e0fa122249dc4f14c12709493fed8054aa0411cc76", size = 750898, upload-time = "2026-06-01T19:39:29.563Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e4/3d637f800c724eff0e2bed64df72557444482366fd0a35b0cec0e6968f6c/aiohttp-3.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc203d6ce6b9106d54e2a93f41dfdfebfbca2d99962ba503bfd3e5921a6549e", size = 506986, upload-time = "2026-06-01T19:39:31.872Z" }, + { url = "https://files.pythonhosted.org/packages/1d/df/35161f3598bf7501d2b2a805b41ab4f45a2e34150c421bcb4ef8c0d281a7/aiohttp-3.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e19d17ab02bf16832a2c8c0d55a486792c5b1645665652ee9531aebcc30cb72", size = 508033, upload-time = "2026-06-01T19:39:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/e5/39/b36e5d3d31e850fb4691dd3e941684ac490a2559249f6fa634b6b0fdf020/aiohttp-3.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d925fba0c14d5b498a8028b0107beebdfd16c5d48d702ff54f879cb017aaaca3", size = 1746213, upload-time = "2026-06-01T19:39:36.654Z" }, + { url = "https://files.pythonhosted.org/packages/b1/28/24e1409e605a9aa5d84abe0e2acb365354b70ae56d40948101cabe3341ab/aiohttp-3.14.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d33e61021222ce7f9792bcac870d6f58d8adfceda33ab857b01264f4560f2c5f", size = 1705862, upload-time = "2026-06-01T19:39:38.968Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d0/e5eb3ff1daeaf644c7e36a957517672494122628e067c38b263fa04eda77/aiohttp-3.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:44eca38755d0105bb32f47d085f5dd449846a449e1245fc105889e3279dcf8e3", size = 1798909, upload-time = "2026-06-01T19:39:41.334Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ba/8943f906f0570342886ababb9a722a44e360f786a028c5e0b0e29e3f735b/aiohttp-3.14.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f13087e06f68fea4941c21a0c541c00553aa16e4f8fd7bbe2b198df761e964d6", size = 1868892, upload-time = "2026-06-01T19:39:43.807Z" }, + { url = "https://files.pythonhosted.org/packages/3a/05/27df32c844b2156e1675a8d8ec22d963e3c8ba469ed7ceb1863320c7b521/aiohttp-3.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff82be7f1ef73634cb77890a770743239bc3d487b848669be1c599889336dc0a", size = 1751659, upload-time = "2026-06-01T19:39:46.398Z" }, + { url = "https://files.pythonhosted.org/packages/7f/62/da182e5910ab912b2e88aa919b61a16046a37a95714a5795b02eb57b2d18/aiohttp-3.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a150c0875ac8fd87f1c398650841308a30d65facf7416b12dbdb9cfdcbe5a48c", size = 1578775, upload-time = "2026-06-01T19:39:48.902Z" }, + { url = "https://files.pythonhosted.org/packages/66/e3/53c67097e8a5ce98625e91e3fa7f43c9c6940de680345d03b3509a72a078/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:edc01ea4e1ec5a1649a28866262bf24195889ff7b27bdd947029a6086741de9b", size = 1710090, upload-time = "2026-06-01T19:39:51.392Z" }, + { url = "https://files.pythonhosted.org/packages/dd/55/0e2732ca598c7a4dfe8a775662376d0ca2977cb1030e48386d4da5d9a456/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:540632bf882ff8fc88f2e1697be0761578e89e0d79fb4a8a6d65dc5da7e729d4", size = 1715016, upload-time = "2026-06-01T19:39:53.807Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/f0b73730798c9ca525afc30b39f1f81bbe24e245d9654c54d3b39d63212d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:860a86bc2c80237f5dff52edcf427e10a8d8352271fd84845429a3e60199e02c", size = 1763810, upload-time = "2026-06-01T19:39:56.31Z" }, + { url = "https://files.pythonhosted.org/packages/71/cc/11acb6c4518f448323405a7312b6f255d0f974a34373ad1db7633c4aadc8/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cbd50e6a50d6b99283a826b18cbdebf65b0797689a7535cb0e9dd37be0f63c3", size = 1573064, upload-time = "2026-06-01T19:39:58.718Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/28c31dde0a7dc98c0ee7d0da2ddcec3f7688c4fc131e5989e278d0c03c0a/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20144819e99db593e22bbd2f3f2691a5e149f879142d6b8670254708853ff4fb", size = 1775765, upload-time = "2026-06-01T19:40:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/b8/69/155c4ef3aec96417d47024800472b33b16c5d8a665371dcd044c2afdf25d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:26b6d79aa54cb4ed50cc7d41ed14e99e0f1fc8e7c2d42f2e05b37aea897b2b52", size = 1733716, upload-time = "2026-06-01T19:40:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/5f/44/6126116fd8a316b712bb615660b855c78466bb67ba1bb1742427eafcf7ac/aiohttp-3.14.0-cp314-cp314-win32.whl", hash = "sha256:106ed074a856f3e21d186b8579e2c8afb6da598e267cdaab01059e13db2fc44d", size = 453684, upload-time = "2026-06-01T19:40:06.277Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d7/eff4c58a88c5cac5e38b55f44fb8a6d3929c3cbd77356e383e094d3220bd/aiohttp-3.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f770846edae8f00ecc57af825bce811f787f87a7dcf0e90d191790efe5b31f7", size = 481758, upload-time = "2026-06-01T19:40:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ed/17b5bd9fbcb46e688f02e572f517754a9a75831e7b54702f027761dc4fa5/aiohttp-3.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:acf1581c4f21ed4b80a2dded504d87b055a071a84d5737ea966435f768275ac6", size = 450557, upload-time = "2026-06-01T19:40:11.03Z" }, + { url = "https://files.pythonhosted.org/packages/12/34/6180103ce9aabc8ebff3f7bb55a1228ffe60f61042823031d9692cb7b101/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6aa1a40f9cbb3da9f80714c5966b8946c21e6a2530d809b9498b33161e3c8733", size = 787878, upload-time = "2026-06-01T19:40:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/92/e9/08954a40e8b7baa3d8beadd2b074b186e9b1e9c8ddabc288678a6265de50/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b62af5a8cc96a194eaa01a9ed7b34a3ffa58d3d8daaa1a0d7a749353ad12d228", size = 524400, upload-time = "2026-06-01T19:40:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/08/6a/b5965a634ac4d5ba99a463314cf4ab214ca073fcdc38a15e0294273701fc/aiohttp-3.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6eb63b1417efaf7d1002a6ad034a40d44376afcc16508a57f8e74b49ad26a095", size = 527904, upload-time = "2026-06-01T19:40:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/06/b4/932bcdd850c354d9bcca30f360e475d7852e30413fbbd44b182782ed5432/aiohttp-3.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c20b9ad156a79eb97be5cf9e069eec01d2f0dc8472ffbd75299a8b2d4c2cbbde", size = 1912162, upload-time = "2026-06-01T19:40:20.825Z" }, + { url = "https://files.pythonhosted.org/packages/c6/85/ce79bab0310d2e3fd2d7bc7e44412abeff7c8338f8a21dd0f2f1714989e5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40ae7b0642c25632c7eabc4a04754012691864d2a1b93becf7cddb76027b838a", size = 1778813, upload-time = "2026-06-01T19:40:23.726Z" }, + { url = "https://files.pythonhosted.org/packages/05/54/ba62ac2d1bc87e010aad23751e383b8794e45d931df67677313a2da78823/aiohttp-3.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95f5217e76a046b9f228a101717ef8d42b1eb3d9d196d15202db5bf41df88936", size = 1899969, upload-time = "2026-06-01T19:40:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/dc/82/7cc7907725d83a19f31551334061e1ab8e108b1d7ac52632a2a844a4acb5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a4a9f17e85b80878c176695c1998c790e83731d8271881e5d356488652a1f9e", size = 1991771, upload-time = "2026-06-01T19:40:29.061Z" }, + { url = "https://files.pythonhosted.org/packages/d0/1c/a57de71a4508c93a830b77c28af3d08cd97f606dedfc6b94275347744508/aiohttp-3.14.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:145262119b07d7f95abc1839add35ba2bfc84551d4b4660ca11542c0b215455b", size = 1868606, upload-time = "2026-06-01T19:40:31.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ae/3839726cd49150a53ed340cc24ce5ba09d4c2117020ef9d45542bec5eb2f/aiohttp-3.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:49a33ded29b0b2fa7a367a02cf0fb89af602bb87542a16177ec8ce1c9c51d12a", size = 1665437, upload-time = "2026-06-01T19:40:35.01Z" }, + { url = "https://files.pythonhosted.org/packages/35/1e/c237923232c7da7f0392ea25d89fc5e60c0e93f685f4ebca8e7bcdd5271c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cc736a9c9fc2bc4dd71fd404815741b6573df27c3f985948ec4076989ac57de", size = 1834090, upload-time = "2026-06-01T19:40:37.733Z" }, + { url = "https://files.pythonhosted.org/packages/98/02/a5a7a2524f92d3911761b405a7c067c751891942144adc13e2ad79611e39/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4141a3e5342ee3053a9cab54d25b64ed28289c1041e4c54b3d99839314d90ce", size = 1816907, upload-time = "2026-06-01T19:40:40.46Z" }, + { url = "https://files.pythonhosted.org/packages/fa/76/a8b9f0d09234d516af9f2d7dd715557f33b5da3b0b56ead41d1170e86e3c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e30871b2d58996cb81aac52d2b1d15ac05257131ef0f90f18c2115a380fbfe7c", size = 1840382, upload-time = "2026-06-01T19:40:43.48Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8e/140e715a0a4bbc211979ea30ec8396ad2ed5bf90ab87d8058fc4668b1923/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:667b881d083ccae3900ea5a241e17e5007ca78844c53ed389bb63d48f729d9c7", size = 1659497, upload-time = "2026-06-01T19:40:46.265Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/7ba5de8af9650b9767b063c675427b8685f43fa7ce563673a7bc3af60f08/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:b584dfe615d151e9b8f0a8ecb3aee6147f2927ec5b95ba25fe621f5377510928", size = 1870829, upload-time = "2026-06-01T19:40:49.583Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bc/2aaab2f85cadb26ea59c091fa2b8e370d625154b5c14b478f1b489d07551/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6199707cc40e0e9cd39c36fbc97bec416c704e1d0ddce03412bb3b3e6a90ccd0", size = 1832281, upload-time = "2026-06-01T19:40:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/39/98/31b9ad9fbc01f0075ee7221002df5fd2d10b647f451ca5f30edc802d9dd6/aiohttp-3.14.0-cp314-cp314t-win32.whl", hash = "sha256:a8d93334d4961c9d566b1f046c81dee475b7c21eb730728d38237bfa70d1c8e6", size = 490597, upload-time = "2026-06-01T19:40:54.937Z" }, + { url = "https://files.pythonhosted.org/packages/59/1f/299b21441c8de42ff70fddc7cfe65e92f810abcf740739a09b56f7835364/aiohttp-3.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d2ffe9b614f50f069068b3b52e73414e4107fc10b7efc939a76acff9251fdd2", size = 525789, upload-time = "2026-06-01T19:40:57.306Z" }, + { url = "https://files.pythonhosted.org/packages/70/11/7f83fcba9ee05d4c54d61b3f8104da0d43a59adac44dd28effc0c9a10422/aiohttp-3.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7a3fc4358e65826c515350f199c210de747cf669998211b1ee6c2e46de364b24", size = 467399, upload-time = "2026-06-01T19:40:59.993Z" }, ] [[package]] @@ -530,17 +546,18 @@ wheels = [ [[package]] name = "fastapi" -version = "0.128.0" +version = "0.136.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, + { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" }, ] [[package]] @@ -1533,7 +1550,7 @@ wheels = [ [[package]] name = "remoteterm-meshcore" -version = "3.14.0" +version = "3.14.1" source = { virtual = "." } dependencies = [ { name = "aiomqtt" }, @@ -1665,15 +1682,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.50.0" +version = "1.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/a3/84e821cc54b4ab50ae6dbc6ac3800a651b65ec35f045cc73785380654057/starlette-1.0.1.tar.gz", hash = "sha256:512399c5f1de7fac99c88572212ded9ddeddef2fb32afa82d724000e88b38f4f", size = 2659596, upload-time = "2026-05-21T21:58:58.433Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/b2df4bc09a1e51ff664c1e17018a4274b42e5e9352e4a478ea540512dc88/starlette-1.0.1-py3-none-any.whl", hash = "sha256:7c0e69b2ee1c848bd54669d908500117a3ee13de603a21427e5c6fc1adf98dcd", size = 72802, upload-time = "2026-05-21T21:58:56.551Z" }, ] [[package]]