diff --git a/.github/workflows/documentation-website.yaml b/.github/workflows/documentation-website.yaml index 14d58d0c..704048ef 100644 --- a/.github/workflows/documentation-website.yaml +++ b/.github/workflows/documentation-website.yaml @@ -23,7 +23,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v6 with: - node-version: 24 + node-version-file: .nvmrc cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm run docs:build diff --git a/e2e/conftest.py b/e2e/conftest.py index 7a7e0391..4b59613b 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -25,7 +25,7 @@ # Make helpers importable from test modules sys.path.insert(0, str(Path(__file__).parent)) -from helpers import ( # noqa: E402 +from helpers import ( BINARY_PATH, MockFCCServer, MockHTTPUpstream, diff --git a/e2e/helpers/__init__.py b/e2e/helpers/__init__.py index c78c9e61..a1671d4d 100644 --- a/e2e/helpers/__init__.py +++ b/e2e/helpers/__init__.py @@ -16,6 +16,7 @@ # Re-export everything so ``from helpers import X`` keeps working. +from .config import build_config, build_single_service_config, write_temp_file from .constants import ( BINARY_PATH, FIXTURES_DIR, @@ -23,7 +24,6 @@ MCAST_ADDR, PROJECT_ROOT, ) -from .config import build_config, build_single_service_config, write_temp_file from .http import ( assert_etag_cache_behavior, extract_catchup_source, diff --git a/e2e/helpers/http.py b/e2e/helpers/http.py index d30ca156..5bdd13cd 100644 --- a/e2e/helpers/http.py +++ b/e2e/helpers/http.py @@ -14,7 +14,7 @@ def get_status_payload(host: str, port: int, timeout: float = 3.0, status_path: sock = socket.create_connection((host, port), timeout=timeout) data = b"" try: - request = "GET %s/sse HTTP/1.0\r\nHost: %s\r\n\r\n" % (status_path.rstrip("/"), host) + request = "GET {}/sse HTTP/1.0\r\nHost: {}\r\n\r\n".format(status_path.rstrip("/"), host) sock.sendall(request.encode()) deadline = time.monotonic() + timeout while time.monotonic() < deadline: @@ -50,7 +50,7 @@ def wait_for_status_payload( if last_payload is not None and predicate(last_payload): return last_payload time.sleep(0.05) - raise AssertionError("Status predicate not satisfied; last payload: %r" % last_payload) + raise AssertionError(f"Status predicate not satisfied; last payload: {last_payload!r}") def http_get( @@ -170,12 +170,12 @@ def raw_http_request( """ sock = socket.create_connection((host, port), timeout=timeout) try: - sock.sendall(("%s %s HTTP/1.1\r\nHost: %s\r\n\r\n" % (method, path, host)).encode()) + sock.sendall((f"{method} {path} HTTP/1.1\r\nHost: {host}\r\n\r\n").encode()) data = b"" while True: try: chunk = sock.recv(4096) - except socket.timeout: + except TimeoutError: break if not chunk: break @@ -199,12 +199,12 @@ def unix_http_request( sock.settimeout(timeout) try: sock.connect(socket_path) - req_lines = ["%s %s HTTP/1.0" % (method, path), "Host: localhost"] + req_lines = [f"{method} {path} HTTP/1.0", "Host: localhost"] payload = body or b"" for k, v in (headers or {}).items(): - req_lines.append("%s: %s" % (k, v)) + req_lines.append(f"{k}: {v}") if payload: - req_lines.append("Content-Length: %d" % len(payload)) + req_lines.append(f"Content-Length: {len(payload)}") req_lines.append("") req_lines.append("") sock.sendall("\r\n".join(req_lines).encode() + payload) @@ -213,7 +213,7 @@ def unix_http_request( while True: try: chunk = sock.recv(4096) - except socket.timeout: + except TimeoutError: break if not chunk: break @@ -241,9 +241,9 @@ def extract_catchup_source(playlist_text, channel_name): for line in playlist_text.splitlines(): if channel_name in line and "catchup-source=" in line: match = re.search(r'catchup-source="([^"]+)"', line) - assert match, "Expected catchup-source in line: %s" % line + assert match, f"Expected catchup-source in line: {line}" return line, match.group(1) - raise AssertionError("Expected catchup-source line for channel: %s" % channel_name) + raise AssertionError(f"Expected catchup-source line for channel: {channel_name}") def stream_get( @@ -267,13 +267,13 @@ def stream_get( """ try: sock = socket.create_connection((host, port), timeout=timeout) - except OSError, socket.timeout: + except TimeoutError, OSError: return 0, {}, b"" try: - host_hdr = "[%s]" % host if ":" in host and not host.startswith("[") else host - req_lines = ["GET %s HTTP/1.0" % path, "Host: %s" % host_hdr] + host_hdr = f"[{host}]" if ":" in host and not host.startswith("[") else host + req_lines = [f"GET {path} HTTP/1.0", f"Host: {host_hdr}"] for k, v in (headers or {}).items(): - req_lines.append("%s: %s" % (k, v)) + req_lines.append(f"{k}: {v}") req_lines.append("") req_lines.append("") sock.sendall("\r\n".join(req_lines).encode()) @@ -289,14 +289,14 @@ def stream_get( sock.settimeout(min(remaining, 1.0)) try: chunk = sock.recv(4096) - except socket.timeout: + except TimeoutError: continue # keep trying until deadline if not chunk: break data += chunk return _parse_raw_http_response(data, lower_header_names=True) - except socket.timeout, OSError: + except TimeoutError, OSError: return 0, {}, b"" finally: sock.close() diff --git a/e2e/helpers/mock_fcc.py b/e2e/helpers/mock_fcc.py index 8e1e95dc..57eb4221 100644 --- a/e2e/helpers/mock_fcc.py +++ b/e2e/helpers/mock_fcc.py @@ -253,7 +253,7 @@ def _loop(self) -> None: while not self._stop.is_set(): try: data, addr = self._sock.recvfrom(4096) - except socket.timeout: + except TimeoutError: continue except OSError: break diff --git a/e2e/helpers/mock_http.py b/e2e/helpers/mock_http.py index 2f128c46..05137a09 100644 --- a/e2e/helpers/mock_http.py +++ b/e2e/helpers/mock_http.py @@ -2,10 +2,10 @@ from __future__ import annotations -from http.server import BaseHTTPRequestHandler, HTTPServer import socket import threading import time +from http.server import BaseHTTPRequestHandler, HTTPServer from .ports import find_free_port @@ -59,7 +59,7 @@ def _dispatch(self, head: bool = False) -> None: if not head: self.wfile.write(body) - def log_message(self, format, *args) -> None: # noqa: ARG002, A002 + def log_message(self, format, *args) -> None: pass # silence @@ -134,17 +134,15 @@ def _accept(self) -> None: assert self._server_sock is not None while not self._stop.is_set(): try: - conn, addr = self._server_sock.accept() + conn, _addr = self._server_sock.accept() t = threading.Thread(target=self._handle, args=(conn,), daemon=True) t.start() - except socket.timeout, OSError: + except TimeoutError, OSError: continue def _handle(self, conn: socket.socket) -> None: try: while not self._stop.is_set(): time.sleep(0.1) - except Exception: - pass finally: conn.close() diff --git a/e2e/helpers/mock_rtsp.py b/e2e/helpers/mock_rtsp.py index 6def4f79..6b997484 100644 --- a/e2e/helpers/mock_rtsp.py +++ b/e2e/helpers/mock_rtsp.py @@ -10,7 +10,6 @@ from .ports import find_free_port, find_free_udp_port_pair from .rtp import TS_NULL_PACKET, make_rtp_packet - # --------------------------------------------------------------------------- # _RTSPServerBase -- shared RTSP protocol scaffolding # --------------------------------------------------------------------------- @@ -122,7 +121,7 @@ def _accept(self) -> None: conn, addr = self._server_sock.accept() t = threading.Thread(target=self._handle, args=(conn, addr), daemon=True) t.start() - except socket.timeout, OSError: + except TimeoutError, OSError: continue def _handle(self, conn: socket.socket, addr: tuple) -> None: @@ -180,20 +179,20 @@ def _handle(self, conn: socket.socket, addr: tuple) -> None: if method == "OPTIONS": session_line = "" if self._options_session_id: - session_line = "Session: %s\r\n" % self._options_session_id + session_line = f"Session: {self._options_session_id}\r\n" conn.sendall( ( - "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" - "%s" - "Public: OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN\r\n\r\n" % (cseq, session_line) + f"RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\n" + f"{session_line}" + "Public: OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN\r\n\r\n" ).encode() ) elif method == "DESCRIBE": if self._redirect_describe_to: conn.sendall( ( - "RTSP/1.0 302 Moved Temporarily\r\nCSeq: %s\r\n" - "Location: %s\r\n\r\n" % (cseq, self._redirect_describe_to) + f"RTSP/1.0 302 Moved Temporarily\r\nCSeq: {cseq}\r\n" + f"Location: {self._redirect_describe_to}\r\n\r\n" ).encode() ) return @@ -201,8 +200,8 @@ def _handle(self, conn: socket.socket, addr: tuple) -> None: self._describe_challenged = True conn.sendall( ( - "RTSP/1.0 401 Unauthorized\r\nCSeq: %s\r\n" - 'WWW-Authenticate: Basic realm="mock"\r\n\r\n' % cseq + f"RTSP/1.0 401 Unauthorized\r\nCSeq: {cseq}\r\n" + 'WWW-Authenticate: Basic realm="mock"\r\n\r\n' ).encode() ) continue @@ -212,7 +211,7 @@ def _handle(self, conn: socket.socket, addr: tuple) -> None: sdp = ( "v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=T\r\n" "c=IN IP4 0.0.0.0\r\nt=0 0\r\n" - "m=video 0 RTP/AVP 33\r\na=control:%s\r\n" % self._sdp_control + f"m=video 0 RTP/AVP 33\r\na=control:{self._sdp_control}\r\n" ) # Build Content-Base header (or omit it) cb_header = "" @@ -222,18 +221,18 @@ def _handle(self, conn: socket.socket, addr: tuple) -> None: # When control is a relative URL, Content-Base must # end with '/' for correct RFC 3986 resolution. cb_val = uri - if self._sdp_control != "*" and not self._sdp_control.startswith("rtsp://"): - if not cb_val.endswith("/"): - cb_val += "/" - cb_header = "Content-Base: %s\r\n" % cb_val + if ( + self._sdp_control != "*" + and not self._sdp_control.startswith("rtsp://") + and not cb_val.endswith("/") + ): + cb_val += "/" + cb_header = f"Content-Base: {cb_val}\r\n" else: - cb_header = "Content-Base: %s\r\n" % self._content_base + cb_header = f"Content-Base: {self._content_base}\r\n" conn.sendall( ( - "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" - "Content-Type: application/sdp\r\n" - "%s" - "Content-Length: %d\r\n\r\n%s" % (cseq, cb_header, len(sdp), sdp) + f"RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\nContent-Type: application/sdp\r\n{cb_header}Content-Length: {len(sdp)}\r\n\r\n{sdp}" ).encode() ) if self._reset_after_describe: @@ -244,21 +243,18 @@ def _handle(self, conn: socket.socket, addr: tuple) -> None: elif method == "SETUP": conn.sendall(self._setup_response(cseq, transport_hdr).encode()) elif method == "PLAY": - extra_headers = "".join("%s: %s\r\n" % item for item in self._play_response_headers) + extra_headers = "".join("{}: {}\r\n".format(*item) for item in self._play_response_headers) conn.sendall( ( - "RTSP/1.0 200 OK\r\nCSeq: %s\r\nSession: %s\r\n%s\r\n" - % (cseq, self._session_id(), extra_headers) + f"RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\nSession: {self._session_id()}\r\n{extra_headers}\r\n" ).encode() ) self._after_play(conn, addr) return elif method == "TEARDOWN": - conn.sendall( - ("RTSP/1.0 200 OK\r\nCSeq: %s\r\nSession: %s\r\n\r\n" % (cseq, self._session_id())).encode() - ) + conn.sendall((f"RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\nSession: {self._session_id()}\r\n\r\n").encode()) return - except socket.timeout, ConnectionError, OSError: + except TimeoutError, ConnectionError, OSError: pass finally: conn.close() @@ -317,11 +313,7 @@ def __init__( self._setup_transport = setup_transport def _setup_response(self, cseq: str, transport_hdr: str) -> str: - return "RTSP/1.0 200 OK\r\nCSeq: %s\r\nTransport: %s\r\nSession: %s\r\n\r\n" % ( - cseq, - self._setup_transport, - self._session_id(), - ) + return f"RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\nTransport: {self._setup_transport}\r\nSession: {self._session_id()}\r\n\r\n" def _after_play(self, conn: socket.socket, addr: tuple) -> None: seq = 0 @@ -373,14 +365,7 @@ def _setup_response(self, cseq: str, transport_hdr: str) -> str: break self._server_rtp_port, self._server_rtcp_port = find_free_udp_port_pair() - return ( - "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" - "Transport: RTP/AVP;unicast;" - "client_port=%d-%d;" - "server_port=%d-%d\r\n" - "Session: t1\r\n\r\n" - % (cseq, self._client_rtp_port, self._client_rtp_port + 1, self._server_rtp_port, self._server_rtcp_port) - ) + return f"RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\nTransport: RTP/AVP;unicast;client_port={self._client_rtp_port}-{(self._client_rtp_port + 1)};server_port={self._server_rtp_port}-{self._server_rtcp_port}\r\nSession: t1\r\n\r\n" def _after_play(self, conn: socket.socket, addr: tuple) -> None: """Send RTP packets over UDP to the client's advertised port.""" @@ -495,12 +480,7 @@ def _setup_response(self, cseq: str, transport_hdr: str) -> str: self._receiver_thread = threading.Thread(target=self._receive_probes, daemon=True) self._receiver_thread.start() - return ( - "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" - "Transport: MP2T/RTP/UDP;unicast;client_port=%d-%d;server_port=%d-%d\r\n" - "Session: t1\r\n\r\n" - % (cseq, self._client_rtp_port, self._client_rtp_port + 1, self._server_rtp_port, self._server_rtcp_port) - ) + return f"RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\nTransport: MP2T/RTP/UDP;unicast;client_port={self._client_rtp_port}-{(self._client_rtp_port + 1)};server_port={self._server_rtp_port}-{self._server_rtcp_port}\r\nSession: t1\r\n\r\n" def _receive_probes(self) -> None: assert self._server_rtp_socket is not None @@ -511,7 +491,7 @@ def _receive_probes(self) -> None: self.udp_datagrams.append((payload, source)) if self._probe_is_valid(payload, source): self._valid_probe.set() - except socket.timeout: + except TimeoutError: if self._play_started.is_set(): return except OSError: @@ -580,8 +560,6 @@ def _handle(self, conn: socket.socket, addr: tuple) -> None: try: while not self._stop.is_set(): time.sleep(0.1) - except Exception: - pass finally: conn.close() @@ -596,9 +574,7 @@ class MockRTSPServerNoMedia(_RTSPServerBase): def _setup_response(self, cseq: str, transport_hdr: str) -> str: return ( - "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" - "Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n" - "Session: t1\r\n\r\n" % cseq + f"RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\nTransport: RTP/AVP/TCP;unicast;interleaved=0-1\r\nSession: t1\r\n\r\n" ) def _after_play(self, conn: socket.socket, addr: tuple) -> None: @@ -610,7 +586,7 @@ def _after_play(self, conn: socket.socket, addr: tuple) -> None: data = conn.recv(4096) if not data: break - except socket.timeout: + except TimeoutError: continue except ConnectionError, OSError: pass @@ -627,9 +603,7 @@ def __init__(self, port: int = 0, num_packets: int = 50): def _setup_response(self, cseq: str, transport_hdr: str) -> str: return ( - "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" - "Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n" - "Session: t1\r\n\r\n" % cseq + f"RTSP/1.0 200 OK\r\nCSeq: {cseq}\r\nTransport: RTP/AVP/TCP;unicast;interleaved=0-1\r\nSession: t1\r\n\r\n" ) def _after_play(self, conn: socket.socket, addr: tuple) -> None: @@ -658,7 +632,7 @@ def _after_play(self, conn: socket.socket, addr: tuple) -> None: if not data: break # Got TEARDOWN (or anything) — just ignore and hold connection - except socket.timeout: + except TimeoutError: continue except ConnectionError, OSError: pass diff --git a/e2e/helpers/mock_stun.py b/e2e/helpers/mock_stun.py index 9f808888..3778c2cf 100644 --- a/e2e/helpers/mock_stun.py +++ b/e2e/helpers/mock_stun.py @@ -68,7 +68,7 @@ def _loop(self) -> None: while not self._stop.is_set(): try: data, addr = self._sock.recvfrom(4096) - except socket.timeout: + except TimeoutError: continue except OSError: break diff --git a/e2e/helpers/ports.py b/e2e/helpers/ports.py index 53432a6f..d8e2e372 100644 --- a/e2e/helpers/ports.py +++ b/e2e/helpers/ports.py @@ -56,7 +56,7 @@ def wait_for_port(port: int, host: str = "127.0.0.1", timeout: float = 5.0) -> b try: with socket.create_connection((host, port), timeout=0.5): return True - except ConnectionRefusedError, OSError, socket.timeout: + except TimeoutError, ConnectionRefusedError, OSError: time.sleep(0.05) return False diff --git a/e2e/helpers/r2h_process.py b/e2e/helpers/r2h_process.py index 0d15aa14..f861ae2b 100644 --- a/e2e/helpers/r2h_process.py +++ b/e2e/helpers/r2h_process.py @@ -15,9 +15,7 @@ def make_m3u_rtsp_config(r2h_port: int, rtsp_port: int, channel_name: str, confi `configured_url_query` is appended verbatim after `/stream` (so callers pass e.g. `"?r2h-seek-mode=range"` or `""` for none).""" - return ( - "[global]\nverbosity = 4\n\n[bind]\n* %d\n\n[services]\n#EXTM3U\n#EXTINF:-1,%s\nrtsp://127.0.0.1:%d/stream%s\n" - ) % (r2h_port, channel_name, rtsp_port, configured_url_query) + return f"[global]\nverbosity = 4\n\n[bind]\n* {r2h_port}\n\n[services]\n#EXTM3U\n#EXTINF:-1,{channel_name}\nrtsp://127.0.0.1:{rtsp_port}/stream{configured_url_query}\n" class R2HProcess: @@ -60,18 +58,19 @@ def start(self, wait: bool = True) -> None: if not wait_for_unix_socket(self.wait_socket_path, timeout=6.0): self.stop() raise RuntimeError( - "rtp2httpd did not start on Unix socket %s.\nCommand: %s" - % (self.wait_socket_path, " ".join(args)) + "rtp2httpd did not start on Unix socket {}.\nCommand: {}".format( + self.wait_socket_path, " ".join(args) + ) ) elif self.listen and self.listen.startswith("/"): if not wait_for_unix_socket(self.listen, timeout=6.0): self.stop() raise RuntimeError( - "rtp2httpd did not start on Unix socket %s.\nCommand: %s" % (self.listen, " ".join(args)) + "rtp2httpd did not start on Unix socket {}.\nCommand: {}".format(self.listen, " ".join(args)) ) elif self.port is not None and not wait_for_port(self.port, timeout=6.0): self.stop() - raise RuntimeError("rtp2httpd did not start on port %d.\nCommand: %s" % (self.port, " ".join(args))) + raise RuntimeError(f"rtp2httpd did not start on port {self.port}.\nCommand: {' '.join(args)}") def stop(self) -> None: if self.process and self.process.poll() is None: diff --git a/e2e/test_access_log.py b/e2e/test_access_log.py index d67bd827..c6eccf86 100644 --- a/e2e/test_access_log.py +++ b/e2e/test_access_log.py @@ -8,7 +8,6 @@ import re import pytest - from helpers import ( LOOPBACK_IF, MCAST_ADDR, diff --git a/e2e/test_auth.py b/e2e/test_auth.py index cdf3deaf..0c5d10a4 100644 --- a/e2e/test_auth.py +++ b/e2e/test_auth.py @@ -8,7 +8,6 @@ from urllib.parse import quote import pytest - from helpers import ( R2HProcess, find_free_port, diff --git a/e2e/test_config.py b/e2e/test_config.py index 452c7274..2fae2fe0 100644 --- a/e2e/test_config.py +++ b/e2e/test_config.py @@ -8,7 +8,6 @@ import time # needed for TestMaxClients deadline loop import pytest - from helpers import ( MockHTTPUpstream, MockRTSPServer, @@ -20,7 +19,6 @@ wait_for_port, ) - _INLINE_M3U = """\ #EXTM3U #EXTINF:-1,Config Test @@ -198,7 +196,7 @@ def _assert_rtsp_user_agent(port: int, expected_user_agent: str): status, headers, body = http_get( "127.0.0.1", port, - "/rtsp/127.0.0.1:%d/stream?r2h-duration=1" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream?r2h-duration=1", timeout=10.0, ) assert status == 200, f"Expected 200, got {status}" @@ -477,6 +475,7 @@ def test_max_clients_enforced(self, r2h_binary): """When maxclients=1 and one client is streaming an RTSP source, a second stream request should get 503 Service Unavailable.""" import socket as _socket + from helpers import MockRTSPServerUDP rtsp = MockRTSPServerUDP(num_packets=2000) @@ -490,11 +489,11 @@ def test_max_clients_enforced(self, r2h_binary): ) try: r2h.start() - url = "/rtsp/127.0.0.1:%d/test" % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/test" # First client: open streaming connection and keep it alive sock1 = _socket.create_connection(("127.0.0.1", port), timeout=20) - sock1.sendall(("GET %s HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n" % url).encode()) + sock1.sendall((f"GET {url} HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n").encode()) # Wait until we get the 200 OK headers resp1 = b"" deadline = time.monotonic() + 20 @@ -502,9 +501,9 @@ def test_max_clients_enforced(self, r2h_binary): while b"\r\n\r\n" not in resp1 and time.monotonic() < deadline: try: resp1 += sock1.recv(4096) - except _socket.timeout: + except TimeoutError: continue - assert b"200" in resp1, "First client did not get 200: %r" % resp1[:80] + assert b"200" in resp1, f"First client did not get 200: {resp1[:80]!r}" # Connection is active - the server has registered this client. time.sleep(0.2) diff --git a/e2e/test_epg.py b/e2e/test_epg.py index 20a30509..ce9d9a4c 100644 --- a/e2e/test_epg.py +++ b/e2e/test_epg.py @@ -23,7 +23,6 @@ import time import pytest - from helpers import ( MockHTTPUpstream, R2HProcess, @@ -91,8 +90,8 @@ def plain_epg_r2h(r2h_binary): port = find_free_port() m3u_content = ( - '#EXTM3U x-tvg-url="http://127.0.0.1:%d/epg.xml"\n#EXTINF:-1,Channel\nrtp://239.0.0.1:1234\n' - ) % upstream.port + f'#EXTM3U x-tvg-url="http://127.0.0.1:{upstream.port}/epg.xml"\n#EXTINF:-1,Channel\nrtp://239.0.0.1:1234\n' + ) upstream.routes["/m3u"] = { "status": 200, "body": m3u_content, @@ -107,7 +106,7 @@ def plain_epg_r2h(r2h_binary): "-m", "100", "-M", - "http://127.0.0.1:%d/m3u" % upstream.port, + f"http://127.0.0.1:{upstream.port}/m3u", ], ) r2h.start() @@ -134,8 +133,8 @@ def gz_epg_r2h(r2h_binary): port = find_free_port() m3u_content = ( - '#EXTM3U x-tvg-url="http://127.0.0.1:%d/epg.xml.gz"\n#EXTINF:-1,Channel\nrtp://239.0.0.1:1234\n' - ) % upstream.port + f'#EXTM3U x-tvg-url="http://127.0.0.1:{upstream.port}/epg.xml.gz"\n#EXTINF:-1,Channel\nrtp://239.0.0.1:1234\n' + ) upstream.routes["/m3u"] = { "status": 200, "body": m3u_content, @@ -150,7 +149,7 @@ def gz_epg_r2h(r2h_binary): "-m", "100", "-M", - "http://127.0.0.1:%d/m3u" % upstream.port, + f"http://127.0.0.1:{upstream.port}/m3u", ], ) r2h.start() diff --git a/e2e/test_error.py b/e2e/test_error.py index 1b513d15..4250dcd2 100644 --- a/e2e/test_error.py +++ b/e2e/test_error.py @@ -5,11 +5,11 @@ and other edge cases. """ +import contextlib import socket import time import pytest - from helpers import ( R2HProcess, find_free_port, @@ -18,7 +18,6 @@ stream_get, ) - # --------------------------------------------------------------------------- # Module-scoped shared rtp2httpd instance (basic -v 4 -m 100) # --------------------------------------------------------------------------- @@ -119,7 +118,7 @@ def test_get_status_200(self, basic_r2h): def test_head_status_200(self, basic_r2h): """HEAD should also work on the status page.""" - status, _, body = http_request("127.0.0.1", basic_r2h.port, "HEAD", "/status") + status, _, _body = http_request("127.0.0.1", basic_r2h.port, "HEAD", "/status") # HEAD on status might return 200 with empty body # or the implementation might not support HEAD explicitly assert status in (200, 501) @@ -150,10 +149,8 @@ def test_garbage_request(self, basic_r2h): # Send garbage sock = socket.create_connection(("127.0.0.1", basic_r2h.port), timeout=3) sock.sendall(b"THIS IS NOT HTTP\r\n\r\n") - try: + with contextlib.suppress(OSError, TimeoutError): sock.recv(1024) - except Exception: - pass sock.close() time.sleep(0.05) diff --git a/e2e/test_fcc.py b/e2e/test_fcc.py index bf890b54..cdd8fbac 100644 --- a/e2e/test_fcc.py +++ b/e2e/test_fcc.py @@ -9,7 +9,6 @@ import time import pytest - from helpers import ( LOOPBACK_IF, MCAST_ADDR, diff --git a/e2e/test_flow_control.py b/e2e/test_flow_control.py index 67d6eb6e..1a6bc2eb 100644 --- a/e2e/test_flow_control.py +++ b/e2e/test_flow_control.py @@ -23,7 +23,6 @@ import time import pytest - from helpers import ( MockHTTPUpstream, R2HProcess, @@ -66,7 +65,7 @@ def _slow_drain_until_eof( sock = socket.create_connection((host, port), timeout=overall_timeout) body = b"" try: - sock.sendall(("GET %s HTTP/1.0\r\nHost: %s\r\n\r\n" % (path, host)).encode()) + sock.sendall((f"GET {path} HTTP/1.0\r\nHost: {host}\r\n\r\n").encode()) deadline = time.monotonic() + overall_timeout buf = b"" @@ -77,7 +76,7 @@ def _slow_drain_until_eof( sock.settimeout(min(remaining, 2.0)) try: piece = sock.recv(chunk_size) - except socket.timeout: + except TimeoutError: continue except OSError: break @@ -124,7 +123,7 @@ def test_slow_client_receives_full_body(self, shared_r2h): # imposed by the ``-b 128`` shared_r2h fixture, so the slow client # forces multiple pause/resume cycles before EOF. body_size = 1024 * 1024 - payload = bytes((i & 0xFF for i in range(body_size))) + payload = bytes(i & 0xFF for i in range(body_size)) upstream = MockHTTPUpstream( routes={"/big.ts": {"status": 200, "body": payload, "headers": {"Content-Type": "video/mp2t"}}} @@ -134,15 +133,14 @@ def test_slow_client_receives_full_body(self, shared_r2h): status, _, received = _slow_drain_until_eof( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/big.ts" % upstream.port, + f"/http/127.0.0.1:{upstream.port}/big.ts", chunk_size=8 * 1024, sleep_per_chunk=0.02, # ~400 KB/s ceiling, well below pool refill rate overall_timeout=20.0, ) assert status == 200, "Slow client should still receive a 200 response" - assert len(received) == body_size, "Slow client received %d/%d bytes — flow control regression?" % ( - len(received), - body_size, + assert len(received) == body_size, ( + f"Slow client received {len(received)}/{body_size} bytes — flow control regression?" ) assert received == payload, "Body content mismatch — corruption in proxy path" finally: diff --git a/e2e/test_http_proxy.py b/e2e/test_http_proxy.py index bc38f842..92d4a3a4 100644 --- a/e2e/test_http_proxy.py +++ b/e2e/test_http_proxy.py @@ -8,7 +8,6 @@ import time import pytest - from helpers import ( MockHTTPUpstream, MockHTTPUpstreamSilent, @@ -102,7 +101,7 @@ def test_json_content_type(self, shared_r2h): ) upstream.start() try: - status, hdrs, body = http_get( + status, _hdrs, body = http_get( "127.0.0.1", shared_r2h.port, f"/http/127.0.0.1:{upstream.port}/api/data", @@ -136,7 +135,7 @@ def test_large_payload(self, shared_r2h): status, _, body = http_get( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/big" % upstream.port, + f"/http/127.0.0.1:{upstream.port}/big", timeout=10.0, ) assert status == 200 @@ -163,7 +162,7 @@ def test_query_forwarded(self, shared_r2h): ) upstream.start() try: - status, _, body = http_get( + status, _, _body = http_get( "127.0.0.1", shared_r2h.port, f"/http/127.0.0.1:{upstream.port}/search?q=test&page=1", @@ -186,7 +185,7 @@ def test_r2h_token_not_forwarded(self, shared_r2h, token_param): status, _, body = http_get( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/search?%s=secret-token&q=test" % (upstream.port, token_param), + f"/http/127.0.0.1:{upstream.port}/search?{token_param}=secret-token&q=test", timeout=5.0, ) assert status == 200 @@ -212,7 +211,7 @@ def test_upstream_down(self, shared_r2h): status, _, _ = stream_get( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/whatever" % dead_port, + f"/http/127.0.0.1:{dead_port}/whatever", read_bytes=512, timeout=6.0, ) @@ -247,7 +246,7 @@ def test_http_proxy_response_timeout(self, r2h_binary): status, _, _ = stream_get( "127.0.0.1", r2h_port, - "/http/127.0.0.1:%d/test" % upstream.port, + f"/http/127.0.0.1:{upstream.port}/test", read_bytes=256, timeout=_HTTP_PROXY_TIMEOUT * _TIMEOUT_MAX_FACTOR + 5, ) @@ -278,10 +277,10 @@ def test_proxy_500(self, shared_r2h): ) upstream.start() try: - status, _, body = http_get( + status, _, _body = http_get( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/err" % upstream.port, + f"/http/127.0.0.1:{upstream.port}/err", timeout=5.0, ) assert status == 500 @@ -301,10 +300,10 @@ def test_proxy_302_body(self, shared_r2h): ) upstream.start() try: - status, hdrs, _ = http_get( + status, _hdrs, _ = http_get( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/redirect" % upstream.port, + f"/http/127.0.0.1:{upstream.port}/redirect", timeout=5.0, ) assert status == 302 @@ -345,7 +344,7 @@ def test_302_location_rewritten(self, shared_r2h): status, hdrs, _ = http_get( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/old" % upstream.port, + f"/http/127.0.0.1:{upstream.port}/old", timeout=5.0, ) assert status == 302 @@ -408,7 +407,7 @@ def test_301_location_rewritten(self, shared_r2h): status, hdrs, _ = http_get( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/moved" % upstream.port, + f"/http/127.0.0.1:{upstream.port}/moved", timeout=5.0, ) assert status == 301 @@ -434,7 +433,7 @@ def test_307_location_rewritten(self, shared_r2h): status, hdrs, _ = http_get( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/temp" % upstream.port, + f"/http/127.0.0.1:{upstream.port}/temp", timeout=5.0, ) assert status == 307 @@ -460,7 +459,7 @@ def test_redirect_location_preserves_query_string(self, shared_r2h): status, hdrs, _ = http_get( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/redir-qs" % upstream.port, + f"/http/127.0.0.1:{upstream.port}/redir-qs", timeout=5.0, ) assert status == 302 @@ -488,7 +487,7 @@ def test_redirect_https_location_not_rewritten(self, shared_r2h): status, hdrs, _ = http_get( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/secure-redir" % upstream.port, + f"/http/127.0.0.1:{upstream.port}/secure-redir", timeout=5.0, ) assert status == 302 @@ -517,7 +516,7 @@ def test_non_redirect_location_not_rewritten(self, shared_r2h): status, hdrs, _ = http_get( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/ok-with-loc" % upstream.port, + f"/http/127.0.0.1:{upstream.port}/ok-with-loc", timeout=5.0, ) assert status == 200 @@ -547,7 +546,7 @@ def test_empty_200(self, shared_r2h): status, _, body = http_get( "127.0.0.1", shared_r2h.port, - "/http/127.0.0.1:%d/empty" % upstream.port, + f"/http/127.0.0.1:{upstream.port}/empty", timeout=5.0, ) assert status == 200 diff --git a/e2e/test_http_proxy_m3u_rewrite.py b/e2e/test_http_proxy_m3u_rewrite.py index cac78c7c..69e05bff 100644 --- a/e2e/test_http_proxy_m3u_rewrite.py +++ b/e2e/test_http_proxy_m3u_rewrite.py @@ -11,7 +11,6 @@ import threading import pytest - from helpers import ( MockHTTPUpstream, R2HProcess, @@ -150,7 +149,7 @@ def _accept(self): while not self._stop.is_set(): try: conn, _ = self._server_sock.accept() - except socket.timeout: + except TimeoutError: continue except OSError: break @@ -967,7 +966,7 @@ def test_mixed_absolute_and_relative(self, shared_r2h): def test_large_playlist_body_is_fully_buffered(self, shared_r2h, upstream_mode): """A large M3U body should be fully read after header parsing.""" segment_count = 4096 - segments = "".join("#EXTINF:10,\nsegment-%04d.ts?token=abcdef0123456789\n" % i for i in range(segment_count)) + segments = "".join(f"#EXTINF:10,\nsegment-{i:04d}.ts?token=abcdef0123456789\n" for i in range(segment_count)) m3u = "#EXTM3U\n#EXT-X-TARGETDURATION:10\n" + segments + "#EXT-X-ENDLIST\n" if upstream_mode == "normal_headers": upstream = _make_m3u_upstream("/lookback/long.m3u8", m3u) diff --git a/e2e/test_ipv6.py b/e2e/test_ipv6.py index a965258d..95bba039 100644 --- a/e2e/test_ipv6.py +++ b/e2e/test_ipv6.py @@ -13,7 +13,6 @@ import time import pytest - from helpers import ( MockHTTPUpstream, MockRTSPServer, @@ -175,7 +174,7 @@ def test_rtsp_tcp_stream_via_ipv6(self, shared_r2h): ) assert status == 200 assert len(body) >= 188 - assert body[0] == 0x47, "Expected TS sync byte 0x47, got 0x%02x" % body[0] + assert body[0] == 0x47, f"Expected TS sync byte 0x47, got 0x{body[0]:02x}" finally: rtsp.stop() diff --git a/e2e/test_m3u.py b/e2e/test_m3u.py index f3eb1c9c..6c9894c0 100644 --- a/e2e/test_m3u.py +++ b/e2e/test_m3u.py @@ -10,7 +10,6 @@ import time import pytest - from helpers import ( MockRTSPServer, R2HProcess, @@ -23,7 +22,6 @@ stream_get, ) - # --------------------------------------------------------------------------- # Inline M3U (from [services] config section) # --------------------------------------------------------------------------- @@ -53,7 +51,7 @@ def test_inline_m3u_served(self, r2h_binary): r2h = R2HProcess(r2h_binary, port, config_content=config) try: r2h.start() - status, hdrs, body = http_get("127.0.0.1", port, "/playlist.m3u") + status, _hdrs, body = http_get("127.0.0.1", port, "/playlist.m3u") text = body.decode() assert status == 200 @@ -403,9 +401,9 @@ def test_question_mark_preserved_no_dynamic_query(self, r2h_binary): assert status == 200 assert "QMark NoDyn Ch" in text line, catchup_source = extract_catchup_source(text, "QMark NoDyn Ch") - assert 'catchup="default"' in line, "Expected transformed append catchup to become default, got:\n%s" % line + assert 'catchup="default"' in line, f"Expected transformed append catchup to become default, got:\n{line}" assert "/QMark%20NoDyn%20Ch/catchup?playseek={utc}-{utcend}" in catchup_source, ( - "Expected direct catchup proxy URL, got:\n%s" % catchup_source + f"Expected direct catchup proxy URL, got:\n{catchup_source}" ) finally: r2h.stop() @@ -433,9 +431,9 @@ def test_ampersand_adjusted_to_question_no_dynamic_query(self, r2h_binary): assert status == 200 assert "Amp NoDyn Ch" in text line, catchup_source = extract_catchup_source(text, "Amp NoDyn Ch") - assert 'catchup="default"' in line, "Expected transformed append catchup to become default, got:\n%s" % line + assert 'catchup="default"' in line, f"Expected transformed append catchup to become default, got:\n{line}" assert "/Amp%20NoDyn%20Ch/catchup?playseek={utc}-{utcend}" in catchup_source, ( - "Expected direct catchup proxy URL, got:\n%s" % catchup_source + f"Expected direct catchup proxy URL, got:\n{catchup_source}" ) finally: r2h.stop() @@ -463,9 +461,9 @@ def test_ampersand_preserved_with_dynamic_query(self, r2h_binary): assert status == 200 assert "Amp Dyn Ch" in text line, catchup_source = extract_catchup_source(text, "Amp Dyn Ch") - assert 'catchup="default"' in line, "Expected transformed append catchup to become default, got:\n%s" % line + assert 'catchup="default"' in line, f"Expected transformed append catchup to become default, got:\n{line}" assert "/Amp%20Dyn%20Ch/catchup?playseek={utc}-{utcend}" in catchup_source, ( - "Expected direct catchup proxy URL, got:\n%s" % catchup_source + f"Expected direct catchup proxy URL, got:\n{catchup_source}" ) assert "http://127.0.0.1:" in line finally: @@ -494,9 +492,9 @@ def test_question_adjusted_to_ampersand_with_dynamic_query(self, r2h_binary): assert status == 200 assert "QMark Dyn Ch" in text line, catchup_source = extract_catchup_source(text, "QMark Dyn Ch") - assert 'catchup="default"' in line, "Expected transformed append catchup to become default, got:\n%s" % line + assert 'catchup="default"' in line, f"Expected transformed append catchup to become default, got:\n{line}" assert "/QMark%20Dyn%20Ch/catchup?playseek={utc}-{utcend}" in catchup_source, ( - "Expected direct catchup proxy URL, got:\n%s" % catchup_source + f"Expected direct catchup proxy URL, got:\n{catchup_source}" ) finally: r2h.stop() @@ -524,9 +522,9 @@ def test_static_query_does_not_count_as_dynamic(self, r2h_binary): assert status == 200 assert "Static Query Ch" in text line, catchup_source = extract_catchup_source(text, "Static Query Ch") - assert 'catchup="default"' in line, "Expected transformed append catchup to become default, got:\n%s" % line + assert 'catchup="default"' in line, f"Expected transformed append catchup to become default, got:\n{line}" assert "/Static%20Query%20Ch/catchup?playseek={utc}-{utcend}" in catchup_source, ( - "Expected direct catchup proxy URL, got:\n%s" % catchup_source + f"Expected direct catchup proxy URL, got:\n{catchup_source}" ) finally: r2h.stop() @@ -554,9 +552,9 @@ def test_no_prefix_catchup_is_promoted(self, r2h_binary): assert status == 200 assert "No Prefix Ch" in text line, catchup_source = extract_catchup_source(text, "No Prefix Ch") - assert 'catchup="default"' in line, "Expected transformed append catchup to become default, got:\n%s" % line + assert 'catchup="default"' in line, f"Expected transformed append catchup to become default, got:\n{line}" assert "/No%20Prefix%20Ch/catchup?playseek={utc}-{utcend}" in catchup_source, ( - "Expected direct catchup proxy URL, got:\n%s" % catchup_source + f"Expected direct catchup proxy URL, got:\n{catchup_source}" ) finally: r2h.stop() @@ -843,7 +841,7 @@ def test_external_http_m3u(self, r2h_binary): "-m", "100", "-M", - "http://127.0.0.1:%d/channels.m3u" % upstream.port, + f"http://127.0.0.1:{upstream.port}/channels.m3u", ], ) try: @@ -889,7 +887,7 @@ def test_external_http_m3u_url_rewritten(self, r2h_binary): "-m", "100", "-M", - "http://127.0.0.1:%d/list.m3u" % upstream.port, + f"http://127.0.0.1:{upstream.port}/list.m3u", ], ) try: @@ -942,7 +940,7 @@ def test_multi_label_distinct_urls(self, r2h_binary): assert "CCTV-1" in text # Should have URLs for both sources url_lines = [line for line in text.splitlines() if line.startswith("http")] - assert len(url_lines) >= 2, "Expected at least 2 URLs for multi-label, got %d" % len(url_lines) + assert len(url_lines) >= 2, f"Expected at least 2 URLs for multi-label, got {len(url_lines)}" assert len(set(url_lines)) == len(url_lines), "URLs should be unique" finally: r2h.stop() @@ -1005,7 +1003,7 @@ def test_request_overrides_configured_via_merged_url_log( rtsp.start() try: config = make_m3u_rtsp_config( - r2h_port, rtsp.port, "MergeWinsLog_%s" % param_name, "?%s=%s" % (param_name, configured_value) + r2h_port, rtsp.port, f"MergeWinsLog_{param_name}", f"?{param_name}={configured_value}" ) r2h = R2HProcess(r2h_binary, r2h_port, config_content=config, capture_log=True) r2h.start() @@ -1015,17 +1013,17 @@ def test_request_overrides_configured_via_merged_url_log( stream_get( "127.0.0.1", r2h_port, - "/MergeWinsLog_%s?%s=%s" % (param_name, param_name, request_value), + f"/MergeWinsLog_{param_name}?{param_name}={request_value}", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) log = r2h.read_log() merged_lines = [line for line in log.splitlines() if "merged URL:" in line] - assert merged_lines, "expected a 'merged URL:' debug log line; got log:\n%s" % log + assert merged_lines, f"expected a 'merged URL:' debug log line; got log:\n{log}" merged_line = merged_lines[-1] - assert "%s=%s" % (param_name, request_value) in merged_line, merged_line - assert "%s=%s" % (param_name, configured_value) not in merged_line, merged_line + assert f"{param_name}={request_value}" in merged_line, merged_line + assert f"{param_name}={configured_value}" not in merged_line, merged_line finally: r2h.stop() finally: diff --git a/e2e/test_multicast.py b/e2e/test_multicast.py index c60ae5f3..b02db99f 100644 --- a/e2e/test_multicast.py +++ b/e2e/test_multicast.py @@ -11,7 +11,6 @@ import struct import pytest - from helpers import ( LOOPBACK_IF, MCAST_ADDR, diff --git a/e2e/test_pages.py b/e2e/test_pages.py index 03d7f054..83ed73e4 100644 --- a/e2e/test_pages.py +++ b/e2e/test_pages.py @@ -12,7 +12,6 @@ from urllib.parse import quote import pytest - from helpers import ( R2HProcess, find_free_port, @@ -22,7 +21,6 @@ write_temp_file, ) - APP_PREFIX = "/app/rtp2httpd" SAMPLE_EPG_XML = """\ @@ -121,7 +119,7 @@ def test_status_returns_html(self, basic_r2h): def test_status_contains_info(self, basic_r2h): """Status page should contain recognizable content. The embedded HTML may be gzip-compressed; request uncompressed.""" - _, hdrs, body = http_get( + _, _hdrs, body = http_get( "127.0.0.1", basic_r2h.port, "/status", diff --git a/e2e/test_pid_file.py b/e2e/test_pid_file.py index f42f0978..f34b84a3 100644 --- a/e2e/test_pid_file.py +++ b/e2e/test_pid_file.py @@ -9,7 +9,6 @@ from pathlib import Path import pytest - from helpers import R2HProcess, build_config, find_free_port diff --git a/e2e/test_rtsp_content_base.py b/e2e/test_rtsp_content_base.py index 87ba87ff..2c067d02 100644 --- a/e2e/test_rtsp_content_base.py +++ b/e2e/test_rtsp_content_base.py @@ -6,7 +6,6 @@ """ import pytest - from helpers import ( MockRTSPServer, R2HProcess, @@ -43,7 +42,7 @@ def test_setup_uses_content_base_with_track_id(self, shared_r2h): stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/live/stream.sdp" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/live/stream.sdp", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -52,7 +51,7 @@ def test_setup_uses_content_base_with_track_id(self, shared_r2h): assert len(setup_reqs) > 0, "Expected SETUP request" setup_uri = setup_reqs[0]["uri"] assert setup_uri.endswith("/live/stream.sdp/trackID=2"), ( - "SETUP URI should resolve to Content-Base/trackID=2, got: %s" % setup_uri + f"SETUP URI should resolve to Content-Base/trackID=2, got: {setup_uri}" ) finally: rtsp.stop() @@ -65,7 +64,7 @@ def test_play_uses_original_url(self, shared_r2h): stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/live/stream.sdp" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/live/stream.sdp", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -73,7 +72,7 @@ def test_play_uses_original_url(self, shared_r2h): play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] assert len(play_reqs) > 0, "Expected PLAY request" play_uri = play_reqs[0]["uri"] - assert "trackID" not in play_uri, "PLAY URI should use original URL, not track URL, got: %s" % play_uri + assert "trackID" not in play_uri, f"PLAY URI should use original URL, not track URL, got: {play_uri}" assert "/live/stream.sdp" in play_uri finally: rtsp.stop() @@ -86,7 +85,7 @@ def test_aggregate_control_uses_original_url(self, shared_r2h): stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/live/stream.sdp" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/live/stream.sdp", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -95,7 +94,7 @@ def test_aggregate_control_uses_original_url(self, shared_r2h): assert len(setup_reqs) > 0, "Expected SETUP request" setup_uri = setup_reqs[0]["uri"] assert setup_uri.endswith("/live/stream.sdp"), ( - "SETUP URI for a=control:* should use original URL, got: %s" % setup_uri + f"SETUP URI for a=control:* should use original URL, got: {setup_uri}" ) assert "trackID" not in setup_uri finally: @@ -116,7 +115,7 @@ def test_absolute_control_url(self, shared_r2h): stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/live/stream.sdp" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/live/stream.sdp", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -124,7 +123,7 @@ def test_absolute_control_url(self, shared_r2h): setup_reqs = [r for r in rtsp.requests_detailed if r["method"] == "SETUP"] assert len(setup_reqs) > 0, "Expected SETUP request" setup_uri = setup_reqs[0]["uri"] - assert setup_uri == abs_url, "SETUP URI for absolute a=control should be used as-is, got: %s" % setup_uri + assert setup_uri == abs_url, f"SETUP URI for absolute a=control should be used as-is, got: {setup_uri}" finally: rtsp.stop() @@ -139,7 +138,7 @@ def test_no_content_base_relative_control(self, shared_r2h): stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/live/stream.sdp" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/live/stream.sdp", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -150,7 +149,7 @@ def test_no_content_base_relative_control(self, shared_r2h): # RFC 3986: resolve "trackID=2" against ".../live/stream.sdp" # → last segment "stream.sdp" replaced → ".../live/trackID=2" assert setup_uri.endswith("/live/trackID=2"), ( - "Without Content-Base, relative control should replace last path segment, got: %s" % setup_uri + f"Without Content-Base, relative control should replace last path segment, got: {setup_uri}" ) finally: rtsp.stop() @@ -171,7 +170,7 @@ def test_no_control_attribute(self, shared_r2h): stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/live/stream.sdp" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/live/stream.sdp", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -180,7 +179,7 @@ def test_no_control_attribute(self, shared_r2h): assert len(setup_reqs) > 0, "Expected SETUP request" setup_uri = setup_reqs[0]["uri"] assert setup_uri.endswith("/live/stream.sdp"), ( - "No a=control in SDP → SETUP should use original URL, got: %s" % setup_uri + f"No a=control in SDP → SETUP should use original URL, got: {setup_uri}" ) finally: rtsp.stop() @@ -207,7 +206,7 @@ def test_multi_track_uses_first_media_control(self, shared_r2h): stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/live/stream.sdp" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/live/stream.sdp", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -217,7 +216,7 @@ def test_multi_track_uses_first_media_control(self, shared_r2h): setup_uri = setup_reqs[0]["uri"] # First media-level control is trackID=1 assert setup_uri.endswith("/live/stream.sdp/trackID=1"), ( - "Multi-track: SETUP should use first media control (trackID=1), got: %s" % setup_uri + f"Multi-track: SETUP should use first media control (trackID=1), got: {setup_uri}" ) finally: rtsp.stop() @@ -240,7 +239,7 @@ def test_session_aggregate_with_media_track_control(self, shared_r2h): stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/live/stream.sdp" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/live/stream.sdp", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -249,7 +248,7 @@ def test_session_aggregate_with_media_track_control(self, shared_r2h): assert len(setup_reqs) > 0, "Expected SETUP request" setup_uri = setup_reqs[0]["uri"] assert setup_uri.endswith("/live/stream.sdp/trackID=3"), ( - "Session a=control:* + media a=control:trackID=3 → SETUP should use media control, got: %s" % setup_uri + f"Session a=control:* + media a=control:trackID=3 → SETUP should use media control, got: {setup_uri}" ) finally: rtsp.stop() @@ -262,14 +261,14 @@ def test_content_base_no_trailing_slash(self, shared_r2h): # Manually set Content-Base without trailing slash rtsp = MockRTSPServer(num_packets=500, sdp_control="trackID=2") # Override content_base to explicit value without trailing slash - cb = "rtsp://127.0.0.1:%d/live/stream.sdp" % rtsp.port + cb = f"rtsp://127.0.0.1:{rtsp.port}/live/stream.sdp" rtsp._content_base = cb rtsp.start() try: stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/live/stream.sdp" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/live/stream.sdp", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -279,7 +278,7 @@ def test_content_base_no_trailing_slash(self, shared_r2h): setup_uri = setup_reqs[0]["uri"] # Content-Base doesn't end with '/' → RFC 3986 replaces last segment assert setup_uri.endswith("/live/trackID=2"), ( - "Content-Base without '/' should replace last segment, got: %s" % setup_uri + f"Content-Base without '/' should replace last segment, got: {setup_uri}" ) finally: rtsp.stop() @@ -295,7 +294,7 @@ def test_content_base_with_query_params(self, shared_r2h): stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/live/stream.sdp?token=abc123&sid=456" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/live/stream.sdp?token=abc123&sid=456", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -306,13 +305,13 @@ def test_content_base_with_query_params(self, shared_r2h): # Content-Base (auto-derived from URI with query) + trackID=2 # The Content-Base includes the query params from the original URI # but the key point is trackID=2 is appended - assert "trackID=2" in setup_uri, "SETUP URI should contain trackID=2, got: %s" % setup_uri + assert "trackID=2" in setup_uri, f"SETUP URI should contain trackID=2, got: {setup_uri}" # PLAY should use the original URL with query params play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] assert len(play_reqs) > 0, "Expected PLAY request" play_uri = play_reqs[0]["uri"] - assert "trackID" not in play_uri, "PLAY should not contain trackID, got: %s" % play_uri + assert "trackID" not in play_uri, f"PLAY should not contain trackID, got: {play_uri}" finally: rtsp.stop() @@ -327,7 +326,7 @@ def test_deep_path_relative_control(self, shared_r2h): stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/iptv/channels/001/live.sdp" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/iptv/channels/001/live.sdp", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -337,7 +336,7 @@ def test_deep_path_relative_control(self, shared_r2h): setup_uri = setup_reqs[0]["uri"] # Content-Base (auto) = .../live.sdp/ + control "stream1" assert setup_uri.endswith("/iptv/channels/001/live.sdp/stream1"), ( - "Deep path SETUP should resolve correctly, got: %s" % setup_uri + f"Deep path SETUP should resolve correctly, got: {setup_uri}" ) finally: rtsp.stop() diff --git a/e2e/test_rtsp_misc.py b/e2e/test_rtsp_misc.py index a17dd3d1..40ac4889 100644 --- a/e2e/test_rtsp_misc.py +++ b/e2e/test_rtsp_misc.py @@ -13,7 +13,6 @@ import time import pytest - from helpers import ( MockRTSPServer, MockRTSPServerNoMedia, @@ -45,7 +44,7 @@ def _get_status_clients(port: int, timeout: float = 3.0) -> list[dict]: extract the active clients list.""" try: sock = socket.create_connection(("127.0.0.1", port), timeout=timeout) - except OSError, socket.timeout: + except TimeoutError, OSError: return [] data = b"" try: @@ -56,14 +55,14 @@ def _get_status_clients(port: int, timeout: float = 3.0) -> list[dict]: sock.settimeout(min(remaining, 1.0)) try: chunk = sock.recv(8192) - except socket.timeout: + except TimeoutError: continue if not chunk: break data += chunk if b"\ndata: {" in data and b'"clients"' in data: break - except socket.timeout, OSError: + except TimeoutError, OSError: pass finally: sock.close() @@ -103,7 +102,7 @@ def test_head_rtsp_unreachable_returns_503(self, shared_r2h): "127.0.0.1", shared_r2h.port, "HEAD", - "/rtsp/127.0.0.1:%d/test" % dead_port, + f"/rtsp/127.0.0.1:{dead_port}/test", timeout=3.0, ) assert status == 503 @@ -116,7 +115,7 @@ def test_rtsp_unreachable(self, shared_r2h): status, _, _ = stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/test" % dead_port, + f"/rtsp/127.0.0.1:{dead_port}/test", read_bytes=512, timeout=8.0, ) @@ -145,7 +144,7 @@ def test_rtsp_response_timeout(self, r2h_binary): status, _, _ = stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=256, timeout=_RTSP_HANDSHAKE_TIMEOUT * _TIMEOUT_MAX_FACTOR + 5, ) @@ -182,7 +181,7 @@ def test_no_media_after_play_timeout(self, r2h_binary): status, _, _ = stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=256, timeout=_RTSP_FIRST_MEDIA_TIMEOUT * _TIMEOUT_MAX_FACTOR + 5, ) @@ -219,7 +218,7 @@ def test_teardown_timeout_cleanup(self, r2h_binary): status, _, body = stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=2048, timeout=15.0, ) @@ -266,16 +265,16 @@ def test_duration_returns_json(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500, custom_sdp=sdp_with_range) rtsp.start() try: - status, hdrs, body = http_get( + status, _hdrs, body = http_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/stream?r2h-duration=1" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream?r2h-duration=1", timeout=_STREAM_TIMEOUT, ) - assert status == 200, "Expected 200 for r2h-duration, got %d" % status + assert status == 200, f"Expected 200 for r2h-duration, got {status}" body_str = body.decode(errors="replace") - assert '"duration"' in body_str, "Response should contain duration JSON, got: %s" % body_str - assert "3600.500" in body_str, "Duration should be 3600.500, got: %s" % body_str + assert '"duration"' in body_str, f"Response should contain duration JSON, got: {body_str}" + assert "3600.500" in body_str, f"Duration should be 3600.500, got: {body_str}" finally: rtsp.stop() @@ -293,15 +292,15 @@ def test_duration_no_setup_or_play(self, shared_r2h): http_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/stream?r2h-duration=1" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream?r2h-duration=1", timeout=_STREAM_TIMEOUT, ) methods = rtsp.requests_received assert "OPTIONS" in methods, "Expected OPTIONS" assert "DESCRIBE" in methods, "Expected DESCRIBE" - assert "SETUP" not in methods, "r2h-duration should NOT send SETUP, got: %s" % methods - assert "PLAY" not in methods, "r2h-duration should NOT send PLAY, got: %s" % methods + assert "SETUP" not in methods, f"r2h-duration should NOT send SETUP, got: {methods}" + assert "PLAY" not in methods, f"r2h-duration should NOT send PLAY, got: {methods}" finally: rtsp.stop() @@ -320,14 +319,14 @@ def test_duration_stripped_from_rtsp_uri(self, shared_r2h): http_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/stream?r2h-duration=1" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream?r2h-duration=1", timeout=_STREAM_TIMEOUT, ) describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert len(describe_reqs) > 0, "Expected DESCRIBE" uri = describe_reqs[0]["uri"] - assert "r2h-duration" not in uri, "r2h-duration should be stripped from RTSP URI, got: %s" % uri + assert "r2h-duration" not in uri, f"r2h-duration should be stripped from RTSP URI, got: {uri}" finally: rtsp.stop() @@ -340,7 +339,7 @@ def test_start_adds_range_header(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = "/rtsp/127.0.0.1:%d/stream?r2h-start=120.5" % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?r2h-start=120.5" stream_get( "127.0.0.1", shared_r2h.port, @@ -362,7 +361,7 @@ def test_start_stripped_from_rtsp_uri(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = "/rtsp/127.0.0.1:%d/stream?r2h-start=60" % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?r2h-start=60" stream_get( "127.0.0.1", shared_r2h.port, @@ -382,7 +381,7 @@ def test_start_with_other_params(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = "/rtsp/127.0.0.1:%d/stream?token=abc&r2h-start=30&sid=123" % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?token=abc&r2h-start=30&sid=123" stream_get( "127.0.0.1", shared_r2h.port, @@ -405,7 +404,7 @@ def test_no_range_header_without_start(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = "/rtsp/127.0.0.1:%d/stream" % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream" stream_get( "127.0.0.1", shared_r2h.port, diff --git a/e2e/test_rtsp_seek_mode.py b/e2e/test_rtsp_seek_mode.py index 8e03e008..72b4c03e 100644 --- a/e2e/test_rtsp_seek_mode.py +++ b/e2e/test_rtsp_seek_mode.py @@ -3,7 +3,6 @@ import time import pytest - from helpers import ( MockRTSPServer, R2HProcess, @@ -51,11 +50,8 @@ class TestRTSPRecentPlayseek: def _build_seek_query(param_name: str, start_str: str, end_str: str) -> str: # All recent-clock tests opt in via r2h-seek-mode=range explicitly. if param_name == "custom_seek": - return "custom_seek=%s-%s&r2h-seek-name=custom_seek&r2h-seek-mode=range" % ( - start_str, - end_str, - ) - return "%s=%s-%s&r2h-seek-mode=range" % (param_name, start_str, end_str) + return f"custom_seek={start_str}-{end_str}&r2h-seek-name=custom_seek&r2h-seek-mode=range" + return f"{param_name}={start_str}-{end_str}&r2h-seek-mode=range" @pytest.mark.parametrize("param_name", ["playseek", "Playseek", "tvdr", "custom_seek"]) def test_recent_playseek_uses_clock_range(self, shared_r2h, param_name): @@ -67,10 +63,7 @@ def test_recent_playseek_uses_clock_range(self, shared_r2h, param_name): start_str = _format_basic_utc(start_ts) end_str = _format_basic_utc(end_ts) query = self._build_seek_query(param_name, start_str, end_str) - url = "/rtsp/127.0.0.1:%d/stream?%s" % ( - rtsp.port, - query, - ) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?{query}" stream_get( "127.0.0.1", @@ -82,14 +75,14 @@ def test_recent_playseek_uses_clock_range(self, shared_r2h, param_name): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert len(describe_reqs) > 0, "Expected DESCRIBE" - assert "%s=" % param_name not in describe_reqs[0]["uri"] + assert f"{param_name}=" not in describe_reqs[0]["uri"] assert "r2h-seek-name=" not in describe_reqs[0]["uri"] assert "r2h-seek-mode=" not in describe_reqs[0]["uri"] play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] assert len(play_reqs) > 0, "Expected PLAY request" play_headers = play_reqs[0]["headers"] - assert play_headers.get("Range") == "clock=%s-" % start_str + assert play_headers.get("Range") == f"clock={start_str}-" assert end_str not in play_headers["Range"] finally: rtsp.stop() @@ -100,11 +93,7 @@ def test_recent_playseek_ignores_r2h_start(self, shared_r2h): try: start_ts = int(time.time()) - 1200 start_str = _format_basic_utc(start_ts) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s-%s&r2h-start=120.5&r2h-seek-mode=range" % ( - rtsp.port, - start_str, - _format_basic_utc(start_ts + 120), - ) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={start_str}-{_format_basic_utc(start_ts + 120)}&r2h-start=120.5&r2h-seek-mode=range" stream_get( "127.0.0.1", @@ -117,7 +106,7 @@ def test_recent_playseek_ignores_r2h_start(self, shared_r2h): play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] assert len(play_reqs) > 0, "Expected PLAY request" play_range = play_reqs[0]["headers"].get("Range", "") - assert play_range == "clock=%s-" % start_str + assert play_range == f"clock={start_str}-" assert "npt=" not in play_range assert "120.5" not in play_range finally: @@ -135,11 +124,7 @@ def test_boundary_playseek_is_forwarded(self, shared_r2h): start_ts = int(time.time()) - 3600 start_str = _format_basic_utc(start_ts) end_str = _format_basic_utc(start_ts + 120) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s-%s&r2h-seek-mode=range" % ( - rtsp.port, - start_str, - end_str, - ) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={start_str}-{end_str}&r2h-seek-mode=range" stream_get( "127.0.0.1", @@ -151,7 +136,7 @@ def test_boundary_playseek_is_forwarded(self, shared_r2h): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert len(describe_reqs) > 0, "Expected DESCRIBE" - assert "playseek=%s-%s" % (start_str, end_str) in describe_reqs[0]["uri"] + assert f"playseek={start_str}-{end_str}" in describe_reqs[0]["uri"] play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] assert len(play_reqs) > 0, "Expected PLAY request" @@ -167,7 +152,7 @@ def test_recent_playseek_default_passthrough(self, shared_r2h): start_ts = int(time.time()) - 1800 start_str = _format_basic_utc(start_ts) end_str = _format_basic_utc(start_ts + 300) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s-%s" % (rtsp.port, start_str, end_str) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={start_str}-{end_str}" stream_get( "127.0.0.1", @@ -179,7 +164,7 @@ def test_recent_playseek_default_passthrough(self, shared_r2h): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert len(describe_reqs) > 0, "Expected DESCRIBE" - assert "playseek=%s-%s" % (start_str, end_str) in describe_reqs[0]["uri"] + assert f"playseek={start_str}-{end_str}" in describe_reqs[0]["uri"] play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] assert len(play_reqs) > 0, "Expected PLAY request" @@ -204,16 +189,12 @@ def test_passthrough_explicit_equals_default(self, shared_r2h): start_ts = int(time.time()) - 1800 start_str = _format_basic_utc(start_ts) end_str = _format_basic_utc(start_ts + 300) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s-%s&r2h-seek-mode=passthrough" % ( - rtsp.port, - start_str, - end_str, - ) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={start_str}-{end_str}&r2h-seek-mode=passthrough" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] - assert "playseek=%s-%s" % (start_str, end_str) in describe_reqs[0]["uri"] + assert f"playseek={start_str}-{end_str}" in describe_reqs[0]["uri"] assert "r2h-seek-mode=" not in describe_reqs[0]["uri"] play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] assert "Range" not in play_reqs[0]["headers"] @@ -228,17 +209,14 @@ def test_range_with_explicit_tz_within_window_uses_clock(self, shared_r2h): start_ts = int(time.time()) - 1800 # Client sends time in CST (UTC+8): represent the same instant in CST cst_str = _format_yyyyMMddHHmmss(start_ts + 8 * 3600) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-mode=range(UTC%%2B8/3600)" % ( - rtsp.port, - cst_str, - ) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={cst_str}&r2h-seek-mode=range(UTC%2B8/3600)" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert "playseek=" not in describe_reqs[0]["uri"] play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] - assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % _expected_clock_str(start_ts) + assert play_reqs[0]["headers"].get("Range") == f"clock={_expected_clock_str(start_ts)}-" finally: rtsp.stop() @@ -248,10 +226,7 @@ def test_range_outside_window_falls_back_passthrough(self, shared_r2h): rtsp.start() try: # Use fixed historical CST timestamps, far outside any window. - url = ( - "/rtsp/127.0.0.1:%d/stream?playseek=20240101180000-20240101230000&r2h-seek-mode=range(UTC%%2B8/3600)" - % (rtsp.port,) - ) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101180000-20240101230000&r2h-seek-mode=range(UTC%2B8/3600)" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) @@ -270,12 +245,12 @@ def test_range_default_utc_within_window_uses_clock(self, shared_r2h): try: start_ts = int(time.time()) - 1500 utc_str = _format_yyyyMMddHHmmss(start_ts) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-mode=range(3600)" % (rtsp.port, utc_str) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={utc_str}&r2h-seek-mode=range(3600)" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] - assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % _expected_clock_str(start_ts) + assert play_reqs[0]["headers"].get("Range") == f"clock={_expected_clock_str(start_ts)}-" finally: rtsp.stop() @@ -286,12 +261,12 @@ def test_range_tz_only_defaults_window_to_3600(self, shared_r2h): try: start_ts = int(time.time()) - 1500 # within 1 hour cst_str = _format_yyyyMMddHHmmss(start_ts + 8 * 3600) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-mode=range(UTC%%2B8)" % (rtsp.port, cst_str) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={cst_str}&r2h-seek-mode=range(UTC%2B8)" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] - assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % _expected_clock_str(start_ts) + assert play_reqs[0]["headers"].get("Range") == f"clock={_expected_clock_str(start_ts)}-" finally: rtsp.stop() @@ -314,13 +289,13 @@ def test_range_syntax_variants_default_to_utc_window_3600(self, shared_r2h, mode try: start_ts = int(time.time()) - 1500 # within 1h, UTC interpretation utc_str = _format_yyyyMMddHHmmss(start_ts) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-mode=%s" % (rtsp.port, utc_str, mode_value) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={utc_str}&r2h-seek-mode={mode_value}" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] - assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % _expected_clock_str(start_ts), ( - "syntax variant %r should enter the clock= path" % mode_value + assert play_reqs[0]["headers"].get("Range") == f"clock={_expected_clock_str(start_ts)}-", ( + f"syntax variant {mode_value!r} should enter the clock= path" ) finally: rtsp.stop() @@ -332,7 +307,7 @@ def test_range_falls_back_to_ua_tz(self, shared_r2h): try: start_ts = int(time.time()) - 1500 cst_str = _format_yyyyMMddHHmmss(start_ts + 8 * 3600) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-mode=range(3600)" % (rtsp.port, cst_str) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={cst_str}&r2h-seek-mode=range(3600)" stream_get( "127.0.0.1", @@ -344,7 +319,7 @@ def test_range_falls_back_to_ua_tz(self, shared_r2h): ) play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] - assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % _expected_clock_str(start_ts) + assert play_reqs[0]["headers"].get("Range") == f"clock={_expected_clock_str(start_ts)}-" finally: rtsp.stop() @@ -356,16 +331,12 @@ def test_range_offset_propagates_into_clock(self, shared_r2h): base_ts = int(time.time()) - 3000 # 50 min ago offset = 1800 # +30 min, still within 60min window relative to begin cst_str = _format_yyyyMMddHHmmss(base_ts + 8 * 3600) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-offset=%d&r2h-seek-mode=range(UTC%%2B8/3600)" % ( - rtsp.port, - cst_str, - offset, - ) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={cst_str}&r2h-seek-offset={offset}&r2h-seek-mode=range(UTC%2B8/3600)" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] - assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % _expected_clock_str(base_ts + offset) + assert play_reqs[0]["headers"].get("Range") == f"clock={_expected_clock_str(base_ts + offset)}-" finally: rtsp.stop() @@ -378,17 +349,12 @@ def test_range_offset_pair_uses_begin_for_clock(self, shared_r2h): begin_offset = 1800 # +30 min, still within 60min window relative to begin end_offset = -1800 cst_str = _format_yyyyMMddHHmmss(base_ts + 8 * 3600) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-offset=%d,%d&r2h-seek-mode=range(UTC%%2B8/3600)" % ( - rtsp.port, - cst_str, - begin_offset, - end_offset, - ) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={cst_str}&r2h-seek-offset={begin_offset},{end_offset}&r2h-seek-mode=range(UTC%2B8/3600)" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] - assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % _expected_clock_str(base_ts + begin_offset) + assert play_reqs[0]["headers"].get("Range") == f"clock={_expected_clock_str(base_ts + begin_offset)}-" finally: rtsp.stop() @@ -400,11 +366,7 @@ def test_range_offset_pushes_outside_window_falls_back(self, shared_r2h): base_ts = int(time.time()) - 3000 # 50 min ago offset = -1800 # makes begin look like 80 min ago, > 60min window cst_str = _format_yyyyMMddHHmmss(base_ts + 8 * 3600) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-offset=%d&r2h-seek-mode=range(UTC%%2B8/3600)" % ( - rtsp.port, - cst_str, - offset, - ) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={cst_str}&r2h-seek-offset={offset}&r2h-seek-mode=range(UTC%2B8/3600)" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) @@ -427,12 +389,12 @@ def test_range_invalid_value_falls_back_to_passthrough(self, shared_r2h): try: start_ts = int(time.time()) - 1500 utc_str = _format_yyyyMMddHHmmss(start_ts) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-mode=range(UTC%%2B999)" % (rtsp.port, utc_str) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={utc_str}&r2h-seek-mode=range(UTC%2B999)" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] - assert "playseek=%s" % utc_str in describe_reqs[0]["uri"] + assert f"playseek={utc_str}" in describe_reqs[0]["uri"] play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] assert "Range" not in play_reqs[0]["headers"] finally: @@ -448,12 +410,12 @@ def test_range_unrecognized_mode_falls_back_to_passthrough(self, shared_r2h): try: start_ts = int(time.time()) - 1500 utc_str = _format_yyyyMMddHHmmss(start_ts) - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-mode=bogus" % (rtsp.port, utc_str) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={utc_str}&r2h-seek-mode=bogus" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] - assert "playseek=%s" % utc_str in describe_reqs[0]["uri"] + assert f"playseek={utc_str}" in describe_reqs[0]["uri"] play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] assert "Range" not in play_reqs[0]["headers"] finally: @@ -482,17 +444,17 @@ def test_configured_seek_mode_applies_when_request_silent(self, r2h_binary): try: start_ts = int(time.time()) - 1500 cst_str = _format_yyyyMMddHHmmss(start_ts + 8 * 3600) - url = "/SeekModeFallback?playseek=%s" % cst_str + url = f"/SeekModeFallback?playseek={cst_str}" stream_get("127.0.0.1", r2h_port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert describe_reqs, "expected at least one DESCRIBE" assert "r2h-seek-mode" not in describe_reqs[0]["uri"], ( - "r2h-seek-mode leaked into upstream URI: %s" % describe_reqs[0]["uri"] + "r2h-seek-mode leaked into upstream URI: {}".format(describe_reqs[0]["uri"]) ) play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] - assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % _expected_clock_str(start_ts) + assert play_reqs[0]["headers"].get("Range") == f"clock={_expected_clock_str(start_ts)}-" finally: r2h.stop() finally: @@ -511,15 +473,15 @@ def test_request_seek_mode_overrides_configured_range(self, r2h_binary): try: start_ts = int(time.time()) - 1500 cst_str = _format_yyyyMMddHHmmss(start_ts + 8 * 3600) - url = "/SeekModeMerge?playseek=%s&r2h-seek-mode=passthrough" % cst_str + url = f"/SeekModeMerge?playseek={cst_str}&r2h-seek-mode=passthrough" stream_get("127.0.0.1", r2h_port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert describe_reqs, "expected at least one DESCRIBE" - assert "playseek=%s" % cst_str in describe_reqs[0]["uri"] + assert f"playseek={cst_str}" in describe_reqs[0]["uri"] assert "r2h-seek-mode" not in describe_reqs[0]["uri"], ( - "r2h-seek-mode leaked into upstream URI: %s" % describe_reqs[0]["uri"] + "r2h-seek-mode leaked into upstream URI: {}".format(describe_reqs[0]["uri"]) ) play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] assert "Range" not in play_reqs[0]["headers"] @@ -541,17 +503,17 @@ def test_request_range_overrides_configured_passthrough(self, r2h_binary): try: start_ts = int(time.time()) - 1500 cst_str = _format_yyyyMMddHHmmss(start_ts + 8 * 3600) - url = "/SeekModeOff?playseek=%s&r2h-seek-mode=range(UTC%%2B8/3600)" % cst_str + url = f"/SeekModeOff?playseek={cst_str}&r2h-seek-mode=range(UTC%2B8/3600)" stream_get("127.0.0.1", r2h_port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert describe_reqs, "expected at least one DESCRIBE" assert "r2h-seek-mode" not in describe_reqs[0]["uri"], ( - "r2h-seek-mode leaked into upstream URI: %s" % describe_reqs[0]["uri"] + "r2h-seek-mode leaked into upstream URI: {}".format(describe_reqs[0]["uri"]) ) play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] - assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % _expected_clock_str(start_ts) + assert play_reqs[0]["headers"].get("Range") == f"clock={_expected_clock_str(start_ts)}-" finally: r2h.stop() finally: @@ -573,7 +535,7 @@ def test_request_seek_offset_overrides_configured(self, r2h_binary): # Use playseek with literal UTC time so we can predict the upstream value. base_ts = 1717000000 # arbitrary fixed UTC seconds base_str = _format_yyyyMMddHHmmss(base_ts) - url = "/OffsetMerge?playseek=%s&r2h-seek-offset=7200" % base_str + url = f"/OffsetMerge?playseek={base_str}&r2h-seek-offset=7200" stream_get("127.0.0.1", r2h_port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) @@ -581,12 +543,12 @@ def test_request_seek_offset_overrides_configured(self, r2h_binary): assert describe_reqs, "expected at least one DESCRIBE" # r2h-seek-offset must not leak. assert "r2h-seek-offset" not in describe_reqs[0]["uri"], ( - "r2h-seek-offset leaked into upstream URI: %s" % describe_reqs[0]["uri"] + "r2h-seek-offset leaked into upstream URI: {}".format(describe_reqs[0]["uri"]) ) # Request offset (7200, not 3600) should have been applied to playseek. expected_str = _format_yyyyMMddHHmmss(base_ts + 7200) - assert "playseek=%s" % expected_str in describe_reqs[0]["uri"], ( - "request offset (7200) should have applied; got URI %s" % describe_reqs[0]["uri"] + assert f"playseek={expected_str}" in describe_reqs[0]["uri"], ( + "request offset (7200) should have applied; got URI {}".format(describe_reqs[0]["uri"]) ) finally: r2h.stop() @@ -610,10 +572,10 @@ def test_configured_seek_offset_pair_fallback(self, r2h_binary): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert describe_reqs, "expected at least one DESCRIBE" assert "r2h-seek-offset" not in describe_reqs[0]["uri"], ( - "r2h-seek-offset leaked into upstream URI: %s" % describe_reqs[0]["uri"] + "r2h-seek-offset leaked into upstream URI: {}".format(describe_reqs[0]["uri"]) ) assert "playseek=20240101120030-20240101125900" in describe_reqs[0]["uri"], ( - "configured offset pair should have applied; got URI %s" % describe_reqs[0]["uri"] + "configured offset pair should have applied; got URI {}".format(describe_reqs[0]["uri"]) ) finally: r2h.stop() @@ -630,18 +592,15 @@ def test_duplicate_request_seek_mode_does_not_leak(self, shared_r2h): start_ts = int(time.time()) - 1500 utc_str = _format_yyyyMMddHHmmss(start_ts) # Two distinct r2h-seek-mode values; the first one (passthrough) wins. - url = ( - "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-mode=passthrough&r2h-seek-mode=range(UTC%%2B8/3600)" - % (rtsp.port, utc_str) - ) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={utc_str}&r2h-seek-mode=passthrough&r2h-seek-mode=range(UTC%2B8/3600)" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert describe_reqs, "expected at least one DESCRIBE" # No r2h-seek-mode should leak into the upstream URI. - assert "r2h-seek-mode" not in describe_reqs[0]["uri"], ( - "r2h-seek-mode leaked into upstream URI: %s" % describe_reqs[0]["uri"] + assert "r2h-seek-mode" not in describe_reqs[0]["uri"], "r2h-seek-mode leaked into upstream URI: {}".format( + describe_reqs[0]["uri"] ) # First value (passthrough) wins → no clock= header. play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] @@ -665,8 +624,8 @@ def test_configured_ifname_does_not_leak(self, r2h_binary): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert describe_reqs, "expected at least one DESCRIBE" - assert "r2h-ifname" not in describe_reqs[0]["uri"], ( - "r2h-ifname leaked into upstream URI: %s" % describe_reqs[0]["uri"] + assert "r2h-ifname" not in describe_reqs[0]["uri"], "r2h-ifname leaked into upstream URI: {}".format( + describe_reqs[0]["uri"] ) finally: r2h.stop() @@ -691,7 +650,7 @@ def test_configured_seek_name_does_not_leak(self, r2h_binary): stream_get( "127.0.0.1", r2h_port, - "/SeekNameFallback?customseek=%s" % base_str, + f"/SeekNameFallback?customseek={base_str}", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -699,9 +658,9 @@ def test_configured_seek_name_does_not_leak(self, r2h_binary): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert describe_reqs, "expected at least one DESCRIBE" uri = describe_reqs[0]["uri"] - assert "r2h-seek-name" not in uri, "r2h-seek-name leaked into upstream URI: %s" % uri - assert "customseek=%s" % base_str in uri, ( - "Custom-named seek value should be forwarded under the configured name; got: %s" % uri + assert "r2h-seek-name" not in uri, f"r2h-seek-name leaked into upstream URI: {uri}" + assert f"customseek={base_str}" in uri, ( + f"Custom-named seek value should be forwarded under the configured name; got: {uri}" ) finally: r2h.stop() @@ -723,7 +682,7 @@ def test_configured_seek_name_does_not_remap_default_playseek_request(self, r2h_ stream_get( "127.0.0.1", r2h_port, - "/SeekNameNoRemap?playseek=%s" % base_str, + f"/SeekNameNoRemap?playseek={base_str}", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -731,11 +690,11 @@ def test_configured_seek_name_does_not_remap_default_playseek_request(self, r2h_ describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert describe_reqs, "expected at least one DESCRIBE" uri = describe_reqs[0]["uri"] - assert "r2h-seek-name" not in uri, "r2h-seek-name leaked into upstream URI: %s" % uri - assert "playseek=%s" % base_str in uri, ( - "playseek should be forwarded as-is when configured seek name is tvdr; got: %s" % uri + assert "r2h-seek-name" not in uri, f"r2h-seek-name leaked into upstream URI: {uri}" + assert f"playseek={base_str}" in uri, ( + f"playseek should be forwarded as-is when configured seek name is tvdr; got: {uri}" ) - assert "tvdr=" not in uri, "playseek must not be remapped to tvdr: %s" % uri + assert "tvdr=" not in uri, f"playseek must not be remapped to tvdr: {uri}" finally: r2h.stop() finally: @@ -759,7 +718,7 @@ def test_request_seek_name_overrides_configured(self, r2h_binary): stream_get( "127.0.0.1", r2h_port, - "/SeekNameMerge?r2h-seek-name=request_name&request_name=%s" % base_str, + f"/SeekNameMerge?r2h-seek-name=request_name&request_name={base_str}", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -767,13 +726,11 @@ def test_request_seek_name_overrides_configured(self, r2h_binary): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert describe_reqs, "expected at least one DESCRIBE" uri = describe_reqs[0]["uri"] - assert "r2h-seek-name" not in uri, "r2h-seek-name leaked into upstream URI: %s" % uri + assert "r2h-seek-name" not in uri, f"r2h-seek-name leaked into upstream URI: {uri}" # Request-side custom name wins. - assert "request_name=%s" % base_str in uri, ( - "request_name (request-side) should be forwarded; got: %s" % uri - ) + assert f"request_name={base_str}" in uri, f"request_name (request-side) should be forwarded; got: {uri}" assert "configured_name=" not in uri, ( - "configured_name (M3U-side) must NOT be used when request overrides; got: %s" % uri + f"configured_name (M3U-side) must NOT be used when request overrides; got: {uri}" ) finally: r2h.stop() @@ -794,14 +751,14 @@ def test_r2h_token_does_not_leak_to_rtsp_upstream(self, r2h_binary, token_param) stream_get( "127.0.0.1", r2h_port, - "/TokenStrip?%s=secret-token" % token_param, + f"/TokenStrip?{token_param}=secret-token", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) assert rtsp.requests_detailed, "expected RTSP requests" for request in rtsp.requests_detailed: - assert "r2h-token" not in request["uri"].lower(), "%s leaked into upstream %s URI: %s" % ( + assert "r2h-token" not in request["uri"].lower(), "{} leaked into upstream {} URI: {}".format( token_param, request["method"], request["uri"], @@ -831,7 +788,7 @@ def test_configured_ifname_fcc_does_not_leak(self, r2h_binary): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert describe_reqs, "expected at least one DESCRIBE" assert "r2h-ifname-fcc" not in describe_reqs[0]["uri"], ( - "r2h-ifname-fcc leaked into upstream URI: %s" % describe_reqs[0]["uri"] + "r2h-ifname-fcc leaked into upstream URI: {}".format(describe_reqs[0]["uri"]) ) finally: r2h.stop() diff --git a/e2e/test_rtsp_stun.py b/e2e/test_rtsp_stun.py index c4823449..548a9cee 100644 --- a/e2e/test_rtsp_stun.py +++ b/e2e/test_rtsp_stun.py @@ -15,7 +15,6 @@ import time import pytest - from helpers import ( MockRTSPServer, R2HProcess, @@ -40,7 +39,7 @@ def test_head_does_not_allocate_media_transport(self, r2h_binary): r2h = R2HProcess( r2h_binary, r2h_port, - extra_args=["-v", "4", "-m", "100", "-N", "127.0.0.1:%d" % stun.port], + extra_args=["-v", "4", "-m", "100", "-N", f"127.0.0.1:{stun.port}"], ) r2h.start() try: @@ -48,7 +47,7 @@ def test_head_does_not_allocate_media_transport(self, r2h_binary): "127.0.0.1", r2h_port, "HEAD", - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", timeout=8.0, ) assert status == 200 @@ -76,14 +75,14 @@ def test_setup_uses_stun_mapped_port(self, r2h_binary): r2h = R2HProcess( r2h_binary, r2h_port, - extra_args=["-v", "4", "-m", "100", "-N", "127.0.0.1:%d" % stun.port], + extra_args=["-v", "4", "-m", "100", "-N", f"127.0.0.1:{stun.port}"], ) r2h.start() try: status, _, body = stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -91,14 +90,14 @@ def test_setup_uses_stun_mapped_port(self, r2h_binary): assert len(body) > 0 # STUN server should have received at least one Binding Request - assert stun.requests_received >= 1, "Expected STUN Binding Request, got %d" % stun.requests_received + assert stun.requests_received >= 1, f"Expected STUN Binding Request, got {stun.requests_received}" # Verify the SETUP Transport header uses the mapped port setup_reqs = [r for r in rtsp.requests_detailed if r["method"] == "SETUP"] assert len(setup_reqs) >= 1, "No SETUP request received" transport = setup_reqs[0]["headers"].get("Transport", "") - assert "client_port=%d-%d" % (mapped_port, mapped_port + 1) in transport, ( - "Expected mapped port %d in Transport, got: %s" % (mapped_port, transport) + assert f"client_port={mapped_port}-{(mapped_port + 1)}" in transport, ( + f"Expected mapped port {mapped_port} in Transport, got: {transport}" ) finally: r2h.stop() @@ -115,20 +114,20 @@ def test_stream_data_is_ts_with_stun(self, r2h_binary): r2h = R2HProcess( r2h_binary, r2h_port, - extra_args=["-v", "4", "-m", "100", "-N", "127.0.0.1:%d" % stun.port], + extra_args=["-v", "4", "-m", "100", "-N", f"127.0.0.1:{stun.port}"], ) r2h.start() try: status, _, body = stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) assert status == 200 assert len(body) >= 188 - assert body[0] == 0x47, "Expected TS sync byte 0x47, got 0x%02x" % body[0] + assert body[0] == 0x47, f"Expected TS sync byte 0x47, got 0x{body[0]:02x}" finally: r2h.stop() rtsp.stop() @@ -149,14 +148,14 @@ def test_fallback_to_local_port_on_timeout(self, r2h_binary): r2h = R2HProcess( r2h_binary, r2h_port, - extra_args=["-v", "4", "-m", "100", "-N", "127.0.0.1:%d" % stun.port], + extra_args=["-v", "4", "-m", "100", "-N", f"127.0.0.1:{stun.port}"], ) r2h.start() try: status, _, body = stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -188,14 +187,14 @@ def test_stun_retries_on_timeout(self, r2h_binary): r2h = R2HProcess( r2h_binary, r2h_port, - extra_args=["-v", "4", "-m", "100", "-N", "127.0.0.1:%d" % stun.port], + extra_args=["-v", "4", "-m", "100", "-N", f"127.0.0.1:{stun.port}"], ) r2h.start() try: stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -204,7 +203,7 @@ def test_stun_retries_on_timeout(self, r2h_binary): # Should have received initial + up to 2 retries = 3 assert stun.requests_received >= 2, ( - "Expected at least 2 STUN requests (initial + retry), got %d" % stun.requests_received + f"Expected at least 2 STUN requests (initial + retry), got {stun.requests_received}" ) finally: r2h.stop() diff --git a/e2e/test_rtsp_transport.py b/e2e/test_rtsp_transport.py index c6270166..ea7e0106 100644 --- a/e2e/test_rtsp_transport.py +++ b/e2e/test_rtsp_transport.py @@ -6,7 +6,6 @@ """ import pytest - from helpers import ( MockRTSPServer, MockRTSPServerSilent, @@ -49,7 +48,7 @@ def test_tcp_stream_returns_200(self, shared_r2h): status, _, body = stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -74,7 +73,7 @@ def test_tcp_stream_metadata_headers(self, shared_r2h): status, headers, body = stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -106,7 +105,7 @@ def test_head_stops_after_describe(self, shared_r2h): "127.0.0.1", shared_r2h.port, "HEAD", - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", timeout=10.0, ) assert status == 200 @@ -134,7 +133,7 @@ def test_head_parses_clock_form_npt_duration(self, shared_r2h): "127.0.0.1", shared_r2h.port, "HEAD", - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", timeout=10.0, ) assert status == 200 @@ -157,7 +156,7 @@ def test_head_metadata_takes_precedence_over_duration_query(self, shared_r2h): "127.0.0.1", shared_r2h.port, "HEAD", - "/rtsp/127.0.0.1:%d/stream?r2h-duration=1" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream?r2h-duration=1", timeout=10.0, ) assert status == 200 @@ -178,7 +177,7 @@ def test_head_survives_upstream_close_after_describe(self, shared_r2h): "127.0.0.1", shared_r2h.port, "HEAD", - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", timeout=10.0, ) assert status == 200 @@ -197,7 +196,7 @@ def test_head_survives_upstream_reset_after_describe(self, shared_r2h): "127.0.0.1", shared_r2h.port, "HEAD", - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", timeout=10.0, ) assert status == 200 @@ -218,7 +217,7 @@ def test_head_probe_timeout_returns_503(self, shared_r2h): "127.0.0.1", shared_r2h.port, "HEAD", - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", timeout=8.0, ) assert status == 503 @@ -232,7 +231,7 @@ def test_head_unreachable_upstream_sends_no_body(self, shared_r2h): "127.0.0.1", shared_r2h.port, "HEAD", - "/rtsp/127.0.0.1:%d/stream" % dead_port, + f"/rtsp/127.0.0.1:{dead_port}/stream", timeout=8.0, ) assert status == 503 @@ -245,7 +244,7 @@ def test_get_error_still_sends_body(self, shared_r2h): "127.0.0.1", shared_r2h.port, "GET", - "/rtsp/127.0.0.1:%d/stream" % dead_port, + f"/rtsp/127.0.0.1:{dead_port}/stream", timeout=8.0, ) assert status == 503 @@ -265,7 +264,7 @@ def test_head_omits_unrepresentable_duration(self, shared_r2h): "127.0.0.1", shared_r2h.port, "HEAD", - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", timeout=10.0, ) assert status == 200 @@ -288,7 +287,7 @@ def test_head_omits_open_ended_npt_range(self, shared_r2h): "127.0.0.1", shared_r2h.port, "HEAD", - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", timeout=10.0, ) assert status == 200 @@ -310,7 +309,7 @@ def test_head_after_auth_challenge_reports_retry_metadata(self, shared_r2h): "127.0.0.1", shared_r2h.port, "HEAD", - "/rtsp/user:pass@127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/user:pass@127.0.0.1:{rtsp.port}/stream", timeout=10.0, ) assert status == 200 @@ -339,7 +338,7 @@ def test_head_redirect_reports_only_final_server_metadata(self, shared_r2h): ) redirect = MockRTSPServer( custom_sdp=source_sdp, - redirect_describe_to="rtsp://127.0.0.1:%d/stream" % target.port, + redirect_describe_to=f"rtsp://127.0.0.1:{target.port}/stream", ) redirect.start() try: @@ -347,7 +346,7 @@ def test_head_redirect_reports_only_final_server_metadata(self, shared_r2h): "127.0.0.1", shared_r2h.port, "HEAD", - "/rtsp/127.0.0.1:%d/stream" % redirect.port, + f"/rtsp/127.0.0.1:{redirect.port}/stream", timeout=10.0, ) assert status == 200 @@ -373,7 +372,7 @@ def test_stream_ignores_invalid_play_metadata(self, shared_r2h): status, headers, body = stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -399,7 +398,7 @@ def test_stream_reports_direct_ts_payload(self, shared_r2h): status, headers, body = stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -422,7 +421,7 @@ def test_stream_sdp_alone_does_not_decide_payload(self, shared_r2h): status, headers, body = stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -440,13 +439,13 @@ def test_tcp_data_is_ts(self, shared_r2h): status, _, body = stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) assert status == 200 assert len(body) >= 188 - assert body[0] == 0x47, "Expected TS sync byte 0x47, got 0x%02x" % body[0] + assert body[0] == 0x47, f"Expected TS sync byte 0x47, got 0x{body[0]:02x}" finally: rtsp.stop() @@ -458,7 +457,7 @@ def test_tcp_protocol_handshake(self, shared_r2h): stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/test" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/test", read_bytes=2048, timeout=_STREAM_TIMEOUT, ) @@ -480,7 +479,7 @@ def test_requests_echo_options_session(self, shared_r2h): status, _, body = stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -490,7 +489,7 @@ def test_requests_echo_options_session(self, shared_r2h): assert rtsp.requests_received.index("OPTIONS") < rtsp.requests_received.index("DESCRIBE") for method in ("DESCRIBE", "SETUP", "PLAY"): req = next(r for r in rtsp.requests_detailed if r["method"] == method) - assert req["headers"].get("Session") == session_id, "%s must echo the OPTIONS Session" % method + assert req["headers"].get("Session") == session_id, f"{method} must echo the OPTIONS Session" finally: rtsp.stop() @@ -510,7 +509,7 @@ def test_udp_stream_returns_200(self, shared_r2h): status, headers, body = stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -529,7 +528,7 @@ def test_udp_data_is_ts(self, shared_r2h): status, _, body = stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=_STREAM_TIMEOUT, ) @@ -546,7 +545,7 @@ def test_udp_protocol_handshake(self, shared_r2h): stream_get( "127.0.0.1", shared_r2h.port, - "/rtsp/127.0.0.1:%d/test" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/test", read_bytes=2048, timeout=_STREAM_TIMEOUT, ) diff --git a/e2e/test_rtsp_zte_nat.py b/e2e/test_rtsp_zte_nat.py index e0dddd32..5b3d67dd 100644 --- a/e2e/test_rtsp_zte_nat.py +++ b/e2e/test_rtsp_zte_nat.py @@ -4,7 +4,6 @@ import struct import pytest - from helpers import ( LOOPBACK_IF, MockRTSPServer, @@ -34,7 +33,7 @@ def test_headers_endpoint_invariants_and_probe_bytes(self, r2h_binary): status, _, body = stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=4096, timeout=20.0, ) @@ -44,7 +43,7 @@ def test_headers_endpoint_invariants_and_probe_bytes(self, r2h_binary): assert rtsp.control_peer is not None tcp_source_ip, tcp_source_port = rtsp.control_peer[:2] - expected_x_nat = "%s:%d" % (tcp_source_ip, tcp_source_port) + expected_x_nat = f"{tcp_source_ip}:{tcp_source_port}" describe = _request(rtsp, "DESCRIBE") setup = _request(rtsp, "SETUP") assert describe["headers"]["x-NAT"] == expected_x_nat @@ -61,8 +60,7 @@ def test_headers_endpoint_invariants_and_probe_bytes(self, r2h_binary): "RTP/AVP/TCP;unicast;interleaved=0-1", ] + [ - "%s;unicast;client_address=%s;client_port=%d-%d;mode=PLAY" - % (profile, tcp_source_ip, rtsp._client_rtp_port, rtsp._client_rtp_port + 1) + f"{profile};unicast;client_address={tcp_source_ip};client_port={rtsp._client_rtp_port}-{(rtsp._client_rtp_port + 1)};mode=PLAY" for profile in ("MP2T/RTP/UDP", "MP2T/UDP", "RTP/AVP") ] ) @@ -87,7 +85,7 @@ def test_headers_endpoint_invariants_and_probe_bytes(self, r2h_binary): # response, and MockRTSPServerZTE withholds media until this exact # packet validates, which covers the protocol dependency without a # cross-protocol scheduling assertion. - assert "RTSP: Upstream interface route-selected, local endpoint %s:" % tcp_source_ip in r2h.read_log() + assert f"RTSP: Upstream interface route-selected, local endpoint {tcp_source_ip}:" in r2h.read_log() finally: r2h.stop() rtsp.stop() @@ -98,11 +96,11 @@ def test_interface_selection_keeps_tcp_and_udp_source_aligned(self, r2h_binary, rtsp.start() r2h_port = find_free_port() extra_args = [] - path = "/rtsp/127.0.0.1:%d/stream" % rtsp.port + path = f"/rtsp/127.0.0.1:{rtsp.port}/stream" if interface_source == "global": extra_args.extend(["--upstream-interface-rtsp", LOOPBACK_IF]) else: - path += "?r2h-ifname=%s" % LOOPBACK_IF + path += f"?r2h-ifname={LOOPBACK_IF}" r2h = R2HProcess(r2h_binary, r2h_port, extra_args=extra_args) r2h.start() try: @@ -127,7 +125,7 @@ def test_tcp_only_server_falls_back_to_interleaved_without_probe(self, r2h_binar status, _, body = stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=188, timeout=20.0, ) @@ -160,7 +158,7 @@ def test_ipv6_upstream_falls_back_to_ordinary_rtsp(self, r2h_binary): status, _, body = stream_get( "127.0.0.1", r2h_port, - "/rtsp/[::1]:%d/stream" % rtsp.port, + f"/rtsp/[::1]:{rtsp.port}/stream", read_bytes=188, timeout=20.0, ) @@ -191,7 +189,7 @@ def test_probe_echo_never_reaches_the_client(self, r2h_binary): status, _, body = stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=188 * (echo_after * 4), timeout=20.0, ) @@ -203,7 +201,7 @@ def test_probe_echo_never_reaches_the_client(self, r2h_binary): assert b"ZXV10STB" not in body aligned_len = len(body) - len(body) % 188 misaligned = [offset for offset in range(0, aligned_len, 188) if body[offset] != 0x47] - assert not misaligned, "TS alignment lost at byte offset(s) %s" % misaligned[:5] + assert not misaligned, f"TS alignment lost at byte offset(s) {misaligned[:5]}" assert "Dropped 84-byte datagram that is neither RTP nor MPEG-TS" in r2h.read_log() finally: @@ -213,7 +211,7 @@ def test_probe_echo_never_reaches_the_client(self, r2h_binary): def test_redirect_recaptures_control_endpoint(self, r2h_binary): target = MockRTSPServerZTE(num_packets=300) target.start() - redirect = MockRTSPServer(redirect_describe_to="rtsp://127.0.0.1:%d/stream" % target.port) + redirect = MockRTSPServer(redirect_describe_to=f"rtsp://127.0.0.1:{target.port}/stream") redirect.start() r2h_port = find_free_port() r2h = R2HProcess(r2h_binary, r2h_port) @@ -222,7 +220,7 @@ def test_redirect_recaptures_control_endpoint(self, r2h_binary): status, _, body = stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % redirect.port, + f"/rtsp/127.0.0.1:{redirect.port}/stream", read_bytes=188, timeout=20.0, ) @@ -230,7 +228,7 @@ def test_redirect_recaptures_control_endpoint(self, r2h_binary): assert body assert target.valid_probe_received assert target.control_peer is not None - expected_x_nat = "%s:%d" % target.control_peer[:2] + expected_x_nat = f"{target.control_peer[0]}:{target.control_peer[1]}" assert _request(target, "DESCRIBE")["headers"]["x-NAT"] == expected_x_nat finally: r2h.stop() @@ -258,7 +256,7 @@ def test_stun_mapping_is_advertised_in_x_nat_and_client_address(self, r2h_binary r2h = R2HProcess( r2h_binary, r2h_port, - extra_args=["-v", "4", "--rtsp-stun-server", "127.0.0.1:%d" % stun.port], + extra_args=["-v", "4", "--rtsp-stun-server", f"127.0.0.1:{stun.port}"], capture_log=True, ) r2h.start() @@ -266,7 +264,7 @@ def test_stun_mapping_is_advertised_in_x_nat_and_client_address(self, r2h_binary status, _, body = stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=188, timeout=20.0, ) @@ -275,14 +273,14 @@ def test_stun_mapping_is_advertised_in_x_nat_and_client_address(self, r2h_binary assert stun.requests_received >= 1 assert rtsp.valid_probe_received - expected_x_nat = "%s:%d" % (mapped_ip, mapped_port) + expected_x_nat = f"{mapped_ip}:{mapped_port}" # DESCRIBE is held back until STUN settles, so it already carries # the public mapping rather than the private endpoint. assert _request(rtsp, "DESCRIBE")["headers"]["x-NAT"] == expected_x_nat setup = _request(rtsp, "SETUP") assert setup["headers"]["x-NAT"] == expected_x_nat transport = setup["headers"]["Transport"] - assert "client_address=%s;client_port=%d-%d" % (mapped_ip, mapped_port, mapped_port + 1) in transport + assert f"client_address={mapped_ip};client_port={mapped_port}-{(mapped_port + 1)}" in transport finally: r2h.stop() rtsp.stop() @@ -298,7 +296,7 @@ def test_silent_stun_falls_back_to_local_endpoint(self, r2h_binary): r2h = R2HProcess( r2h_binary, r2h_port, - extra_args=["-v", "4", "--rtsp-stun-server", "127.0.0.1:%d" % stun.port], + extra_args=["-v", "4", "--rtsp-stun-server", f"127.0.0.1:{stun.port}"], capture_log=True, ) r2h.start() @@ -306,7 +304,7 @@ def test_silent_stun_falls_back_to_local_endpoint(self, r2h_binary): status, _, body = stream_get( "127.0.0.1", r2h_port, - "/rtsp/127.0.0.1:%d/stream" % rtsp.port, + f"/rtsp/127.0.0.1:{rtsp.port}/stream", read_bytes=188, timeout=30.0, ) @@ -314,7 +312,7 @@ def test_silent_stun_falls_back_to_local_endpoint(self, r2h_binary): assert body assert rtsp.valid_probe_received assert rtsp.control_peer is not None - assert _request(rtsp, "DESCRIBE")["headers"]["x-NAT"] == "%s:%d" % rtsp.control_peer[:2] + assert _request(rtsp, "DESCRIBE")["headers"]["x-NAT"] == f"{rtsp.control_peer[0]}:{rtsp.control_peer[1]}" log = r2h.read_log() # DESCRIBE really was parked, and STUN's ~3s retry budget did not diff --git a/e2e/test_unix_socket.py b/e2e/test_unix_socket.py index 8414f67d..72853a3b 100644 --- a/e2e/test_unix_socket.py +++ b/e2e/test_unix_socket.py @@ -101,7 +101,7 @@ def _open_unix_http_stream(socket_path: str, path: str) -> socket.socket: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(5.0) sock.connect(socket_path) - request = "GET %s HTTP/1.0\r\nHost: localhost\r\n\r\n" % path + request = f"GET {path} HTTP/1.0\r\nHost: localhost\r\n\r\n" sock.sendall(request.encode()) return sock diff --git a/e2e/test_url_template_http.py b/e2e/test_url_template_http.py index f6d58f86..1a146129 100644 --- a/e2e/test_url_template_http.py +++ b/e2e/test_url_template_http.py @@ -6,14 +6,15 @@ import time import pytest - from helpers import ( MockHTTPUpstream, R2HProcess, find_free_port, - get_upstream_path as _get_upstream_path, http_get, ) +from helpers import ( + get_upstream_path as _get_upstream_path, +) _TIMEOUT = 10.0 _STREAM_TIMEOUT = 20.0 @@ -50,8 +51,8 @@ def _make_upstream(*paths): def _extract_query_param(path, param_name): """Extract a query parameter value from a path or URI.""" - match = re.search(r"[?&]%s=([^&]+)" % re.escape(param_name), path) - assert match, "Expected %s= in path, got: %s" % (param_name, path) + match = re.search(rf"[?&]{re.escape(param_name)}=([^&]+)", path) + assert match, f"Expected {param_name}= in path, got: {path}" return match.group(1) @@ -65,11 +66,7 @@ def test_begin_end_yyyyMMddHHmmss(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file.m3u8" - "?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file.m3u8?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 @@ -148,7 +145,7 @@ def test_placeholder_substitution(self, shared_r2h, url_path_template, expected_ upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d%s?playseek=%s") % (upstream.port, url_path_template, playseek) + url = f"/http/127.0.0.1:{upstream.port}{url_path_template}?playseek={playseek}" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -161,9 +158,7 @@ def test_all_components_together(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream/{Y}/{m}/{d}/{H}/{M}/{S}?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{Y}}/{{m}}/{{d}}/{{H}}/{{M}}/{{S}}?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -176,11 +171,7 @@ def test_custom_format_with_literal_dash(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMdd-HHmmss}/${(e)yyyyMMdd-HHmmss}/file.m3u8" - "?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMdd-HHmmss}}/${{(e)yyyyMMdd-HHmmss}}/file.m3u8?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -192,14 +183,12 @@ def test_no_seek_value_passthrough(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ("/http/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file.m3u8") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file.m3u8" # No playseek parameter - URL should be forwarded as-is http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) - assert "${(b)yyyyMMddHHmmss}" in path, ( - "Without seek value, template should be passed through, got: %s" % path - ) + assert "${(b)yyyyMMddHHmmss}" in path, f"Without seek value, template should be passed through, got: {path}" finally: upstream.stop() @@ -209,9 +198,7 @@ def test_mixed_path_and_query_templates(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/data?ts={utc}&playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/data?ts={{utc}}&playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 @@ -220,7 +207,7 @@ def test_mixed_path_and_query_templates(self, shared_r2h): assert full_path.startswith("/path/20240101120000/data") # Query template also substituted (ISO8601) assert "ts=2024-01-01T12" in full_path, ( - "Query template {utc} should be substituted as ISO8601, got: %s" % full_path + f"Query template {{utc}} should be substituted as ISO8601, got: {full_path}" ) finally: upstream.stop() @@ -231,9 +218,7 @@ def test_multiple_begin_templates(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/path/${(b)yyyyMMdd}/${(b)HHmmss}/file?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMdd}}/${{(b)HHmmss}}/file?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -246,7 +231,7 @@ def test_begin_only_no_end_in_seek(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/file?playseek=20240101120000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/file?playseek=20240101120000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -261,11 +246,7 @@ def test_unix_timestamp_as_seek_value(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file?playseek=%d-%d") % ( - upstream.port, - _BEGIN_EPOCH, - _END_EPOCH, - ) + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file?playseek={_BEGIN_EPOCH}-{_END_EPOCH}" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -277,17 +258,17 @@ def test_offset_placeholder_replaced(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/{offset}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{offset}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) # {offset} should be replaced with a large positive number # (current time - 2024-01-01 12:00:00 UTC) match = re.match(r"/stream/(\d+)", path) - assert match, "Expected /stream/, got: %s" % path + assert match, f"Expected /stream/, got: {path}" offset_val = int(match.group(1)) # The offset should be at least a few million seconds (we're past 2024) - assert offset_val > 1000000, "Offset should be large (time since 2024-01-01), got: %d" % offset_val + assert offset_val > 1000000, f"Offset should be large (time since 2024-01-01), got: {offset_val}" finally: upstream.stop() @@ -297,9 +278,7 @@ def test_utc_and_begin_format_in_same_url(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/path/{utc}/${(b)yyyyMMddHHmmss}/file?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/{{utc}}/${{(b)yyyyMMddHHmmss}}/file?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -312,10 +291,7 @@ def test_iso8601_seek_range_in_template_mode(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/archive/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/video.ts" - "?playseek=2024-01-01T12:00:00.000Z-2024-01-01T13:00:00.000Z" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/archive/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/video.ts?playseek=2024-01-01T12:00:00.000Z-2024-01-01T13:00:00.000Z" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -328,9 +304,7 @@ def test_end_template_only(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/archive/${(e)yyyyMMddHHmmss}/video.ts?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/archive/${{(e)yyyyMMddHHmmss}}/video.ts?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -343,9 +317,7 @@ def test_begin_and_end_different_formats(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/archive/${(b)yyyyMMdd}/${(e)HHmmss}/video.ts?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/archive/${{(b)yyyyMMdd}}/${{(e)HHmmss}}/video.ts?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -357,18 +329,16 @@ def test_utc_and_end_in_query_only(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream?begin={utc}&end={end}&playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?begin={{utc}}&end={{end}}&playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 full_path = _get_upstream_path(upstream) assert "begin=2024-01-01T12:00:00.000Z" in full_path, ( - "Query {utc} should be substituted as ISO8601, got: %s" % full_path + f"Query {{utc}} should be substituted as ISO8601, got: {full_path}" ) assert "end=2024-01-01T13:00:00.000Z" in full_path, ( - "Query {end} should be substituted as ISO8601, got: %s" % full_path + f"Query {{end}} should be substituted as ISO8601, got: {full_path}" ) finally: upstream.stop() @@ -379,7 +349,7 @@ def test_duration_zero_same_begin_end(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/{duration}?playseek=20240101120000-20240101120000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{duration}}?playseek=20240101120000-20240101120000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -392,7 +362,7 @@ def test_long_duration_24h(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/{duration}?playseek=20240101000000-20240102000000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{duration}}?playseek=20240101000000-20240102000000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -405,9 +375,7 @@ def test_multiple_end_templates(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/path/${(e)yyyyMMdd}/${(e)HHmmss}/file?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(e)yyyyMMdd}}/${{(e)HHmmss}}/file?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -426,7 +394,7 @@ def test_component_boundary_value(self, shared_r2h, placeholder, expected_path, upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/%s?playseek=%s") % (upstream.port, placeholder, playseek) + url = f"/http/127.0.0.1:{upstream.port}/stream/{placeholder}?playseek={playseek}" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -438,17 +406,15 @@ def test_end_and_utc_iso8601_with_duration(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream/{utc}/{end}/{duration}?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{utc}}/{{end}}/{{duration}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) parts = path.strip("/").split("/") - assert len(parts) >= 4, "Expected /stream/utc/end/duration, got: %s" % path - assert "2024-01-01T12:00:00.000Z" == parts[1], "Expected begin ISO8601, got: %s" % parts[1] - assert "2024-01-01T13:00:00.000Z" == parts[2], "Expected end ISO8601, got: %s" % parts[2] - assert parts[3] == "3600", "Expected duration 3600, got: %s" % parts[3] + assert len(parts) >= 4, f"Expected /stream/utc/end/duration, got: {path}" + assert "2024-01-01T12:00:00.000Z" == parts[1], f"Expected begin ISO8601, got: {parts[1]}" + assert "2024-01-01T13:00:00.000Z" == parts[2], f"Expected end ISO8601, got: {parts[2]}" + assert parts[3] == "3600", f"Expected duration 3600, got: {parts[3]}" finally: upstream.stop() @@ -457,21 +423,19 @@ def test_multiple_templates_utc_lutc_end_duration(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ( - "/http/127.0.0.1:%d/s/{utc}/{lutc}/{end}/{duration}?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/s/{{utc}}/{{lutc}}/{{end}}/{{duration}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) parts = path.strip("/").split("/") - assert len(parts) >= 5, "Expected 5 parts, got: %s" % path - assert parts[1] == "2024-01-01T12:00:00.000Z", "Expected {utc} ISO8601 begin, got: %s" % parts[1] + assert len(parts) >= 5, f"Expected 5 parts, got: {path}" + assert parts[1] == "2024-01-01T12:00:00.000Z", f"Expected {{utc}} ISO8601 begin, got: {parts[1]}" # {lutc} is current time ISO8601 - verify format assert re.match(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z", parts[2]), ( - "Expected {lutc} ISO8601 format, got: %s" % parts[2] + f"Expected {{lutc}} ISO8601 format, got: {parts[2]}" ) - assert parts[3] == "2024-01-01T13:00:00.000Z", "Expected {end} ISO8601 end, got: %s" % parts[3] - assert parts[4] == "3600", "Expected {duration} 3600, got: %s" % parts[4] + assert parts[3] == "2024-01-01T13:00:00.000Z", f"Expected {{end}} ISO8601 end, got: {parts[3]}" + assert parts[4] == "3600", f"Expected {{duration}} 3600, got: {parts[4]}" finally: upstream.stop() @@ -494,17 +458,13 @@ def test_tz_format_passthrough(self, shared_r2h, tz): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file" - "?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file?playseek=20240101120000-20240101130000" status, _, _ = http_get( "127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT, - headers={"User-Agent": "TestPlayer/1.0 %s" % tz}, + headers={"User-Agent": f"TestPlayer/1.0 {tz}"}, ) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -518,11 +478,7 @@ def test_seek_offset_applied(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file.m3u8" - "?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file.m3u8?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -536,11 +492,7 @@ def test_seek_offset_pair_applied(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file.m3u8" - "?playseek=20240101120000-20240101130000&r2h-seek-offset=30,-60" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file.m3u8?playseek=20240101120000-20240101130000&r2h-seek-offset=30,-60" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -553,9 +505,7 @@ def test_seek_offset_pair_begin_only_template(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/file.m3u8?playseek=20240101120000&r2h-seek-offset=30,-60" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/file.m3u8?playseek=20240101120000&r2h-seek-offset=30,-60" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -568,11 +518,7 @@ def test_tz_and_offset_combined(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file.m3u8" - "?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file.m3u8?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" status, _, _ = http_get( "127.0.0.1", shared_r2h.port, @@ -599,18 +545,15 @@ def test_seek_mode_is_noop_for_http_template(self, shared_r2h): cst_str = time.strftime("%Y%m%d%H%M%S", time.gmtime(ts_utc + 8 * 3600)) # The HTTP template uses local time (no UA TZ): the rendered path # equals the input string verbatim. - expected_path = "/path/%s/file.m3u8" % cst_str + expected_path = f"/path/{cst_str}/file.m3u8" upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/file.m3u8?playseek=%s&r2h-seek-mode=range(UTC%%2B8/3600)" - ) % (upstream.port, cst_str) + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/file.m3u8?playseek={cst_str}&r2h-seek-mode=range(UTC%2B8/3600)" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path, ( - "r2h-seek-mode leaked into HTTP template: got %s, expected %s" - % (_get_upstream_path(upstream), expected_path) + f"r2h-seek-mode leaked into HTTP template: got {_get_upstream_path(upstream)}, expected {expected_path}" ) finally: upstream.stop() @@ -620,7 +563,7 @@ def test_lutc_with_timezone(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/{lutc}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{lutc}}?playseek=20240101120000-20240101130000" http_get( "127.0.0.1", shared_r2h.port, @@ -632,7 +575,7 @@ def test_lutc_with_timezone(self, shared_r2h): path = _get_upstream_path(upstream) # {lutc} outputs current time as ISO8601 - verify format match = re.match(r"/stream/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z)", path) - assert match, "Expected ISO8601 format for {lutc}, got: %s" % path + assert match, f"Expected ISO8601 format for {{lutc}}, got: {path}" finally: upstream.stop() @@ -643,7 +586,7 @@ def test_utc_with_timezone(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/{utc}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{utc}}?playseek=20240101120000-20240101130000" status, _, _ = http_get( "127.0.0.1", shared_r2h.port, @@ -662,7 +605,7 @@ def test_duration_unaffected_by_timezone(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/{duration}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{duration}}?playseek=20240101120000-20240101130000" status, _, _ = http_get( "127.0.0.1", shared_r2h.port, @@ -682,11 +625,7 @@ def test_negative_seek_offset(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file" - "?playseek=20240101120000-20240101130000&r2h-seek-offset=-30" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file?playseek=20240101120000-20240101130000&r2h-seek-offset=-30" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -699,9 +638,7 @@ def test_no_user_agent_defaults_utc(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/file?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/file?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -715,11 +652,7 @@ def test_seek_offset_with_unix_timestamp_seek(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file" - "?playseek=%d-%d&r2h-seek-offset=3600" - ) % (upstream.port, _BEGIN_EPOCH, _END_EPOCH) + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file?playseek={_BEGIN_EPOCH}-{_END_EPOCH}&r2h-seek-offset=3600" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -732,11 +665,7 @@ def test_seek_offset_pair_with_unix_timestamp_seek(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file" - "?playseek=%d-%d&r2h-seek-offset=30,-60" - ) % (upstream.port, _BEGIN_EPOCH, _END_EPOCH) + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file?playseek={_BEGIN_EPOCH}-{_END_EPOCH}&r2h-seek-offset=30,-60" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -754,7 +683,7 @@ def test_template_adjacent_to_slash(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d/{utc}/?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/{{utc}}/?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -767,9 +696,7 @@ def test_url_with_port_and_template(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/cctv/${(b)yyyyMMddHHmmss}/data.ts?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/cctv/${{(b)yyyyMMddHHmmss}}/data.ts?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -787,14 +714,11 @@ def test_url_with_port_and_template(self, shared_r2h): ) def test_begin_format_pattern(self, shared_r2h, format_pattern, expected_segment): """${(b)FORMAT} should format begin time according to the pattern.""" - expected_path = "/path/%s/file" % expected_segment + expected_path = f"/path/{expected_segment}/file" upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d/path/${(b)%s}/file?playseek=20240101120000-20240101130000") % ( - upstream.port, - format_pattern, - ) + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b){format_pattern}}}/file?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -806,11 +730,11 @@ def test_unrecognized_placeholder_passthrough(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/{foo}/{utc}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{foo}}/{{utc}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) - assert "{foo}" in path, "Unknown placeholder should pass through, got: %s" % path + assert "{foo}" in path, f"Unknown placeholder should pass through, got: {path}" assert "2024-01-01T12:00:00.000Z" in path, "Known placeholder {utc} should still be substituted as ISO8601" finally: upstream.stop() @@ -822,10 +746,7 @@ def test_template_date_boundary_midnight(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/file" - "?playseek=20240101230000-20240102000000&r2h-seek-offset=7200" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/file?playseek=20240101230000-20240102000000&r2h-seek-offset=7200" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -838,11 +759,7 @@ def test_custom_seek_name_with_template(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file" - "?r2h-seek-name=myseek&myseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file?r2h-seek-name=myseek&myseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -855,7 +772,7 @@ def test_open_ended_seek_range(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/file?playseek=20240101120000-") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/file?playseek=20240101120000-" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -868,15 +785,13 @@ def test_template_with_existing_static_query_params(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/file?quality=hd&playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/file?quality=hd&playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 full_path = _get_upstream_path(upstream) assert full_path.startswith("/path/20240101120000/file") - assert "quality=hd" in full_path, "Static query params should be preserved, got: %s" % full_path + assert "quality=hd" in full_path, f"Static query params should be preserved, got: {full_path}" finally: upstream.stop() @@ -885,7 +800,7 @@ def test_end_placeholder_requires_end_value(self, shared_r2h): upstream = _make_upstream("/unused") upstream.start() try: - url = ("/http/127.0.0.1:%d/archive/${(e)yyyyMMddHHmmss}/video.ts?playseek=20240101120000-") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/archive/${{(e)yyyyMMddHHmmss}}/video.ts?playseek=20240101120000-" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status >= 400, "Expected request to fail without an end time" assert len(upstream.requests_log) == 0, ( @@ -900,9 +815,7 @@ def test_february_29_leap_year(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/archive/${(b)yyyyMMdd}/${(b)HHmmss}/file?playseek=20240229120000-20240229130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/archive/${{(b)yyyyMMdd}}/${{(b)HHmmss}}/file?playseek=20240229120000-20240229130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -916,10 +829,7 @@ def test_year_boundary(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/archive/${(b)yyyyMMddHHmmss}/file" - "?playseek=20241231230000-20250101010000&r2h-seek-offset=7200" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/archive/${{(b)yyyyMMddHHmmss}}/file?playseek=20241231230000-20250101010000&r2h-seek-offset=7200" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -932,9 +842,7 @@ def test_gmt_suffix_seek_with_template(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/file?playseek=20240101120000GMT-20240101130000GMT" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/file?playseek=20240101120000GMT-20240101130000GMT" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -947,9 +855,7 @@ def test_template_preserves_url_path_structure(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/a/b/${(b)yyyyMMddHHmmss}/c/d/file.m3u8?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/a/b/${{(b)yyyyMMddHHmmss}}/c/d/file.m3u8?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -962,11 +868,11 @@ def test_format_with_colons(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ("/http/127.0.0.1:%d/path/${(b)HH:mm:ss}/file?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)HH:mm:ss}}/file?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) - assert "12:00:00" in path, "Format with colons should produce 12:00:00, got: %s" % path + assert "12:00:00" in path, f"Format with colons should produce 12:00:00, got: {path}" finally: upstream.stop() @@ -980,15 +886,13 @@ def test_playseek_appended(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ("/http/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 path = _get_upstream_path(upstream) - assert "playseek=" in path, "Query-append mode should append playseek, got: %s" % path - assert path.startswith("/stream?") or path.startswith("/stream&"), ( - "Path should be /stream with query, got: %s" % path - ) + assert "playseek=" in path, f"Query-append mode should append playseek, got: {path}" + assert path.startswith(("/stream?", "/stream&")), f"Path should be /stream with query, got: {path}" finally: upstream.stop() @@ -1002,14 +906,12 @@ def test_custom_seek_name_forwarded(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream?r2h-seek-name=myseek&myseek=20240301100000-20240301110000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?r2h-seek-name=myseek&myseek=20240301100000-20240301110000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 path = _get_upstream_path(upstream) - assert "myseek=" in path, "Custom seek param should be forwarded, got: %s" % path + assert "myseek=" in path, f"Custom seek param should be forwarded, got: {path}" finally: upstream.stop() @@ -1018,13 +920,11 @@ def test_r2h_seek_name_stripped(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream?r2h-seek-name=myseek&myseek=20240301100000-20240301110000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?r2h-seek-name=myseek&myseek=20240301100000-20240301110000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) - assert "r2h-seek-name" not in path, "r2h-seek-name should be stripped, got: %s" % path + assert "r2h-seek-name" not in path, f"r2h-seek-name should be stripped, got: {path}" assert "myseek=" in path finally: upstream.stop() @@ -1039,9 +939,7 @@ def test_positive_offset(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) @@ -1055,8 +953,8 @@ def test_offset_pair(self, shared_r2h): upstream.start() try: url = ( - "/http/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=30,-60" - ) % upstream.port + f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=30,-60" + ) http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) @@ -1069,9 +967,7 @@ def test_negative_offset(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=-30" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=-30" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) @@ -1084,15 +980,12 @@ def test_overflow_offset_rejected(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000" - "&r2h-seek-offset=999999999999999999999999999999" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=999999999999999999999999999999" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) assert _extract_query_param(path, "playseek") == "20240101120000-20240101130000" - assert "r2h-seek-offset" not in path, "r2h-seek-offset should be stripped, got: %s" % path + assert "r2h-seek-offset" not in path, f"r2h-seek-offset should be stripped, got: {path}" finally: upstream.stop() @@ -1101,13 +994,11 @@ def test_offset_stripped_from_upstream(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) - assert "r2h-seek-offset" not in path, "r2h-seek-offset should be stripped, got: %s" % path + assert "r2h-seek-offset" not in path, f"r2h-seek-offset should be stripped, got: {path}" finally: upstream.stop() @@ -1116,7 +1007,7 @@ def test_offset_with_unix_timestamp(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ("/http/127.0.0.1:%d/stream?playseek=1704096000-1704099600&r2h-seek-offset=3600") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=1704096000-1704099600&r2h-seek-offset=3600" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) @@ -1129,7 +1020,7 @@ def test_offset_pair_with_unix_timestamp(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ("/http/127.0.0.1:%d/stream?playseek=1704096000-1704099600&r2h-seek-offset=30,-60") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=1704096000-1704099600&r2h-seek-offset=30,-60" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) @@ -1147,7 +1038,7 @@ def test_tz_utc_minus_5_converts_yyyyMMddHHmmss(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ("/http/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000-20240101130000" http_get( "127.0.0.1", shared_r2h.port, @@ -1166,7 +1057,7 @@ def test_no_tz_no_conversion(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ("/http/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000-20240101130000" http_get( "127.0.0.1", shared_r2h.port, @@ -1185,7 +1076,7 @@ def test_unix_timestamp_skips_tz_conversion(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ("/http/127.0.0.1:%d/stream?playseek=1704096000-1704099600") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=1704096000-1704099600" http_get( "127.0.0.1", shared_r2h.port, @@ -1204,9 +1095,7 @@ def test_tz_with_offset_combined(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" http_get( "127.0.0.1", shared_r2h.port, @@ -1225,9 +1114,7 @@ def test_gmt_suffix_preserved(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream?playseek=20240101120000GMT-20240101130000GMT&r2h-seek-offset=3600" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000GMT-20240101130000GMT&r2h-seek-offset=3600" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) @@ -1240,7 +1127,7 @@ def test_unix_timestamp_format_preserved(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ("/http/127.0.0.1:%d/stream?playseek=1704096000-1704099600") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=1704096000-1704099600" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) @@ -1254,7 +1141,7 @@ def test_gmt_suffix_ignores_ua_tz(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ("/http/127.0.0.1:%d/stream?playseek=20240101120000GMT-20240101130000GMT") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000GMT-20240101130000GMT" http_get( "127.0.0.1", shared_r2h.port, @@ -1274,9 +1161,7 @@ def test_gmt_suffix_with_ua_tz_still_applies_seek_offset(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream?playseek=20240101120000GMT-20240101130000GMT&r2h-seek-offset=3600" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000GMT-20240101130000GMT&r2h-seek-offset=3600" http_get( "127.0.0.1", shared_r2h.port, @@ -1296,12 +1181,12 @@ def test_tvdr_appended(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ("/http/127.0.0.1:%d/stream?tvdr=20240601080000-20240601090000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?tvdr=20240601080000-20240601090000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 path = _get_upstream_path(upstream) - assert "tvdr=" in path, "tvdr should be appended, got: %s" % path + assert "tvdr=" in path, f"tvdr should be appended, got: {path}" finally: upstream.stop() @@ -1310,12 +1195,12 @@ def test_no_template_no_seek(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = "/http/127.0.0.1:%d/stream" % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 path = _get_upstream_path(upstream) - assert path == "/stream", "URL without seek should be unchanged, got: %s" % path + assert path == "/stream", f"URL without seek should be unchanged, got: {path}" finally: upstream.stop() @@ -1324,14 +1209,14 @@ def test_append_with_existing_query(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ("/http/127.0.0.1:%d/stream?token=abc&playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?token=abc&playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 path = _get_upstream_path(upstream) assert "token=abc" in path, "Existing query params should be preserved" assert "playseek=" in path, "Seek should be appended" - assert "&playseek=" in path, "Seek should be appended with & separator, got: %s" % path + assert "&playseek=" in path, f"Seek should be appended with & separator, got: {path}" finally: upstream.stop() @@ -1340,7 +1225,7 @@ def test_tz_conversion_in_query_append(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ("/http/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=20240101120000-20240101130000" status, _, _ = http_get( "127.0.0.1", shared_r2h.port, @@ -1354,9 +1239,7 @@ def test_tz_conversion_in_query_append(self, shared_r2h): match = re.search(r"playseek=([^&]+)", path) assert match, "Expected playseek in path" val = match.group(1) - assert val == "20240101040000-20240101050000", ( - "TZ conversion should work in query-append mode, got: %s" % val - ) + assert val == "20240101040000-20240101050000", f"TZ conversion should work in query-append mode, got: {val}" finally: upstream.stop() @@ -1365,9 +1248,7 @@ def test_iso8601_seek_range_in_query_append(self, shared_r2h): upstream = _make_upstream("/stream") upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream?playseek=2024-01-01T12:00:00.000Z-2024-01-01T13:00:00.000Z" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream?playseek=2024-01-01T12:00:00.000Z-2024-01-01T13:00:00.000Z" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 @@ -1375,7 +1256,7 @@ def test_iso8601_seek_range_in_query_append(self, shared_r2h): match = re.search(r"playseek=([^&]+)", path) assert match, "Expected playseek in path" assert match.group(1) == "2024-01-01T12:00:00.000Z-2024-01-01T13:00:00.000Z", ( - "ISO8601 seek range should remain intact after splitting, got: %s" % match.group(1) + f"ISO8601 seek range should remain intact after splitting, got: {match.group(1)}" ) finally: upstream.stop() diff --git a/e2e/test_url_template_m3u.py b/e2e/test_url_template_m3u.py index 25b41a28..bbb7e5da 100644 --- a/e2e/test_url_template_m3u.py +++ b/e2e/test_url_template_m3u.py @@ -5,16 +5,17 @@ import re import pytest - from helpers import ( MockHTTPUpstream, R2HProcess, build_config, extract_catchup_source, find_free_port, - get_upstream_path as _get_upstream_path, http_get, ) +from helpers import ( + get_upstream_path as _get_upstream_path, +) _TIMEOUT = 10.0 _STREAM_TIMEOUT = 20.0 @@ -42,7 +43,7 @@ def _make_upstream(*paths): def _absolute_url_to_path(url): """Convert an absolute proxy URL back into an HTTP path for local requests.""" match = re.match(r"https?://[^/]+(/.*)$", url) - assert match, "Expected absolute HTTP URL, got: %s" % url + assert match, f"Expected absolute HTTP URL, got: {url}" return match.group(1) @@ -96,21 +97,21 @@ def test_path_template_gets_playseek_injected(self, catchup_rewrite_playlist): text = catchup_rewrite_playlist assert "Template Channel" in text assert "playseek=${(b)yyyyMMddHHmmss}-${(e)yyyyMMddHHmmss}" in text, ( - "Path template catchup-source should keep original placeholder format, got:\n%s" % text + f"Path template catchup-source should keep original placeholder format, got:\n{text}" ) def test_query_only_templates_become_playseek_carrier(self, catchup_rewrite_playlist): """Query-only catchup templates should be folded into a playseek carrier.""" _, catchup_source = extract_catchup_source(catchup_rewrite_playlist, "Query Template Ch") assert "playseek={utc:YmdHMS}-{utcend:YmdHMS}" in catchup_source, ( - "Expected query templates to become playseek carrier, got: %s" % catchup_source + f"Expected query templates to become playseek carrier, got: {catchup_source}" ) def test_query_only_tvdr_range_preserves_seek_param_name(self, catchup_rewrite_playlist): """A tvdr range template should keep tvdr as the playlist seek carrier.""" _, catchup_source = extract_catchup_source(catchup_rewrite_playlist, "TVDR Template Ch") assert "tvdr=${(b)yyyyMMddHHmmss}GMT-${(e)yyyyMMddHHmmss}GMT" in catchup_source, ( - "Expected tvdr range to stay under tvdr, got: %s" % catchup_source + f"Expected tvdr range to stay under tvdr, got: {catchup_source}" ) assert "playseek=" not in catchup_source @@ -118,7 +119,7 @@ def test_query_only_explicit_seek_name_preserves_param_name(self, catchup_rewrit """A configured custom seek name should be preserved as the playlist seek carrier.""" _, catchup_source = extract_catchup_source(catchup_rewrite_playlist, "Custom Seek Template Ch") assert "customseek={utc:YmdHMS}-{utcend:YmdHMS}" in catchup_source, ( - "Expected explicit seek name to stay under customseek, got: %s" % catchup_source + f"Expected explicit seek name to stay under customseek, got: {catchup_source}" ) assert "playseek=" not in catchup_source @@ -136,17 +137,17 @@ def test_path_template_with_dollar_brace(self, catchup_rewrite_playlist): def test_path_template_uses_original_playseek_format(self, catchup_rewrite_playlist): """Multi-part path templates should keep the original placeholder fragments in playseek.""" assert "playseek=${(b)yyyyMMdd}${(b)HHmmss}-${(e)yyyyMMdd}${(e)HHmmss}" in catchup_rewrite_playlist, ( - "Expected original placeholder fragments in playseek, got:\n%s" % catchup_rewrite_playlist + f"Expected original placeholder fragments in playseek, got:\n{catchup_rewrite_playlist}" ) def test_query_templates_prefer_playseek_carrier(self, catchup_rewrite_playlist): """Query templates should become the proxied playseek carrier when present.""" _, catchup_source = extract_catchup_source(catchup_rewrite_playlist, "Mixed Channel") assert "playseek={utc}" in catchup_source, ( - "Expected query begin template to become playseek carrier, got: %s" % catchup_source + f"Expected query begin template to become playseek carrier, got: {catchup_source}" ) assert "begin={utc}" not in catchup_source, ( - "Expected original query template to be folded into playseek, got: %s" % catchup_source + f"Expected original query template to be folded into playseek, got: {catchup_source}" ) def test_main_service_query_templates_are_preserved_in_playlist(self, catchup_rewrite_playlist): @@ -159,31 +160,31 @@ def test_main_service_query_templates_are_preserved_in_playlist(self, catchup_re service_url = lines[channel_index + 1] assert "/Main%20Query%20Template%20Ch?begin={utc}" in service_url, ( - "Expected transformed main service URL to preserve template query, got: %s" % service_url + f"Expected transformed main service URL to preserve template query, got: {service_url}" ) assert "token=1" not in service_url, ( - "Expected static query params to stay stripped from transformed main service URL, got: %s" % service_url + f"Expected static query params to stay stripped from transformed main service URL, got: {service_url}" ) def test_append_catchup_exposes_playseek_carrier(self, catchup_rewrite_playlist): """Append-mode query templates should also expose a fixed playseek carrier.""" _, catchup_source = extract_catchup_source(catchup_rewrite_playlist, "Placeholder Ch") assert "playseek={utc}-{utcend}" in catchup_source, ( - "Expected append-mode placeholders to expose playseek carrier, got: %s" % catchup_source + f"Expected append-mode placeholders to expose playseek carrier, got: {catchup_source}" ) def test_append_query_templates_become_playseek_carrier(self, catchup_rewrite_playlist): """Append-mode start/end query templates should be folded into playseek.""" _, catchup_source = extract_catchup_source(catchup_rewrite_playlist, "Append Offset Ch") assert "playseek=${(b)yyyyMMdd|UTC}T${(b)HHmmss|UTC}-${(e)yyyyMMdd|UTC}T${(e)HHmmss|UTC}" in catchup_source, ( - "Expected append-mode query templates to become playseek carrier, got: %s" % catchup_source + f"Expected append-mode query templates to become playseek carrier, got: {catchup_source}" ) def test_append_query_templates_without_prefix_become_playseek_carrier(self, catchup_rewrite_playlist): """Append-mode query templates without a leading separator should also be folded into playseek.""" _, catchup_source = extract_catchup_source(catchup_rewrite_playlist, "Append No Prefix Ch") assert "playseek=${(b)yyyyMMdd|UTC}T${(b)HHmmss|UTC}-${(e)yyyyMMdd|UTC}T${(e)HHmmss|UTC}" in catchup_source, ( - "Expected append-mode query templates without prefix to become playseek carrier, got: %s" % catchup_source + f"Expected append-mode query templates without prefix to become playseek carrier, got: {catchup_source}" ) @@ -358,10 +359,10 @@ def test_current_time_templates_survive_playseek_rewrite(self, r2h_binary): assert full_path.startswith(expected_path) assert "begin=2024-01-01T12:00:00.000Z" in full_path assert re.search(r"[?&]now=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z", full_path), ( - "Expected {lutc} to resolve as ISO8601 UTC, got: %s" % full_path + f"Expected {{lutc}} to resolve as ISO8601 UTC, got: {full_path}" ) assert re.search(r"[?&]ts=\d+", full_path), ( - "Expected {timestamp} to resolve as Unix timestamp, got: %s" % full_path + f"Expected {{timestamp}} to resolve as Unix timestamp, got: {full_path}" ) finally: r2h.stop() @@ -410,10 +411,10 @@ def test_query_template_offset_applies_via_playseek_carrier(self, r2h_binary): full_path = _get_upstream_path(upstream) assert "starttime=20240101T130000" in full_path, ( - "Expected r2h-seek-offset to shift starttime by +1h, got: %s" % full_path + f"Expected r2h-seek-offset to shift starttime by +1h, got: {full_path}" ) assert "endtime=20240101T140000" in full_path, ( - "Expected r2h-seek-offset to shift endtime by +1h, got: %s" % full_path + f"Expected r2h-seek-offset to shift endtime by +1h, got: {full_path}" ) finally: r2h.stop() @@ -462,10 +463,10 @@ def test_append_query_template_offset_applies_via_playseek_carrier(self, r2h_bin full_path = _get_upstream_path(upstream) assert "starttime=20240101T130000" in full_path, ( - "Expected append-mode r2h-seek-offset to shift starttime by +1h, got: %s" % full_path + f"Expected append-mode r2h-seek-offset to shift starttime by +1h, got: {full_path}" ) assert "endtime=20240101T140000" in full_path, ( - "Expected append-mode r2h-seek-offset to shift endtime by +1h, got: %s" % full_path + f"Expected append-mode r2h-seek-offset to shift endtime by +1h, got: {full_path}" ) finally: r2h.stop() @@ -514,10 +515,10 @@ def test_append_no_prefix_query_template_offset_applies_via_playseek_carrier(sel full_path = _get_upstream_path(upstream) assert "starttime=20240101T130000" in full_path, ( - "Expected append-mode no-prefix r2h-seek-offset to shift starttime by +1h, got: %s" % full_path + f"Expected append-mode no-prefix r2h-seek-offset to shift starttime by +1h, got: {full_path}" ) assert "endtime=20240101T140000" in full_path, ( - "Expected append-mode no-prefix r2h-seek-offset to shift endtime by +1h, got: %s" % full_path + f"Expected append-mode no-prefix r2h-seek-offset to shift endtime by +1h, got: {full_path}" ) finally: r2h.stop() diff --git a/e2e/test_url_template_placeholders.py b/e2e/test_url_template_placeholders.py index 67ec884f..9c86dabc 100644 --- a/e2e/test_url_template_placeholders.py +++ b/e2e/test_url_template_placeholders.py @@ -5,14 +5,15 @@ import re import pytest - from helpers import ( MockHTTPUpstream, R2HProcess, find_free_port, - get_upstream_path as _get_upstream_path, http_get, ) +from helpers import ( + get_upstream_path as _get_upstream_path, +) _TIMEOUT = 10.0 _STREAM_TIMEOUT = 20.0 @@ -54,7 +55,7 @@ def _assert_template_path(shared_r2h, path_template, expected_path, playseek=_DE upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d%s?playseek=%s") % (upstream.port, path_template, playseek) + url = f"/http/127.0.0.1:{upstream.port}{path_template}?playseek={playseek}" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT, headers=headers) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -106,25 +107,25 @@ def test_dollar_brace_timestamp(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/${timestamp}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/${{timestamp}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) match = re.match(r"/stream/(\d+)", path) - assert match, "Expected /stream/, got: %s" % path + assert match, f"Expected /stream/, got: {path}" ts = int(match.group(1)) # Should be a recent epoch - assert ts > 1700000000, "Timestamp should be a recent epoch, got: %d" % ts + assert ts > 1700000000, f"Timestamp should be a recent epoch, got: {ts}" finally: upstream.stop() def test_be_timestamp_keyword(self, shared_r2h): """${(b)timestamp} outputs begin epoch.""" - expected_path = "/stream/%d" % _BEGIN_EPOCH + expected_path = f"/stream/{_BEGIN_EPOCH}" upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/${(b)timestamp}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/${{(b)timestamp}}?playseek=20240101120000-20240101130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -141,12 +142,12 @@ def test_now_placeholder(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/{now}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{now}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) match = re.match(r"/stream/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z)", path) - assert match, "Expected ISO8601 format for {now}, got: %s" % path + assert match, f"Expected ISO8601 format for {{now}}, got: {path}" finally: upstream.stop() @@ -197,7 +198,7 @@ class TestPlaceholderSyntaxLongKeywords: pytest.param("/stream/${utcend}", "/stream/2024-01-01T13:00:00.000Z", id="utcend"), pytest.param("/stream/${start}", "/stream/2024-01-01T12:00:00.000Z", id="start"), pytest.param("/stream/${end}", "/stream/2024-01-01T13:00:00.000Z", id="end"), - pytest.param("/stream/${(e)timestamp}", "/stream/%d" % _END_EPOCH, id="end-timestamp"), + pytest.param("/stream/${(e)timestamp}", f"/stream/{_END_EPOCH}", id="end-timestamp"), ], ) def test_deterministic_long_keyword_syntax(self, shared_r2h, path_template, expected_path): @@ -209,11 +210,11 @@ def test_dollar_brace_lutc(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/${lutc}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/${{lutc}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) assert re.match(r"/stream/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z", path), ( - "Expected ISO8601 for ${lutc}, got: %s" % path + f"Expected ISO8601 for ${{lutc}}, got: {path}" ) finally: upstream.stop() @@ -223,11 +224,11 @@ def test_dollar_brace_now(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/${now}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/${{now}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) assert re.match(r"/stream/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z", path), ( - "Expected ISO8601 for ${now}, got: %s" % path + f"Expected ISO8601 for ${{now}}, got: {path}" ) finally: upstream.stop() @@ -237,11 +238,11 @@ def test_dollar_brace_offset(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/${offset}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/${{offset}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) match = re.match(r"/stream/(\d+)", path) - assert match, "Expected numeric offset, got: %s" % path + assert match, f"Expected numeric offset, got: {path}" assert int(match.group(1)) > 1000000 finally: upstream.stop() @@ -270,13 +271,11 @@ def test_lutc_long_format(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream/${lutc:yyyyMMddHHmmss}?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/${{lutc:yyyyMMddHHmmss}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) assert re.match(r"/stream/\d{14}", path), ( - "Expected 14-digit formatted time for ${lutc:FORMAT}, got: %s" % path + f"Expected 14-digit formatted time for ${{lutc:FORMAT}}, got: {path}" ) finally: upstream.stop() @@ -287,12 +286,12 @@ def test_now_long_format(self, shared_r2h): upstream.start() try: url = ( - "/http/127.0.0.1:%d/stream/${now:yyyyMMddHHmmss}?playseek=20240101120000-20240101130000" - ) % upstream.port + f"/http/127.0.0.1:{upstream.port}/stream/${{now:yyyyMMddHHmmss}}?playseek=20240101120000-20240101130000" + ) http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) assert re.match(r"/stream/\d{14}", path), ( - "Expected 14-digit formatted time for ${now:FORMAT}, got: %s" % path + f"Expected 14-digit formatted time for ${{now:FORMAT}}, got: {path}" ) finally: upstream.stop() @@ -302,13 +301,11 @@ def test_timestamp_long_format(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream/${timestamp:yyyyMMddHHmmss}?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/${{timestamp:yyyyMMddHHmmss}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) assert re.match(r"/stream/\d{14}", path), ( - "Expected 14-digit formatted time for ${timestamp:FORMAT}, got: %s" % path + f"Expected 14-digit formatted time for ${{timestamp:FORMAT}}, got: {path}" ) finally: upstream.stop() @@ -318,11 +315,11 @@ def test_lutc_short_format(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/{lutc:YmdHMS}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{lutc:YmdHMS}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) assert re.match(r"/stream/\d{14}", path), ( - "Expected 14-digit formatted time for {lutc:FORMAT}, got: %s" % path + f"Expected 14-digit formatted time for {{lutc:FORMAT}}, got: {path}" ) finally: upstream.stop() @@ -332,11 +329,11 @@ def test_now_short_format(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/{now:YmdHMS}?playseek=20240101120000-20240101130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{now:YmdHMS}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) assert re.match(r"/stream/\d{14}", path), ( - "Expected 14-digit formatted time for {now:FORMAT}, got: %s" % path + f"Expected 14-digit formatted time for {{now:FORMAT}}, got: {path}" ) finally: upstream.stop() @@ -346,13 +343,11 @@ def test_timestamp_short_format(self, shared_r2h): upstream = _make_upstream() upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream/{timestamp:YmdHMS}?playseek=20240101120000-20240101130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/{{timestamp:YmdHMS}}?playseek=20240101120000-20240101130000" http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) path = _get_upstream_path(upstream) assert re.match(r"/stream/\d{14}", path), ( - "Expected 14-digit formatted time for {timestamp:FORMAT}, got: %s" % path + f"Expected 14-digit formatted time for {{timestamp:FORMAT}}, got: {path}" ) finally: upstream.stop() @@ -380,8 +375,8 @@ class TestPlaceholderSyntaxShortBeginEnd: _TZ_PLUS_8_HEADERS, id="end-utc-modifier", ), - pytest.param("/stream/{(b)timestamp}", "/stream/%d" % _BEGIN_EPOCH, None, id="begin-timestamp"), - pytest.param("/stream/{(e)timestamp}", "/stream/%d" % _END_EPOCH, None, id="end-timestamp"), + pytest.param("/stream/{(b)timestamp}", f"/stream/{_BEGIN_EPOCH}", None, id="begin-timestamp"), + pytest.param("/stream/{(e)timestamp}", f"/stream/{_END_EPOCH}", None, id="end-timestamp"), ], ) def test_short_begin_end_syntax(self, shared_r2h, path_template, expected_path, headers): @@ -399,11 +394,7 @@ def test_basic_iso8601_seek_value(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file.m3u8" - "?playseek=20240101T120000-20240101T130000" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file.m3u8?playseek=20240101T120000-20240101T130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -416,11 +407,7 @@ def test_basic_iso8601_seek_with_z_suffix(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file.m3u8" - "?playseek=20240101T120000Z-20240101T130000Z" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file.m3u8?playseek=20240101T120000Z-20240101T130000Z" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -435,11 +422,7 @@ def test_iso8601_seek_with_positive_tz_offset(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file.m3u8" - "?playseek=2024-01-01T20:00:00.000%%2B08:00-2024-01-01T21:00:00.000%%2B08:00" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file.m3u8?playseek=2024-01-01T20:00:00.000%2B08:00-2024-01-01T21:00:00.000%2B08:00" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -454,11 +437,7 @@ def test_iso8601_seek_with_negative_tz_offset(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file.m3u8" - "?playseek=2024-01-01T07:00:00.000-05:00-2024-01-01T08:00:00.000-05:00" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file.m3u8?playseek=2024-01-01T07:00:00.000-05:00-2024-01-01T08:00:00.000-05:00" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -473,11 +452,7 @@ def test_basic_iso8601_seek_with_tz_offset(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/file.m3u8" - "?playseek=20240101T200000%%2B08:00-20240101T210000%%2B08:00" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/file.m3u8?playseek=20240101T200000%2B08:00-20240101T210000%2B08:00" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 assert _get_upstream_path(upstream) == expected_path @@ -490,7 +465,7 @@ def test_basic_iso8601_seek_in_query_append_mode(self, shared_r2h): upstream = _make_upstream(expected_path) upstream.start() try: - url = ("/http/127.0.0.1:%d/stream/live.m3u8?playseek=20240101T120000-20240101T130000") % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/live.m3u8?playseek=20240101T120000-20240101T130000" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 full_path = _get_upstream_path(upstream) @@ -507,9 +482,7 @@ def test_basic_iso8601_with_tz_offset_preserves_bytes_in_query_append(self, shar upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream/live.m3u8?playseek=20240101T200000%%2B08:00-20240101T210000%%2B08:00" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/live.m3u8?playseek=20240101T200000%2B08:00-20240101T210000%2B08:00" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 full_path = _get_upstream_path(upstream) @@ -524,10 +497,7 @@ def test_basic_iso8601_with_tz_offset_and_seek_offset_shifts_clock_in_original_t upstream = _make_upstream(expected_path) upstream.start() try: - url = ( - "/http/127.0.0.1:%d/stream/live.m3u8" - "?playseek=20240101T200000%%2B08:00-20240101T210000%%2B08:00&r2h-seek-offset=3600" - ) % upstream.port + url = f"/http/127.0.0.1:{upstream.port}/stream/live.m3u8?playseek=20240101T200000%2B08:00-20240101T210000%2B08:00&r2h-seek-offset=3600" status, _, _ = http_get("127.0.0.1", shared_r2h.port, url, timeout=_TIMEOUT) assert status == 200 full_path = _get_upstream_path(upstream) diff --git a/e2e/test_url_template_rtsp.py b/e2e/test_url_template_rtsp.py index 4961f758..6fb87928 100644 --- a/e2e/test_url_template_rtsp.py +++ b/e2e/test_url_template_rtsp.py @@ -6,7 +6,6 @@ import time import pytest - from helpers import ( MockRTSPServer, MockRTSPServerUDP, @@ -38,8 +37,8 @@ def shared_r2h(r2h_binary): def _extract_query_param(path, param_name): """Extract a query parameter value from a path or URI.""" - match = re.search(r"[?&]%s=([^&]+)" % re.escape(param_name), path) - assert match, "Expected %s= in path, got: %s" % (param_name, path) + match = re.search(rf"[?&]{re.escape(param_name)}=([^&]+)", path) + assert match, f"Expected {param_name}= in path, got: {path}" return match.group(1) @@ -59,12 +58,8 @@ def test_begin_end_in_describe_uri(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ( - "/rtsp/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/${(e)yyyyMMddHHmmss}/stream" - "?playseek=20240101120000-20240101130000" - ) % rtsp.port - status, _, body = stream_get( + url = f"/rtsp/127.0.0.1:{rtsp.port}/path/${{(b)yyyyMMddHHmmss}}/${{(e)yyyyMMddHHmmss}}/stream?playseek=20240101120000-20240101130000" + status, _, _body = stream_get( "127.0.0.1", shared_r2h.port, url, @@ -76,9 +71,9 @@ def test_begin_end_in_describe_uri(self, shared_r2h): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert len(describe_reqs) > 0, "Expected DESCRIBE request" uri = describe_reqs[0]["uri"] - assert "20240101120000" in uri, "Begin template should be substituted in DESCRIBE URI, got: %s" % uri - assert "20240101130000" in uri, "End template should be substituted in DESCRIBE URI, got: %s" % uri - assert "${" not in uri, "No unresolved templates should remain in URI, got: %s" % uri + assert "20240101120000" in uri, f"Begin template should be substituted in DESCRIBE URI, got: {uri}" + assert "20240101130000" in uri, f"End template should be substituted in DESCRIBE URI, got: {uri}" + assert "${" not in uri, f"No unresolved templates should remain in URI, got: {uri}" finally: rtsp.stop() @@ -87,8 +82,8 @@ def test_utc_in_rtsp_uri(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream/{utc}?playseek=20240101120000-20240101130000") % rtsp.port - status, _, body = stream_get( + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream/{{utc}}?playseek=20240101120000-20240101130000" + status, _, _body = stream_get( "127.0.0.1", shared_r2h.port, url, @@ -100,7 +95,7 @@ def test_utc_in_rtsp_uri(self, shared_r2h): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert len(describe_reqs) > 0 uri = describe_reqs[0]["uri"] - assert "2024-01-01T12:00:00.000Z" in uri, "Expected ISO8601 in URI, got: %s" % uri + assert "2024-01-01T12:00:00.000Z" in uri, f"Expected ISO8601 in URI, got: {uri}" finally: rtsp.stop() @@ -109,12 +104,8 @@ def test_template_with_custom_seek_name(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ( - "/rtsp/127.0.0.1:%d" - "/path/${(b)yyyyMMddHHmmss}/stream" - "?r2h-seek-name=myseek&myseek=20240101120000-20240101130000" - ) % rtsp.port - status, _, body = stream_get( + url = f"/rtsp/127.0.0.1:{rtsp.port}/path/${{(b)yyyyMMddHHmmss}}/stream?r2h-seek-name=myseek&myseek=20240101120000-20240101130000" + status, _, _body = stream_get( "127.0.0.1", shared_r2h.port, url, @@ -126,9 +117,9 @@ def test_template_with_custom_seek_name(self, shared_r2h): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert len(describe_reqs) > 0 uri = describe_reqs[0]["uri"] - assert "20240101120000" in uri, "Template should be substituted in RTSP URI, got: %s" % uri - assert "${" not in uri, "No unresolved templates, got: %s" % uri - assert "r2h-seek-name" not in uri, "r2h-seek-name should be stripped, got: %s" % uri + assert "20240101120000" in uri, f"Template should be substituted in RTSP URI, got: {uri}" + assert "${" not in uri, f"No unresolved templates, got: {uri}" + assert "r2h-seek-name" not in uri, f"r2h-seek-name should be stripped, got: {uri}" finally: rtsp.stop() @@ -137,8 +128,8 @@ def test_rtsp_duration_placeholder(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream/{duration}?playseek=20240101120000-20240101130000") % rtsp.port - status, _, body = stream_get( + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream/{{duration}}?playseek=20240101120000-20240101130000" + status, _, _body = stream_get( "127.0.0.1", shared_r2h.port, url, @@ -150,7 +141,7 @@ def test_rtsp_duration_placeholder(self, shared_r2h): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert len(describe_reqs) > 0 uri = describe_reqs[0]["uri"] - assert "/stream/3600" in uri, "Duration should be 3600 in RTSP URI, got: %s" % uri + assert "/stream/3600" in uri, f"Duration should be 3600 in RTSP URI, got: {uri}" finally: rtsp.stop() @@ -168,22 +159,19 @@ def test_template_expands_when_recent_clock_path_fires(self, shared_r2h): # echoes the same string back via ${(b)yyyyMMddHHmmss}, and the # recent-clock path also fires (verified via PLAY Range header). utc_str = time.strftime("%Y%m%d%H%M%S", time.gmtime(start_ts)) - url = ("/rtsp/127.0.0.1:%d/path/${(b)yyyyMMddHHmmss}/stream?playseek=%s&r2h-seek-mode=range(3600)") % ( - rtsp.port, - utc_str, - ) + url = f"/rtsp/127.0.0.1:{rtsp.port}/path/${{(b)yyyyMMddHHmmss}}/stream?playseek={utc_str}&r2h-seek-mode=range(3600)" stream_get("127.0.0.1", shared_r2h.port, url, read_bytes=4096, timeout=_STREAM_TIMEOUT) uri = _get_describe_uri(rtsp) - assert "${" not in uri, "Template placeholder left unresolved in DESCRIBE URI: %s" % uri - assert ("/path/%s/stream" % utc_str) in uri, ( - "Begin template should be substituted in DESCRIBE URI, got: %s" % uri + assert "${" not in uri, f"Template placeholder left unresolved in DESCRIBE URI: {uri}" + assert (f"/path/{utc_str}/stream") in uri, ( + f"Begin template should be substituted in DESCRIBE URI, got: {uri}" ) # Cross-check the recent-clock path actually fired. play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] expected_clock = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(start_ts)) - assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % expected_clock + assert play_reqs[0]["headers"].get("Range") == f"clock={expected_clock}-" finally: rtsp.stop() @@ -197,8 +185,8 @@ def test_query_append_no_template(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000") % rtsp.port - status, _, body = stream_get( + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101120000-20240101130000" + status, _, _body = stream_get( "127.0.0.1", shared_r2h.port, url, @@ -210,7 +198,7 @@ def test_query_append_no_template(self, shared_r2h): describe_reqs = [r for r in rtsp.requests_detailed if r["method"] == "DESCRIBE"] assert len(describe_reqs) > 0 uri = describe_reqs[0]["uri"] - assert "playseek=" in uri, "Query-append RTSP should have playseek in URI, got: %s" % uri + assert "playseek=" in uri, f"Query-append RTSP should have playseek in URI, got: {uri}" finally: rtsp.stop() @@ -219,7 +207,7 @@ def test_playseek_udp(self, shared_r2h): rtsp = MockRTSPServerUDP() rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101120000-20240101130000" status, _, body = stream_get( "127.0.0.1", shared_r2h.port, @@ -237,7 +225,7 @@ def test_tvdr_forwarded_in_uri(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?tvdr=20240601080000-20240601090000") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?tvdr=20240601080000-20240601090000" status, _, _ = stream_get( "127.0.0.1", shared_r2h.port, @@ -257,7 +245,7 @@ def test_playseek_correct_path_in_uri(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/live/channel1?playseek=20240101120000-20240101130000") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/live/channel1?playseek=20240101120000-20240101130000" stream_get( "127.0.0.1", shared_r2h.port, @@ -276,7 +264,7 @@ def test_custom_seek_forwarded_in_uri(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?r2h-seek-name=myseek&myseek=20240301100000-20240301110000") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?r2h-seek-name=myseek&myseek=20240301100000-20240301110000" status, _, _ = stream_get( "127.0.0.1", shared_r2h.port, @@ -302,7 +290,7 @@ def test_positive_offset(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=3600") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" stream_get( "127.0.0.1", shared_r2h.port, @@ -321,9 +309,7 @@ def test_offset_pair(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ( - "/rtsp/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=30,-60" - ) % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=30,-60" stream_get( "127.0.0.1", shared_r2h.port, @@ -342,7 +328,7 @@ def test_negative_offset(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=-30") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=-30" stream_get( "127.0.0.1", shared_r2h.port, @@ -361,7 +347,7 @@ def test_offset_stripped_from_uri(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=3600") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" stream_get( "127.0.0.1", shared_r2h.port, @@ -380,7 +366,7 @@ def test_offset_with_unix_timestamp(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=1704096000-1704099600&r2h-seek-offset=3600") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=1704096000-1704099600&r2h-seek-offset=3600" stream_get( "127.0.0.1", shared_r2h.port, @@ -399,7 +385,7 @@ def test_tz_utc_plus_8_converts_yyyyMMddHHmmss(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101120000-20240101130000" stream_get( "127.0.0.1", shared_r2h.port, @@ -419,7 +405,7 @@ def test_tz_utc_minus_5_converts_yyyyMMddHHmmss(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101120000-20240101130000" stream_get( "127.0.0.1", shared_r2h.port, @@ -439,7 +425,7 @@ def test_no_tz_no_conversion(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101120000-20240101130000" stream_get( "127.0.0.1", shared_r2h.port, @@ -459,7 +445,7 @@ def test_unix_timestamp_skips_tz_conversion(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=1704096000-1704099600") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=1704096000-1704099600" stream_get( "127.0.0.1", shared_r2h.port, @@ -479,7 +465,7 @@ def test_tz_with_offset_combined(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=3600") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101120000-20240101130000&r2h-seek-offset=3600" stream_get( "127.0.0.1", shared_r2h.port, @@ -500,8 +486,8 @@ def test_gmt_suffix_preserved(self, shared_r2h): rtsp.start() try: url = ( - "/rtsp/127.0.0.1:%d/stream?playseek=20240101120000GMT-20240101130000GMT&r2h-seek-offset=3600" - ) % rtsp.port + f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101120000GMT-20240101130000GMT&r2h-seek-offset=3600" + ) stream_get( "127.0.0.1", shared_r2h.port, @@ -520,7 +506,7 @@ def test_unix_timestamp_format_preserved(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=1704096000-1704099600") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=1704096000-1704099600" stream_get( "127.0.0.1", shared_r2h.port, @@ -540,7 +526,7 @@ def test_gmt_suffix_ignores_ua_tz(self, shared_r2h): rtsp = MockRTSPServer(num_packets=500) rtsp.start() try: - url = ("/rtsp/127.0.0.1:%d/stream?playseek=20240101120000GMT-20240101130000GMT") % rtsp.port + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek=20240101120000GMT-20240101130000GMT" stream_get( "127.0.0.1", shared_r2h.port, @@ -568,7 +554,7 @@ def test_iso8601_z_ignores_range_mode_tz(self, shared_r2h): iso_str = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(start_ts)) # range(UTC+9) AND a UA TZ that disagrees with both the range TZ # and the embedded `Z` — the `Z` wins regardless. - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-mode=range(UTC%%2B9/3600)" % (rtsp.port, iso_str) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={iso_str}&r2h-seek-mode=range(UTC%2B9/3600)" stream_get( "127.0.0.1", @@ -581,9 +567,10 @@ def test_iso8601_z_ignores_range_mode_tz(self, shared_r2h): play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] expected_clock = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(start_ts)) - assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % expected_clock, ( - "Embedded `Z` must take precedence over both range() and UA TZ — " - "got Range header %r" % play_reqs[0]["headers"].get("Range") + assert play_reqs[0]["headers"].get("Range") == f"clock={expected_clock}-", ( + "Embedded `Z` must take precedence over both range() and UA TZ — got Range header {!r}".format( + play_reqs[0]["headers"].get("Range") + ) ) finally: rtsp.stop() @@ -598,7 +585,7 @@ def test_gmt_suffix_ignores_range_mode_tz(self, shared_r2h): try: start_ts = int(time.time()) - 1500 # 25 min ago, within 1h window gmt_str = time.strftime("%Y%m%d%H%M%S", time.gmtime(start_ts)) + "GMT" - url = "/rtsp/127.0.0.1:%d/stream?playseek=%s&r2h-seek-mode=range(UTC%%2B9/3600)" % (rtsp.port, gmt_str) + url = f"/rtsp/127.0.0.1:{rtsp.port}/stream?playseek={gmt_str}&r2h-seek-mode=range(UTC%2B9/3600)" stream_get( "127.0.0.1", @@ -611,9 +598,10 @@ def test_gmt_suffix_ignores_range_mode_tz(self, shared_r2h): play_reqs = [r for r in rtsp.requests_detailed if r["method"] == "PLAY"] expected_clock = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(start_ts)) - assert play_reqs[0]["headers"].get("Range") == "clock=%s-" % expected_clock, ( - "GMT suffix must take precedence over both range() and UA TZ — " - "got Range header %r" % play_reqs[0]["headers"].get("Range") + assert play_reqs[0]["headers"].get("Range") == f"clock={expected_clock}-", ( + "GMT suffix must take precedence over both range() and UA TZ — got Range header {!r}".format( + play_reqs[0]["headers"].get("Range") + ) ) finally: rtsp.stop() diff --git a/e2e/test_worker_recovery.py b/e2e/test_worker_recovery.py index b3f6961f..777e55ab 100644 --- a/e2e/test_worker_recovery.py +++ b/e2e/test_worker_recovery.py @@ -2,14 +2,13 @@ from __future__ import annotations -import os import json +import os import signal import socket import time import pytest - from helpers import ( MockRTSPServer, R2HProcess, @@ -27,8 +26,8 @@ def _open_stream(port: int, rtsp_port: int) -> socket.socket: sock.settimeout(10) sock.bind(("127.0.0.1", 0)) sock.connect(("127.0.0.1", port)) - path = "/rtsp/127.0.0.1:%d/stream" % rtsp_port - sock.sendall(("GET %s HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n" % path).encode()) + path = f"/rtsp/127.0.0.1:{rtsp_port}/stream" + sock.sendall((f"GET {path} HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n").encode()) response = b"" deadline = time.monotonic() + 10 sock.settimeout(1.0) @@ -53,7 +52,7 @@ def _wait_log_contains(r2h: R2HProcess, text: str, timeout: float = 6.0) -> str: if text in log: return log time.sleep(0.05) - raise AssertionError("Expected log text %r; log: %s" % (text, log)) + raise AssertionError(f"Expected log text {text!r}; log: {log}") def _read_sse_payload(sock: socket.socket, buffered: bytes, predicate, timeout: float = 6.0) -> tuple[dict, bytes]: @@ -181,7 +180,7 @@ def test_worker_reduction_reaps_retiring_worker(r2h_binary): config = build_single_service_config( port, "Recovery", - "rtsp://127.0.0.1:%d/stream" % upstream.port, + f"rtsp://127.0.0.1:{upstream.port}/stream", global_lines=["maxclients = 10", "workers = 2"], ) r2h = R2HProcess(r2h_binary, port, config_content=config, capture_log=True) @@ -215,9 +214,9 @@ def test_worker_reduction_reaps_retiring_worker(r2h_binary): timeout=8, ) assert len(reduced["workers"]) == 1 - log = _wait_log_contains(r2h, "Worker 1 (pid %d)" % retiring_pid) + log = _wait_log_contains(r2h, f"Worker 1 (pid {retiring_pid})") assert "Reducing worker count from 2 to 1" in log - assert "Worker 1 (pid %d)" % retiring_pid in log + assert f"Worker 1 (pid {retiring_pid})" in log if had_retiring_client: assert all(client["workerPid"] != retiring_pid for client in reduced["clients"]) finally: diff --git a/e2e/test_zerocopy.py b/e2e/test_zerocopy.py index 7e797a29..7305fc3c 100644 --- a/e2e/test_zerocopy.py +++ b/e2e/test_zerocopy.py @@ -11,11 +11,10 @@ regular send(), so these tests validate the fallback path too. """ -import sys import concurrent.futures +import sys import pytest - from helpers import ( LOOPBACK_IF, MCAST_ADDR, diff --git a/package.json b/package.json index ecf31c7b..11b3569b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.1.0", "type": "module", - "packageManager": "pnpm@11.9.0", + "packageManager": "pnpm@11.21.0", "scripts": { "type-check": "pnpm run type-check:tsc && pnpm run type-check:basedpyright", "type-check:tsc": "tsc --noEmit", @@ -28,22 +28,22 @@ "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "lucide-react": "^1.26.0", + "lucide-react": "^1.31.0", "react": "^19.2.8", "react-dom": "^19.2.8" }, "devDependencies": { - "@biomejs/biome": "^2.5.5", + "@biomejs/biome": "^2.5.8", "@tailwindcss/vite": "^4.3.3", - "@types/node": "^26.1.1", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.4", + "@types/node": "^26.2.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.0.5", "tailwindcss": "^4.3.3", "typescript": "^7.0.2", - "vite": "^8.1.5", + "vite": "^8.2.1", "vite-bundle-analyzer": "^1.3.9", - "vitepress": "2.0.0-alpha.17", - "vitepress-plugin-llms": "^1.13.3" + "vitepress": "2.0.0-alpha.19", + "vitepress-plugin-llms": "^1.13.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a09fae2b..c2cbfa32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 lucide-react: - specifier: ^1.26.0 - version: 1.26.0(react@19.2.8) + specifier: ^1.31.0 + version: 1.31.0(react@19.2.8) react: specifier: ^19.2.8 version: 19.2.8 @@ -25,23 +25,23 @@ importers: version: 19.2.8(react@19.2.8) devDependencies: '@biomejs/biome': - specifier: ^2.5.5 - version: 2.5.5 + specifier: ^2.5.8 + version: 2.5.8 '@tailwindcss/vite': specifier: ^4.3.3 - version: 4.3.3(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)) + version: 4.3.3(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0)) '@types/node': - specifier: ^26.1.1 - version: 26.1.1 + specifier: ^26.2.0 + version: 26.2.0 '@types/react': - specifier: ^19.2.17 - version: 19.2.17 + specifier: ^19.2.18 + version: 19.2.18 '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) + specifier: ^19.2.4 + version: 19.2.4(@types/react@19.2.18) '@vitejs/plugin-react': - specifier: ^6.0.4 - version: 6.0.4(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)) + specifier: ^6.0.5 + version: 6.0.5(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0)) tailwindcss: specifier: ^4.3.3 version: 4.3.3 @@ -49,20 +49,24 @@ importers: specifier: ^7.0.2 version: 7.0.2 vite: - specifier: ^8.1.5 - version: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0) + specifier: ^8.2.1 + version: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) vite-bundle-analyzer: specifier: ^1.3.9 version: 1.3.9 vitepress: - specifier: 2.0.0-alpha.17 - version: 2.0.0-alpha.17(@types/node@26.1.1)(jiti@2.7.0)(lightningcss@1.33.0)(postcss@8.5.22)(typescript@7.0.2) + specifier: 2.0.0-alpha.19 + version: 2.0.0-alpha.19(@types/node@26.2.0)(jiti@2.7.0)(postcss@8.5.26)(typescript@7.0.2) vitepress-plugin-llms: - specifier: ^1.13.3 - version: 1.13.3 + specifier: ^1.13.4 + version: 1.13.4 packages: + '@11ty/gray-matter@2.1.0': + resolution: {integrity: sha512-fNdBOb3MgDz/1UCIoeY4wDuGbp+/3s5y6UrsyfMabECbh/CVycj7Er33JXqSxRwxrRKXcGE1Jipuq/1mODInsQ==} + engines: {node: '>=11'} + '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -71,248 +75,83 @@ packages: resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.5.5': - resolution: {integrity: sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ==} + '@biomejs/biome@2.5.8': + resolution: {integrity: sha512-aeAeeJB9fSDc7Gq+2GqpQxA0qBj6gj1k2R6L1cYqGePKP/baIq1WX8y6B+D+nRsO5ViQL22K/8IwbqERW0q1nw==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.5.5': - resolution: {integrity: sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag==} + '@biomejs/cli-darwin-arm64@2.5.8': + resolution: {integrity: sha512-mk1QON9PHllvvLN5gU3f4rMxeh4syK5p9OvKyWH6/W8ueh04uaC8TUXXByhGufWf/y5mQc03ZLM45zU+cmqMjA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.5.5': - resolution: {integrity: sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw==} + '@biomejs/cli-darwin-x64@2.5.8': + resolution: {integrity: sha512-bsGwFMBNyHPyiLSsQcZJxdoRrg1V4JL+d7wEsvUBczlP9U9lwM+7mzQHxI4o1mhBsTmdOBbAb6fHU3Z3snN45w==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.5.5': - resolution: {integrity: sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg==} + '@biomejs/cli-linux-arm64-musl@2.5.8': + resolution: {integrity: sha512-VcJNbstduTHx83NGAdhp78/JOcP45BZHXL7yNsfI1uGzdUgegAz2s+mSoT7wK6PBNzLoqG0zDOXaz/RQYVtSiw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.5.5': - resolution: {integrity: sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ==} + '@biomejs/cli-linux-arm64@2.5.8': + resolution: {integrity: sha512-XmFiA0WPYFC+uiUDC8WRFzAIH9bo7vwQLav38Uoq4ETC+T/+uBi0TsYGJECkugY3r8USl3jc+Ae2/irAF6F2lQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.5.5': - resolution: {integrity: sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A==} + '@biomejs/cli-linux-x64-musl@2.5.8': + resolution: {integrity: sha512-kKmiyokeISRGq2FLwvr+TzsgBusfxaZ0FZNLcOYOpCK/78tRrEjeEBLvq3xLZMpqbANgJdRPI7vZX8ZL37u9/w==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.5.5': - resolution: {integrity: sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ==} + '@biomejs/cli-linux-x64@2.5.8': + resolution: {integrity: sha512-S5wcm9OBDvLHodD4PUaN488hCpco9QD/9ZxuYJiw4euWtr/oQvLR72z2ixItH8Wd5BCm6FZaeb+YNvOoM1xHtQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.5.5': - resolution: {integrity: sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw==} + '@biomejs/cli-win32-arm64@2.5.8': + resolution: {integrity: sha512-nILH0mzm3Hi3iEdd7o7GpB8kBR/mSQwfQG/tyBqyNrY2GFtcgwfV9nV8xLmbtUpMNY/Oi0Ml1XgfR4flOdq+AA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.5.5': - resolution: {integrity: sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g==} + '@biomejs/cli-win32-x64@2.5.8': + resolution: {integrity: sha512-I2czzXTY61f3nFJxXoMDq80t7MivxDEnCjE+8sDKoFfcKMaoQdkqhIFQ3KyY0XLzeSpUBYeNAXgD+iOV/BU0VA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] - '@docsearch/css@4.6.3': - resolution: {integrity: sha512-nlOwcXcsNAptQl4vlL4MA78qNJKO0Qlds5GuBjCoePgkebTXLSf8Qt1oyZ3YBshYupKXG9VRGEsk1zr23d+bzQ==} - - '@docsearch/js@4.6.3': - resolution: {integrity: sha512-qUIX2b4Apew3tv4F0qhmgShsl/Lfw4m6mqv/5/5dWNxwTcDdLMp2s3YwZ+NMGh3IKCg0pBaXm7Q5VdyU5Rj+cQ==} - - '@docsearch/sidepanel-js@4.6.3': - resolution: {integrity: sha512-grGSmvXzG0if+mrzdIKykvpIAuEQ9u0sEJ2eLRRCaQfJvsWqh2C2/aY04bIzWvDh7myi5rvl8D+tUNsVrjYQ3A==} - - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - - '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - - '@esbuild/aix-ppc64@0.28.1': - resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.28.1': - resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.28.1': - resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.28.1': - resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.28.1': - resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.28.1': - resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.28.1': - resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.28.1': - resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.28.1': - resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.28.1': - resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.28.1': - resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.28.1': - resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.28.1': - resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.28.1': - resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.28.1': - resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.28.1': - resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.28.1': - resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.28.1': - resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.28.1': - resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.28.1': - resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.28.1': - resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.28.1': - resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] + '@docsearch/css@4.7.0': + resolution: {integrity: sha512-Sk5xkdRFeE7PeWjG9l4AfTwdvMfr9wHiwNNCpHXT4v4SNyNMKdHGvEILc31BgaVFGDDNbv5u/a73tofRiwbEZw==} - '@esbuild/sunos-x64@0.28.1': - resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] + '@docsearch/js@4.7.0': + resolution: {integrity: sha512-x5lCqu1tetgsJFkjQ6VSocbHldsRkGEgwg5N98Vx21sq/V5wcmj4u226PY9k+TEpIgQ772zlYbPLTPicWyGnpA==} - '@esbuild/win32-arm64@0.28.1': - resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] + '@docsearch/sidepanel-js@4.7.0': + resolution: {integrity: sha512-A8r34jCU8kcIk2viECEn2msA28ojUF1BLi/3v5OWWc5G2N3jOuuumBXoeYjfr8dA0UxgFSy5R2bt12dnFJQSyA==} - '@esbuild/win32-ia32@0.28.1': - resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.28.1': - resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@iconify-json/simple-icons@1.2.91': - resolution: {integrity: sha512-plQJHZxzpkIXaeYjJF0Sc9nkzYF32EDX9olx7j1hcKYFaiTrQXbcngZmvzuV0pG6uZPU3ZuA/GIsgxSHOm6jrQ==} + '@iconify-json/simple-icons@1.2.93': + resolution: {integrity: sha512-/XhANjfGYOuqvSR3TmUnkQkINvQ4GVjVuukvymRbxtVFBvIq/yiXJqCDycKcQPT401OYT9H2vIY6ihAlz1QIAw==} '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -333,106 +172,95 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@oxc-project/types@0.139.0': - resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@oxc-project/types@0.144.0': + resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==} - '@rolldown/binding-android-arm64@1.1.5': - resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + '@rolldown/binding-android-arm64@1.2.4': + resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.1.5': - resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + '@rolldown/binding-darwin-arm64@1.2.4': + resolution: {integrity: sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.5': - resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + '@rolldown/binding-darwin-x64@1.2.4': + resolution: {integrity: sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.5': - resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + '@rolldown/binding-freebsd-x64@1.2.4': + resolution: {integrity: sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': + resolution: {integrity: sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + '@rolldown/binding-linux-arm64-gnu@1.2.4': + resolution: {integrity: sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + '@rolldown/binding-linux-arm64-musl@1.2.4': + resolution: {integrity: sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.4': + resolution: {integrity: sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + '@rolldown/binding-linux-s390x-gnu@1.2.4': + resolution: {integrity: sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + '@rolldown/binding-linux-x64-gnu@1.2.4': + resolution: {integrity: sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + '@rolldown/binding-linux-x64-musl@1.2.4': + resolution: {integrity: sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + '@rolldown/binding-openharmony-arm64@1.2.4': + resolution: {integrity: sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + '@rolldown/binding-win32-arm64-msvc@1.2.4': + resolution: {integrity: sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + '@rolldown/binding-win32-x64-msvc@1.2.4': + resolution: {integrity: sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -440,164 +268,37 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - '@rollup/rollup-android-arm-eabi@4.62.2': - resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.62.2': - resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.62.2': - resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.62.2': - resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.62.2': - resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.62.2': - resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.62.2': - resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.62.2': - resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.62.2': - resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.62.2': - resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.62.2': - resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.62.2': - resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.62.2': - resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.62.2': - resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.62.2': - resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.62.2': - resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.62.2': - resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.62.2': - resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.62.2': - resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.62.2': - resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.62.2': - resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} - cpu: [x64] - os: [win32] + '@shikijs/core@4.4.3': + resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} + engines: {node: '>=20'} - '@shikijs/core@3.23.0': - resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} + '@shikijs/engine-javascript@4.4.3': + resolution: {integrity: sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==} + engines: {node: '>=20'} - '@shikijs/engine-javascript@3.23.0': - resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} + '@shikijs/engine-oniguruma@4.4.3': + resolution: {integrity: sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==} + engines: {node: '>=20'} - '@shikijs/engine-oniguruma@3.23.0': - resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + '@shikijs/langs@4.4.3': + resolution: {integrity: sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==} + engines: {node: '>=20'} - '@shikijs/langs@3.23.0': - resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + '@shikijs/primitive@4.4.3': + resolution: {integrity: sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==} + engines: {node: '>=20'} - '@shikijs/themes@3.23.0': - resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + '@shikijs/themes@4.4.3': + resolution: {integrity: sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==} + engines: {node: '>=20'} - '@shikijs/transformers@3.23.0': - resolution: {integrity: sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==} + '@shikijs/transformers@4.4.3': + resolution: {integrity: sha512-oJSARV6NaWd+rnNJbtnpAdj3Zg0ZVyzsnMgb3vi3HA+35y8lBWUCpOnWsmyiXZIikY+x1BDqrQUgmxfzWh7Jvw==} + engines: {node: '>=20'} - '@shikijs/types@3.23.0': - resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + '@shikijs/types@4.4.3': + resolution: {integrity: sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==} + engines: {node: '>=20'} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -696,15 +397,9 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/hast@3.0.5': resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} @@ -723,16 +418,16 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@26.1.1': - resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + '@types/react-dom@19.2.4': + resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -863,8 +558,8 @@ packages: '@ungap/structured-clone@1.3.3': resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} - '@vitejs/plugin-react@6.0.4': - resolution: {integrity: sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==} + '@vitejs/plugin-react@6.0.5': + resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 @@ -883,49 +578,49 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 - '@vue/compiler-core@3.5.40': - resolution: {integrity: sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==} + '@vue/compiler-core@3.5.41': + resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==} - '@vue/compiler-dom@3.5.40': - resolution: {integrity: sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==} + '@vue/compiler-dom@3.5.41': + resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==} - '@vue/compiler-sfc@3.5.40': - resolution: {integrity: sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==} + '@vue/compiler-sfc@3.5.41': + resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==} - '@vue/compiler-ssr@3.5.40': - resolution: {integrity: sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==} + '@vue/compiler-ssr@3.5.41': + resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==} - '@vue/devtools-api@8.1.5': - resolution: {integrity: sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==} + '@vue/devtools-api@8.2.1': + resolution: {integrity: sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==} - '@vue/devtools-kit@8.1.5': - resolution: {integrity: sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==} + '@vue/devtools-kit@8.2.1': + resolution: {integrity: sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==} - '@vue/devtools-shared@8.1.5': - resolution: {integrity: sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==} + '@vue/devtools-shared@8.2.1': + resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==} - '@vue/reactivity@3.5.40': - resolution: {integrity: sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==} + '@vue/reactivity@3.5.41': + resolution: {integrity: sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==} - '@vue/runtime-core@3.5.40': - resolution: {integrity: sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==} + '@vue/runtime-core@3.5.41': + resolution: {integrity: sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==} - '@vue/runtime-dom@3.5.40': - resolution: {integrity: sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==} + '@vue/runtime-dom@3.5.41': + resolution: {integrity: sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==} - '@vue/server-renderer@3.5.40': - resolution: {integrity: sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==} + '@vue/server-renderer@3.5.41': + resolution: {integrity: sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==} - '@vue/shared@3.5.40': - resolution: {integrity: sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==} + '@vue/shared@3.5.41': + resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==} - '@vueuse/core@14.3.0': - resolution: {integrity: sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==} + '@vueuse/core@14.4.0': + resolution: {integrity: sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==} peerDependencies: vue: ^3.5.0 - '@vueuse/integrations@14.3.0': - resolution: {integrity: sha512-76I5FT2ESvCmCaSwapI+a/u/CFtNXmzl9f9lNp1hRtx8vKB8hfiokJr8IvQqcQG5ckGXElyXK516b54ozV3MvA==} + '@vueuse/integrations@14.4.0': + resolution: {integrity: sha512-oJz9qTgczvA7L1nXQFRU7h8tQbOCoiceqvMMhT9XYMyOGTqLJ2rEa09PON+nD2t48sZUfeOmg4eaWJXV4sZb/w==} peerDependencies: async-validator: ^4 axios: ^1 @@ -966,11 +661,11 @@ packages: universal-cookie: optional: true - '@vueuse/metadata@14.3.0': - resolution: {integrity: sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==} + '@vueuse/metadata@14.4.0': + resolution: {integrity: sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==} - '@vueuse/shared@14.3.0': - resolution: {integrity: sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==} + '@vueuse/shared@14.4.0': + resolution: {integrity: sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==} peerDependencies: vue: ^3.5.0 @@ -982,9 +677,6 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -998,8 +690,8 @@ packages: birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} - brace-expansion@5.0.8: - resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} ccount@2.0.1: @@ -1064,8 +756,8 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - enhanced-resolve@5.24.3: - resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -1076,11 +768,6 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} - esbuild@0.28.1: - resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} - engines: {node: '>=18'} - hasBin: true - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1089,11 +776,6 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -1135,10 +817,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} @@ -1167,8 +845,8 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true - js-yaml@3.15.0: - resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true kind-of@6.0.3: @@ -1329,8 +1007,8 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - lucide-react@1.26.0: - resolution: {integrity: sha512-raglYVR2+VkMfJL158krjVmE+rV5ST2lzA/KQm1FRSjMHT4MnWaegHxoVEpmc2So3nOEhp9oGejJwAPX8MoAjg==} + lucide-react@1.31.0: + resolution: {integrity: sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1439,8 +1117,8 @@ packages: resolution: {integrity: sha512-H/E3J6t+DQs/F2YgfDhxUVZz/dF8JXPPKTLHL/yHCcLZLtCXJDUaqvhJXQwqOVBvbyNn4T0WjLpIHd7PAw7fBA==} hasBin: true - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} minisearch@7.2.0: @@ -1449,8 +1127,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1473,8 +1151,8 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} - postcss@8.5.22: - resolution: {integrity: sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} pretty-bytes@7.1.1: @@ -1522,16 +1200,11 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - rolldown@1.1.5: - resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + rolldown@1.2.4: + resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup@4.62.2: - resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1539,8 +1212,9 @@ packages: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} - shiki@3.23.0: - resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} + shiki@4.4.3: + resolution: {integrity: sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==} + engines: {node: '>=20'} source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} @@ -1549,9 +1223,6 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1563,10 +1234,6 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - tabbable@6.5.0: resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==} @@ -1581,8 +1248,8 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tokenx@1.3.0: - resolution: {integrity: sha512-NLdXTEZkKiO0gZuLtMoZKjCXTREXeZZt8nnnNeyoXtNZAfG/GKGSbQtLU5STspc0rMSwcA+UJfWZkbNU01iKmQ==} + tokenx@1.6.0: + resolution: {integrity: sha512-CKTjk345ajvBAUp5xUI9a5KKN0zU0lBueVHQbCskH1Hp6WkUKsPW2qGCYNs0pxNyfzxfo+IIjdt2W4sMbw/qBw==} trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -1590,9 +1257,6 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} @@ -1635,53 +1299,13 @@ packages: resolution: {integrity: sha512-hhy975B+ToseJaSJp3mURHriZUrMgaM2qx0BkP62kN6Yp46rw7EE3dl8ExFWYdn00OIwe7GbsA5PknvstNWG4Q==} hasBin: true - vite@7.3.6: - resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vite@8.1.5: - resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + vite@8.2.1: + resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 + '@vitejs/devtools': ^0.4.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -1718,27 +1342,24 @@ packages: yaml: optional: true - vitepress-plugin-llms@1.13.3: - resolution: {integrity: sha512-C6bygRzuVp54kveM6pngVNg4H9sKMx7K67iwoO4PUzBK22NiXF9SoFm/WdjKp2fJrMcOabKtQMuyNFF20c2Anw==} + vitepress-plugin-llms@1.13.4: + resolution: {integrity: sha512-3/L1ceexjpJirWWGlfvqx2hmaDTFGSkSHrn3y2C7RZm6lNa/vmvU49Ayr4GxLYQfOFfo9CP6rsWdD+6rEbnIxA==} engines: {node: '>=18'} - vitepress@2.0.0-alpha.17: - resolution: {integrity: sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==} + vitepress@2.0.0-alpha.19: + resolution: {integrity: sha512-WnBsb0Bwr43kXKyiis+lld/7ri3hnMbthS8N3hpFtjjwsdLO4IRmiAE08D7aud4q6oMDf9uwRowxzNqRFe/amw==} hasBin: true peerDependencies: markdown-it-mathjax3: ^4 - oxc-minify: '*' postcss: ^8 peerDependenciesMeta: markdown-it-mathjax3: optional: true - oxc-minify: - optional: true postcss: optional: true - vue@3.5.40: - resolution: {integrity: sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==} + vue@3.5.41: + resolution: {integrity: sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -1766,155 +1387,66 @@ packages: snapshots: + '@11ty/gray-matter@2.1.0': + dependencies: + js-yaml: 4.3.1 + section-matter: 1.0.0 + '@babel/helper-string-parser@7.29.7': {} '@babel/helper-validator-identifier@7.29.7': {} - '@babel/parser@7.29.7': + '@babel/parser@7.29.8': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 - '@babel/types@7.29.7': + '@babel/types@7.29.8': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@biomejs/biome@2.5.5': + '@biomejs/biome@2.5.8': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.5 - '@biomejs/cli-darwin-x64': 2.5.5 - '@biomejs/cli-linux-arm64': 2.5.5 - '@biomejs/cli-linux-arm64-musl': 2.5.5 - '@biomejs/cli-linux-x64': 2.5.5 - '@biomejs/cli-linux-x64-musl': 2.5.5 - '@biomejs/cli-win32-arm64': 2.5.5 - '@biomejs/cli-win32-x64': 2.5.5 - - '@biomejs/cli-darwin-arm64@2.5.5': - optional: true - - '@biomejs/cli-darwin-x64@2.5.5': - optional: true - - '@biomejs/cli-linux-arm64-musl@2.5.5': - optional: true - - '@biomejs/cli-linux-arm64@2.5.5': - optional: true - - '@biomejs/cli-linux-x64-musl@2.5.5': - optional: true - - '@biomejs/cli-linux-x64@2.5.5': - optional: true - - '@biomejs/cli-win32-arm64@2.5.5': - optional: true - - '@biomejs/cli-win32-x64@2.5.5': - optional: true - - '@docsearch/css@4.6.3': {} - - '@docsearch/js@4.6.3': {} - - '@docsearch/sidepanel-js@4.6.3': {} - - '@emnapi/core@1.11.1': - dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.11.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.28.1': - optional: true - - '@esbuild/android-arm64@0.28.1': - optional: true - - '@esbuild/android-arm@0.28.1': - optional: true - - '@esbuild/android-x64@0.28.1': - optional: true - - '@esbuild/darwin-arm64@0.28.1': - optional: true - - '@esbuild/darwin-x64@0.28.1': - optional: true - - '@esbuild/freebsd-arm64@0.28.1': - optional: true - - '@esbuild/freebsd-x64@0.28.1': - optional: true - - '@esbuild/linux-arm64@0.28.1': - optional: true - - '@esbuild/linux-arm@0.28.1': - optional: true - - '@esbuild/linux-ia32@0.28.1': - optional: true + '@biomejs/cli-darwin-arm64': 2.5.8 + '@biomejs/cli-darwin-x64': 2.5.8 + '@biomejs/cli-linux-arm64': 2.5.8 + '@biomejs/cli-linux-arm64-musl': 2.5.8 + '@biomejs/cli-linux-x64': 2.5.8 + '@biomejs/cli-linux-x64-musl': 2.5.8 + '@biomejs/cli-win32-arm64': 2.5.8 + '@biomejs/cli-win32-x64': 2.5.8 - '@esbuild/linux-loong64@0.28.1': + '@biomejs/cli-darwin-arm64@2.5.8': optional: true - '@esbuild/linux-mips64el@0.28.1': + '@biomejs/cli-darwin-x64@2.5.8': optional: true - '@esbuild/linux-ppc64@0.28.1': + '@biomejs/cli-linux-arm64-musl@2.5.8': optional: true - '@esbuild/linux-riscv64@0.28.1': + '@biomejs/cli-linux-arm64@2.5.8': optional: true - '@esbuild/linux-s390x@0.28.1': + '@biomejs/cli-linux-x64-musl@2.5.8': optional: true - '@esbuild/linux-x64@0.28.1': + '@biomejs/cli-linux-x64@2.5.8': optional: true - '@esbuild/netbsd-arm64@0.28.1': + '@biomejs/cli-win32-arm64@2.5.8': optional: true - '@esbuild/netbsd-x64@0.28.1': + '@biomejs/cli-win32-x64@2.5.8': optional: true - '@esbuild/openbsd-arm64@0.28.1': - optional: true + '@docsearch/css@4.7.0': {} - '@esbuild/openbsd-x64@0.28.1': - optional: true - - '@esbuild/openharmony-arm64@0.28.1': - optional: true - - '@esbuild/sunos-x64@0.28.1': - optional: true - - '@esbuild/win32-arm64@0.28.1': - optional: true - - '@esbuild/win32-ia32@0.28.1': - optional: true + '@docsearch/js@4.7.0': {} - '@esbuild/win32-x64@0.28.1': - optional: true + '@docsearch/sidepanel-js@4.7.0': {} - '@iconify-json/simple-icons@1.2.91': + '@iconify-json/simple-icons@1.2.93': dependencies: '@iconify/types': 2.0.0 @@ -1939,173 +1471,91 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@oxc-project/types@0.139.0': {} + '@oxc-project/types@0.144.0': {} - '@rolldown/binding-android-arm64@1.1.5': + '@rolldown/binding-android-arm64@1.2.4': optional: true - '@rolldown/binding-darwin-arm64@1.1.5': + '@rolldown/binding-darwin-arm64@1.2.4': optional: true - '@rolldown/binding-darwin-x64@1.1.5': + '@rolldown/binding-darwin-x64@1.2.4': optional: true - '@rolldown/binding-freebsd-x64@1.1.5': + '@rolldown/binding-freebsd-x64@1.2.4': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + '@rolldown/binding-linux-arm-gnueabihf@1.2.4': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.5': + '@rolldown/binding-linux-arm64-gnu@1.2.4': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.5': + '@rolldown/binding-linux-arm64-musl@1.2.4': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.5': + '@rolldown/binding-linux-ppc64-gnu@1.2.4': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.5': + '@rolldown/binding-linux-s390x-gnu@1.2.4': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.5': + '@rolldown/binding-linux-x64-gnu@1.2.4': optional: true - '@rolldown/binding-linux-x64-musl@1.1.5': + '@rolldown/binding-linux-x64-musl@1.2.4': optional: true - '@rolldown/binding-openharmony-arm64@1.1.5': - optional: true - - '@rolldown/binding-wasm32-wasi@1.1.5': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@rolldown/binding-openharmony-arm64@1.2.4': optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.5': + '@rolldown/binding-win32-arm64-msvc@1.2.4': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.5': + '@rolldown/binding-win32-x64-msvc@1.2.4': optional: true '@rolldown/pluginutils@1.0.1': {} - '@rollup/rollup-android-arm-eabi@4.62.2': - optional: true - - '@rollup/rollup-android-arm64@4.62.2': - optional: true - - '@rollup/rollup-darwin-arm64@4.62.2': - optional: true - - '@rollup/rollup-darwin-x64@4.62.2': - optional: true - - '@rollup/rollup-freebsd-arm64@4.62.2': - optional: true - - '@rollup/rollup-freebsd-x64@4.62.2': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.62.2': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.62.2': - optional: true - - '@rollup/rollup-linux-x64-musl@4.62.2': - optional: true - - '@rollup/rollup-openbsd-x64@4.62.2': - optional: true - - '@rollup/rollup-openharmony-arm64@4.62.2': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.62.2': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.62.2': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.62.2': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.62.2': - optional: true - - '@shikijs/core@3.23.0': + '@shikijs/core@4.4.3': dependencies: - '@shikijs/types': 3.23.0 + '@shikijs/primitive': 4.4.3 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@3.23.0': + '@shikijs/engine-javascript@4.4.3': dependencies: - '@shikijs/types': 3.23.0 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 - '@shikijs/engine-oniguruma@3.23.0': + '@shikijs/engine-oniguruma@4.4.3': dependencies: - '@shikijs/types': 3.23.0 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.23.0': + '@shikijs/langs@4.4.3': dependencies: - '@shikijs/types': 3.23.0 + '@shikijs/types': 4.4.3 - '@shikijs/themes@3.23.0': + '@shikijs/primitive@4.4.3': dependencies: - '@shikijs/types': 3.23.0 + '@shikijs/types': 4.4.3 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 - '@shikijs/transformers@3.23.0': + '@shikijs/themes@4.4.3': dependencies: - '@shikijs/core': 3.23.0 - '@shikijs/types': 3.23.0 + '@shikijs/types': 4.4.3 - '@shikijs/types@3.23.0': + '@shikijs/transformers@4.4.3': + dependencies: + '@shikijs/core': 4.4.3 + '@shikijs/types': 4.4.3 + + '@shikijs/types@4.4.3': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 @@ -2115,7 +1565,7 @@ snapshots: '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.24.3 + enhanced-resolve: 5.24.5 jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 @@ -2173,24 +1623,17 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0))': + '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0) - - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 - optional: true + vite: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 - '@types/estree@1.0.9': {} - '@types/hast@3.0.5': dependencies: '@types/unist': 3.0.3 @@ -2210,15 +1653,15 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@26.1.1': + '@types/node@26.2.0': dependencies: undici-types: 8.3.0 - '@types/react-dom@19.2.3(@types/react@19.2.17)': + '@types/react-dom@19.2.4(@types/react@19.2.18)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@types/react@19.2.17': + '@types/react@19.2.18': dependencies: csstype: 3.2.3 @@ -2288,104 +1731,104 @@ snapshots: '@ungap/structured-clone@1.3.3': {} - '@vitejs/plugin-react@6.0.4(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0))': + '@vitejs/plugin-react@6.0.5(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0) + vite: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) - '@vitejs/plugin-vue@6.0.8(vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0)(lightningcss@1.33.0))(vue@3.5.40(typescript@7.0.2))': + '@vitejs/plugin-vue@6.0.8(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0))(vue@3.5.41(typescript@7.0.2))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 7.3.6(@types/node@26.1.1)(jiti@2.7.0)(lightningcss@1.33.0) - vue: 3.5.40(typescript@7.0.2) + vite: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) + vue: 3.5.41(typescript@7.0.2) - '@vue/compiler-core@3.5.40': + '@vue/compiler-core@3.5.41': dependencies: - '@babel/parser': 7.29.7 - '@vue/shared': 3.5.40 + '@babel/parser': 7.29.8 + '@vue/shared': 3.5.41 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.40': + '@vue/compiler-dom@3.5.41': dependencies: - '@vue/compiler-core': 3.5.40 - '@vue/shared': 3.5.40 + '@vue/compiler-core': 3.5.41 + '@vue/shared': 3.5.41 - '@vue/compiler-sfc@3.5.40': + '@vue/compiler-sfc@3.5.41': dependencies: - '@babel/parser': 7.29.7 - '@vue/compiler-core': 3.5.40 - '@vue/compiler-dom': 3.5.40 - '@vue/compiler-ssr': 3.5.40 - '@vue/shared': 3.5.40 + '@babel/parser': 7.29.8 + '@vue/compiler-core': 3.5.41 + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-ssr': 3.5.41 + '@vue/shared': 3.5.41 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.22 + postcss: 8.5.26 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.40': + '@vue/compiler-ssr@3.5.41': dependencies: - '@vue/compiler-dom': 3.5.40 - '@vue/shared': 3.5.40 + '@vue/compiler-dom': 3.5.41 + '@vue/shared': 3.5.41 - '@vue/devtools-api@8.1.5': + '@vue/devtools-api@8.2.1': dependencies: - '@vue/devtools-kit': 8.1.5 + '@vue/devtools-kit': 8.2.1 - '@vue/devtools-kit@8.1.5': + '@vue/devtools-kit@8.2.1': dependencies: - '@vue/devtools-shared': 8.1.5 + '@vue/devtools-shared': 8.2.1 birpc: 2.9.0 hookable: 5.5.3 perfect-debounce: 2.1.0 - '@vue/devtools-shared@8.1.5': {} + '@vue/devtools-shared@8.2.1': {} - '@vue/reactivity@3.5.40': + '@vue/reactivity@3.5.41': dependencies: - '@vue/shared': 3.5.40 + '@vue/shared': 3.5.41 - '@vue/runtime-core@3.5.40': + '@vue/runtime-core@3.5.41': dependencies: - '@vue/reactivity': 3.5.40 - '@vue/shared': 3.5.40 + '@vue/reactivity': 3.5.41 + '@vue/shared': 3.5.41 - '@vue/runtime-dom@3.5.40': + '@vue/runtime-dom@3.5.41': dependencies: - '@vue/reactivity': 3.5.40 - '@vue/runtime-core': 3.5.40 - '@vue/shared': 3.5.40 + '@vue/reactivity': 3.5.41 + '@vue/runtime-core': 3.5.41 + '@vue/shared': 3.5.41 csstype: 3.2.3 - '@vue/server-renderer@3.5.40': + '@vue/server-renderer@3.5.41': dependencies: - '@vue/compiler-ssr': 3.5.40 - '@vue/runtime-dom': 3.5.40 - '@vue/shared': 3.5.40 + '@vue/compiler-ssr': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/shared': 3.5.41 - '@vue/shared@3.5.40': {} + '@vue/shared@3.5.41': {} - '@vueuse/core@14.3.0(vue@3.5.40(typescript@7.0.2))': + '@vueuse/core@14.4.0(vue@3.5.41(typescript@7.0.2))': dependencies: '@types/web-bluetooth': 0.0.21 - '@vueuse/metadata': 14.3.0 - '@vueuse/shared': 14.3.0(vue@3.5.40(typescript@7.0.2)) - vue: 3.5.40(typescript@7.0.2) + '@vueuse/metadata': 14.4.0 + '@vueuse/shared': 14.4.0(vue@3.5.41(typescript@7.0.2)) + vue: 3.5.41(typescript@7.0.2) - '@vueuse/integrations@14.3.0(focus-trap@8.2.2)(vue@3.5.40(typescript@7.0.2))': + '@vueuse/integrations@14.4.0(focus-trap@8.2.2)(vue@3.5.41(typescript@7.0.2))': dependencies: - '@vueuse/core': 14.3.0(vue@3.5.40(typescript@7.0.2)) - '@vueuse/shared': 14.3.0(vue@3.5.40(typescript@7.0.2)) - vue: 3.5.40(typescript@7.0.2) + '@vueuse/core': 14.4.0(vue@3.5.41(typescript@7.0.2)) + '@vueuse/shared': 14.4.0(vue@3.5.41(typescript@7.0.2)) + vue: 3.5.41(typescript@7.0.2) optionalDependencies: focus-trap: 8.2.2 - '@vueuse/metadata@14.3.0': {} + '@vueuse/metadata@14.4.0': {} - '@vueuse/shared@14.3.0(vue@3.5.40(typescript@7.0.2))': + '@vueuse/shared@14.4.0(vue@3.5.41(typescript@7.0.2))': dependencies: - vue: 3.5.40(typescript@7.0.2) + vue: 3.5.41(typescript@7.0.2) ansi-regex@5.0.1: {} @@ -2393,10 +1836,6 @@ snapshots: dependencies: color-convert: 2.0.1 - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - argparse@2.0.1: {} bail@2.0.2: {} @@ -2405,7 +1844,7 @@ snapshots: birpc@2.9.0: {} - brace-expansion@5.0.8: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -2457,7 +1896,7 @@ snapshots: emoji-regex@8.0.0: {} - enhanced-resolve@5.24.3: + enhanced-resolve@5.24.5: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -2466,41 +1905,10 @@ snapshots: entities@7.0.1: {} - esbuild@0.28.1: - optionalDependencies: - '@esbuild/aix-ppc64': 0.28.1 - '@esbuild/android-arm': 0.28.1 - '@esbuild/android-arm64': 0.28.1 - '@esbuild/android-x64': 0.28.1 - '@esbuild/darwin-arm64': 0.28.1 - '@esbuild/darwin-x64': 0.28.1 - '@esbuild/freebsd-arm64': 0.28.1 - '@esbuild/freebsd-x64': 0.28.1 - '@esbuild/linux-arm': 0.28.1 - '@esbuild/linux-arm64': 0.28.1 - '@esbuild/linux-ia32': 0.28.1 - '@esbuild/linux-loong64': 0.28.1 - '@esbuild/linux-mips64el': 0.28.1 - '@esbuild/linux-ppc64': 0.28.1 - '@esbuild/linux-riscv64': 0.28.1 - '@esbuild/linux-s390x': 0.28.1 - '@esbuild/linux-x64': 0.28.1 - '@esbuild/netbsd-arm64': 0.28.1 - '@esbuild/netbsd-x64': 0.28.1 - '@esbuild/openbsd-arm64': 0.28.1 - '@esbuild/openbsd-x64': 0.28.1 - '@esbuild/openharmony-arm64': 0.28.1 - '@esbuild/sunos-x64': 0.28.1 - '@esbuild/win32-arm64': 0.28.1 - '@esbuild/win32-ia32': 0.28.1 - '@esbuild/win32-x64': 0.28.1 - escalade@3.2.0: {} escape-string-regexp@5.0.0: {} - esprima@4.0.1: {} - estree-walker@2.0.2: {} extend-shallow@2.0.1: @@ -2530,13 +1938,6 @@ snapshots: graceful-fs@4.2.11: {} - gray-matter@4.0.3: - dependencies: - js-yaml: 3.15.0 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.5 @@ -2567,10 +1968,9 @@ snapshots: jiti@2.7.0: {} - js-yaml@3.15.0: + js-yaml@4.3.1: dependencies: - argparse: 1.0.10 - esprima: 4.0.1 + argparse: 2.0.1 kind-of@6.0.3: {} @@ -2678,7 +2078,7 @@ snapshots: longest-streak@3.1.0: {} - lucide-react@1.26.0(react@19.2.8): + lucide-react@1.31.0(react@19.2.8): dependencies: react: 19.2.8 @@ -2906,15 +2306,15 @@ snapshots: dependencies: yargs: 17.7.3 - minimatch@10.2.5: + minimatch@10.2.6: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.9 minisearch@7.2.0: {} ms@2.1.3: {} - nanoid@3.3.16: {} + nanoid@3.3.18: {} oniguruma-parser@0.12.2: {} @@ -2932,9 +2332,9 @@ snapshots: picomatch@4.0.5: {} - postcss@8.5.22: + postcss@8.5.26: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -2996,57 +2396,25 @@ snapshots: require-directory@2.1.1: {} - rolldown@1.1.5: + rolldown@1.2.4: dependencies: - '@oxc-project/types': 0.139.0 + '@oxc-project/types': 0.144.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.5 - '@rolldown/binding-darwin-arm64': 1.1.5 - '@rolldown/binding-darwin-x64': 1.1.5 - '@rolldown/binding-freebsd-x64': 1.1.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 - '@rolldown/binding-linux-arm64-gnu': 1.1.5 - '@rolldown/binding-linux-arm64-musl': 1.1.5 - '@rolldown/binding-linux-ppc64-gnu': 1.1.5 - '@rolldown/binding-linux-s390x-gnu': 1.1.5 - '@rolldown/binding-linux-x64-gnu': 1.1.5 - '@rolldown/binding-linux-x64-musl': 1.1.5 - '@rolldown/binding-openharmony-arm64': 1.1.5 - '@rolldown/binding-wasm32-wasi': 1.1.5 - '@rolldown/binding-win32-arm64-msvc': 1.1.5 - '@rolldown/binding-win32-x64-msvc': 1.1.5 - - rollup@4.62.2: - dependencies: - '@types/estree': 1.0.9 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.62.2 - '@rollup/rollup-android-arm64': 4.62.2 - '@rollup/rollup-darwin-arm64': 4.62.2 - '@rollup/rollup-darwin-x64': 4.62.2 - '@rollup/rollup-freebsd-arm64': 4.62.2 - '@rollup/rollup-freebsd-x64': 4.62.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 - '@rollup/rollup-linux-arm-musleabihf': 4.62.2 - '@rollup/rollup-linux-arm64-gnu': 4.62.2 - '@rollup/rollup-linux-arm64-musl': 4.62.2 - '@rollup/rollup-linux-loong64-gnu': 4.62.2 - '@rollup/rollup-linux-loong64-musl': 4.62.2 - '@rollup/rollup-linux-ppc64-gnu': 4.62.2 - '@rollup/rollup-linux-ppc64-musl': 4.62.2 - '@rollup/rollup-linux-riscv64-gnu': 4.62.2 - '@rollup/rollup-linux-riscv64-musl': 4.62.2 - '@rollup/rollup-linux-s390x-gnu': 4.62.2 - '@rollup/rollup-linux-x64-gnu': 4.62.2 - '@rollup/rollup-linux-x64-musl': 4.62.2 - '@rollup/rollup-openbsd-x64': 4.62.2 - '@rollup/rollup-openharmony-arm64': 4.62.2 - '@rollup/rollup-win32-arm64-msvc': 4.62.2 - '@rollup/rollup-win32-ia32-msvc': 4.62.2 - '@rollup/rollup-win32-x64-gnu': 4.62.2 - '@rollup/rollup-win32-x64-msvc': 4.62.2 - fsevents: 2.3.3 + '@rolldown/binding-android-arm64': 1.2.4 + '@rolldown/binding-darwin-arm64': 1.2.4 + '@rolldown/binding-darwin-x64': 1.2.4 + '@rolldown/binding-freebsd-x64': 1.2.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.4 + '@rolldown/binding-linux-arm64-gnu': 1.2.4 + '@rolldown/binding-linux-arm64-musl': 1.2.4 + '@rolldown/binding-linux-ppc64-gnu': 1.2.4 + '@rolldown/binding-linux-s390x-gnu': 1.2.4 + '@rolldown/binding-linux-x64-gnu': 1.2.4 + '@rolldown/binding-linux-x64-musl': 1.2.4 + '@rolldown/binding-openharmony-arm64': 1.2.4 + '@rolldown/binding-win32-arm64-msvc': 1.2.4 + '@rolldown/binding-win32-x64-msvc': 1.2.4 scheduler@0.27.0: {} @@ -3055,14 +2423,14 @@ snapshots: extend-shallow: 2.0.1 kind-of: 6.0.3 - shiki@3.23.0: + shiki@4.4.3: dependencies: - '@shikijs/core': 3.23.0 - '@shikijs/engine-javascript': 3.23.0 - '@shikijs/engine-oniguruma': 3.23.0 - '@shikijs/langs': 3.23.0 - '@shikijs/themes': 3.23.0 - '@shikijs/types': 3.23.0 + '@shikijs/core': 4.4.3 + '@shikijs/engine-javascript': 4.4.3 + '@shikijs/engine-oniguruma': 4.4.3 + '@shikijs/langs': 4.4.3 + '@shikijs/themes': 4.4.3 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 @@ -3070,8 +2438,6 @@ snapshots: space-separated-tokens@2.0.2: {} - sprintf-js@1.0.3: {} - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -3087,8 +2453,6 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-bom-string@1.0.0: {} - tabbable@6.5.0: {} tailwindcss@4.3.3: {} @@ -3100,15 +2464,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 - tokenx@1.3.0: {} + tokenx@1.6.0: {} trim-lines@3.0.1: {} trough@2.2.0: {} - tslib@2.8.1: - optional: true - typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 @@ -3187,87 +2548,73 @@ snapshots: vite-bundle-analyzer@1.3.9: {} - vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0)(lightningcss@1.33.0): - dependencies: - esbuild: 0.28.1 - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - postcss: 8.5.22 - rollup: 4.62.2 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 26.1.1 - fsevents: 2.3.3 - jiti: 2.7.0 - lightningcss: 1.33.0 - - vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0): + vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.22 - rolldown: 1.1.5 + postcss: 8.5.26 + rolldown: 1.2.4 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.1.1 - esbuild: 0.28.1 + '@types/node': 26.2.0 fsevents: 2.3.3 jiti: 2.7.0 - vitepress-plugin-llms@1.13.3: + vitepress-plugin-llms@1.13.4: dependencies: - gray-matter: 4.0.3 + '@11ty/gray-matter': 2.1.0 markdown-it: 14.3.0 markdown-title: 1.0.2 mdast-util-from-markdown: 2.0.3 millify: 6.1.0 - minimatch: 10.2.5 + minimatch: 10.2.6 path-to-regexp: 6.3.0 picocolors: 1.1.1 pretty-bytes: 7.1.1 remark: 15.0.1 remark-frontmatter: 5.0.0 - tokenx: 1.3.0 + tokenx: 1.6.0 unist-util-remove: 4.0.0 unist-util-visit: 5.1.0 transitivePeerDependencies: - supports-color - vitepress@2.0.0-alpha.17(@types/node@26.1.1)(jiti@2.7.0)(lightningcss@1.33.0)(postcss@8.5.22)(typescript@7.0.2): + vitepress@2.0.0-alpha.19(@types/node@26.2.0)(jiti@2.7.0)(postcss@8.5.26)(typescript@7.0.2): dependencies: - '@docsearch/css': 4.6.3 - '@docsearch/js': 4.6.3 - '@docsearch/sidepanel-js': 4.6.3 - '@iconify-json/simple-icons': 1.2.91 - '@shikijs/core': 3.23.0 - '@shikijs/transformers': 3.23.0 - '@shikijs/types': 3.23.0 + '@docsearch/css': 4.7.0 + '@docsearch/js': 4.7.0 + '@docsearch/sidepanel-js': 4.7.0 + '@iconify-json/simple-icons': 1.2.93 + '@shikijs/core': 4.4.3 + '@shikijs/transformers': 4.4.3 + '@shikijs/types': 4.4.3 '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 6.0.8(vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0)(lightningcss@1.33.0))(vue@3.5.40(typescript@7.0.2)) - '@vue/devtools-api': 8.1.5 - '@vue/shared': 3.5.40 - '@vueuse/core': 14.3.0(vue@3.5.40(typescript@7.0.2)) - '@vueuse/integrations': 14.3.0(focus-trap@8.2.2)(vue@3.5.40(typescript@7.0.2)) + '@vitejs/plugin-vue': 6.0.8(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0))(vue@3.5.41(typescript@7.0.2)) + '@vue/devtools-api': 8.2.1 + '@vue/shared': 3.5.41 + '@vueuse/core': 14.4.0(vue@3.5.41(typescript@7.0.2)) + '@vueuse/integrations': 14.4.0(focus-trap@8.2.2)(vue@3.5.41(typescript@7.0.2)) focus-trap: 8.2.2 mark.js: 8.11.1 minisearch: 7.2.0 - shiki: 3.23.0 - vite: 7.3.6(@types/node@26.1.1)(jiti@2.7.0)(lightningcss@1.33.0) - vue: 3.5.40(typescript@7.0.2) + shiki: 4.4.3 + vite: 8.2.1(@types/node@26.2.0)(jiti@2.7.0) + vue: 3.5.41(typescript@7.0.2) optionalDependencies: - postcss: 8.5.22 + postcss: 8.5.26 transitivePeerDependencies: - '@types/node' + - '@vitejs/devtools' - async-validator - axios - change-case - drauu + - esbuild - fuse.js - idb-keyval - jiti - jwt-decode - less - - lightningcss - nprogress - qrcode - sass @@ -3281,13 +2628,13 @@ snapshots: - universal-cookie - yaml - vue@3.5.40(typescript@7.0.2): + vue@3.5.41(typescript@7.0.2): dependencies: - '@vue/compiler-dom': 3.5.40 - '@vue/compiler-sfc': 3.5.40 - '@vue/runtime-dom': 3.5.40 - '@vue/server-renderer': 3.5.40 - '@vue/shared': 3.5.40 + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-sfc': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/server-renderer': 3.5.41 + '@vue/shared': 3.5.41 optionalDependencies: typescript: 7.0.2 diff --git a/pyproject.toml b/pyproject.toml index 2a265fb1..c3e76385 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ [dependency-groups] dev = [ - "ruff>=0.15.12", + "ruff>=0.16.3", "basedpyright>=1.39.3", "clang-format>=22.1.4", ] diff --git a/tools/devlab/devlab.py b/tools/devlab/devlab.py old mode 100644 new mode 100755 index df1b8930..2b30315a --- a/tools/devlab/devlab.py +++ b/tools/devlab/devlab.py @@ -44,7 +44,7 @@ import tempfile import threading import time -from datetime import datetime, timezone +from datetime import UTC, datetime from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import parse_qs, urlparse @@ -223,7 +223,7 @@ def catchup_filter(profile: str, begin_epoch: int) -> str: # Burn the *requested* time into the picture so seek correctness is visible: # SEEK shows the playseek begin time; the elapsed counter shows it advancing. # (Colon-free time format avoids ffmpeg drawtext expansion/escaping pitfalls.) - iso = datetime.fromtimestamp(begin_epoch, timezone.utc).strftime("%Y-%m-%d %H-%M-%S") + iso = datetime.fromtimestamp(begin_epoch, UTC).strftime("%Y-%m-%d %H-%M-%S") label = _drawtext(f"CATCHUP {profile}", 24, expansion=False) seek = _drawtext(f"SEEK {iso} UTC", 88, expansion=False) elapsed = _drawtext(r"+%{pts}s", 152, expansion=True) @@ -259,7 +259,7 @@ def _parse_time_token(tok: str) -> int: if tok.isdigit() and len(tok) <= 10: return int(tok) if len(tok) >= 14 and tok[:14].isdigit(): - dt = datetime.strptime(tok[:14], "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc) + dt = datetime.strptime(tok[:14], "%Y%m%d%H%M%S").replace(tzinfo=UTC) return int(dt.timestamp()) return int(time.time()) @@ -300,7 +300,7 @@ def __init__(self, ffmpeg: str, profile: str, outdir: str, seg_type: str = "mpeg self.seg_type = seg_type self.log_path = os.path.join(outdir, "ffmpeg.log") self.proc: subprocess.Popen[bytes] | None = None - self._stderr = None + self._stderr_fd: int | None = None def start(self) -> None: os.makedirs(self.outdir, exist_ok=True) @@ -344,8 +344,8 @@ def start(self) -> None: *hls_opts, os.path.join(self.outdir, "index.m3u8"), ] - self._stderr = open(self.log_path, "ab") - self.proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=self._stderr) + self._stderr_fd = os.open(self.log_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644) + self.proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=self._stderr_fd) def ready(self) -> bool: return os.path.isfile(os.path.join(self.outdir, "index.m3u8")) @@ -358,8 +358,9 @@ def failure_message(self) -> str: def stop(self) -> None: if self.proc and self.proc.poll() is None: self.proc.terminate() - if self._stderr: - self._stderr.close() + if self._stderr_fd is not None: + os.close(self._stderr_fd) + self._stderr_fd = None def wait_for_live_hls(gens: list[LiveHLS], timeout: float) -> None: @@ -449,7 +450,7 @@ def make_http_handler(host: str, port: int, live_root: str, catchup: CatchupHLS) class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" - def log_message(self, format: str, *args: object) -> None: # noqa: A002 (http.server API) + def log_message(self, format: str, *args: object) -> None: pass def _send(self, code: int, ctype: str, body: bytes) -> None: @@ -507,7 +508,7 @@ def _catchup_seg(self, profile: str, begin: int, idx: int) -> None: if proc.poll() is None: proc.terminate() - def do_GET(self) -> None: # noqa: N802 (http.server API) + def do_GET(self) -> None: parsed = urlparse(self.path) qs = parse_qs(parsed.query) parts = [p for p in parsed.path.split("/") if p] diff --git a/tools/stress-test/stress_test.py b/tools/stress-test/stress_test.py index 9d674047..66d7b311 100755 --- a/tools/stress-test/stress_test.py +++ b/tools/stress-test/stress_test.py @@ -12,10 +12,9 @@ import sys import threading import time +from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path -from typing import Callable - # ============================================================================= # Program Configurations @@ -255,6 +254,7 @@ def _get_cpu_via_top(self, pids: list[int]) -> dict[int, float]: capture_output=True, text=True, timeout=10, + check=False, ) if proc.returncode != 0: return result @@ -628,7 +628,7 @@ def main() -> int: replay_stats = None curl_stats: list[ProcessStats] = [] - for pid, stats in monitor.stats.items(): + for stats in monitor.stats.values(): if stats.name == args.program: server_stats = stats elif stats.name == "replay": diff --git a/tools/udp-replay/udp_replay.py b/tools/udp-replay/udp_replay.py old mode 100644 new mode 100755 index a8a42bf2..1b74e074 --- a/tools/udp-replay/udp_replay.py +++ b/tools/udp-replay/udp_replay.py @@ -17,9 +17,10 @@ from collections.abc import Callable from dataclasses import dataclass from pathlib import Path -from threading import Thread, Event, Lock +from threading import Event, Lock, Thread -from scapy.all import PcapNgReader, UDP, IP, Raw # pyright: ignore[reportAttributeAccessIssue] +from scapy.all import IP, UDP, PcapNgReader, Raw # pyright: ignore[reportAttributeAccessIssue] +from scapy.error import Scapy_Exception def is_rtp_packet(payload: bytes) -> bool: @@ -148,7 +149,7 @@ def load_packets(filepath: Path) -> list[PacketInfo]: if packets: duration = packets[-1].timestamp - packets[0].timestamp - dests = set((p.dst_addr, p.dst_port) for p in packets) + dests = {(p.dst_addr, p.dst_port) for p in packets} print( f"Loaded {len(packets)} UDP packets (duration: {duration:.2f}s)", flush=True, @@ -189,7 +190,7 @@ def check_igmp_membership(group_addr: str) -> bool: with open("/proc/net/igmp", "r") as f: content = f.read() return target in content - except IOError: + except OSError: return False @@ -249,7 +250,7 @@ def get_igmp_joined_groups(subnets: list[str]) -> set[str]: break except ValueError, IndexError: continue - except IOError: + except OSError: pass return joined @@ -422,10 +423,10 @@ def replay_loop( stream_seq_offsets[port] = 0 # Get unique ports from pcap - pcap_ports = set(p.dst_port for p in packets) + pcap_ports = {p.dst_port for p in packets} - pcap_dests = set((p.dst_addr, p.dst_port) for p in packets) - pcap_addrs = set(p.dst_addr for p in packets) + pcap_dests = {(p.dst_addr, p.dst_port) for p in packets} + pcap_addrs = {p.dst_addr for p in packets} if direct: dest_str = ", ".join(f"{addr}:{port}" for addr, port in sorted(pcap_dests)) @@ -765,7 +766,7 @@ def main() -> int: try: packets = load_packets(args.pcapng_file) - except Exception as e: + except (OSError, ValueError, Scapy_Exception) as e: print(f"Error loading pcapng file: {e}", file=sys.stderr) return 1 @@ -775,8 +776,8 @@ def main() -> int: # Calculate subnets to monitor based on destination addresses in pcap # Each unique destination IP gets its corresponding /24 subnet monitored - pcap_addrs = set(p.dst_addr for p in packets) - subnets = sorted(set(get_subnet_for_ip(addr) for addr in pcap_addrs)) + pcap_addrs = {p.dst_addr for p in packets} + subnets = sorted({get_subnet_for_ip(addr) for addr in pcap_addrs}) igmp_available = Path("/proc/net/igmp").is_file() direct = args.direct or not igmp_available diff --git a/uv.lock b/uv.lock index e951d40a..aa690c72 100644 --- a/uv.lock +++ b/uv.lock @@ -84,11 +84,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.2" +version = "26.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] @@ -166,32 +166,32 @@ requires-dist = [ dev = [ { name = "basedpyright", specifier = ">=1.39.3" }, { name = "clang-format", specifier = ">=22.1.4" }, - { name = "ruff", specifier = ">=0.15.12" }, + { name = "ruff", specifier = ">=0.16.3" }, ] [[package]] name = "ruff" -version = "0.15.21" +version = "0.16.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" }, - { url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" }, - { url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" }, - { url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" }, - { url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" }, - { url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" }, - { url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" }, - { url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" }, - { url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" }, + { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, + { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, + { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, + { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, ] [[package]]