From b1ff40359487df55dcd7f9a0105d2d1c1d310903 Mon Sep 17 00:00:00 2001 From: Hamza <12420351+0xMH@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:06:29 +0100 Subject: [PATCH] Fix text/image alignment, add dithering, extend Classic BT to Windows --- fichero/__init__.py | 6 +- fichero/cli.py | 89 +++++++++++--- fichero/imaging.py | 57 ++++++++- fichero/printer.py | 133 +++++++++++++++++--- pyproject.toml | 1 + tests/__init__.py | 0 tests/test_rfcomm.py | 281 +++++++++++++++++++++++++++++++++++++++++++ uv.lock | 154 ++++++++++++++++++++++++ web/index.html | 77 ++++++++++-- 9 files changed, 756 insertions(+), 42 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_rfcomm.py diff --git a/fichero/__init__.py b/fichero/__init__.py index 6aa9fad..559f7b5 100644 --- a/fichero/__init__.py +++ b/fichero/__init__.py @@ -1,21 +1,25 @@ -"""Fichero D11s thermal label printer - BLE interface.""" +"""Fichero D11s thermal label printer - BLE + Classic Bluetooth interface.""" from fichero.printer import ( + RFCOMM_CHANNEL, PrinterClient, PrinterError, PrinterNotFound, PrinterNotReady, PrinterStatus, PrinterTimeout, + RFCOMMClient, connect, ) __all__ = [ + "RFCOMM_CHANNEL", "PrinterClient", "PrinterError", "PrinterNotFound", "PrinterNotReady", "PrinterStatus", "PrinterTimeout", + "RFCOMMClient", "connect", ] diff --git a/fichero/cli.py b/fichero/cli.py index f7e0302..978e694 100644 --- a/fichero/cli.py +++ b/fichero/cli.py @@ -21,6 +21,15 @@ from fichero.printer import ( connect, ) +DOTS_PER_MM = 8 # 203 DPI + + +def _resolve_label_height(args: argparse.Namespace) -> int: + """Return label height in pixels from --label-length (mm) or --label-height (px).""" + if args.label_length is not None: + return args.label_length * DOTS_PER_MM + return args.label_height + async def do_print( pc: PrinterClient, @@ -28,17 +37,15 @@ async def do_print( density: int = 1, paper: int = PAPER_GAP, copies: int = 1, + dither: bool = True, + max_rows: int = 240, ) -> bool: - img = prepare_image(img) + img = prepare_image(img, max_rows=max_rows, dither=dither) rows = img.height raster = image_to_raster(img) print(f" Image: {img.width}x{rows}, {len(raster)} bytes, {copies} copies") - status = await pc.get_status() - if not status.ok: - raise PrinterNotReady(f"Printer not ready: {status}") - await pc.set_density(density) await asyncio.sleep(DELAY_AFTER_DENSITY) @@ -46,6 +53,11 @@ async def do_print( if copies > 1: print(f" Copy {copy_num + 1}/{copies}...") + # Check status before each copy (matches decompiled app behaviour) + status = await pc.get_status() + if not status.ok: + raise PrinterNotReady(f"Printer not ready: {status}") + # AiYin print sequence (from decompiled APK) await pc.set_paper_type(paper) await asyncio.sleep(DELAY_COMMAND_GAP) @@ -72,7 +84,7 @@ async def do_print( async def cmd_info(args: argparse.Namespace) -> None: - async with connect(args.address) as pc: + async with connect(args.address, classic=args.classic, channel=args.channel) as pc: info = await pc.get_info() for k, v in info.items(): print(f" {k}: {v}") @@ -84,7 +96,7 @@ async def cmd_info(args: argparse.Namespace) -> None: async def cmd_status(args: argparse.Namespace) -> None: - async with connect(args.address) as pc: + async with connect(args.address, classic=args.classic, channel=args.channel) as pc: status = await pc.get_status() print(f" Status: {status}") print(f" Raw: 0x{status.raw:02X} ({status.raw:08b})") @@ -95,23 +107,28 @@ async def cmd_status(args: argparse.Namespace) -> None: async def cmd_text(args: argparse.Namespace) -> None: text = " ".join(args.text) - img = text_to_image(text, font_size=args.font_size, label_height=args.label_height) - async with connect(args.address) as pc: + label_h = _resolve_label_height(args) + img = text_to_image(text, font_size=args.font_size, label_height=label_h) + async with connect(args.address, classic=args.classic, channel=args.channel) as pc: print(f'Printing "{text}"...') - ok = await do_print(pc, img, args.density, copies=args.copies) + ok = await do_print(pc, img, args.density, paper=args.paper, + copies=args.copies, dither=False, max_rows=label_h) print("Done." if ok else "FAILED.") async def cmd_image(args: argparse.Namespace) -> None: img = Image.open(args.path) - async with connect(args.address) as pc: + label_h = _resolve_label_height(args) + async with connect(args.address, classic=args.classic, channel=args.channel) as pc: print(f"Printing {args.path}...") - ok = await do_print(pc, img, args.density, copies=args.copies) + ok = await do_print(pc, img, args.density, paper=args.paper, + copies=args.copies, dither=not args.no_dither, + max_rows=label_h) print("Done." if ok else "FAILED.") async def cmd_set(args: argparse.Namespace) -> None: - async with connect(args.address) as pc: + async with connect(args.address, classic=args.classic, channel=args.channel) as pc: if args.setting == "density": val = int(args.value) if not 0 <= val <= 2: @@ -143,10 +160,39 @@ async def cmd_set(args: argparse.Namespace) -> None: print(f" Set paper={args.value}: {'OK' if ok else 'FAILED'}") +def _add_paper_arg(parser: argparse.ArgumentParser) -> None: + """Add --paper argument to a subparser.""" + parser.add_argument( + "--paper", type=str, default="gap", + help="Paper type: gap (default), black, continuous", + ) + + +def _parse_paper(value: str) -> int: + """Convert paper string/int to protocol value.""" + types = {"gap": 0, "black": 1, "continuous": 2} + if value in types: + return types[value] + try: + val = int(value) + if 0 <= val <= 2: + return val + except ValueError: + pass + print(f" WARNING: unknown paper type '{value}', using gap") + return 0 + + def main() -> None: parser = argparse.ArgumentParser(description="Fichero D11s Label Printer") parser.add_argument("--address", default=os.environ.get("FICHERO_ADDR"), help="BLE address (skip scanning, or set FICHERO_ADDR)") + parser.add_argument("--classic", action="store_true", + default=os.environ.get("FICHERO_TRANSPORT", "").lower() == "classic", + help="Use Classic Bluetooth (RFCOMM) instead of BLE (Linux only, " + "or set FICHERO_TRANSPORT=classic)") + parser.add_argument("--channel", type=int, default=1, + help="RFCOMM channel (default: 1, only used with --classic)") sub = parser.add_subparsers(dest="command", required=True) p_info = sub.add_parser("info", help="Show device info") @@ -161,8 +207,11 @@ def main() -> None: help="Print density: 0=light, 1=medium, 2=thick") p_text.add_argument("--copies", type=int, default=1, help="Number of copies") p_text.add_argument("--font-size", type=int, default=30, help="Font size in points") + p_text.add_argument("--label-length", type=int, default=None, + help="Label length in mm (default: 30mm)") p_text.add_argument("--label-height", type=int, default=240, - help="Label height in pixels (default: 240)") + help="Label height in pixels (default: 240, prefer --label-length)") + _add_paper_arg(p_text) p_text.set_defaults(func=cmd_text) p_image = sub.add_parser("image", help="Print image file") @@ -170,6 +219,13 @@ def main() -> None: p_image.add_argument("--density", type=int, default=2, choices=[0, 1, 2], help="Print density: 0=light, 1=medium, 2=thick") p_image.add_argument("--copies", type=int, default=1, help="Number of copies") + p_image.add_argument("--no-dither", action="store_true", + help="Disable Floyd-Steinberg dithering (use simple threshold)") + p_image.add_argument("--label-length", type=int, default=None, + help="Label length in mm (default: 30mm)") + p_image.add_argument("--label-height", type=int, default=240, + help="Max image height in pixels (default: 240, prefer --label-length)") + _add_paper_arg(p_image) p_image.set_defaults(func=cmd_image) p_set = sub.add_parser("set", help="Change printer settings") @@ -179,6 +235,11 @@ def main() -> None: p_set.set_defaults(func=cmd_set) args = parser.parse_args() + + # Resolve --paper string to int for print commands + if hasattr(args, "paper") and isinstance(args.paper, str): + args.paper = _parse_paper(args.paper) + try: asyncio.run(args.func(args)) except PrinterError as e: diff --git a/fichero/imaging.py b/fichero/imaging.py index 6f2662d..5d1554b 100644 --- a/fichero/imaging.py +++ b/fichero/imaging.py @@ -2,6 +2,7 @@ import logging +import numpy as np from PIL import Image, ImageDraw, ImageFont, ImageOps from fichero.printer import PRINTHEAD_PX @@ -9,16 +10,60 @@ from fichero.printer import PRINTHEAD_PX log = logging.getLogger(__name__) -def prepare_image(img: Image.Image, max_rows: int = 240) -> Image.Image: - """Convert any image to 96px wide, 1-bit, black on white.""" +def floyd_steinberg_dither(img: Image.Image) -> Image.Image: + """Floyd-Steinberg error-diffusion dithering to 1-bit. + + Same algorithm as PrinterImageProcessor.ditherFloydSteinberg() in the + decompiled Fichero APK: distributes quantisation error to neighbouring + pixels with weights 7/16, 3/16, 5/16, 1/16. + """ + arr = np.array(img, dtype=np.float32) + h, w = arr.shape + + for y in range(h): + for x in range(w): + old = arr[y, x] + new = 0.0 if old < 128 else 255.0 + arr[y, x] = new + err = old - new + if x + 1 < w: + arr[y, x + 1] += err * 7 / 16 + if y + 1 < h: + if x - 1 >= 0: + arr[y + 1, x - 1] += err * 3 / 16 + arr[y + 1, x] += err * 5 / 16 + if x + 1 < w: + arr[y + 1, x + 1] += err * 1 / 16 + + arr = np.clip(arr, 0, 255).astype(np.uint8) + return Image.fromarray(arr, mode="L") + + +def prepare_image( + img: Image.Image, max_rows: int = 240, dither: bool = True +) -> Image.Image: + """Convert any image to 96px wide, 1-bit, black on white. + + When *dither* is True (default), uses Floyd-Steinberg error diffusion + for better quality on photos and gradients. Set False for crisp text. + """ img = img.convert("L") w, h = img.size new_h = int(h * (PRINTHEAD_PX / w)) + img = img.resize((PRINTHEAD_PX, new_h), Image.LANCZOS) + if new_h > max_rows: log.warning("Image height %dpx exceeds max %dpx, cropping bottom", new_h, max_rows) - new_h = max_rows - img = img.resize((PRINTHEAD_PX, new_h), Image.LANCZOS) + img = img.crop((0, 0, PRINTHEAD_PX, max_rows)) + img = ImageOps.autocontrast(img, cutoff=1) + + if dither: + img = floyd_steinberg_dither(img) + + # Pack to 1-bit. PIL mode "1" tobytes() uses 0-bit=black, 1-bit=white, + # but the printer wants 1-bit=black. Mapping dark->1 via point() inverts + # the PIL convention so the final packed bits match what the printer needs. img = img.point(lambda x: 1 if x < 128 else 0, "1") return img @@ -44,8 +89,8 @@ def text_to_image(text: str, font_size: int = 30, label_height: int = 240) -> Im bbox = draw.textbbox((0, 0), text, font=font) tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] - x = (canvas_w - tw) // 2 - y = (canvas_h - th) // 2 + x = (canvas_w - tw) // 2 - bbox[0] + y = (canvas_h - th) // 2 - bbox[1] draw.text((x, y), text, fill=0, font=font) img = img.rotate(90, expand=True) diff --git a/fichero/printer.py b/fichero/printer.py index 996d169..cd42067 100644 --- a/fichero/printer.py +++ b/fichero/printer.py @@ -1,5 +1,5 @@ """ -Fichero / D11s thermal label printer - BLE interface. +Fichero / D11s thermal label printer - BLE + Classic Bluetooth interface. Protocol reverse-engineered from decompiled Fichero APK (com.lj.fichero). Device class: AiYinNormalDevice (LuckPrinter SDK) @@ -7,11 +7,22 @@ Device class: AiYinNormalDevice (LuckPrinter SDK) """ import asyncio +import sys from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from bleak import BleakClient, BleakGATTCharacteristic, BleakScanner +# --- RFCOMM (Classic Bluetooth) support - Linux + Windows (Python 3.9+) --- + +_RFCOMM_AVAILABLE = False +if sys.platform in ("linux", "win32"): + import socket as _socket + + _RFCOMM_AVAILABLE = hasattr(_socket, "AF_BLUETOOTH") + +RFCOMM_CHANNEL = 1 + # --- BLE identifiers --- PRINTER_NAME_PREFIXES = ("FICHERO", "D11s_") @@ -24,7 +35,8 @@ NOTIFY_UUID = "00002af0-0000-1000-8000-00805f9b34fb" PRINTHEAD_PX = 96 BYTES_PER_ROW = PRINTHEAD_PX // 8 # 12 -CHUNK_SIZE = 200 +CHUNK_SIZE_BLE = 200 # BLE MTU-limited +CHUNK_SIZE_CLASSIC = 16384 # from decompiled app (C1703d.java), stream-based # --- Paper types for 10 FF 84 nn --- @@ -111,6 +123,81 @@ class PrinterStatus: return not (self.cover_open or self.no_paper or self.overheated) +# --- RFCOMM client (duck-types the BleakClient interface) --- + + +class RFCOMMClient: + """Classic Bluetooth (RFCOMM) transport. Linux + Windows (Python 3.9+). + + Implements the same async context manager + write_gatt_char/start_notify + interface that PrinterClient expects from BleakClient. Zero dependencies + beyond stdlib. + """ + + is_classic = True # transport marker for PrinterClient chunk sizing + + def __init__(self, address: str, channel: int = RFCOMM_CHANNEL): + self._address = address + self._channel = channel + self._sock: "_socket.socket | None" = None + self._reader_task: asyncio.Task | None = None + + async def __aenter__(self) -> "RFCOMMClient": + if not _RFCOMM_AVAILABLE: + raise PrinterError( + "RFCOMM transport requires socket.AF_BLUETOOTH " + "(Linux with BlueZ, or Windows with Python 3.9+). " + "Not available on this platform." + ) + import socket as _socket + + sock = _socket.socket( + _socket.AF_BLUETOOTH, _socket.SOCK_STREAM, _socket.BTPROTO_RFCOMM + ) + sock.setblocking(False) + loop = asyncio.get_running_loop() + try: + await asyncio.wait_for( + loop.sock_connect(sock, (self._address, self._channel)), + timeout=10.0, + ) + except Exception: + sock.close() + raise + self._sock = sock + return self + + async def __aexit__(self, *exc) -> None: + if self._reader_task is not None: + self._reader_task.cancel() + try: + await self._reader_task + except asyncio.CancelledError: + pass + self._reader_task = None + if self._sock is not None: + self._sock.close() + self._sock = None + + async def write_gatt_char(self, _uuid: str, data: bytes, response: bool = False) -> None: + loop = asyncio.get_running_loop() + await loop.sock_sendall(self._sock, data) + + async def start_notify(self, _uuid: str, callback) -> None: + self._reader_task = asyncio.create_task(self._reader_loop(callback)) + + async def _reader_loop(self, callback) -> None: + loop = asyncio.get_running_loop() + while True: + try: + data = await loop.sock_recv(self._sock, 1024) + except (OSError, asyncio.CancelledError): + return + if not data: + return + callback(None, bytearray(data)) + + # --- Client --- @@ -120,6 +207,7 @@ class PrinterClient: self._buf = bytearray() self._event = asyncio.Event() self._lock = asyncio.Lock() + self._is_classic = getattr(client, "is_classic", False) def _on_notify(self, _char: BleakGATTCharacteristic, data: bytearray) -> None: self._buf.extend(data) @@ -140,13 +228,18 @@ class PrinterClient: await asyncio.sleep(DELAY_NOTIFY_EXTRA) except asyncio.TimeoutError: raise PrinterTimeout(f"No response within {timeout}s") - return bytes(self._buf) + return bytes(self._buf) - async def send_chunked(self, data: bytes, chunk_size: int = CHUNK_SIZE) -> None: - for i in range(0, len(data), chunk_size): - chunk = data[i : i + chunk_size] - await self.client.write_gatt_char(WRITE_UUID, chunk, response=False) - await asyncio.sleep(DELAY_CHUNK_GAP) + async def send_chunked(self, data: bytes, chunk_size: int | None = None) -> None: + if chunk_size is None: + chunk_size = CHUNK_SIZE_CLASSIC if self._is_classic else CHUNK_SIZE_BLE + delay = 0 if self._is_classic else DELAY_CHUNK_GAP + async with self._lock: + for i in range(0, len(data), chunk_size): + chunk = data[i : i + chunk_size] + await self.client.write_gatt_char(WRITE_UUID, chunk, response=False) + if delay: + await asyncio.sleep(delay) # --- Info commands (all tested and confirmed on D11s fw 2.4.6) --- @@ -266,10 +359,22 @@ class PrinterClient: @asynccontextmanager -async def connect(address: str | None = None) -> AsyncGenerator[PrinterClient, None]: +async def connect( + address: str | None = None, + classic: bool = False, + channel: int = RFCOMM_CHANNEL, +) -> AsyncGenerator[PrinterClient, None]: """Discover printer, connect, and yield a ready PrinterClient.""" - addr = address or await find_printer() - async with BleakClient(addr) as client: - pc = PrinterClient(client) - await pc.start() - yield pc + if classic: + if not address: + raise PrinterError("--address is required for Classic Bluetooth (no scanning)") + async with RFCOMMClient(address, channel) as client: + pc = PrinterClient(client) + await pc.start() + yield pc + else: + addr = address or await find_printer() + async with BleakClient(addr) as client: + pc = PrinterClient(client) + await pc.start() + yield pc diff --git a/pyproject.toml b/pyproject.toml index 5e1e4bf..e012c53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "Fichero D11s thermal label printer - BLE CLI tool" requires-python = ">=3.10" dependencies = [ "bleak", + "numpy", "pillow", ] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_rfcomm.py b/tests/test_rfcomm.py new file mode 100644 index 0000000..316df17 --- /dev/null +++ b/tests/test_rfcomm.py @@ -0,0 +1,281 @@ +"""Tests for the RFCOMM (Classic Bluetooth) transport layer.""" + +import asyncio +import sys +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from fichero.printer import ( + RFCOMM_CHANNEL, + PrinterClient, + PrinterError, + RFCOMMClient, + connect, +) + + +# --- RFCOMMClient unit tests --- + + +class TestRFCOMMClientInit: + def test_defaults(self): + c = RFCOMMClient("AA:BB:CC:DD:EE:FF") + assert c._address == "AA:BB:CC:DD:EE:FF" + assert c._channel == RFCOMM_CHANNEL + assert c._sock is None + assert c._reader_task is None + + def test_custom_channel(self): + c = RFCOMMClient("AA:BB:CC:DD:EE:FF", channel=3) + assert c._channel == 3 + + +class TestRFCOMMClientPlatformGuard: + @pytest.mark.asyncio + async def test_raises_on_unavailable_platform(self): + with patch("fichero.printer._RFCOMM_AVAILABLE", False): + client = RFCOMMClient("AA:BB:CC:DD:EE:FF") + with pytest.raises(PrinterError, match="requires socket.AF_BLUETOOTH"): + async with client: + pass + + +class TestRFCOMMClientConnect: + @pytest.mark.asyncio + async def test_connect_and_close(self): + mock_sock = MagicMock() + mock_sock.close = MagicMock() + + with ( + patch("fichero.printer._RFCOMM_AVAILABLE", True), + patch("fichero.printer.RFCOMMClient.__aenter__") as mock_enter, + patch("fichero.printer.RFCOMMClient.__aexit__") as mock_exit, + ): + client = RFCOMMClient("AA:BB:CC:DD:EE:FF") + mock_enter.return_value = client + mock_exit.return_value = None + client._sock = mock_sock + + async with client: + assert client._sock is mock_sock + + @pytest.mark.asyncio + async def test_socket_closed_on_connect_failure(self): + """If sock_connect fails, the socket must be closed.""" + mock_sock = MagicMock() + mock_sock.setblocking = MagicMock() + mock_sock.close = MagicMock() + + mock_socket_mod = MagicMock() + mock_socket_mod.AF_BLUETOOTH = 31 + mock_socket_mod.SOCK_STREAM = 1 + mock_socket_mod.BTPROTO_RFCOMM = 3 + mock_socket_mod.socket.return_value = mock_sock + + async def fail_connect(sock, addr): + raise ConnectionRefusedError("refused") + + with ( + patch("fichero.printer._RFCOMM_AVAILABLE", True), + patch.dict("sys.modules", {"socket": mock_socket_mod}), + ): + client = RFCOMMClient("AA:BB:CC:DD:EE:FF") + loop = asyncio.get_running_loop() + with patch.object(loop, "sock_connect", fail_connect): + with pytest.raises(ConnectionRefusedError): + await client.__aenter__() + mock_sock.close.assert_called_once() + + +class TestRFCOMMClientIO: + @pytest.mark.asyncio + async def test_write_gatt_char_sends_data(self): + client = RFCOMMClient("AA:BB:CC:DD:EE:FF") + client._sock = MagicMock() + + loop = asyncio.get_running_loop() + with patch.object(loop, "sock_sendall", new_callable=AsyncMock) as mock_send: + await client.write_gatt_char("ignored-uuid", b"\x10\xff\x40") + mock_send.assert_called_once_with(client._sock, b"\x10\xff\x40") + + @pytest.mark.asyncio + async def test_write_gatt_char_ignores_uuid_and_response(self): + client = RFCOMMClient("AA:BB:CC:DD:EE:FF") + client._sock = MagicMock() + + loop = asyncio.get_running_loop() + with patch.object(loop, "sock_sendall", new_callable=AsyncMock) as mock_send: + await client.write_gatt_char("any-uuid", b"\xAB", response=True) + mock_send.assert_called_once_with(client._sock, b"\xAB") + + @pytest.mark.asyncio + async def test_start_notify_launches_reader(self): + client = RFCOMMClient("AA:BB:CC:DD:EE:FF") + client._sock = MagicMock() + + callback = MagicMock() + + # Mock sock_recv to return data once then empty (EOF) + loop = asyncio.get_running_loop() + call_count = 0 + + async def mock_recv(sock, size): + nonlocal call_count + call_count += 1 + if call_count == 1: + return b"\x01\x02" + return b"" + + with patch.object(loop, "sock_recv", mock_recv): + await client.start_notify("ignored-uuid", callback) + assert client._reader_task is not None + # Let the reader loop run + await client._reader_task + + callback.assert_called_once_with(None, bytearray(b"\x01\x02")) + + @pytest.mark.asyncio + async def test_reader_loop_handles_oserror(self): + client = RFCOMMClient("AA:BB:CC:DD:EE:FF") + client._sock = MagicMock() + callback = MagicMock() + + loop = asyncio.get_running_loop() + + async def mock_recv(sock, size): + raise OSError("socket closed") + + with patch.object(loop, "sock_recv", mock_recv): + await client.start_notify("uuid", callback) + await client._reader_task + + callback.assert_not_called() + + +class TestRFCOMMClientExit: + @pytest.mark.asyncio + async def test_exit_cancels_reader_and_closes_socket(self): + client = RFCOMMClient("AA:BB:CC:DD:EE:FF") + mock_sock = MagicMock() + client._sock = mock_sock + + # Create a long-running task to cancel + async def hang_forever(): + await asyncio.sleep(999) + + client._reader_task = asyncio.create_task(hang_forever()) + + await client.__aexit__(None, None, None) + + assert client._sock is None + assert client._reader_task is None + mock_sock.close.assert_called_once() + + @pytest.mark.asyncio + async def test_exit_no_reader_no_socket(self): + """Exit is safe even if never connected.""" + client = RFCOMMClient("AA:BB:CC:DD:EE:FF") + await client.__aexit__(None, None, None) + assert client._sock is None + assert client._reader_task is None + + +# --- connect() integration tests --- + + +class TestConnectClassic: + @pytest.mark.asyncio + async def test_classic_requires_address(self): + with pytest.raises(PrinterError, match="--address is required"): + async with connect(classic=True): + pass + + @pytest.mark.asyncio + async def test_classic_uses_rfcomm_client(self): + mock_rfcomm = AsyncMock() + mock_rfcomm.__aenter__ = AsyncMock(return_value=mock_rfcomm) + mock_rfcomm.__aexit__ = AsyncMock(return_value=None) + mock_rfcomm.start_notify = AsyncMock() + + with patch("fichero.printer.RFCOMMClient", return_value=mock_rfcomm) as mock_cls: + async with connect("AA:BB:CC:DD:EE:FF", classic=True, channel=3) as pc: + assert isinstance(pc, PrinterClient) + mock_cls.assert_called_once_with("AA:BB:CC:DD:EE:FF", 3) + + @pytest.mark.asyncio + async def test_ble_path_unchanged(self): + """classic=False still uses BleakClient.""" + mock_bleak = AsyncMock() + mock_bleak.__aenter__ = AsyncMock(return_value=mock_bleak) + mock_bleak.__aexit__ = AsyncMock(return_value=None) + mock_bleak.start_notify = AsyncMock() + + with patch("fichero.printer.BleakClient", return_value=mock_bleak) as mock_cls: + async with connect("AA:BB:CC:DD:EE:FF", classic=False) as pc: + assert isinstance(pc, PrinterClient) + mock_cls.assert_called_once_with("AA:BB:CC:DD:EE:FF") + + +# --- CLI arg parsing tests --- + + +class TestCLIArgs: + def test_classic_flag_default_false(self): + from fichero.cli import main + import argparse + + with patch("argparse.ArgumentParser.parse_args") as mock_parse: + mock_parse.return_value = argparse.Namespace( + address=None, classic=False, channel=1, + command="status", func=AsyncMock(), + ) + # Just verify the parser accepts --classic + from fichero.cli import main as cli_main + parser = argparse.ArgumentParser() + parser.add_argument("--classic", action="store_true", default=False) + parser.add_argument("--channel", type=int, default=1) + args = parser.parse_args([]) + assert args.classic is False + assert args.channel == 1 + + def test_classic_flag_set(self): + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--classic", action="store_true", default=False) + parser.add_argument("--channel", type=int, default=1) + args = parser.parse_args(["--classic", "--channel", "5"]) + assert args.classic is True + assert args.channel == 5 + + def test_env_var_transport(self): + import argparse + + with patch.dict("os.environ", {"FICHERO_TRANSPORT": "classic"}): + parser = argparse.ArgumentParser() + import os + parser.add_argument( + "--classic", action="store_true", + default=os.environ.get("FICHERO_TRANSPORT", "").lower() == "classic", + ) + args = parser.parse_args([]) + assert args.classic is True + + +# --- Exports --- + + +class TestExports: + def test_rfcomm_client_exported(self): + from fichero import RFCOMMClient as RC + assert RC is RFCOMMClient + + def test_rfcomm_channel_exported(self): + from fichero import RFCOMM_CHANNEL as CH + assert CH == 1 + + def test_all_contains_new_symbols(self): + import fichero + assert "RFCOMMClient" in fichero.__all__ + assert "RFCOMM_CHANNEL" in fichero.__all__ diff --git a/uv.lock b/uv.lock index 6d11059..600b81b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] [[package]] name = "async-timeout" @@ -76,15 +80,165 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "bleak" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pillow" }, ] [package.metadata] requires-dist = [ { name = "bleak" }, + { name = "numpy" }, { name = "pillow" }, ] +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, +] + [[package]] name = "pillow" version = "12.1.1" diff --git a/web/index.html b/web/index.html index 583541d..b94527f 100644 --- a/web/index.html +++ b/web/index.html @@ -310,6 +310,9 @@