diff --git a/CHANGELOG.md b/CHANGELOG.md index 87aa00d..12be72d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.3.2] - 2026-01-29 + +### Changed +- **Pump.fun data source migrated to PumpPortal** - The old Pump.fun API endpoints are dead + - Now uses PumpPortal WebSocket API (`wss://pumpportal.fun/api/data`) + - Real-time streaming for new tokens, trades, and migrations + - Free tier: Bonding curve data (no API key needed) + - BYOK support: Set `PUMPPORTAL_API_KEY` for PumpSwap data after migration + - Backward compatible: `PumpFunClient` alias still works + +### Fixed +- **Dead API endpoints removed** - Removed non-functional `frontend-api.pump.fun` and Heroku endpoints +- **Added `websockets` dependency** - Required for PumpPortal WebSocket connections + ## [0.3.1] - 2026-01-29 ### Added @@ -270,7 +284,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- -[Unreleased]: https://github.com/BAGWATCHER/SlopeSniper/compare/v0.3.03...HEAD +[Unreleased]: https://github.com/BAGWATCHER/SlopeSniper/compare/v0.3.2...HEAD +[0.3.2]: https://github.com/BAGWATCHER/SlopeSniper/compare/v0.3.1...v0.3.2 +[0.3.1]: https://github.com/BAGWATCHER/SlopeSniper/compare/v0.3.03...v0.3.1 [0.3.03]: https://github.com/BAGWATCHER/SlopeSniper/compare/v0.3.02...v0.3.03 [0.3.02]: https://github.com/BAGWATCHER/SlopeSniper/compare/v0.3.0...v0.3.02 [0.3.0]: https://github.com/BAGWATCHER/SlopeSniper/compare/v0.2.92...v0.3.0 diff --git a/README.md b/README.md index 61dbc5a..341b01b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) -[![Version](https://img.shields.io/badge/version-0.3.1-green.svg)](https://github.com/BAGWATCHER/SlopeSniper/releases) +[![Version](https://img.shields.io/badge/version-0.3.2-green.svg)](https://github.com/BAGWATCHER/SlopeSniper/releases) [Quick Start](#-quick-start) · [Features](#-features) · [Documentation](#-documentation) · [Contributing](#-contributing) diff --git a/docs/MOLTBOT_COMPATIBILITY.md b/docs/MOLTBOT_COMPATIBILITY.md index 14bfdc9..c9f0a70 100644 --- a/docs/MOLTBOT_COMPATIBILITY.md +++ b/docs/MOLTBOT_COMPATIBILITY.md @@ -1,6 +1,6 @@ # SlopeSniper + MoltBot Compatibility Assessment -*Created: 2026-01-28 | Updated: 2026-01-29 | Version: 0.3.1* +*Created: 2026-01-28 | Updated: 2026-01-29 | Version: 0.3.2* ## Executive Summary diff --git a/mcp-extension/pyproject.toml b/mcp-extension/pyproject.toml index 70fb565..0670850 100644 --- a/mcp-extension/pyproject.toml +++ b/mcp-extension/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "slopesniper-mcp" -version = "0.3.1" +version = "0.3.2" description = "SlopeSniper MCP Server - Safe Solana Token Trading" requires-python = ">=3.10" dependencies = [ @@ -11,6 +11,7 @@ dependencies = [ "fastapi>=0.109.0", "uvicorn>=0.27.0", "cryptography>=42.0.0", + "websockets>=16.0", ] [project.scripts] diff --git a/mcp-extension/src/slopesniper_skill/__init__.py b/mcp-extension/src/slopesniper_skill/__init__.py index ca10113..8e6d978 100644 --- a/mcp-extension/src/slopesniper_skill/__init__.py +++ b/mcp-extension/src/slopesniper_skill/__init__.py @@ -25,7 +25,7 @@ # Version is the single source of truth - update here for releases # Follow semantic versioning: MAJOR.MINOR.PATCH # Beta versions use 0.x.x (0.MINOR.PATCH) -__version__ = "0.3.1" +__version__ = "0.3.2" from .tools import ( export_wallet, diff --git a/mcp-extension/src/slopesniper_skill/sdk/pumpfun_client.py b/mcp-extension/src/slopesniper_skill/sdk/pumpfun_client.py index 1ea0de4..8493cf5 100644 --- a/mcp-extension/src/slopesniper_skill/sdk/pumpfun_client.py +++ b/mcp-extension/src/slopesniper_skill/sdk/pumpfun_client.py @@ -1,40 +1,64 @@ """ -Pump.fun API Client. +Pump.fun Data Client via PumpPortal. -Provides access to Pump.fun data including: -- Graduated/migrated tokens (bonding curve completed) -- New token launches -- Token details and trading activity +Provides access to Pump.fun data via PumpPortal's WebSocket API: +- Real-time new token launches +- Token trades and migrations +- Bonding curve data (free tier) +- PumpSwap data after migration (requires API key - BYOK) -Note: Pump.fun's API is unofficial and may change. +API Docs: https://pumpportal.fun/data-api/real-time """ from __future__ import annotations +import asyncio +import json +import os from datetime import datetime -from typing import Any +from typing import Any, AsyncIterator, Callable -import aiohttp +import websockets +from websockets.exceptions import ConnectionClosed from .utils import Utils -class PumpFunClient: +class PumpPortalClient: """ - Client for Pump.fun data. + Client for Pump.fun data via PumpPortal WebSocket API. - Endpoints discovered from their frontend. - May break if they change their API. + Free tier: Bonding curve trades and new token events + Paid tier: PumpSwap data after migration (set PUMPPORTAL_API_KEY) + + Usage: + client = PumpPortalClient() + + # Stream new tokens + async for token in client.stream_new_tokens(limit=10): + print(token) + + # Get recent tokens (collects from stream) + tokens = await client.get_latest_tokens(limit=20, timeout=30) """ - BASE_URL = "https://frontend-api.pump.fun" - GRADUATED_URL = "https://client-api-2-74b1891ee9f9.herokuapp.com" + WS_URL = "wss://pumpportal.fun/api/data" - def __init__(self) -> None: - self.logger = Utils.setup_logger("PumpFunClient") + def __init__(self, api_key: str | None = None) -> None: + """ + Initialize PumpPortal client. + + Args: + api_key: Optional PumpPortal API key for PumpSwap data. + Can also be set via PUMPPORTAL_API_KEY env var. + """ + self.logger = Utils.setup_logger("PumpPortalClient") + self.api_key = api_key or os.environ.get("PUMPPORTAL_API_KEY") + self._ws: websockets.WebSocketClientProtocol | None = None + self._subscriptions: set[str] = set() def _get_version(self) -> str: - """Get package version for User-Agent.""" + """Get package version for logging.""" try: from .. import __version__ @@ -42,138 +66,394 @@ def _get_version(self) -> str: except Exception: return "unknown" - async def _request(self, url: str, params: dict | None = None, timeout: int = 15) -> Any: - """Make API request.""" - self.logger.debug(f"[_request] GET {url}") + @property + def ws_url(self) -> str: + """Get WebSocket URL with API key if available.""" + if self.api_key: + return f"{self.WS_URL}?api-key={self.api_key}" + return self.WS_URL + + async def connect(self) -> None: + """Establish WebSocket connection.""" + if self._ws is not None: + return + self.logger.debug("[connect] Connecting to PumpPortal...") try: - async with aiohttp.ClientSession() as session: - async with session.get( - url, - params=params, - timeout=aiohttp.ClientTimeout(total=timeout), - headers={ - "User-Agent": f"Mozilla/5.0 (compatible; SlopeSniper/{self._get_version()})", - "Accept": "application/json", - }, - ) as resp: - if resp.status == 200: - return await resp.json() - else: - self.logger.warning(f"[_request] Status {resp.status}") - return None + self._ws = await websockets.connect( + self.ws_url, + close_timeout=5, + user_agent_header=f"SlopeSniper/{self._get_version()}", + ) + self.logger.info("[connect] Connected to PumpPortal WebSocket") except Exception as e: - self.logger.error(f"[_request] Error: {e}") + self.logger.error(f"[connect] Failed: {e}") + raise + + async def disconnect(self) -> None: + """Close WebSocket connection.""" + if self._ws: + await self._ws.close() + self._ws = None + self._subscriptions.clear() + self.logger.debug("[disconnect] Disconnected from PumpPortal") + + async def _send(self, payload: dict) -> None: + """Send a message to the WebSocket.""" + if not self._ws: + await self.connect() + await self._ws.send(json.dumps(payload)) + + async def _recv(self, timeout: float = 30) -> dict | None: + """Receive a message from the WebSocket.""" + if not self._ws: + return None + try: + msg = await asyncio.wait_for(self._ws.recv(), timeout=timeout) + return json.loads(msg) + except asyncio.TimeoutError: return None + except ConnectionClosed: + self._ws = None + return None + + async def subscribe_new_tokens(self) -> None: + """Subscribe to new token creation events.""" + await self._send({"method": "subscribeNewToken"}) + self._subscriptions.add("newToken") + self.logger.debug("[subscribe] Subscribed to new token events") + + async def subscribe_migrations(self) -> None: + """Subscribe to token migration events (bonding curve -> DEX).""" + await self._send({"method": "subscribeMigration"}) + self._subscriptions.add("migration") + self.logger.debug("[subscribe] Subscribed to migration events") + + async def subscribe_token_trades(self, mints: list[str]) -> None: + """ + Subscribe to trades for specific tokens. + + Args: + mints: List of token mint addresses to watch + """ + await self._send({"method": "subscribeTokenTrade", "keys": mints}) + self._subscriptions.add(f"tokenTrade:{','.join(mints[:3])}") + self.logger.debug(f"[subscribe] Subscribed to trades for {len(mints)} tokens") + + async def subscribe_account_trades(self, accounts: list[str]) -> None: + """ + Subscribe to trades by specific accounts. + + Args: + accounts: List of wallet addresses to watch + """ + await self._send({"method": "subscribeAccountTrade", "keys": accounts}) + self._subscriptions.add(f"accountTrade:{','.join(accounts[:3])}") + self.logger.debug(f"[subscribe] Subscribed to trades for {len(accounts)} accounts") + + async def unsubscribe_new_tokens(self) -> None: + """Unsubscribe from new token events.""" + await self._send({"method": "unsubscribeNewToken"}) + self._subscriptions.discard("newToken") + + async def unsubscribe_token_trades(self, mints: list[str]) -> None: + """Unsubscribe from token trade events.""" + await self._send({"method": "unsubscribeTokenTrade", "keys": mints}) + + async def stream_new_tokens( + self, + limit: int | None = None, + timeout: float = 60, + on_token: Callable[[dict], None] | None = None, + ) -> AsyncIterator[dict]: + """ + Stream new token creation events. + + Args: + limit: Max number of tokens to yield (None = unlimited) + timeout: Timeout in seconds for each message + on_token: Optional callback for each token - async def get_graduated_tokens(self, limit: int = 50) -> list[dict]: + Yields: + Token data dictionaries """ - Get tokens that have graduated (completed bonding curve). + await self.connect() + await self.subscribe_new_tokens() + + count = 0 + try: + while limit is None or count < limit: + data = await self._recv(timeout=timeout) + if data is None: + break + + # Skip subscription confirmations + if "message" in data and "subscribed" in data.get("message", "").lower(): + continue + + # Only yield actual token events + if "mint" in data and data.get("txType") == "create": + formatted = self.format_token_event(data) + if on_token: + on_token(formatted) + yield formatted + count += 1 + + except ConnectionClosed: + self.logger.warning("[stream] Connection closed") + finally: + await self.disconnect() + + async def stream_migrations( + self, + limit: int | None = None, + timeout: float = 120, + ) -> AsyncIterator[dict]: + """ + Stream token migration events (graduated tokens). + + Args: + limit: Max number of migrations to yield + timeout: Timeout for each message - These are tokens that filled their bonding curve and - migrated to Raydium for open trading. + Yields: + Migration event dictionaries """ - self.logger.info("[get_graduated_tokens] Fetching graduated tokens") + await self.connect() + await self.subscribe_migrations() + + count = 0 + try: + while limit is None or count < limit: + data = await self._recv(timeout=timeout) + if data is None: + break + + if "message" in data: + continue + + if "mint" in data: + yield self.format_migration_event(data) + count += 1 + + except ConnectionClosed: + self.logger.warning("[stream] Connection closed") + finally: + await self.disconnect() + + async def stream_token_trades( + self, + mint: str, + limit: int | None = None, + timeout: float = 60, + ) -> AsyncIterator[dict]: + """ + Stream trades for a specific token. + + Args: + mint: Token mint address + limit: Max trades to yield + timeout: Timeout per message + + Yields: + Trade event dictionaries + """ + await self.connect() + await self.subscribe_token_trades([mint]) + + count = 0 + try: + while limit is None or count < limit: + data = await self._recv(timeout=timeout) + if data is None: + break - # Try the graduated coins endpoint - data = await self._request( - f"{self.GRADUATED_URL}/coins/graduated", {"limit": limit, "offset": 0} - ) + if "message" in data: + continue - if data and isinstance(data, list): - self.logger.info(f"[get_graduated_tokens] Found {len(data)} graduated") - return data + if "mint" in data and data.get("mint") == mint: + yield self.format_trade_event(data) + count += 1 - # Fallback: try king of the hill (top tokens) - data = await self._request( - f"{self.BASE_URL}/coins/king-of-the-hill", {"includeNsfw": "false"} - ) + except ConnectionClosed: + self.logger.warning("[stream] Connection closed") + finally: + await self.disconnect() - if data and isinstance(data, list): - self.logger.info(f"[get_graduated_tokens] Found {len(data)} from KOTH") - return data + async def get_latest_tokens(self, limit: int = 20, timeout: float = 30) -> list[dict]: + """ + Get recently created tokens by collecting from stream. - return [] + Args: + limit: Number of tokens to collect + timeout: Max time to wait - async def get_latest_tokens(self, limit: int = 50) -> list[dict]: + Returns: + List of token dictionaries """ - Get most recently created tokens on Pump.fun. + self.logger.info(f"[get_latest_tokens] Collecting {limit} tokens (timeout: {timeout}s)") + tokens = [] + + try: + async for token in self.stream_new_tokens(limit=limit, timeout=timeout / max(limit, 1)): + tokens.append(token) + if len(tokens) >= limit: + break + except Exception as e: + self.logger.warning(f"[get_latest_tokens] Stopped early: {e}") + + self.logger.info(f"[get_latest_tokens] Collected {len(tokens)} tokens") + return tokens - These are brand new tokens still in bonding curve phase. + async def get_graduated_tokens(self, limit: int = 20, timeout: float = 60) -> list[dict]: """ - self.logger.info("[get_latest_tokens] Fetching latest tokens") + Get recently graduated/migrated tokens. - data = await self._request( - f"{self.BASE_URL}/coins", - {"offset": 0, "limit": limit, "sort": "created_timestamp", "order": "DESC"}, - ) + Note: Migrations are less frequent than new tokens. - if data and isinstance(data, list): - self.logger.info(f"[get_latest_tokens] Found {len(data)} tokens") - return data + Args: + limit: Number of migrations to collect + timeout: Max time to wait - return [] + Returns: + List of migration event dictionaries + """ + self.logger.info(f"[get_graduated_tokens] Collecting migrations (timeout: {timeout}s)") + migrations = [] - async def get_token(self, mint: str) -> dict | None: - """Get detailed info for a specific token.""" - self.logger.info(f"[get_token] Fetching {mint[:8]}...") + try: + async for event in self.stream_migrations(limit=limit, timeout=timeout): + migrations.append(event) + if len(migrations) >= limit: + break + except Exception as e: + self.logger.warning(f"[get_graduated_tokens] Stopped early: {e}") - data = await self._request(f"{self.BASE_URL}/coins/{mint}") - return data + self.logger.info(f"[get_graduated_tokens] Collected {len(migrations)} migrations") + return migrations - async def get_token_trades(self, mint: str, limit: int = 50) -> list[dict]: - """Get recent trades for a token.""" - self.logger.info(f"[get_token_trades] Fetching trades for {mint[:8]}...") + async def get_token_trades(self, mint: str, limit: int = 20, timeout: float = 30) -> list[dict]: + """ + Get recent trades for a token. - data = await self._request(f"{self.BASE_URL}/trades/latest/{mint}", {"limit": limit}) + Args: + mint: Token mint address + limit: Number of trades to collect + timeout: Max time to wait + + Returns: + List of trade dictionaries + """ + self.logger.info(f"[get_token_trades] Collecting trades for {mint[:8]}...") + trades = [] + + try: + async for trade in self.stream_token_trades(mint, limit=limit, timeout=timeout): + trades.append(trade) + if len(trades) >= limit: + break + except Exception as e: + self.logger.warning(f"[get_token_trades] Stopped early: {e}") + + return trades + + def format_token_event(self, data: dict) -> dict: + """ + Format a PumpPortal token creation event. - if data and isinstance(data, list): - return data - return [] + Args: + data: Raw event from WebSocket - async def search_tokens(self, query: str, limit: int = 20) -> list[dict]: - """Search for tokens by name or symbol.""" - self.logger.info(f"[search_tokens] Searching: {query}") + Returns: + Normalized token dictionary + """ + # Calculate bonding curve progress from virtual reserves + v_sol = float(data.get("vSolInBondingCurve", 0)) + market_cap_sol = float(data.get("marketCapSol", 0)) - data = await self._request( - f"{self.BASE_URL}/coins/search", {"query": query, "limit": limit} - ) + # Estimate USD value (rough, would need price feed for accuracy) + # Using ~$150/SOL as rough estimate + sol_price_estimate = 150 + market_cap_usd = market_cap_sol * sol_price_estimate - if data and isinstance(data, list): - self.logger.info(f"[search_tokens] Found {len(data)} results") - return data + # Bonding curve graduates around 85 SOL (~$12k at current prices) + bonding_progress = min(100, (v_sol / 85) * 100) - return [] + return { + "symbol": data.get("symbol", "???"), + "name": data.get("name", "Unknown"), + "mint": data.get("mint"), + "market_cap_sol": round(market_cap_sol, 4), + "market_cap_usd": round(market_cap_usd, 2), + "bonding_progress": round(bonding_progress, 1), + "is_graduated": False, + "signature": data.get("signature"), + "creator": data.get("traderPublicKey"), + "initial_buy_sol": float(data.get("solAmount", 0)), + "bonding_curve_key": data.get("bondingCurveKey"), + "uri": data.get("uri"), + "pool": data.get("pool", "pump"), + "is_mayhem_mode": data.get("is_mayhem_mode", False), + "timestamp": datetime.now().isoformat(), + } - async def get_trending(self, limit: int = 20) -> list[dict]: + def format_migration_event(self, data: dict) -> dict: """ - Get trending tokens on Pump.fun. + Format a token migration/graduation event. + + Args: + data: Raw migration event - Based on recent trading activity and volume. + Returns: + Normalized migration dictionary """ - self.logger.info("[get_trending] Fetching trending tokens") + return { + "symbol": data.get("symbol", "???"), + "name": data.get("name", "Unknown"), + "mint": data.get("mint"), + "is_graduated": True, + "signature": data.get("signature"), + "pool": data.get("pool"), + "timestamp": datetime.now().isoformat(), + } - # Try multiple endpoints - endpoints = [ - (f"{self.BASE_URL}/coins/featured", {}), - (f"{self.BASE_URL}/coins/king-of-the-hill", {"includeNsfw": "false"}), - ] + def format_trade_event(self, data: dict) -> dict: + """ + Format a trade event. - for url, params in endpoints: - data = await self._request(url, params) - if data and isinstance(data, list) and len(data) > 0: - self.logger.info(f"[get_trending] Found {len(data)} trending") - return data[:limit] + Args: + data: Raw trade event - return [] + Returns: + Normalized trade dictionary + """ + return { + "type": data.get("txType", "unknown"), + "mint": data.get("mint"), + "symbol": data.get("symbol"), + "trader": data.get("traderPublicKey"), + "sol_amount": float(data.get("solAmount", 0)), + "token_amount": float(data.get("tokenAmount", 0)) if data.get("tokenAmount") else None, + "market_cap_sol": float(data.get("marketCapSol", 0)), + "signature": data.get("signature"), + "is_buy": data.get("txType") == "buy", + "timestamp": datetime.now().isoformat(), + } def format_token_summary(self, token: dict) -> dict: - """Format a Pump.fun token into a clean summary.""" - # Calculate progress through bonding curve + """ + Format token for display (backward compatibility). + + Handles both old Pump.fun format and new PumpPortal format. + """ + # Check if already in new format + if "market_cap_sol" in token: + return token + + # Convert old format market_cap = float(token.get("usd_market_cap", 0)) - bonding_progress = min(100, (market_cap / 69000) * 100) # ~$69k to graduate + bonding_progress = min(100, (market_cap / 69000) * 100) - # Parse created time age_str = "unknown" created = token.get("created_timestamp") if created: @@ -207,3 +487,7 @@ def format_token_summary(self, token: dict) -> dict: "king_of_the_hill": token.get("king_of_the_hill_timestamp") is not None, "raydium_pool": token.get("raydium_pool"), } + + +# Backward compatibility alias +PumpFunClient = PumpPortalClient diff --git a/mcp-extension/uv.lock b/mcp-extension/uv.lock index 4fdad3f..b954af3 100644 --- a/mcp-extension/uv.lock +++ b/mcp-extension/uv.lock @@ -1244,7 +1244,7 @@ wheels = [ [[package]] name = "slopesniper-mcp" -version = "0.3.3" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -1254,6 +1254,7 @@ dependencies = [ { name = "mcp" }, { name = "solders" }, { name = "uvicorn" }, + { name = "websockets" }, ] [package.metadata] @@ -1265,6 +1266,7 @@ requires-dist = [ { name = "mcp", specifier = ">=1.0.0" }, { name = "solders", specifier = ">=0.21.0" }, { name = "uvicorn", specifier = ">=0.27.0" }, + { name = "websockets", specifier = ">=16.0" }, ] [[package]] @@ -1348,6 +1350,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + [[package]] name = "yarl" version = "1.22.0"