This commit is contained in:
2026-03-12 11:46:19 -04:00
parent df045f338e
commit 7dd45fff2c
10 changed files with 794 additions and 1044 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

988
main.py

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,8 @@ requires-python = ">=3.14"
dependencies = [
"fastapi==0.135.1",
"pydantic==2.12.5",
"flask==3.1.3",
"flask-cors==6.0.2",
"pymobiledevice3==7.8.3",
"pymobiledevice3==9.0.0",
"python-socketio==5.16.1",
"typing==3.10.0.0",
"uvicorn==0.41.0",
]

568
server.py Normal file
View File

@@ -0,0 +1,568 @@
import asyncio
import dataclasses
import json
import logging
import os
import signal
import traceback
import warnings
import fastapi
import random
from fastapi import FastAPI
from typing import Optional
import socketio
from contextlib import asynccontextmanager, suppress
from ssl import SSLEOFError
from typing import Optional, Union
import construct
from pymobiledevice3.bonjour import browse_remoted
from pymobiledevice3.cli.cli_common import print_json
with warnings.catch_warnings():
# Ignore: "Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater."
warnings.simplefilter("ignore", category=UserWarning)
import fastapi
import uvicorn
from construct import StreamError
from fastapi import FastAPI
from packaging.version import Version
from pymobiledevice3 import usbmux
from pymobiledevice3.exceptions import (
ConnectionFailedError,
ConnectionFailedToUsbmuxdError,
ConnectionTerminatedError,
DeviceNotFoundError,
GetProhibitedError,
IncorrectModeError,
InvalidServiceError,
LockdownError,
MuxException,
PairingError,
QuicProtocolNotSupportedError,
StreamClosedError,
TunneldConnectionError
)
from pymobiledevice3.lockdown import create_using_usbmux, get_mobdev2_lockdowns
from pymobiledevice3.osu.os_utils import get_os_utils
from pymobiledevice3.remote.common import TunnelProtocol
from pymobiledevice3.remote.module_imports import start_tunnel
from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService
from pymobiledevice3.remote.tunnel_service import (
CoreDeviceTunnelProxy,
RemotePairingProtocol,
TunnelResult,
create_core_device_tunnel_service_using_rsd,
get_remote_pairing_tunnel_services,
)
from pymobiledevice3.remote.utils import get_rsds, stop_remoted
from pymobiledevice3.utils import asyncio_print_traceback
from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation
from pymobiledevice3.services.dvt.instruments.dvt_provider import DvtProvider
from pymobiledevice3.tunneld.server import TunneldCore, TunnelTask
from pymobiledevice3.tunneld.api import (
TUNNELD_DEFAULT_ADDRESS,
get_tunneld_device_by_udid,
get_tunneld_devices,
)
class JsonFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
payload = {
"ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if record.exc_info:
payload["exc_info"] = self.formatException(record.exc_info)
return json.dumps(payload, ensure_ascii=True)
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
root_logger = logging.getLogger()
root_logger.handlers = [handler]
root_logger.setLevel(logging.INFO)
logger = logging.getLogger("ios-api")
# bugfix: after the device reboots, it might take some time for remoted to start answering the bonjour queries
REATTEMPT_INTERVAL = 5
REATTEMPT_COUNT = 5
REMOTEPAIRING_INTERVAL = 5
MOBDEV2_INTERVAL = 5
# USB monitor will periodically forget what interfaces it has seen
# and force a full rescan. The value is number of iterations of the
# inner loop (which sleeps one second each) before blowing away the
# `previous_ips` cache.
USB_MONITOR_RESCAN_INTERVAL = 30
USBMUX_INTERVAL = 2
OSUTILS = get_os_utils()
TUNNEL_ACQUIRE_TIMEOUT_SECONDS = 15
DVT_CONNECT_TIMEOUT_SECONDS = 20
class LocationSimulationState:
def __init__(self):
self.latitude: Optional[float] = None
self.longitude: Optional[float] = None
self.udid: Optional[str] = None
self.simulation_active: bool = False
self.queue: asyncio.Queue = asyncio.Queue()
self.simulation_task: Optional[asyncio.Task] = None
self.sio: socketio.AsyncServer = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
class TunneldRunnerSio:
"""TunneldRunner orchestrate between the webserver and TunneldCore"""
@classmethod
def create(
cls,
host: str,
port: int,
context: LocationSimulationState = LocationSimulationState(),
protocol: TunnelProtocol = TunnelProtocol.QUIC,
usb_monitor: bool = True,
wifi_monitor: bool = True,
usbmux_monitor: bool = True,
mobdev2_monitor: bool = True,
) -> None:
cls(
host,
port,
protocol=protocol,
usb_monitor=usb_monitor,
wifi_monitor=wifi_monitor,
usbmux_monitor=usbmux_monitor,
mobdev2_monitor=mobdev2_monitor,
context=context,
)._run_app()
def __init__(
self,
host: str,
port: int,
context: LocationSimulationState = LocationSimulationState(),
protocol: TunnelProtocol = TunnelProtocol.QUIC,
usb_monitor: bool = True,
wifi_monitor: bool = True,
usbmux_monitor: bool = True,
mobdev2_monitor: bool = True,
):
@asynccontextmanager
async def lifespan(app: FastAPI):
self._tunneld_core.start()
yield
logger.info("Closing tunneld tasks...")
await empty_queue()
await self._tunneld_core.close()
await self.context.sio.shutdown()
self.host = host
self.port = port
self.protocol = protocol
self.context = context
self._tunneld_api_address = ("127.0.0.1" if host in ("0.0.0.0", "::") else host, port)
self._app = FastAPI(title="iOS Device Management API", lifespan=lifespan, cors_allowed_origins="*")
self._asgi_app = socketio.ASGIApp(self.context.sio, self._app)
self._tunneld_core = TunneldCore(
protocol=protocol,
wifi_monitor=wifi_monitor,
usb_monitor=usb_monitor,
usbmux_monitor=usbmux_monitor,
mobdev2_monitor=mobdev2_monitor,
)
async def get_tun(
udid: Optional[str] = None, max_retries: int = 10, retry_delay: float = 0.5
) -> RemoteServiceDiscoveryService:
"""Try to connect to tunneld with retries to handle startup delay."""
for attempt in range(max_retries):
try:
if udid:
rsd = await get_tunneld_device_by_udid(udid, self._tunneld_api_address)
if rsd is not None:
logger.info("Connected to tunnel for udid %s after %s retries", udid, attempt)
return rsd
rsds = await get_tunneld_devices(self._tunneld_api_address)
if rsds:
if udid:
logger.warning("Tunnel for udid %s not found; using first available tunnel", udid)
logger.info("Connected to tunneld after %s retries", attempt)
return rsds[0]
except TunneldConnectionError:
if attempt < max_retries - 1:
logger.info("Waiting for tunneld to be ready... (attempt %s/%s)", attempt + 1, max_retries)
await asyncio.sleep(retry_delay)
else:
logger.error("Failed to connect to tunneld after max retries")
raise
raise TunneldConnectionError()
async def empty_queue():
"""Empties all items from an asyncio.Queue."""
logger.info("Clearing location simulation queue... resetting ios location")
q = self.context.queue
while not q.empty():
try:
q.get_nowait()
q.task_done()
await q.join()
except asyncio.QueueEmpty:
break
tun = await get_tun(self.context.udid)
if tun is not None:
async with DvtProvider(tun) as dvt, LocationSimulationQueue(dvt, self.context) as locate_simulation:
await locate_simulation.clear()
self.context.simulation_active = False
async def start_queue():
logger.info("Starting location simulation worker...")
self.context.simulation_active = True
try:
if self.context.udid is None:
active_udids = sorted(
{t.udid for t in self._tunneld_core.tunnel_tasks.values() if t.udid is not None and t.tunnel is not None}
)
if len(active_udids) == 1:
self.context.udid = active_udids[0]
logger.info("Simulation worker: auto-selected udid=%s from active tunnel", self.context.udid)
elif len(active_udids) == 0:
logger.error("Simulation worker: no active tunnel with udid available")
await self.context.sio.emit(
"error",
{"type": "simulation_no_tunnel", "message": "No active tunnel found. Start tunnel first."},
namespace="/",
)
return
else:
logger.error("Simulation worker: multiple active tunnels; explicit udid required: %s", active_udids)
await self.context.sio.emit(
"error",
{
"type": "simulation_udid_required",
"message": "Multiple active tunnels found; provide udid in start_simulate_location.",
"udids": active_udids,
},
namespace="/",
)
return
logger.info("Simulation worker: acquiring tunnel (udid=%s)", self.context.udid)
tun = await asyncio.wait_for(
get_tun(self.context.udid),
timeout=TUNNEL_ACQUIRE_TIMEOUT_SECONDS,
)
logger.info("Simulation worker: tunnel acquired, connecting DVT provider")
dvt_provider = DvtProvider(tun)
dvt = await asyncio.wait_for(dvt_provider.__aenter__(), timeout=DVT_CONNECT_TIMEOUT_SECONDS)
logger.info("Simulation worker: DVT provider connected, starting queue playback")
try:
async with LocationSimulationQueue(dvt, self.context) as locate_simulation:
await locate_simulation.play_queue()
finally:
logger.info("Simulation worker: closing DVT provider")
await dvt_provider.__aexit__(None, None, None)
logger.info("Simulation worker: DVT provider closed")
except TimeoutError:
logger.error(
"Simulation worker timeout. tunnel_timeout=%ss dvt_timeout=%ss udid=%s",
TUNNEL_ACQUIRE_TIMEOUT_SECONDS,
DVT_CONNECT_TIMEOUT_SECONDS,
self.context.udid,
)
await self.context.sio.emit(
"error",
{
"type": "simulation_timeout",
"udid": self.context.udid,
"tunnel_timeout_seconds": TUNNEL_ACQUIRE_TIMEOUT_SECONDS,
"dvt_timeout_seconds": DVT_CONNECT_TIMEOUT_SECONDS,
},
namespace="/",
)
except Exception:
logger.exception("Simulation worker crashed")
await self.context.sio.emit(
"error",
{"type": "simulation_crash", "udid": self.context.udid},
namespace="/",
)
finally:
self.context.simulation_active = False
self.context.simulation_task = None
def iterate_multidim(d):
mydict = {}
for key, value in d.items():
if isinstance(value, dict):
iterate_multidim(value)
elif isinstance(value, str) or isinstance(value, int) or isinstance(value, float) or isinstance(value, bool) or isinstance(value, list):
mydict[key] = value
else:
mydict[key] = ''
return mydict
@self._app.get("/")
@self.context.sio.event
async def list_tunnels() -> dict[str, list[dict]]:
"""Retrieve the available tunnels and format them as {UUID: TUNNEL_ADDRESS}"""
tunnels = {}
for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items():
if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
continue
if active_tunnel.udid not in tunnels:
tunnels[active_tunnel.udid] = []
tunnels[active_tunnel.udid].append({
"tunnel-address": active_tunnel.tunnel.address,
"tunnel-port": active_tunnel.tunnel.port,
"interface": ip,
})
return tunnels
@self._app.get("/device_info")
async def device_info():
"""Get device information"""
tunnels = {}
for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items():
if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
continue
if active_tunnel.udid not in tunnels:
tunnels[active_tunnel.udid] = {}
try:
lockdown = await create_using_usbmux(serial=active_tunnel.udid, autopair=False)
tunnels[active_tunnel.udid] = iterate_multidim(lockdown.all_values)
except Exception as e:
logger.error(f"Failed to create lockdown session for device {active_tunnel.udid}: {e}")
continue
return tunnels
@self._app.get("/shutdown")
async def shutdown() -> fastapi.Response:
"""Shutdown Tunneld"""
os.kill(os.getpid(), signal.SIGINT)
data = {"operation": "shutdown", "data": True, "message": "Server shutting down..."}
return generate_http_response(data)
@self._app.get("/clear_tunnels")
async def clear_tunnels() -> fastapi.Response:
self._tunneld_core.clear()
data = {"operation": "clear_tunnels", "data": True, "message": "Cleared tunnels..."}
return generate_http_response(data)
@self._app.get("/cancel")
async def cancel_tunnel(udid: str) -> fastapi.Response:
self._tunneld_core.cancel(udid=udid)
data = {"operation": "cancel", "udid": udid, "data": True, "message": f"tunnel {udid} Canceled ..."}
return generate_http_response(data)
@self._app.get("/hello")
async def hello() -> fastapi.Response:
data = {"message": "Hello, I'm alive"}
return generate_http_response(data)
def generate_http_response(
data: dict, status_code: int = 200, media_type: str = "application/json"
) -> fastapi.Response:
return fastapi.Response(status_code=status_code, media_type=media_type, content=json.dumps(data))
@self._app.get("/start-tunnel")
@self.context.sio.event
async def start_tunnel(
udid: str, ip: Optional[str] = None, connection_type: Optional[str] = None
) -> fastapi.Response:
udid_tunnels = [
t.tunnel for t in self._tunneld_core.tunnel_tasks.values() if t.udid == udid and t.tunnel is not None
]
if len(udid_tunnels) > 0:
self.context.udid = udid
data = {
"interface": udid_tunnels[0].interface,
"port": udid_tunnels[0].port,
"address": udid_tunnels[0].address,
}
return generate_http_response(data)
queue = asyncio.Queue()
created_task = False
try:
if not created_task and connection_type in ("usbmux", None):
task_identifier = f"usbmux-{udid}"
try:
async with await create_using_usbmux(udid) as lockdown:
service = await CoreDeviceTunnelProxy.create(lockdown)
task = asyncio.create_task(
self._tunneld_core.start_tunnel_task(
task_identifier, service, protocol=TunnelProtocol.TCP, queue=queue
),
name=f"start-tunnel-task-{task_identifier}",
)
self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask(task=task, udid=udid)
created_task = True
except (ConnectionFailedError, InvalidServiceError, MuxException):
pass
if connection_type in ("usb", None):
for rsd in await get_rsds(udid=udid):
rsd_ip = rsd.service.address[0]
if ip is not None and rsd_ip != ip:
await rsd.close()
continue
task = asyncio.create_task(
self._tunneld_core.start_tunnel_task(
rsd_ip, await create_core_device_tunnel_service_using_rsd(rsd), queue=queue
),
name=f"start-tunnel-usb-{rsd_ip}",
)
self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask(task=task, udid=rsd.udid)
created_task = True
if not created_task and connection_type in ("wifi", None):
for remotepairing in await get_remote_pairing_tunnel_services(udid=udid):
remotepairing_ip = remotepairing.hostname
if ip is not None and remotepairing_ip != ip:
await remotepairing.close()
continue
task = asyncio.create_task(
self._tunneld_core.start_tunnel_task(remotepairing_ip, remotepairing, queue=queue),
name=f"start-tunnel-wifi-{remotepairing_ip}",
)
self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask(
task=task, udid=remotepairing.remote_identifier
)
created_task = True
except Exception as e:
return fastapi.Response(
status_code=501,
content=json.dumps({
"error": {
"exception": e.__class__.__name__,
"traceback": traceback.format_exc(),
}
}),
)
if not created_task:
return fastapi.Response(status_code=501, content=json.dumps({"error": "task not created"}))
tunnel: Optional[TunnelResult] = await queue.get()
if tunnel is not None:
self.context.udid = udid
data = {"interface": tunnel.interface, "port": tunnel.port, "address": tunnel.address}
return generate_http_response(data)
else:
return fastapi.Response(
status_code=404, content=json.dumps({"error": "something went wrong during tunnel creation"})
)
@self.context.sio.event
async def connect(sid, environ):
logger.info("Client connected: %s", sid)
await self.context.sio.emit("connect", sid, namespace="/")
@self.context.sio.event
async def request_update(sid, data):
logger.info("Update request from %s", sid)
# await self.context.sio.emit("status_update", await get_status())
@self.context.sio.event
async def message(sid, data):
logger.info("Received message from %s: %s", data, sid)
await self.context.sio.emit("message", f"Received message from {sid}: {data}", namespace="/")
@self.context.sio.event
async def simulate_location(sid, data):
logger.info("Simulate location request from %s: %s", sid, data)
latitude = data.get("latitude") if isinstance(data, dict) else getattr(data, "latitude", None)
longitude = data.get("longitude") if isinstance(data, dict) else getattr(data, "longitude", None)
delay = data.get("delay", 0) if isinstance(data, dict) else getattr(data, "delay", 0)
if latitude is not None and longitude is not None:
logger.info("Adding location %s, %s with %ss delay to the queue", latitude, longitude, delay)
await self.context.queue.put((latitude, longitude, delay))
await self.context.sio.emit("simulation", {"status": self.context.simulation_active,
"data": {"latitude": self.context.latitude,
"cur_longitude": longitude, "next_move": delay}},
namespace="/")
else:
logger.warning("Invalid location data received from %s: %s", sid, data)
await self.context.sio.emit("error", "Invalid location data", namespace="/")
@self.context.sio.event
async def start_simulate_location(sid, data):
logger.info("Start location simulation request from %s", sid)
if isinstance(data, dict) and data.get("udid"):
self.context.udid = data["udid"]
if self.context.simulation_task is None or self.context.simulation_task.done():
self.context.simulation_active = True
self.context.simulation_task = asyncio.create_task(
start_queue(),
name="location-simulation-worker",
)
await self.context.sio.emit("simulation", {"status": self.context.simulation_active, "data": None},
namespace="/")
@self.context.sio.event
async def end_simulate_location(sid, data):
logger.info("End location simulation request from %s", sid)
if self.context.simulation_task is not None and not self.context.simulation_task.done():
await self.context.queue.put((None, None, None))
with suppress(asyncio.CancelledError):
await self.context.simulation_task
await empty_queue()
await self.context.sio.emit("simulation", {"status": self.context.simulation_active, "data": None},
namespace="/")
@self.context.sio.event
async def disconnect(sid):
logger.info("Client disconnected: %s", sid)
@self.context.sio.event
async def start_tunneld(sid, data):
logger.info("Start tunneld request from %s: %s", sid, data)
try:
self._tunneld_core.start()
logger.info("Tunneld started successfully")
except Exception as e:
logger.error("Error starting tunneld: %s", e)
def _run_app(self) -> None:
uvicorn.run(self._asgi_app, host=self.host, port=self.port, loop="asyncio", workers=1)
class LocationSimulationQueue(LocationSimulation):
def __init__(self, dvt, context: LocationSimulationState):
super().__init__(dvt)
self.context = context
async def play_queue(self, disable_sleep: bool = False, timing_randomness_range: int = 0) -> None:
while True:
latitude, longitude, delay = await self.context.queue.get()
if (latitude, longitude, delay) == (None, None, None):
break
if delay > 0 and not disable_sleep:
if timing_randomness_range > 0:
delay = delay + random.uniform(-timing_randomness_range, timing_randomness_range)
for i in range(delay, 0, -1):
await self.context.sio.emit("simulation", {"status": self.context.simulation_active,
"data": {"latitude": self.context.latitude,
"longitude": self.context.longitude,
"next_move": i}}, namespace="/")
await asyncio.sleep(1)
await self.set(latitude, longitude)
self.context.latitude = latitude
self.context.longitude = longitude
await self.context.sio.emit("simulation", {"status": self.context.simulation_active,
"data": {"latitude": self.context.latitude,
"longitude": self.context.longitude,
"next_move": None}}, namespace="/")
logger.info("Set simulated location to %s, %s after %ss delay", latitude, longitude, delay)
self.context.queue.task_done()

75
socktest.py Normal file
View File

@@ -0,0 +1,75 @@
import socketio
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
# 1. Create the FastAPI app
app = FastAPI()
# 2. Create the Socket.IO server (Async mode for FastAPI)
sio = socketio.AsyncServer(async_mode='asgi', cors_allowed_origins='*')
# 3. Wrap FastAPI with Socket.IO
# This allows 'socket_app' to handle socket traffic and pass
# everything else to 'app'
socket_app = socketio.ASGIApp(sio, app)
# --- Socket.IO Events ---
@sio.event
async def connect(sid, environ):
print(f"Client connected: {sid}")
await sio.emit('response', {'data': 'Connected to Server!'})
@sio.event
async def message(sid, data):
print(f"Received from {sid}: {data}")
# Broadcast to everyone (including sender)
await sio.emit('response', {'data': f"Echo: {data}"})
@sio.event
async def disconnect(sid):
print(f"Client disconnected: {sid}")
# --- FastAPI Routes ---
html_client = """
<!DOCTYPE html>
<html>
<head>
<title>FastAPI Socket.IO Test</title>
<!-- Load Socket.IO Client from CDN -->
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<script>
const socket = io();
socket.on('connect', () => {
document.getElementById('status').innerText = 'Connected: ' + socket.id;
document.getElementById('status').style.color = 'green';
});
socket.on('response', (data) => {
const li = document.createElement("li");
li.innerText = JSON.stringify(data);
document.getElementById("messages").appendChild(li);
});
function sendMessage() {
const input = document.getElementById("msg");
socket.emit('message', input.value);
input.value = "";
}
</script>
</head>
<body>
<h1>Socket.IO Test</h1>
<h3 id="status" style="color:red">Disconnected</h3>
<input id="msg" type="text" placeholder="Type a message..." />
<button onclick="sendMessage()">Send</button>
<ul id="messages"></ul>
</body>
</html>
"""
@app.get("/")
async def index():
return HTMLResponse(html_client)

182
uv.lock generated
View File

@@ -94,10 +94,9 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "fastapi" },
{ name = "flask" },
{ name = "flask-cors" },
{ name = "pydantic" },
{ name = "pymobiledevice3" },
{ name = "python-socketio" },
{ name = "typing" },
{ name = "uvicorn" },
]
@@ -105,14 +104,22 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "fastapi", specifier = "==0.135.1" },
{ name = "flask", specifier = "==3.1.3" },
{ name = "flask-cors", specifier = "==6.0.2" },
{ name = "pydantic", specifier = "==2.12.5" },
{ name = "pymobiledevice3", specifier = "==7.8.3" },
{ name = "pymobiledevice3", specifier = "==9.0.0" },
{ name = "python-socketio", specifier = "==5.16.1" },
{ name = "typing", specifier = "==3.10.0.0" },
{ name = "uvicorn", specifier = "==0.41.0" },
]
[[package]]
name = "bidict"
version = "0.23.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" },
]
[[package]]
name = "blessed"
version = "1.32.0"
@@ -126,15 +133,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/47/de8f185a1f537fdb5117fcde7050472b8cde3561179e9a68e1a566a6e6c6/blessed-1.32.0-py3-none-any.whl", hash = "sha256:c6fdc18838491ebc7f0460234917eff4e172074934f5f80e82672417bd74be70", size = 111172, upload-time = "2026-02-28T20:58:58.59Z" },
]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
]
[[package]]
name = "bpylist2"
version = "4.1.1"
@@ -346,6 +344,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
]
[[package]]
name = "defusedxml"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
]
[[package]]
name = "developer-disk-image"
version = "0.2.0"
@@ -405,36 +412,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" },
]
[[package]]
name = "flask"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
]
[[package]]
name = "flask-cors"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flask" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" },
]
[[package]]
name = "gpxpy"
version = "1.6.2"
@@ -564,15 +541,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
]
[[package]]
name = "jedi"
version = "0.19.2"
@@ -585,18 +553,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "jinxed"
version = "1.3.0"
@@ -654,36 +610,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
name = "matplotlib-inline"
version = "0.2.1"
@@ -705,15 +631,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "nest-asyncio"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
]
[[package]]
name = "opack2"
version = "0.0.1"
@@ -1020,7 +937,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/9ae75ede398b7adf5
[[package]]
name = "pymobiledevice3"
version = "7.8.3"
version = "9.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asn1" },
@@ -1031,6 +948,7 @@ dependencies = [
{ name = "construct-typing" },
{ name = "cryptography" },
{ name = "daemonize" },
{ name = "defusedxml" },
{ name = "developer-disk-image" },
{ name = "fastapi" },
{ name = "gpxpy" },
@@ -1040,7 +958,6 @@ dependencies = [
{ name = "inquirer3" },
{ name = "ipsw-parser" },
{ name = "ipython" },
{ name = "nest-asyncio" },
{ name = "opack2" },
{ name = "packaging" },
{ name = "parameter-decorators" },
@@ -1066,9 +983,9 @@ dependencies = [
{ name = "wsproto" },
{ name = "xonsh" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/16/6290b69b2ab04b01184536d93a605305749e09e2056ecacc768ec600bb3f/pymobiledevice3-7.8.3.tar.gz", hash = "sha256:3d88af218dea373249318412a08de999978a3a8f502d5929e2cd8e7820e8c6c6", size = 658504, upload-time = "2026-03-04T07:16:50.629Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/89/aed5abbb6a4ece29bed2b9e85ccfbc28a8197658a4923e8cd2d5193796d8/pymobiledevice3-9.0.0.tar.gz", hash = "sha256:e85c169d67cf17d1dcf4ce26e3a84a801e86d13a0144ff7fb57eb532745ddcfb", size = 735101, upload-time = "2026-03-11T08:37:05.172Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/51/e83eeab57581319bbce7e7516c871cf9bb07cb7aabe93f70b18831875701/pymobiledevice3-7.8.3-py3-none-any.whl", hash = "sha256:93aa53611ebfe7a10819661abe16b7a83b02134cfbc4cbb7dc5ad5b4c0e3bd7b", size = 709133, upload-time = "2026-03-04T07:16:48.152Z" },
{ url = "https://files.pythonhosted.org/packages/a6/ae/aaff1375b383c78729b6b25abac1458e9fd5c0c84b8da364bf204559dc16/pymobiledevice3-9.0.0-py3-none-any.whl", hash = "sha256:7366533cc8807299ef0b88c6c56a77120f7d984914ec6436191a7cc2991d3ae7", size = 789609, upload-time = "2026-03-11T08:37:01.731Z" },
]
[[package]]
@@ -1092,6 +1009,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "python-engineio"
version = "4.13.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "simple-websocket" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348, upload-time = "2026-02-06T23:38:06.12Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" },
]
[[package]]
name = "python-pcapng"
version = "2.1.1"
@@ -1101,6 +1030,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/a4/141a5fbb51c8e3dee10445d02785d44f8a66a150af6916b4e1776e5065c6/python_pcapng-2.1.1-py3-none-any.whl", hash = "sha256:2c83e9f9f60d61cbb6c86f80fa9e3d722f1bb606a59a64a96d6ba0179d97ffcf", size = 33503, upload-time = "2022-08-23T18:59:08.754Z" },
]
[[package]]
name = "python-socketio"
version = "5.16.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bidict" },
{ name = "python-engineio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508, upload-time = "2026-02-06T23:42:07Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" },
]
[[package]]
name = "pytun-pmd3"
version = "3.0.3"
@@ -1244,6 +1186,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
name = "simple-websocket"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wsproto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" },
]
[[package]]
name = "six"
version = "1.17.0"
@@ -1430,18 +1384,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
]
[[package]]
name = "werkzeug"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" },
]
[[package]]
name = "wsproto"
version = "1.3.2"