diff --git a/justfile b/justfile new file mode 100644 index 00000000..ea830287 --- /dev/null +++ b/justfile @@ -0,0 +1,59 @@ +# Cross platform shebang: +shebang := if os() == 'windows' { + 'pwsh.exe' +} else { + '/usr/bin/env pwsh' +} + +# Set shell for non-Windows OSs: +set shell := ["pwsh", "-c"] + +# Set shell for Windows OSs: +set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"] + +# Show recipes +help: + @just --list + + + +# install project in edi mode and install dev dependencies +sync: + uv sync --extra dev,test + +# run full pytest suite +test: + uv run pytest -v + +# create coverage report, build html report +coverage: + uv run pytest --cov --cov-report=term-missing + coverage html + start coverage/index.html + +# bump mpflash's version +bump bump="patch": + uv version --bump {{bump}} + +# build the project for distribution +build: + uv build + +# publish the project to PyPI +publish : build + uv publish + +# delete and regen the lockfile - useful in merge conficts +lock: + #!{{shebang}} + del uv.lock -erroraction ignore + uv lock + + +# [script('python')] +# python: +# print('Hello from python!') +# from pathlib import Path +# print(f'Current directory: {Path.cwd()}') + + diff --git a/mpflash/cli_download.py b/mpflash/cli_download.py index c63b3fe5..4b7f3542 100644 --- a/mpflash/cli_download.py +++ b/mpflash/cli_download.py @@ -1,6 +1,5 @@ """CLI to Download MicroPython firmware for specific ports, boards and versions.""" -from pathlib import Path import rich_click as click from loguru import logger as log @@ -15,7 +14,6 @@ from .ask_input import ask_missing_params from .cli_group import cli from .common import DownloadParams -from .config import config from .download import download diff --git a/mpflash/cli_flash.py b/mpflash/cli_flash.py index a44210a9..6df66c56 100644 --- a/mpflash/cli_flash.py +++ b/mpflash/cli_flash.py @@ -1,15 +1,15 @@ +import sys from typing import List -import rich_click as click -from loguru import logger as log - import mpflash.download.jid as jid import mpflash.mpboard_id as mpboard_id +import rich_click as click +from loguru import logger as log from mpflash.ask_input import ask_missing_params from mpflash.cli_download import connected_ports_boards_variants from mpflash.cli_group import cli from mpflash.cli_list import show_mcus -from mpflash.common import BootloaderMethod, FlashParams, filtered_comports +from mpflash.common import BootloaderMethod, FlashMethod, FlashParams, filtered_comports from mpflash.errors import MPFlashError from mpflash.flash import flash_tasks from mpflash.flash.worklist import FlashTaskList, create_worklist @@ -112,6 +112,27 @@ show_default=True, help="""How to enter the (MicroPython) bootloader before flashing.""", ) +@click.option( + "--method", + "--flash-method", + "flash_method", + type=click.Choice([e.value for e in FlashMethod]), + default="auto", + show_default=True, + help="""Flash programming method. 'auto' uses serial bootloader methods (existing behavior). Use 'pyocd' for SWD/JTAG programming via debug probe.""", +) +@click.option( + "--probe", + "--probe-id", # Keep as alias for backwards compatibility + "probe_id", + help="""Specific pyOCD probe ID to use (partial match). Required when multiple probes are connected.""", + metavar="PROBE_ID", +) +@click.option( + "--auto-install-packs/--no-auto-install-packs", + default=True, + help="""Automatically install CMSIS packs for missing pyOCD targets. Default: enabled.""", +) @click.option( "--force", "-f", @@ -144,6 +165,14 @@ def cli_flash_board(**kwargs) -> int: kwargs.pop("board") else: kwargs["boards"] = [kwargs.pop("board")] + + # Convert flash_method to method and convert to enum + flash_method_str = kwargs.pop("flash_method", "auto") + flash_method = FlashMethod(flash_method_str) + + # Extract pyOCD options + probe_id = kwargs.pop("probe_id", None) + auto_install_packs = kwargs.pop("auto_install_packs", True) params = FlashParams(**kwargs) params.versions = list(params.versions) @@ -187,7 +216,7 @@ def cli_flash_board(**kwargs) -> int: # Ask for missing input if needed params = ask_missing_params(params) if not params: # Cancelled by user - return 2 + sys.exit(2) assert isinstance(params, FlashParams) if len(params.versions) > 1: @@ -240,6 +269,7 @@ def cli_flash_board(**kwargs) -> int: params.versions[0], serial_ports=comports, board_id=board_id, + custom_firmware=params.custom, port=params.ports[0] if params.ports else None, ) else: @@ -251,10 +281,14 @@ def cli_flash_board(**kwargs) -> int: ) if not params.custom: jid.ensure_firmware_downloaded_tasks(tasks, version=params.versions[0], force=params.force) + # TODO: CHECK MERGE if flashed := flash_tasks( tasks, params.erase, params.bootloader, + method=flash_method, + probe_id=probe_id, + auto_install_packs=auto_install_packs, flash_mode=params.flash_mode, ): log.info(f"Flashed {len(flashed)} boards") @@ -262,4 +296,4 @@ def cli_flash_board(**kwargs) -> int: return 0 else: log.error("No boards were flashed") - return 1 + sys.exit(1) diff --git a/mpflash/cli_pyocd.py b/mpflash/cli_pyocd.py new file mode 100644 index 00000000..c869899e --- /dev/null +++ b/mpflash/cli_pyocd.py @@ -0,0 +1,263 @@ +""" +CLI commands for pyOCD debug probe management and information. +""" + +import rich_click as click +from rich.console import Console +from rich.table import Table +from loguru import logger as log + +from mpflash.cli_group import cli +from mpflash.errors import MPFlashError + +try: + from mpflash.flash.pyocd_flash import ( + list_pyocd_probes, + pyocd_info, + ) + from mpflash.flash.pyocd_core import ( + is_pyocd_available, + get_pyocd_targets + ) + PYOCD_AVAILABLE = True +except ImportError: + PYOCD_AVAILABLE = False + +def list_supported_targets(): + """Get supported targets for CLI display.""" + try: + targets = get_pyocd_targets() + return {name: info.get("part_number", name) for name, info in targets.items()} + except Exception: + return {} + +console = Console() + +@cli.command( + "list-probes", + short_help="List available pyOCD debug probes and their target information.", +) +@click.option( + "--detect-targets/--no-detect-targets", + default=True, + show_default=True, + help="Attempt to auto-detect target types connected to probes.", +) +def cli_list_probes(detect_targets: bool) -> int: + """ + List all connected pyOCD debug probes with their capabilities. + + This command discovers debug probes (ST-Link, DAP-Link, etc.) that can be used + for SWD/JTAG programming with the --method pyocd option. + """ + if not PYOCD_AVAILABLE: + log.error("pyOCD is not installed. Install with: uv add pyocd") + return 1 + + if not is_pyocd_available(): + log.error("pyOCD is installed but not functioning properly") + return 1 + + try: + probes = list_pyocd_probes() + + if not probes: + console.print("No pyOCD debug probes found.") + console.print("\nMake sure your debug probe is connected and recognized by the system.") + console.print("Common debug probes include ST-Link, DAP-Link, J-Link, etc.") + return 1 + + table = Table(title="Available PyOCD Debug Probes") + table.add_column("Probe ID", style="cyan", no_wrap=True) + table.add_column("Description", style="white") + table.add_column("Vendor", style="blue") + table.add_column("Product", style="blue") + table.add_column("Target Type", style="green") + table.add_column("Status", style="yellow") + + for probe in probes: + # Optionally detect target type + target_type = "Unknown" + status = "Connected" + + if detect_targets: + try: + detected = probe.detect_target_type() + if detected: + target_type = detected + status = "Target Detected" + else: + status = "No Target" + except Exception as e: + target_type = "Detection Failed" + status = f"Error: {str(e)[:30]}..." + else: + status = "Not Checked" + + table.add_row( + probe.unique_id, + probe.description, + probe.vendor_name, + probe.product_name, + target_type, + status + ) + + console.print(table) + + console.print(f"\n[green]Found {len(probes)} debug probe(s)[/green]") + console.print("\nTo use a specific probe with mpflash:") + console.print(" mpflash flash --method pyocd --probe-id ") + console.print("\nTo flash with automatic probe selection:") + console.print(" mpflash flash --method pyocd") + + return 0 + + except Exception as e: + log.error(f"Failed to list pyOCD probes: {e}") + return 1 + +@cli.command( + "pyocd-info", + short_help="Show pyOCD installation and target support information.", +) +def cli_pyocd_info() -> int: + """ + Display information about pyOCD installation, version, and supported targets. + + This command shows the current pyOCD status, available debug probes, + and information about target support for SWD/JTAG programming. + """ + info = pyocd_info() if PYOCD_AVAILABLE else {"available": False} + + # PyOCD Installation Status + console.print("[bold blue]PyOCD Installation Status[/bold blue]") + if info["available"]: + console.print(f"✅ pyOCD is installed (version: {info.get('version', 'unknown')})") + else: + console.print("❌ pyOCD is not installed") + console.print(" Install with: uv add pyocd") + return 1 + + # Debug Probes + console.print(f"\n[bold blue]Connected Debug Probes[/bold blue]") + probes = info.get("probes", []) + if probes: + for probe in probes: + console.print(f"🔌 {probe['unique_id']}: {probe['description']}") + if probe.get('target_type'): + console.print(f" Target: {probe['target_type']}") + else: + console.print("No debug probes found") + + # Supported Targets + console.print(f"\n[bold blue]Built-in Target Support[/bold blue]") + if PYOCD_AVAILABLE: + targets = list_supported_targets() + console.print(f"📋 {len(targets)} board mappings available") + + # Group by target family + stm32_boards = [bid for bid in targets.keys() if targets[bid].startswith("stm32")] + rp2040_boards = [bid for bid in targets.keys() if targets[bid].startswith("rp20")] + samd_boards = [bid for bid in targets.keys() if targets[bid].startswith("samd")] + + console.print(f" STM32 boards: {len(stm32_boards)}") + console.print(f" RP2040 boards: {len(rp2040_boards)}") + console.print(f" SAMD boards: {len(samd_boards)}") + + console.print(f"\n[dim]Note: ESP32/ESP8266 not supported (use esptool instead)[/dim]") + + # Usage Examples + console.print(f"\n[bold blue]Usage Examples[/bold blue]") + console.print("Flash with pyOCD (auto-detect probe and target):") + console.print(" mpflash flash --method pyocd") + console.print("\nFlash with specific probe:") + console.print(" mpflash flash --method pyocd --probe-id ") + console.print("\nList available probes:") + console.print(" mpflash list-probes") + + return 0 + +@cli.command( + "pyocd-targets", + short_help="List supported pyOCD target mappings.", +) +@click.option( + "--board-filter", + "-b", + help="Filter targets by board name (case-insensitive substring match)", + metavar="PATTERN" +) +@click.option( + "--target-filter", + "-t", + help="Filter by pyOCD target type (case-insensitive substring match)", + metavar="PATTERN" +) +def cli_pyocd_targets(board_filter: str, target_filter: str) -> int: + """ + Display the mapping between MPFlash board IDs and pyOCD target types. + + This shows which boards can be programmed using pyOCD SWD/JTAG interface + and what target type pyOCD will use for each board. + """ + if not PYOCD_AVAILABLE: + log.error("pyOCD is not installed. Install with: uv add pyocd") + return 1 + + try: + targets = list_supported_targets() + + # Apply filters + filtered_targets = targets + if board_filter: + filtered_targets = { + board_id: target for board_id, target in targets.items() + if board_filter.lower() in board_id.lower() + } + if target_filter: + filtered_targets = { + board_id: target for board_id, target in filtered_targets.items() + if target_filter.lower() in target.lower() + } + + if not filtered_targets: + console.print("No targets match the specified filters.") + return 1 + + table = Table(title="PyOCD Target Mappings") + table.add_column("Board ID", style="cyan", no_wrap=True) + table.add_column("PyOCD Target", style="green", no_wrap=True) + table.add_column("Family", style="blue") + + # Sort by board ID for consistent output + for board_id in sorted(filtered_targets.keys()): + target = filtered_targets[board_id] + + # Determine family + if target.startswith("stm32"): + family = "STM32" + elif target.startswith("rp20"): + family = "RP2040/RP2350" + elif target.startswith("samd"): + family = "SAMD" + else: + family = "Other" + + table.add_row(board_id, target, family) + + console.print(table) + console.print(f"\n[green]Showing {len(filtered_targets)} of {len(targets)} supported targets[/green]") + + if board_filter or target_filter: + console.print(f"\nFilters applied:") + if board_filter: + console.print(f" Board: {board_filter}") + if target_filter: + console.print(f" Target: {target_filter}") + + return 0 + + except Exception as e: + log.error(f"Failed to list pyOCD targets: {e}") + return 1 \ No newline at end of file diff --git a/mpflash/common.py b/mpflash/common.py index 43b72aca..3d6613c3 100644 --- a/mpflash/common.py +++ b/mpflash/common.py @@ -56,6 +56,15 @@ class BootloaderMethod(Enum): NONE = "none" +class FlashMethod(Enum): + AUTO = "auto" + SERIAL = "serial" # Traditional serial bootloader methods + PYOCD = "pyocd" # SWD/JTAG programming via pyOCD + UF2 = "uf2" # UF2 file copy method + DFU = "dfu" # STM32 DFU method + ESPTOOL = "esptool" # ESP32/ESP8266 esptool method + + @dataclass class FlashParams(Params): """Parameters for flashing a board""" diff --git a/mpflash/flash/__init__.py b/mpflash/flash/__init__.py index ecbb5c1a..d918f9d7 100644 --- a/mpflash/flash/__init__.py +++ b/mpflash/flash/__init__.py @@ -1,11 +1,17 @@ from pathlib import Path from loguru import logger as log -from mpflash.common import PORT_FWTYPES, UF2_PORTS, BootloaderMethod +from mpflash.common import (PORT_FWTYPES, UF2_PORTS, BootloaderMethod, + FlashMethod) from mpflash.config import config from mpflash.errors import MPFlashError +# Import debug probe support +from .debug_probe import is_debug_programming_available from .esp import flash_esp +from .pyocd_core import is_pyocd_available as pyocd_available +from .pyocd_core import is_pyocd_supported as is_pyocd_supported_from_mcu +from .pyocd_flash import flash_pyocd, pyocd_info from .stm32 import flash_stm32 from .uf2 import flash_uf2 from .worklist import FlashTaskList @@ -17,6 +23,7 @@ def flash_tasks( tasks: FlashTaskList, erase: bool, bootloader: BootloaderMethod, + method: FlashMethod = FlashMethod.AUTO, **kwargs, ): """Flash a list of FlashTask items directly.""" @@ -27,13 +34,13 @@ def flash_tasks( if not fw_info: log.error(f"Firmware not found for {mcu.board} on {mcu.serialport}, skipping") continue - fw_file = config.firmware_folder / fw_info.firmware_file + fw_file: Path = config.firmware_folder / fw_info.firmware_file if not fw_file.exists(): log.error(f"File {fw_file} does not exist, skipping {mcu.board} on {mcu.serialport}") continue log.info(f"Updating {mcu.board} on {mcu.serialport} to {fw_info.version}") try: - updated = flash_mcu(mcu, fw_file=fw_file, erase=erase, bootloader=bootloader, **kwargs) + updated = flash_mcu(mcu, fw_file=fw_file, erase=erase, bootloader=bootloader, method=method, **kwargs) except MPFlashError as e: log.error(f"Failed to flash {mcu.board} on {mcu.serialport}: {e}") continue @@ -53,31 +60,134 @@ def flash_tasks( def flash_mcu( - mcu, - *, - fw_file: Path, - erase: bool = False, - bootloader: BootloaderMethod = BootloaderMethod.AUTO, - **kwargs - ): - """Flash a single MCU with the specified firmware.""" - from mpflash.bootloader.activate import enter_bootloader - - updated = None - try: - if mcu.port in UF2_PORTS and fw_file.suffix == ".uf2": - if not enter_bootloader(mcu, bootloader): - raise MPFlashError(f"Failed to enter bootloader for {mcu.board} on {mcu.serialport}") - updated = flash_uf2(mcu, fw_file=fw_file, erase=erase) - elif mcu.port in ["stm32"]: - if not enter_bootloader(mcu, bootloader): - raise MPFlashError(f"Failed to enter bootloader for {mcu.board} on {mcu.serialport}") - updated = flash_stm32(mcu, fw_file, erase=erase) - elif mcu.port in ["esp32", "esp8266"]: - # bootloader is handled by esptool for esp32/esp8266 - updated = flash_esp(mcu, fw_file=fw_file, erase=erase, **kwargs) - else: - raise MPFlashError(f"Don't (yet) know how to flash {mcu.port}-{mcu.board} on {mcu.serialport}") - except Exception as e: - raise MPFlashError(f"Failed to flash {mcu.board} on {mcu.serialport}") from e - return updated \ No newline at end of file + mcu, + *, + fw_file: Path, + erase: bool = False, + bootloader: BootloaderMethod = BootloaderMethod.AUTO, + method: FlashMethod = FlashMethod.AUTO, + **kwargs, +): + """Flash a single MCU with the specified firmware.""" + from mpflash.bootloader.activate import enter_bootloader + + updated = None + + # Determine the actual flash method to use + flash_method = _select_flash_method(mcu, method, fw_file) + + log.debug(f"Using flash method: {flash_method.value} for {mcu.board_id}") + + try: + if flash_method == FlashMethod.PYOCD: + # PyOCD SWD/JTAG programming + if not is_debug_programming_available(): + raise MPFlashError("Debug probe programming not available. Install with: uv sync --extra pyocd") + updated = flash_pyocd(mcu, fw_file=fw_file, erase=erase, **kwargs) + + elif flash_method == FlashMethod.UF2: + # UF2 file copy method (RP2040, SAMD) + if not enter_bootloader(mcu, bootloader): + raise MPFlashError(f"Failed to enter bootloader for {mcu.board} on {mcu.serialport}") + updated = flash_uf2(mcu, fw_file=fw_file, erase=erase) + + elif flash_method == FlashMethod.DFU: + # STM32 DFU method + if not enter_bootloader(mcu, bootloader): + raise MPFlashError(f"Failed to enter bootloader for {mcu.board} on {mcu.serialport}") + updated = flash_stm32(mcu, fw_file, erase=erase) + + elif flash_method == FlashMethod.ESPTOOL: + # ESP32/ESP8266 esptool method (bootloader handled by esptool) + updated = flash_esp(mcu, fw_file=fw_file, erase=erase, **kwargs) + + else: + raise MPFlashError(f"Unsupported flash method: {flash_method.value}") + + except Exception as e: + raise MPFlashError(f"Failed to flash {mcu.board} on {mcu.serialport}") from e + + return updated + + +def _select_flash_method(mcu, requested_method: FlashMethod, fw_file: Path) -> FlashMethod: + """ + Select the appropriate flash method based on board type and user preference. + + Args: + mcu: MPRemoteBoard instance + requested_method: User-requested flash method + fw_file: Firmware file path + + Returns: + FlashMethod to use + + Raises: + MPFlashError: If no suitable method available + """ + # If user specified a specific method, validate and use it + if requested_method != FlashMethod.AUTO: + if requested_method == FlashMethod.PYOCD: + if not is_debug_programming_available(): + raise MPFlashError("Debug probe programming not available. Install with: uv sync --extra pyocd") + if not is_pyocd_supported_from_mcu(mcu): + raise MPFlashError(f"pyOCD does not support {mcu.board_id} ({mcu.cpu})") + return FlashMethod.PYOCD + + elif requested_method == FlashMethod.UF2: + if mcu.port not in UF2_PORTS or fw_file.suffix != ".uf2": + raise MPFlashError(f"UF2 method not suitable for {mcu.port} with {fw_file.suffix}") + return FlashMethod.UF2 + + elif requested_method == FlashMethod.DFU: + if mcu.port != "stm32": + raise MPFlashError(f"DFU method not suitable for {mcu.port}") + return FlashMethod.DFU + + elif requested_method == FlashMethod.ESPTOOL: + if mcu.port not in ["esp32", "esp8266"]: + raise MPFlashError(f"esptool method not suitable for {mcu.port}") + return FlashMethod.ESPTOOL + + elif requested_method == FlashMethod.SERIAL: + # Use traditional serial-based methods + return _select_serial_method(mcu, fw_file) + + # Auto-select the best method + return _auto_select_flash_method(mcu, fw_file) + + +def _auto_select_flash_method(mcu, fw_file: Path) -> FlashMethod: + """ + Automatically select the best flash method for a board. + + Priority order (maintains existing behavior as default): + 1. Platform-specific serial methods (UF2, DFU, esptool) - no extra hardware needed + 2. Fall back to serial bootloader methods + + Note: PyOCD is NOT included in auto-selection as it requires debug probe hardware. + Use --method pyocd to explicitly enable SWD/JTAG programming. + """ + + # First priority: Platform-specific serial methods (existing behavior) + if mcu.port in UF2_PORTS and fw_file.suffix == ".uf2": + return FlashMethod.UF2 + elif mcu.port == "stm32": + return FlashMethod.DFU + elif mcu.port in ["esp32", "esp8266"]: + return FlashMethod.ESPTOOL + + # Fall back to serial method selection + return _select_serial_method(mcu, fw_file) + + +def _select_serial_method(mcu, fw_file: Path) -> FlashMethod: + """Select appropriate serial-based flash method.""" + if mcu.port in UF2_PORTS and fw_file.suffix == ".uf2": + return FlashMethod.UF2 + elif mcu.port == "stm32": + return FlashMethod.DFU + elif mcu.port in ["esp32", "esp8266"]: + return FlashMethod.ESPTOOL + else: + raise MPFlashError(f"Don't know how to flash {mcu.port}-{mcu.board} on {mcu.serialport}") diff --git a/mpflash/flash/debug_probe.py b/mpflash/flash/debug_probe.py new file mode 100644 index 00000000..71e4296b --- /dev/null +++ b/mpflash/flash/debug_probe.py @@ -0,0 +1,113 @@ +""" +Debug probe abstraction for MPFlash. + +Provides extensible interface for debug probe implementations (pyOCD, OpenOCD, J-Link, etc.). +""" + +from abc import ABC, abstractmethod +from typing import List, Optional +from pathlib import Path + +from mpflash.logger import log +from mpflash.errors import MPFlashError + + +class DebugProbe(ABC): + """Abstract base class for debug probe implementations.""" + + def __init__(self, unique_id: str, description: str): + self.unique_id = unique_id + self.description = description + self.target_type: Optional[str] = None + + @abstractmethod + def program_flash(self, firmware_path: Path, target_type: str, **options) -> bool: + """Program flash memory via the debug probe.""" + pass + + @classmethod + @abstractmethod + def is_implementation_available(cls) -> bool: + """Check if this probe implementation is available.""" + pass + + @classmethod + @abstractmethod + def discover(cls) -> List['DebugProbe']: + """Discover all probes of this type.""" + pass + + def __str__(self) -> str: + return f"{self.__class__.__name__}({self.unique_id})" + + +# Registry for probe implementations +_probe_implementations = {} + + +def register_probe_implementation(name: str, probe_class: type): + """Register a probe implementation for discovery.""" + if not issubclass(probe_class, DebugProbe): + raise ValueError("Probe class must inherit from DebugProbe") + _probe_implementations[name] = probe_class + log.debug(f"Registered {name} probe implementation") + + +def get_debug_probes() -> List[DebugProbe]: + """Discover all available debug probes across all implementations.""" + probes = [] + + for name, probe_class in _probe_implementations.items(): + try: + if probe_class.is_implementation_available(): + discovered = probe_class.discover() + probes.extend(discovered) + log.debug(f"Found {len(discovered)} {name} probes") + except Exception as e: + log.debug(f"Failed to discover {name} probes: {e}") + + return probes + + +def find_debug_probe(probe_id: Optional[str] = None) -> Optional[DebugProbe]: + """Find a debug probe by ID (supports partial matching), or return first available.""" + probes = get_debug_probes() + + if not probes: + return None + + if not probe_id: + return probes[0] + + # Exact match first + for probe in probes: + if probe.unique_id == probe_id: + return probe + + # Partial match + matches = [p for p in probes if probe_id in p.unique_id] + if len(matches) == 1: + return matches[0] + elif len(matches) > 1: + raise MPFlashError( + f"Ambiguous probe ID '{probe_id}' matches multiple probes: " + f"{[p.unique_id for p in matches]}" + ) + + return None + + +def is_debug_programming_available() -> bool: + """Check if any debug probe programming is available.""" + return any( + impl.is_implementation_available() + for impl in _probe_implementations.values() + ) + + +# Auto-register pyOCD if available +try: + from .pyocd_flash import PyOCDProbe + register_probe_implementation("pyocd", PyOCDProbe) +except ImportError: + log.debug("pyOCD probe implementation not available") \ No newline at end of file diff --git a/mpflash/flash/pyocd_core.py b/mpflash/flash/pyocd_core.py new file mode 100644 index 00000000..125bf049 --- /dev/null +++ b/mpflash/flash/pyocd_core.py @@ -0,0 +1,660 @@ +""" +Core pyOCD functionality for MPFlash. + +This module contains the essential pyOCD integration logic including +target detection, fuzzy matching, and CMSIS pack management. +""" + +import re +import subprocess +import sys +from dataclasses import dataclass +from functools import lru_cache +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +from mpflash.errors import MPFlashError +from mpflash.logger import log +from mpflash.mpremoteboard import MPRemoteBoard + +# ============================================================================= +# Secure Subprocess Utilities +# ============================================================================= + +def _run_pyocd_command(args: List[str], timeout: int = 30) -> subprocess.CompletedProcess: + """ + Run pyOCD command with security validation and error handling. + + Args: + args: List of command arguments (excluding 'pyocd') + timeout: Timeout in seconds + + Returns: + subprocess.CompletedProcess object + + Raises: + MPFlashError: If command execution fails or times out + """ + # Validate arguments - should be safe for pyOCD commands + for arg in args: + if not isinstance(arg, str): + raise MPFlashError(f"Invalid argument type: {type(arg)}") + # Allow alphanumeric, dashes, dots, and common pyOCD options + if not re.match(r'^[a-zA-Z0-9._-]+$', arg): + raise MPFlashError(f"Invalid argument format: {arg}") + + cmd = ['pyocd'] + args + + try: + log.debug(f"Running command: {' '.join(cmd)}") + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + check=False # Don't raise on non-zero exit + ) + return result + + except subprocess.TimeoutExpired: + raise MPFlashError(f"Command timed out after {timeout}s: {' '.join(cmd)}") + except FileNotFoundError: + raise MPFlashError( + "pyOCD command not found. Ensure pyOCD is installed and in PATH: " + "uv sync --extra pyocd" + ) + except Exception as e: + raise MPFlashError(f"Command execution failed: {e}") + + +def _check_libusb_windows() -> None: + """Warn if pyusb cannot load a libusb backend on Windows. + + libusb-package (pyocd's official solution) only ships pre-built wheels up + to Python 3.13. On newer Python versions the source build silently produces + an empty package, so pyocd finds no probes. + """ + if sys.platform != "win32": + return + try: + import libusb_package # type: ignore + + if libusb_package.get_libusb1_backend() is None: + log.warning( + "pyocd: no libusb backend available on Windows — debug probes will not be detected.\n" + f" Current Python: {sys.version.split()[0]}\n" + " libusb-package only bundles a pre-built DLL for Python ≤ 3.13.\n" + " Workaround: recreate your venv with Python 3.13 (e.g. `uv venv --python 3.13 --clear`).\n" + " See: https://github.com/pyocd/libusb-package/issues/28" + ) + except ImportError: + log.warning( + "pyocd: libusb-package is not installed — debug probes may not be detected on Windows.\n" + " Fix: install it with `uv sync --extra pyocd`." + ) + + +# Lazy import for pyOCD to handle optional dependency +_pyocd_available = None +_pyocd_modules = {} + + +def _ensure_pyocd(): + """Ensure pyOCD modules are imported and available.""" + global _pyocd_available, _pyocd_modules + + if _pyocd_available is None: + _check_libusb_windows() + try: + import pyocd + _pyocd_modules['pyocd_version'] = pyocd.__version__ + _pyocd_available = True + log.debug(f"pyOCD {pyocd.__version__} available") + except ImportError as e: + _pyocd_available = False + log.debug(f"pyOCD not available: {e}") + + if not _pyocd_available: + raise MPFlashError("pyOCD is not installed. Install with: uv sync --extra pyocd") + + return _pyocd_modules + + +def is_pyocd_available() -> bool: + """Check if pyOCD is available for use.""" + try: + _ensure_pyocd() + return True + except MPFlashError: + return False + + +# ============================================================================= +# MCU Information Parsing +# ============================================================================= + +def parse_mcu_info(mcu: MPRemoteBoard) -> Dict[str, str]: + """ + Parse MCU information from connected device. + + Args: + mcu: Connected MPRemoteBoard instance + + Returns: + Dictionary with parsed MCU information: + - chip_family: e.g., "STM32WB55", "RP2040", "SAMD51" + - chip_variant: e.g., "RGV6", "P19A" + - board_name: e.g., "NUCLEO-WB55", "RPI_PICO" + - full_description: Complete description string + + Examples: + "NUCLEO-WB55 with STM32WB55RGV6" -> { + "chip_family": "STM32WB55", + "chip_variant": "RGV6", + "board_name": "NUCLEO-WB55", + "full_description": "NUCLEO-WB55 with STM32WB55RGV6" + } + """ + info = { + "chip_family": "", + "chip_variant": "", + "board_name": "", + "full_description": mcu.description, + "cpu": mcu.cpu, + "port": mcu.port + } + + # Parse description field (sys.implementation._machine) + description = mcu.description.strip() + + # Pattern 1: "BOARD_NAME with CHIP_FAMILY_VARIANT" + # Example: "NUCLEO-WB55 with STM32WB55RGV6" + match = re.match(r"^(.+?)\s+with\s+(.+)$", description, re.IGNORECASE) + if match: + info["board_name"] = match.group(1).strip() + chip_full = match.group(2).strip() + + # Extract family and variant from chip name + # Pattern for STM32 chips: STM32[FAMILY][VARIANT] + # Examples: STM32F429ZI -> STM32F429 + ZI, STM32WB55RGV6 -> STM32WB55 + RGV6 + chip_match = re.match(r"^(STM32[A-Z]+\d+)([A-Z0-9]*)$", chip_full, re.IGNORECASE) + if chip_match: + info["chip_family"] = chip_match.group(1).upper() + info["chip_variant"] = chip_match.group(2).upper() + else: + info["chip_family"] = chip_full.upper() + + log.debug(f"Parsed MCU info: {info}") + return info + + # Pattern 2: Direct chip name (RP2040, SAMD51, etc.) + # Example: "RP2040", "SAMD51P19A" + if description.upper().startswith("RP20"): + info["chip_family"] = "RP2040" if "2040" in description else "RP2350" + info["board_name"] = mcu.board_id or "RP2040_BOARD" + log.debug(f"Parsed RP2040 info: {info}") + return info + + # Pattern 3: SAMD chips + samd_match = re.match(r"^(SAMD\d+)([A-Z]\d+[A-Z]?).*$", description, re.IGNORECASE) + if samd_match: + info["chip_family"] = samd_match.group(1).upper() + info["chip_variant"] = samd_match.group(2).upper() + info["board_name"] = mcu.board_id or "SAMD_BOARD" + log.debug(f"Parsed SAMD info: {info}") + return info + + # Fallback: Use CPU and port information + if mcu.cpu: + cpu_upper = mcu.cpu.upper() + if cpu_upper.startswith("STM32"): + info["chip_family"] = cpu_upper + elif "RP2040" in cpu_upper: + info["chip_family"] = "RP2040" + elif "SAMD" in cpu_upper: + info["chip_family"] = cpu_upper + else: + info["chip_family"] = cpu_upper + + info["board_name"] = mcu.board_id or "UNKNOWN_BOARD" + + log.debug(f"Fallback MCU info: {info}") + return info + + +# ============================================================================= +# pyOCD Target Discovery +# ============================================================================= + +@lru_cache(maxsize=1) +def get_pyocd_targets() -> Dict[str, Dict[str, str]]: + """ + Get all available pyOCD targets using comprehensive discovery. + + Returns: + Dictionary mapping target_name -> {vendor, part_number, source} + + Raises: + MPFlashError: If pyOCD is not available or discovery fails + """ + _ensure_pyocd() + targets = {} + + # Try API-based approach first (fast, but may miss pack targets) + try: + from pyocd.target import BUILTIN_TARGETS as TARGET_CLASSES + + for target_name, target_class in TARGET_CLASSES.items(): + try: + if hasattr(target_class, 'VENDOR'): + vendor = getattr(target_class, 'VENDOR', 'Unknown') + part_number = getattr(target_class, '__name__', target_name) + else: + vendor = getattr(target_class, 'vendor', 'Unknown') + part_number = getattr(target_class, 'part_number', target_name) + + targets[target_name] = { + "vendor": vendor, + "part_number": part_number, + "source": 'builtin' + } + except Exception as e: + log.debug(f"Skipped target {target_name}: {e}") + continue + + log.debug(f"API method loaded {len(targets)} built-in targets") + + except Exception as api_error: + log.debug(f"API-based target discovery failed: {api_error}") + + # Use subprocess to get complete target list including pack targets + # This is more reliable for getting all available targets + try: + result = _run_pyocd_command(['list', '--targets'], timeout=30) + + if result.returncode == 0: + lines = result.stdout.strip().split('\n') + subprocess_targets = {} + + # Parse the table output (skip header and separator) + for line in lines[2:]: + line = line.strip() + if not line: + continue + + # Split on multiple spaces to handle table format + parts = re.split(r'\s{2,}', line) + if len(parts) >= 5: + target_name = parts[0].strip() + vendor = parts[1].strip() + part_number = parts[2].strip() + source = parts[4].strip() + + subprocess_targets[target_name] = { + "vendor": vendor, + "part_number": part_number, + "source": source + } + + # Merge subprocess results (subprocess is authoritative) + if len(subprocess_targets) > len(targets): + targets = subprocess_targets + log.debug(f"Subprocess method loaded {len(targets)} total targets") + else: + # Supplement API results with any pack targets from subprocess + pack_targets = {k: v for k, v in subprocess_targets.items() + if v['source'] == 'pack' and k not in targets} + targets.update(pack_targets) + log.debug(f"Added {len(pack_targets)} pack targets from subprocess") + + except Exception as subprocess_error: + log.debug(f"Subprocess target discovery failed: {subprocess_error}") + + log.debug(f"Loaded {len(targets)} pyOCD targets total") + return targets + + +# ============================================================================= +# Fuzzy Target Matching +# ============================================================================= + +def fuzzy_match_target(mcu_info: Dict[str, str], pyocd_targets: Dict[str, Dict[str, str]]) -> Optional[str]: + """ + Find the best matching pyOCD target using fuzzy string matching. + + Args: + mcu_info: Parsed MCU information + pyocd_targets: Available pyOCD targets + + Returns: + Best matching pyOCD target name or None + """ + from difflib import SequenceMatcher + + chip_family = mcu_info.get("chip_family", "").upper() + chip_variant = mcu_info.get("chip_variant", "").upper() + port = mcu_info.get("port", "").lower() + + if not chip_family: + log.debug("No chip family found for fuzzy matching") + return None + + log.debug(f"Fuzzy matching for chip: {chip_family}{chip_variant}, port: {port}") + + best_match = None + best_score = 0.0 + matches = [] + + for target_name, target_info in pyocd_targets.items(): + target_lower = target_name.lower() + part_number = target_info.get("part_number", "").upper() + + # Calculate similarity scores + scores = [] + + # 1. Direct chip family match + if chip_family.lower() in target_lower: + family_score = 1.0 + else: + family_score = SequenceMatcher(None, chip_family.lower(), target_lower).ratio() + scores.append(("family", family_score * 0.5)) + + # 2. Part number match (if available) + if part_number and chip_family in part_number: + part_score = 1.0 + elif part_number: + part_score = SequenceMatcher(None, chip_family, part_number).ratio() + else: + part_score = 0.0 + scores.append(("part", part_score * 0.3)) + + # 3. Port/platform match + port_score = 0.0 + if port == "stm32" and target_lower.startswith("stm32"): + port_score = 0.2 + elif port == "rp2" and "rp20" in target_lower: + port_score = 0.2 + elif port == "samd" and "samd" in target_lower: + port_score = 0.2 + scores.append(("port", port_score)) + + # Calculate total score + total_score = sum(score for _, score in scores) + + if total_score > 0.6: # Minimum threshold for reliable matches + matches.append((target_name, total_score, scores)) + + if total_score > best_score: + best_score = total_score + best_match = target_name + + # Log matching results for debugging + if matches: + log.debug("Target matching results:") + for target, score, detailed_scores in sorted(matches, key=lambda x: x[1], reverse=True)[:5]: + detail_str = ", ".join(f"{name}:{score:.2f}" for name, score in detailed_scores) + log.debug(f" {target}: {score:.3f} ({detail_str})") + + if best_match: + log.info(f"Best target match: {best_match} (score: {best_score:.3f})") + else: + log.debug("No suitable target match found") + + return best_match + + +# ============================================================================= +# CMSIS Pack Management +# ============================================================================= + +def auto_install_pack_for_target(chip_family: str) -> bool: + """ + Automatically find and install CMSIS pack for a missing target. + + Args: + chip_family: The chip family to search for (e.g., "STM32H563", "STM32F429") + + Returns: + True if a pack was found and installed, False otherwise + """ + try: + log.info(f"Searching for CMSIS pack containing {chip_family}") + + # Basic validation: chip_family should be alphanumeric + if not chip_family or not re.match(r'^[A-Z0-9]+$', chip_family, re.IGNORECASE): + log.warning(f"Invalid chip family format: {chip_family}") + return False + + # Search for packs containing the target + result = _run_pyocd_command(['pack', 'find', chip_family], timeout=60) + + if result.returncode != 0: + log.debug(f"Pack search failed: {result.stderr}") + return False + + # Parse the output to find suitable packs + lines = result.stdout.strip().split('\n') + packs_to_install = set() + + for line in lines: + line = line.strip() + if not line or line.startswith('Part') or line.startswith('-'): + continue + + # Parse pack info line + parts = re.split(r'\s{2,}', line) + if len(parts) >= 4: + part_number = parts[0].strip() + vendor = parts[1].strip() + pack_name = parts[2].strip() + installed = parts[4].strip().lower() if len(parts) > 4 else 'false' + + # Check if this part matches our chip family and isn't installed + if (chip_family.upper() in part_number.upper() and + installed == 'false' and + pack_name not in packs_to_install): + packs_to_install.add(pack_name) + + if not packs_to_install: + log.debug(f"No uninstalled packs found for {chip_family}") + return False + + # Install the first suitable pack (usually the most relevant) + pack_to_install = list(packs_to_install)[0] + log.info(f"Installing CMSIS pack: {pack_to_install}") + + install_result = _run_pyocd_command(['pack', 'install', chip_family], timeout=300) + + if install_result.returncode == 0: + log.info(f"Successfully installed pack for {chip_family}") + + # Clear the target cache so new targets are discovered + if hasattr(get_pyocd_targets, 'cache_clear'): + get_pyocd_targets.cache_clear() + log.debug("Cleared target cache after pack installation") + + return True + else: + log.debug(f"Pack installation failed: {install_result.stderr}") + return False + + except subprocess.TimeoutExpired: + log.warning(f"Pack installation for {chip_family} timed out") + return False + except Exception as e: + log.debug(f"Auto pack installation failed: {e}") + return False + + +# ============================================================================= +# Main Target Detection API +# ============================================================================= + +# Simple cache to avoid redundant target detection for the same board +_target_cache = {} + +def detect_pyocd_target(mcu: MPRemoteBoard, auto_install_packs: bool = True) -> Optional[str]: + """ + Detect pyOCD target type for a connected MCU with automatic pack installation. + + Args: + mcu: Connected MPRemoteBoard instance + auto_install_packs: If True, automatically install missing CMSIS packs + + Returns: + pyOCD target type string or None if no match found + + Examples: + >>> mcu.description = "NUCLEO-WB55 with STM32WB55RGV6" + >>> detect_pyocd_target(mcu) + 'stm32wb55xg' + """ + # Create cache key from board_id and chip info + cache_key = f"{mcu.board_id}_{mcu.cpu}_{getattr(mcu, 'port', '')}" + + # Check cache first + if cache_key in _target_cache: + log.debug(f"Using cached target for {mcu.board_id}: {_target_cache[cache_key]}") + return _target_cache[cache_key] + + try: + # Parse MCU information for fuzzy matching + mcu_info = parse_mcu_info(mcu) + chip_family = mcu_info.get('chip_family', '') + + # Get available targets and try fuzzy matching + pyocd_targets = get_pyocd_targets() + target = fuzzy_match_target(mcu_info, pyocd_targets) + + if target: + log.debug(f"Target detection: {mcu.board_id} -> {target}") + _target_cache[cache_key] = target + return target + + # No target found - try automatic pack installation if enabled + if auto_install_packs and chip_family: + log.info(f"No pyOCD target found for {chip_family}, attempting automatic pack installation") + + pack_installed = auto_install_pack_for_target(chip_family) + if pack_installed: + # Retry target detection with updated pack targets + log.info("Retrying target detection after pack installation") + pyocd_targets = get_pyocd_targets() # Refresh target list + target = fuzzy_match_target(mcu_info, pyocd_targets) + + if target: + log.info(f"Target found after pack installation: {mcu.board_id} -> {target}") + _target_cache[cache_key] = target + return target + else: + log.warning(f"Still no target found for {chip_family} after pack installation") + else: + log.debug(f"Automatic pack installation failed for {chip_family}") + + log.debug(f"No target found for {mcu.board_id} ({chip_family})") + _target_cache[cache_key] = None + return None + + except Exception as e: + log.debug(f"Target detection failed: {e}") + _target_cache[cache_key] = None + return None + + +def is_pyocd_supported(mcu: MPRemoteBoard) -> bool: + """ + Check if MCU is supported by pyOCD. + + Args: + mcu: MPRemoteBoard instance + + Returns: + True if pyOCD can program this MCU + """ + return detect_pyocd_target(mcu, auto_install_packs=False) is not None + + +def get_unsupported_reason(mcu: MPRemoteBoard) -> str: + """ + Get actionable reason why MCU is not supported by pyOCD. + + Args: + mcu: MPRemoteBoard instance + + Returns: + Human-readable reason string with suggested actions + """ + mcu_info = parse_mcu_info(mcu) + chip_family = mcu_info.get("chip_family", "Unknown") + port = mcu_info.get("port", "unknown") + + if port in ["esp32", "esp8266"]: + return ( + f"ESP32/ESP8266 use Xtensa/RISC-V cores, not Cortex-M. " + f"Use 'mpflash flash --method esptool' instead of pyOCD." + ) + elif chip_family.startswith("STM32"): + return ( + f"STM32 variant {chip_family} not found in pyOCD targets. " + f"Try: 1) Enable pack installation with --auto-install-packs, " + f"2) Run 'pyocd pack find {chip_family}' to search for CMSIS packs, " + f"3) Check pyOCD version with 'pyocd --version'." + ) + elif chip_family.startswith("SAMD"): + return ( + f"SAMD variant {chip_family} not found in pyOCD targets. " + f"Try: 1) Enable pack installation with --auto-install-packs, " + f"2) Run 'pyocd pack find {chip_family}' to search for CMSIS packs, " + f"3) Check if Microchip CMSIS packs are available." + ) + elif chip_family.startswith("RP20"): + return ( + f"RP2040/RP2350 not supported. " + f"Try: 1) Update pyOCD to latest version, " + f"2) Use UF2 bootloader instead: 'mpflash flash --method uf2', " + f"3) Check if target is in bootloader mode." + ) + else: + return ( + f"MCU {chip_family} ({port}) not supported by pyOCD. " + f"Supported architectures: ARM Cortex-M (STM32, SAMD, LPC, etc.). " + f"Run 'pyocd list --targets' to see all supported targets." + ) + + +# ============================================================================= +# Cache Management +# ============================================================================= + +@dataclass(frozen=True) +class MCUIdentifier: + """Immutable MCU identifier for caching target lookups.""" + board_id: str + cpu: str + description: str + port: str + + @classmethod + def from_mcu(cls, mcu: MPRemoteBoard) -> 'MCUIdentifier': + """Create identifier from MPRemoteBoard instance.""" + return cls( + board_id=mcu.board_id or "unknown", + cpu=mcu.cpu or "unknown", + description=mcu.description or "unknown", + port=mcu.port or "unknown" + ) + + +@lru_cache(maxsize=128) +def cached_target_lookup(mcu_id: MCUIdentifier) -> Optional[str]: + """Cached version of target lookup for performance.""" + # Create minimal MCU-like object for parsing + class MCUProxy: + def __init__(self, mcu_id: MCUIdentifier): + self.board_id = mcu_id.board_id + self.cpu = mcu_id.cpu + self.description = mcu_id.description + self.port = mcu_id.port + + proxy = MCUProxy(mcu_id) + return detect_pyocd_target(proxy, auto_install_packs=False) \ No newline at end of file diff --git a/mpflash/flash/pyocd_flash.py b/mpflash/flash/pyocd_flash.py new file mode 100644 index 00000000..3c134d7a --- /dev/null +++ b/mpflash/flash/pyocd_flash.py @@ -0,0 +1,452 @@ +""" +PyOCD flash programming implementation for MPFlash. + +This module provides SWD/JTAG flash programming using pyOCD as an alternative +to serial bootloader methods. Includes probe discovery, target detection, +and flash programming operations. +""" + +from typing import List, Optional, Dict, Any +from pathlib import Path + +from mpflash.logger import log +from mpflash.errors import MPFlashError +from mpflash.mpremoteboard import MPRemoteBoard +from .debug_probe import DebugProbe +from .pyocd_core import ( + detect_pyocd_target, + is_pyocd_supported, + get_unsupported_reason, + is_pyocd_available +) + + +# Lazy import pyOCD to handle optional dependency +_pyocd_available = None +_pyocd_modules = {} + + +def _ensure_pyocd(): + """Ensure pyOCD modules are imported and available.""" + global _pyocd_available, _pyocd_modules + + if _pyocd_available is None: + try: + from pyocd.core.helpers import ConnectHelper + from pyocd.flash.file_programmer import FileProgrammer + from pyocd.core.exceptions import Error as PyOCDError + + _pyocd_modules.update({ + 'ConnectHelper': ConnectHelper, + 'FileProgrammer': FileProgrammer, + 'PyOCDError': PyOCDError + }) + _pyocd_available = True + log.debug("pyOCD modules loaded successfully") + + except ImportError as e: + _pyocd_available = False + log.debug(f"pyOCD not available: {e}") + + if not _pyocd_available: + raise MPFlashError("pyOCD is not installed. Install with: uv sync --extra pyocd") + + return _pyocd_modules + + +# ============================================================================= +# PyOCD Probe Implementation +# ============================================================================= + +class PyOCDProbe(DebugProbe): + """PyOCD debug probe implementation.""" + + def __init__(self, unique_id: str, description: str, pyocd_probe_obj=None): + super().__init__(unique_id, description) + self._pyocd_probe = pyocd_probe_obj + self._session = None + self._connected = False + + @classmethod + def is_implementation_available(cls) -> bool: + """Check if pyOCD implementation is available.""" + try: + _ensure_pyocd() + return True + except MPFlashError: + return False + + @classmethod + def discover(cls) -> List['PyOCDProbe']: + """Discover all connected pyOCD probes.""" + try: + modules = _ensure_pyocd() + ConnectHelper = modules['ConnectHelper'] + + pyocd_probes = ConnectHelper.get_all_connected_probes(blocking=False) + probes = [] + + for pyocd_probe in pyocd_probes: + probe = cls( + unique_id=pyocd_probe.unique_id, + description=pyocd_probe.description, + pyocd_probe_obj=pyocd_probe + ) + probes.append(probe) + + log.debug(f"Discovered {len(probes)} pyOCD probes") + return probes + + except Exception as e: + log.debug(f"Failed to discover pyOCD probes: {e}") + return [] + + def connect(self) -> bool: + """Connect to the pyOCD probe.""" + if self._connected: + return True + + try: + modules = _ensure_pyocd() + ConnectHelper = modules['ConnectHelper'] + + self._session = ConnectHelper.session_with_chosen_probe( + unique_id=self.unique_id, + options={"halt_on_connect": False, "auto_unlock": True} + ) + + if self._session: + self._connected = True + log.debug(f"Connected to pyOCD probe {self.unique_id}") + return True + else: + raise MPFlashError(f"Failed to create session with probe {self.unique_id}") + + except Exception as e: + self._connected = False + log.error(f"Failed to connect to pyOCD probe {self.unique_id}: {e}") + raise MPFlashError( + f"Cannot connect to probe {self.unique_id}. " + f"Ensure the target is powered and SWD/JTAG pins are connected. " + f"Error: {e}" + ) + + def disconnect(self) -> None: + """Disconnect from the pyOCD probe.""" + if self._session: + try: + self._session.close() + log.debug(f"Disconnected from pyOCD probe {self.unique_id}") + except Exception as e: + log.debug(f"Error during disconnect: {e}") + finally: + self._session = None + self._connected = False + + def program_flash(self, firmware_path: Path, target_type: str, **options) -> bool: + """ + Program flash memory using pyOCD. + + Args: + firmware_path: Path to firmware file (.bin, .hex, .elf) + target_type: pyOCD target type string + **options: Programming options (erase, frequency, etc.) + + Returns: + True if programming succeeded + + Raises: + MPFlashError: If programming fails + """ + if not firmware_path.exists(): + raise MPFlashError(f"Firmware file not found: {firmware_path}") + + # Connect if not already connected + if not self._connected: + self.connect() + + try: + modules = _ensure_pyocd() + FileProgrammer = modules['FileProgrammer'] + + # Extract programming options + erase_option = "chip" if options.get("erase", False) else "sector" + frequency = options.get("frequency", 4000000) + + # Create programmer with session + programmer = FileProgrammer(self._session) + + log.info(f"Programming {firmware_path.name} to {target_type} via {self.description}") + log.debug(f"Options: erase={erase_option}, frequency={frequency}Hz") + + # Program the firmware + programmer.program( + str(firmware_path), + file_format=None, # Auto-detect format + erase=erase_option, + reset=True, + verify=True + ) + + log.info(f"Successfully programmed {firmware_path.name}") + return True + + except Exception as e: + error_msg = f"Flash programming failed: {e}" + log.error(error_msg) + raise MPFlashError(error_msg) + + def detect_target(self) -> Optional[str]: + """Detect the target type connected to the probe.""" + try: + if not self._connected: + self.connect() + + if self._session and self._session.target: + target_name = self._session.target.part_number.lower() + log.info(f"Detected target: {target_name}") + return target_name + + except Exception as e: + log.debug(f"Target detection failed: {e}") + + return None + + def __enter__(self): + """Context manager entry.""" + self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit.""" + try: + self.disconnect() + except Exception: + pass # Don't raise in __exit__ + return False # Don't suppress original exception + + +# ============================================================================= +# Flash Programming Interface +# ============================================================================= + +class PyOCDFlash: + """High-level pyOCD flash programming interface.""" + + def __init__(self, mcu: MPRemoteBoard, probe_id: Optional[str] = None, auto_install_packs: bool = True): + """ + Initialize PyOCD flash programmer. + + Args: + mcu: MPRemoteBoard instance with board information + probe_id: Specific probe unique ID to use (optional) + auto_install_packs: Automatically install missing CMSIS packs + """ + self.mcu = mcu + self.probe_id = probe_id + + # Detect target type using core functionality + self.target_type = detect_pyocd_target(mcu, auto_install_packs=auto_install_packs) + + if not is_pyocd_available(): + raise MPFlashError("No debug probe support available. Install with: uv sync --extra pyocd") + + if not self.target_type: + reason = get_unsupported_reason(mcu) + raise MPFlashError(f"Board {mcu.board_id} ({mcu.cpu}) not supported by pyOCD: {reason}") + + def flash_firmware(self, fw_file: Path, erase: bool = False, **kwargs) -> bool: + """ + Flash firmware using pyOCD. + + Args: + fw_file: Path to firmware file (.bin, .hex, .elf) + erase: Whether to perform chip erase before programming + **kwargs: Additional options passed to pyOCD + + Returns: + True if flashing succeeded + + Raises: + MPFlashError: If flashing fails + """ + if not fw_file.exists(): + raise MPFlashError(f"Firmware file not found: {fw_file}") + + # Find appropriate probe + probe = find_pyocd_probe(self.probe_id) + if not probe: + if self.probe_id: + raise MPFlashError( + f"PyOCD probe '{self.probe_id}' not found. " + f"Use 'mpflash list-probes' to see available probes." + ) + else: + raise MPFlashError( + "No PyOCD debug probes available. " + "Connect a debug probe and ensure pyOCD can detect it." + ) + + log.info(f"Flashing {fw_file.name} to {self.mcu.board_id} via pyOCD SWD/JTAG") + log.debug(f"Target type: {self.target_type}, Probe: {probe.description}") + + # Build programming options + options = { + "erase": erase, + "frequency": kwargs.get("frequency", 4000000), + "pyocd_options": kwargs.get("pyocd_options", {}) + } + + # Program using the probe + return probe.program_flash(fw_file, self.target_type, **options) + + +# ============================================================================= +# Probe Discovery Functions +# ============================================================================= + +def list_pyocd_probes() -> List[PyOCDProbe]: + """ + Discover all connected pyOCD debug probes. + + Returns: + List of PyOCDProbe instances + """ + return PyOCDProbe.discover() + + +def find_pyocd_probe(probe_id: Optional[str] = None) -> Optional[PyOCDProbe]: + """ + Find a pyOCD debug probe by ID, or handle multi-probe selection. + + Args: + probe_id: Specific probe ID to find (supports partial matching) + + Returns: + PyOCDProbe instance or None if not found + + Raises: + MPFlashError: When multiple probes are available but no specific probe_id provided + """ + from loguru import logger as log + from mpflash.exceptions import MPFlashError + + probes = list_pyocd_probes() + + if not probes: + return None + + if not probe_id: + if len(probes) == 1: + return probes[0] + else: + # Multiple probes available - user must specify which one + log.error(f"Multiple debug probes detected ({len(probes)}). Please specify which probe to use with --probe :") + for i, probe in enumerate(probes, 1): + log.error(f" {i}. {probe.description} (ID: {probe.unique_id})") + raise MPFlashError( + f"Multiple debug probes found. Use --probe to specify which probe to use.\n" + f"Available probes: {', '.join(p.unique_id for p in probes)}" + ) + + # Exact match first + for probe in probes: + if probe.unique_id == probe_id: + return probe + + # Partial match + matches = [p for p in probes if probe_id in p.unique_id] + if len(matches) == 1: + return matches[0] + elif len(matches) > 1: + raise MPFlashError( + f"Ambiguous probe ID '{probe_id}' matches multiple probes: " + f"{[p.unique_id for p in matches]}. " + f"Use a more specific ID or the full unique ID." + ) + + return None + + +# ============================================================================= +# Main Public API +# ============================================================================= + +def flash_pyocd(mcu: MPRemoteBoard, fw_file: Path, erase: bool = False, + probe_id: Optional[str] = None, auto_install_packs: bool = True, **kwargs) -> bool: + """ + Flash MCU using pyOCD SWD/JTAG interface. + + Args: + mcu: MPRemoteBoard instance with board information + fw_file: Path to firmware file + erase: Whether to erase flash before programming + probe_id: Specific debug probe ID to use (optional) + auto_install_packs: Automatically install missing CMSIS packs + **kwargs: Additional options + + Returns: + True if flashing succeeded + + Raises: + MPFlashError: If flashing fails + """ + if not is_pyocd_supported(mcu): + reason = get_unsupported_reason(mcu) + raise MPFlashError(f"PyOCD flash not supported: {reason}") + + # Create flasher and program + flasher = PyOCDFlash(mcu, probe_id=probe_id, auto_install_packs=auto_install_packs) + return flasher.flash_firmware(fw_file, erase=erase, **kwargs) + + +def pyocd_info() -> Dict[str, Any]: + """ + Get information about pyOCD installation and available probes. + + Returns: + Dictionary with pyOCD status information + """ + info = { + "available": is_pyocd_available(), + "probes": [], + "version": None + } + + if info["available"]: + try: + import pyocd + info["version"] = pyocd.__version__ + except ImportError: + pass + + info["probes"] = [ + { + "unique_id": probe.unique_id, + "description": probe.description, + "vendor": getattr(probe, 'vendor_name', 'Unknown'), + "product": getattr(probe, 'product_name', 'Unknown'), + "target_type": probe.target_type + } + for probe in list_pyocd_probes() + ] + + return info + + +# ============================================================================= +# Compatibility Functions (for migration) +# ============================================================================= + +def find_probe_for_target(target_type: str, probe_id: Optional[str] = None) -> Optional[PyOCDProbe]: + """ + Find a suitable debug probe for the target type. + + Args: + target_type: pyOCD target type string + probe_id: Specific probe ID to find (optional) + + Returns: + PyOCDProbe instance or None if not found + """ + return find_pyocd_probe(probe_id) # target_type not needed for probe selection \ No newline at end of file diff --git a/mpflash/flash/worklist.py b/mpflash/flash/worklist.py index 647ca1bb..0626ff60 100644 --- a/mpflash/flash/worklist.py +++ b/mpflash/flash/worklist.py @@ -28,16 +28,15 @@ from typing import List, Optional, Tuple from loguru import logger as log -from serial.tools.list_ports_common import ListPortInfo -from typing_extensions import TypeAlias - -from mpflash.common import filtered_portinfos -from mpflash.db.models import Firmware +from mpflash.common import FlashMethod, filtered_portinfos from mpflash.downloaded import find_downloaded_firmware from mpflash.errors import MPFlashError from mpflash.list import show_mcus from mpflash.mpboard_id import find_known_board from mpflash.mpremoteboard import MPRemoteBoard +from typing_extensions import TypeAlias + +from mpflash.db.models import Firmware # ######################################################################################################### @@ -62,7 +61,7 @@ def board_id(self) -> str: @property def firmware_version(self) -> str: """Get the firmware version for this task.""" - return self.firmware.version if self.firmware else "unknown" + return str(self.firmware.version) if self.firmware else "unknown" @dataclass @@ -101,6 +100,8 @@ def for_filtered_boards( FlashTaskList: TypeAlias = List[FlashTask] +FlashItem: TypeAlias = Tuple[MPRemoteBoard, Optional[Firmware]] +WorkList: TypeAlias = List[FlashItem] # ######################################################################################################### @@ -131,10 +132,8 @@ def _find_firmware_for_board(board: MPRemoteBoard, version: str, custom: bool = def _create_manual_board(serial_port: str, board_id: str, version: str, custom: bool = False, port: str = "") -> FlashTask: """Create a FlashTask for manually specified board parameters.""" log.debug(f"Creating manual board task: {serial_port} {board_id} {version}") - board = MPRemoteBoard(serial_port) - # Look up board information, preferring the user-specified port try: info = find_known_board(board_id, port=port) board.port = info.port @@ -149,12 +148,153 @@ def _create_manual_board(serial_port: str, board_id: str, version: str, custom: return _create_flash_task(board, firmware) +def select_firmware_for_method(firmwares: List[Firmware], method: FlashMethod) -> Firmware: + """ + Select the best firmware file based on the flash method. + + Args: + firmwares: List of available firmware files for the board + method: Flash method to be used + + Returns: + Best firmware file for the specified method + """ + if not firmwares: + raise MPFlashError("No firmware files available") + + if len(firmwares) == 1: + return firmwares[0] + + # Define preferred file extensions for each method + method_preferences = { + FlashMethod.PYOCD: ['.hex', '.bin', '.elf'], + FlashMethod.DFU: ['.dfu'], + FlashMethod.UF2: ['.uf2'], + FlashMethod.ESPTOOL: ['.bin'], + FlashMethod.SERIAL: ['.dfu', '.hex', '.bin', '.uf2'], # Allow any for serial methods + FlashMethod.AUTO: ['.dfu', '.hex', '.bin', '.uf2', '.elf'] # Default order + } + + preferred_extensions = method_preferences.get(method, method_preferences[FlashMethod.AUTO]) + + # Try to find firmware with preferred extensions in order + for ext in preferred_extensions: + for fw in firmwares: + if fw.firmware_file.lower().endswith(ext): + log.debug(f"Selected {fw.firmware_file} for method {method.value} (preferred extension: {ext})") + return fw + + # If no preferred format found, use the last one (original behavior) + log.debug(f"No preferred format found for method {method.value}, using default: {firmwares[-1].firmware_file}") + return firmwares[-1] + + +# ######################################################################################################### + + +def auto_update_worklist( + conn_boards: List[MPRemoteBoard], + target_version: str, + method: FlashMethod = FlashMethod.AUTO, +) -> WorkList: + """Builds a list of boards to update based on the connected boards and the firmwares available locally in the firmware folder. + + Args: + conn_boards (List[MPRemoteBoard]): List of connected boards + target_version (str): Target firmware version + selector (Optional[Dict[str, str]], optional): Selector for filtering firmware. Defaults to None. + + Returns: + WorkList: List of boards and firmware information to update + """ + log.debug(f"auto_update_worklist: {len(conn_boards)} boards, target version: {target_version}") + wl: WorkList = [] + for mcu in conn_boards: + if mcu.family not in ("micropython", "unknown"): + log.warning(f"Skipping flashing {mcu.family} {mcu.port} {mcu.board} on {mcu.serialport} as it is not a MicroPython firmware") + continue + board_firmwares = find_downloaded_firmware( + board_id=f"{mcu.board}-{mcu.variant}" if mcu.variant else mcu.board, + version=target_version, + port=mcu.port, + ) + + if not board_firmwares: + log.warning(f"No {target_version} firmware found for {mcu.board} on {mcu.serialport}.") + wl.append((mcu, None)) + continue + + if len(board_firmwares) > 1: + log.warning(f"Multiple {target_version} firmwares found for {mcu.board} on {mcu.serialport}.") + + # Select firmware based on flash method + fw_info = select_firmware_for_method(board_firmwares, method) + log.info(f"Found {target_version} firmware {fw_info.firmware_file} for {mcu.board} on {mcu.serialport}.") + wl.append((mcu, fw_info)) + return wl + + +def manual_worklist( + serial: List[str], + *, + board_id: str, + version: str, + custom: bool = False, + method: FlashMethod = FlashMethod.AUTO, +) -> WorkList: + """Create a worklist for manually specified boards.""" + log.debug(f"manual_worklist: {len(serial)} serial ports, board_id: {board_id}, version: {version}") + wl: WorkList = [] + for comport in serial: + log.trace(f"Manual updating {comport} to {board_id} {version}") + wl.append(manual_board(comport, board_id=board_id, version=version, custom=custom, method=method)) + return wl + + +def manual_board( + serial: str, + *, + board_id: str, + version: str, + custom: bool = False, + method: FlashMethod = FlashMethod.AUTO, +) -> FlashItem: + """Create a Flash work item for a single board specified manually.""" + + board = MPRemoteBoard(serial) + + # Look up board information, preferring the user-specified port + try: + info = find_known_board(board_id) + board.port = info.port + board.cpu = info.mcu # Need CPU type for esptool + except (LookupError, MPFlashError) as e: + log.error(f"Board {board_id} not found in board database") + log.exception(e) + return _create_flash_task(board, None) + + board.board = board_id + firmware = _find_firmware_for_board(board, version, custom) + return _create_flash_task(board, firmware) + + def _filter_connected_comports( all_boards: List[MPRemoteBoard], include: List[str], ignore: List[str], ) -> List[MPRemoteBoard]: """Filter connected boards based on include/ignore patterns.""" + + log.debug(f"_filter_connected_comports: {len(all_boards)} boards, include: {include}, ignore: {ignore}") + return filter_boards(all_boards, include=include, ignore=ignore) or [] + + +def filter_boards( + all_boards: List[MPRemoteBoard], + *, + include: List[str], + ignore: List[str], +): try: allowed_ports = [ p.device diff --git a/pyproject.toml b/pyproject.toml index c645fd36..f7e6c0c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,9 +74,12 @@ test = [ "fasteners>=0.19", "mock>=4.0.3,<6.0.0", ] -# perf = [ -# "scalene>=1.5.51", -# ] +perf = [ + "scalene>=1.5.51", +] +pyocd = [ + "pyocd>=0.36.0", +] [project.scripts] mpflash = "mpflash.cli_main:mpflash" @@ -125,7 +128,7 @@ testpaths = ["tests"] norecursedirs = [".*", ".*/*"] junit_family = "xunit1" -addopts = "--capture=no --cov-branch --cov-report=xml -m 'not slow'" +addopts = "--capture=no --cov-branch --cov-report=xml -m 'not slow and not hil'" # --capture=no markers = [ @@ -136,6 +139,7 @@ markers = [ "legacy: reeally old tests that need to be updated or removed", "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "hil: Hardware-In-the-Loop tests requiring physical hardware (deselected by default)", "integration: Integration tests (slower)", "mocked: to replace/compensate for most of the slow and git tests", # diff --git a/tests/cli/test_cli_flash.py b/tests/cli/test_cli_flash.py index 938cb7d4..c1504731 100644 --- a/tests/cli/test_cli_flash.py +++ b/tests/cli/test_cli_flash.py @@ -3,12 +3,12 @@ import pytest from click.testing import CliRunner +from mpflash.common import DownloadParams +from mpflash.mpremoteboard import MPRemoteBoard from pytest_mock import MockerFixture # # module under test : from mpflash import cli_main -from mpflash.common import DownloadParams -from mpflash.mpremoteboard import MPRemoteBoard # mark all tests pytestmark = pytest.mark.mpflash @@ -112,7 +112,7 @@ def test_mpflash_connected_comports( return_value=(ports, boards, variants, [MPRemoteBoard(p) for p in serialports]), autospec=True, ) - m_flash_tasks = mocker.patch("mpflash.cli_flash.flash_tasks", return_value=None, autospec=True) # type: ignore + m_flash_tasks = mocker.patch("mpflash.cli_flash.flash_tasks", return_value=fakes if fakes else [fakeboard()], autospec=True) # type: ignore m_ask_missing_params = mocker.patch( "mpflash.cli_flash.ask_missing_params", Mock(side_effect=fake_ask_missing_params), diff --git a/tests/conftest.py b/tests/conftest.py index 8098a804..2f2049cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ import sqlite3 import sys from pathlib import Path +from unittest.mock import Mock, patch import peewee import pytest @@ -12,6 +13,14 @@ # --------------------------------------------------------------------------- +# Import test fixtures for pyOCD testing +try: + from tests.fixtures.mock_pyocd_data import ALL_PYOCD_TARGETS, MOCK_MCUS, MOCK_PROBES +except ImportError: + # Fallback if fixtures not available + MOCK_MCUS = {} + MOCK_PROBES = [] + ALL_PYOCD_TARGETS = {} @pytest.fixture @@ -110,3 +119,105 @@ def db_mem(): def session_mem(db_mem): """Yield the empty in-memory database for unit tests.""" yield db_mem + + +# def session_mem(connection_mem): +# transaction = connection_mem.begin() +# testSession = sessionmaker(bind=connection_fx) # type: ignore +# yield testSession +# transaction.rollback() + + +############################################################# +# Fixtures for pyOCD testing +############################################################# + + +@pytest.fixture +def mock_mcu(): + """Provide a mock MCU for testing.""" + if "stm32wb55" in MOCK_MCUS: + return MOCK_MCUS["stm32wb55"] + + # Fallback mock + class MockMCU: + def __init__(self): + self.board_id = "TEST_BOARD" + self.cpu = "STM32WB55RGV6" + self.description = "Test MCU with STM32WB55RGV6" + self.port = "stm32" + + return MockMCU() + + +@pytest.fixture +def mock_esp32_mcu(): + """Provide an ESP32 mock MCU for testing unsupported scenarios.""" + if "esp32" in MOCK_MCUS: + return MOCK_MCUS["esp32"] + + # Fallback mock + class MockESP32: + def __init__(self): + self.board_id = "ESP32_DEV" + self.cpu = "ESP32" + self.description = "ESP32-DevKitC with ESP32-WROOM-32" + self.port = "esp32" + + return MockESP32() + + +@pytest.fixture +def mock_pyocd_targets(): + """Provide mock pyOCD target data.""" + return ALL_PYOCD_TARGETS + + +@pytest.fixture +def mock_probes(): + """Provide mock probe data.""" + return MOCK_PROBES + + +@pytest.fixture +def temp_firmware_file(tmp_path): + """Create a temporary firmware file for testing.""" + firmware_file = tmp_path / "test_firmware.bin" + firmware_file.write_bytes(b"fake firmware content") + return firmware_file + + +@pytest.fixture(autouse=True) +def reset_probe_registry(): + """Reset the probe registry before each test.""" + try: + from mpflash.flash.debug_probe import _probe_implementations + + original_implementations = _probe_implementations.copy() + _probe_implementations.clear() + + yield + + # Restore original implementations + _probe_implementations.clear() + _probe_implementations.update(original_implementations) + except ImportError: + # If debug_probe module not available, just yield + yield + + +@pytest.fixture +def mock_subprocess(): + """Mock subprocess.run for testing command execution.""" + with patch("subprocess.run") as mock_run: + yield mock_run + + +# Test markers for categorizing tests +def pytest_configure(config): + """Configure pytest markers.""" + config.addinivalue_line("markers", "unit: mark test as a unit test") + config.addinivalue_line("markers", "integration: mark test as an integration test") + config.addinivalue_line("markers", "cli: mark test as a CLI test") + config.addinivalue_line("markers", "pyocd: mark test as a pyOCD-related test") + config.addinivalue_line("markers", "slow: mark test as slow running") diff --git a/tests/fixtures/mock_pyocd_data.py b/tests/fixtures/mock_pyocd_data.py new file mode 100644 index 00000000..c9870c00 --- /dev/null +++ b/tests/fixtures/mock_pyocd_data.py @@ -0,0 +1,185 @@ +""" +Mock data for pyOCD testing. + +Contains sample target data, MCU descriptions, and command outputs +that mimic real pyOCD behavior for testing without hardware dependencies. +""" + +from typing import Dict, List, Any + +# Sample MCU descriptions from sys.implementation._machine +SAMPLE_MCU_DESCRIPTIONS = { + "stm32wb55": "NUCLEO-WB55 with STM32WB55RGV6", + "stm32f429": "NUCLEO-F429ZI with STM32F429ZI", + "stm32h563": "NUCLEO-H563ZI with STM32H563ZI", + "stm32f412": "NUCLEO-F412ZG with STM32F412ZG", + "rp2040": "Raspberry Pi Pico with RP2040", + "samd51": "Adafruit Metro M4 with SAMD51J19A", + "esp32": "ESP32-DevKitC with ESP32-WROOM-32", + "malformed": "Invalid Format", + "empty": "", +} + +# Sample pyOCD target data (built-in targets) +BUILTIN_PYOCD_TARGETS = { + "stm32f429xi": { + "vendor": "STMicroelectronics", + "part_number": "STM32F429XI", + "source": "builtin" + }, + "stm32f412xg": { + "vendor": "STMicroelectronics", + "part_number": "STM32F412XG", + "source": "builtin" + }, + "stm32wb55xg": { + "vendor": "STMicroelectronics", + "part_number": "STM32WB55XG", + "source": "builtin" + }, + "rp2040": { + "vendor": "Raspberry Pi", + "part_number": "RP2040", + "source": "builtin" + }, + "samd51j19a": { + "vendor": "Microchip", + "part_number": "SAMD51J19A", + "source": "builtin" + } +} + +# Sample pack targets (from CMSIS packs) +PACK_PYOCD_TARGETS = { + "stm32h563zitx": { + "vendor": "STMicroelectronics", + "part_number": "STM32H563ZI", + "source": "pack" + }, + "stm32h503cbtx": { + "vendor": "STMicroelectronics", + "part_number": "STM32H503CB", + "source": "pack" + } +} + +# Combined target data +ALL_PYOCD_TARGETS = {**BUILTIN_PYOCD_TARGETS, **PACK_PYOCD_TARGETS} + +# Mock subprocess outputs +MOCK_SUBPROCESS_OUTPUTS = { + "pyocd_list_targets": """ +Name Vendor Part Number Architecture Source +---------------------------------------------------------------------- +rp2040 Raspberry Pi RP2040 ARMv6-M builtin +stm32f412xg STMicroelectronics STM32F412XG ARMv7E-M builtin +stm32f429xi STMicroelectronics STM32F429XI ARMv7E-M builtin +stm32wb55xg STMicroelectronics STM32WB55XG ARMv7E-M builtin +stm32h563zitx STMicroelectronics STM32H563ZI ARMv8-M pack +stm32h503cbtx STMicroelectronics STM32H503CB ARMv8-M pack +samd51j19a Microchip SAMD51J19A ARMv7E-M builtin +""", + + "pyocd_pack_find_stm32h563": """ +Part Number Vendor Pack Installed +------------------------------------------------------------------------------- +STM32H563ZI STMicroelectronics Keil.STM32H5xx_DFP false +STM32H563VE STMicroelectronics Keil.STM32H5xx_DFP false +STM32H563ZGT6 STMicroelectronics Keil.STM32H5xx_DFP false +""", + + "pyocd_pack_find_stm32h503": """ +Part Number Vendor Pack Installed +------------------------------------------------------------------------------- +STM32H503CB STMicroelectronics Keil.STM32H5xx_DFP false +STM32H503RB STMicroelectronics Keil.STM32H5xx_DFP false +""", + + "pyocd_pack_install_success": "Successfully installed pack Keil.STM32H5xx_DFP\n", + + "pyocd_pack_install_failure": "Error: Failed to download pack from repository\n", + + "pyocd_list_probes": """ +# Probe/Board Unique ID Target Type +---------------------------------------------------------------------- +0 ST-Link v3 066CFF505750827567154312 stm32h563zitx +1 CMSIS-DAP Probe 0D28C20417A04C1D +""", + + "empty_output": "", + "command_not_found": "pyocd: command not found\n", +} + +# Mock probe data +MOCK_PROBES = [ + { + "unique_id": "066CFF505750827567154312", + "description": "ST-Link v3", + "vendor_name": "STMicroelectronics", + "product_name": "ST-LINK/V3", + "target_type": "stm32h563zitx" + }, + { + "unique_id": "0D28C20417A04C1D", + "description": "CMSIS-DAP Probe", + "vendor_name": "ARM", + "product_name": "DAPLink CMSIS-DAP", + "target_type": None + } +] + +# Expected fuzzy matching results +EXPECTED_FUZZY_MATCHES = { + "STM32WB55": "stm32wb55xg", + "STM32F429": "stm32f429xi", + "STM32F412": "stm32f412xg", + "STM32H563": "stm32h563zitx", # From pack + "RP2040": "rp2040", + "SAMD51": "samd51j19a", + "ESP32": None, # Not supported by pyOCD + "UNKNOWN": None, +} + +# Test MCU objects (mock MPRemoteBoard) +class MockMCU: + """Mock MPRemoteBoard for testing.""" + + def __init__(self, board_id: str, cpu: str, description: str, port: str = "unknown"): + self.board_id = board_id + self.cpu = cpu + self.description = description + self.port = port + +MOCK_MCUS = { + "stm32wb55": MockMCU("NUCLEO_WB55", "STM32WB55RGV6", "NUCLEO-WB55 with STM32WB55RGV6", "stm32"), + "stm32f429": MockMCU("NUCLEO_F429ZI", "STM32F429ZI", "NUCLEO-F429ZI with STM32F429ZI", "stm32"), + "stm32h563": MockMCU("NUCLEO_H563ZI", "STM32H563ZI", "NUCLEO-H563ZI with STM32H563ZI", "stm32"), + "rp2040": MockMCU("RPI_PICO", "RP2040", "Raspberry Pi Pico with RP2040", "rp2"), + "samd51": MockMCU("METRO_M4", "SAMD51J19A", "Adafruit Metro M4 with SAMD51J19A", "samd"), + "esp32": MockMCU("ESP32_DEV", "ESP32", "ESP32-DevKitC with ESP32-WROOM-32", "esp32"), + "malformed": MockMCU("UNKNOWN", "UNKNOWN", "Invalid Format", "unknown"), +} + +# Error scenarios for testing +ERROR_SCENARIOS = { + "pyocd_not_installed": { + "exception": ImportError("No module named 'pyocd'"), + "expected_error": "pyOCD is not installed" + }, + "no_probes_found": { + "probes": [], + "expected_error": "No debug probes available" + }, + "probe_connection_failed": { + "exception": Exception("Failed to connect to target"), + "expected_error": "Cannot connect to probe" + }, + "invalid_firmware_file": { + "file_path": "/nonexistent/firmware.bin", + "expected_error": "Firmware file not found" + }, + "pack_install_timeout": { + "exception": TimeoutError("Pack installation timed out"), + "expected_error": "timed out" + } +} \ No newline at end of file diff --git a/tests/flash/test_flash_tasks.py b/tests/flash/test_flash_tasks.py index 605d9842..2b90e561 100644 --- a/tests/flash/test_flash_tasks.py +++ b/tests/flash/test_flash_tasks.py @@ -3,11 +3,11 @@ import pytest -from mpflash.common import BootloaderMethod +from mpflash.common import BootloaderMethod, FlashMethod from mpflash.db.models import Firmware from mpflash.download.jid import ensure_firmware_downloaded_tasks from mpflash.errors import MPFlashError -from mpflash.flash import flash_tasks +from mpflash.flash import _auto_select_flash_method, _select_flash_method, _select_serial_method, flash_mcu, flash_tasks from mpflash.flash.worklist import FlashTask, FlashTaskList from mpflash.mpremoteboard import MPRemoteBoard @@ -198,6 +198,273 @@ def test_flash_tasks_custom_firmware(mock_config, mock_flash_mcu): assert board.toml["mpflash"]["custom_id"] == "custom_123" +# --------------------------------------------------------------------------- +# Tests for flash_mcu (covers lines 130-154 in flash/__init__.py) +# --------------------------------------------------------------------------- + + +def _make_mcu(port: str = "esp32", board: str = "ESP32_GENERIC", serialport: str = "COM1"): + mcu = MagicMock() + mcu.port = port + mcu.board = board + mcu.serialport = serialport + mcu.board_id = board + mcu.cpu = "ESP32" + return mcu + + +@patch("mpflash.flash._select_flash_method") +@patch("mpflash.flash.flash_esp") +@patch("mpflash.flash.enter_bootloader", create=True) +def test_flash_mcu_esptool(mock_bootloader, mock_flash_esp, mock_select_method): + """Test flash_mcu routes to flash_esp for ESPTOOL method.""" + mock_select_method.return_value = FlashMethod.ESPTOOL + expected = MagicMock() + mock_flash_esp.return_value = expected + + mcu = _make_mcu(port="esp32") + fw_file = Path("fw.bin") + + result = flash_mcu(mcu, fw_file=fw_file, erase=False, bootloader=BootloaderMethod.AUTO) + + assert result is expected + mock_flash_esp.assert_called_once_with(mcu, fw_file=fw_file, erase=False) + + +@patch("mpflash.flash._select_flash_method") +@patch("mpflash.flash.flash_uf2") +@patch("mpflash.flash.enter_bootloader", create=True) +def test_flash_mcu_uf2(mock_enter_bl, mock_flash_uf2, mock_select_method): + """Test flash_mcu routes to flash_uf2 for UF2 method.""" + mock_select_method.return_value = FlashMethod.UF2 + mock_enter_bl.return_value = True + expected = MagicMock() + mock_flash_uf2.return_value = expected + + mcu = _make_mcu(port="rp2") + fw_file = Path("fw.uf2") + + with patch("mpflash.bootloader.activate.enter_bootloader", return_value=True): + result = flash_mcu(mcu, fw_file=fw_file, erase=False, bootloader=BootloaderMethod.AUTO) + + assert result is expected + + +@patch("mpflash.flash._select_flash_method") +@patch("mpflash.flash.flash_stm32") +@patch("mpflash.flash.enter_bootloader", create=True) +def test_flash_mcu_dfu(mock_enter_bl, mock_flash_stm32, mock_select_method): + """Test flash_mcu routes to flash_stm32 for DFU method.""" + mock_select_method.return_value = FlashMethod.DFU + mock_enter_bl.return_value = True + expected = MagicMock() + mock_flash_stm32.return_value = expected + + mcu = _make_mcu(port="stm32") + fw_file = Path("fw.dfu") + + with patch("mpflash.bootloader.activate.enter_bootloader", return_value=True): + result = flash_mcu(mcu, fw_file=fw_file, erase=False, bootloader=BootloaderMethod.AUTO) + + assert result is expected + + +@patch("mpflash.flash._select_flash_method") +@patch("mpflash.flash.is_debug_programming_available") +@patch("mpflash.flash.flash_pyocd") +def test_flash_mcu_pyocd(mock_flash_pyocd, mock_is_available, mock_select_method): + """Test flash_mcu routes to flash_pyocd for PYOCD method.""" + mock_select_method.return_value = FlashMethod.PYOCD + mock_is_available.return_value = True + expected = MagicMock() + mock_flash_pyocd.return_value = expected + + mcu = _make_mcu(port="stm32") + fw_file = Path("fw.hex") + + result = flash_mcu(mcu, fw_file=fw_file, erase=False, bootloader=BootloaderMethod.AUTO) + + assert result is expected + mock_flash_pyocd.assert_called_once() + + +@patch("mpflash.flash._select_flash_method") +@patch("mpflash.flash.is_debug_programming_available") +def test_flash_mcu_pyocd_not_available(mock_is_available, mock_select_method): + """Test flash_mcu raises MPFlashError when pyocd not available.""" + mock_select_method.return_value = FlashMethod.PYOCD + mock_is_available.return_value = False + + mcu = _make_mcu(port="stm32") + fw_file = Path("fw.hex") + + with pytest.raises(MPFlashError): + flash_mcu(mcu, fw_file=fw_file, erase=False, bootloader=BootloaderMethod.AUTO) + + +@patch("mpflash.flash._select_flash_method") +def test_flash_mcu_unsupported_method(mock_select_method): + """Test flash_mcu raises MPFlashError for unsupported method.""" + # Return a method value not in the if/elif chain + mock_select_method.return_value = FlashMethod.SERIAL + + mcu = _make_mcu() + fw_file = Path("fw.bin") + + with pytest.raises(MPFlashError): + flash_mcu(mcu, fw_file=fw_file, erase=False, bootloader=BootloaderMethod.AUTO) + + +@patch("mpflash.flash._select_flash_method") +@patch("mpflash.flash.flash_esp") +def test_flash_mcu_wraps_exception(mock_flash_esp, mock_select_method): + """Test flash_mcu wraps unexpected exceptions in MPFlashError.""" + mock_select_method.return_value = FlashMethod.ESPTOOL + mock_flash_esp.side_effect = RuntimeError("unexpected error") + + mcu = _make_mcu() + fw_file = Path("fw.bin") + + with pytest.raises(MPFlashError, match="Failed to flash"): + flash_mcu(mcu, fw_file=fw_file, erase=False, bootloader=BootloaderMethod.AUTO) + + +# --------------------------------------------------------------------------- +# Tests for _select_flash_method (covers lines 180-193 in flash/__init__.py) +# --------------------------------------------------------------------------- + + +@patch("mpflash.flash.is_debug_programming_available") +@patch("mpflash.flash.is_pyocd_supported_from_mcu") +def test_select_flash_method_pyocd_valid(mock_is_supported, mock_is_available): + """Test _select_flash_method returns PYOCD when explicitly requested and supported.""" + mock_is_available.return_value = True + mock_is_supported.return_value = True + + mcu = _make_mcu(port="stm32") + result = _select_flash_method(mcu, FlashMethod.PYOCD, Path("fw.hex")) + + assert result == FlashMethod.PYOCD + + +@patch("mpflash.flash.is_debug_programming_available") +def test_select_flash_method_pyocd_not_available(mock_is_available): + """Test _select_flash_method raises when pyocd not available (lines 181-182).""" + mock_is_available.return_value = False + + mcu = _make_mcu(port="stm32") + with pytest.raises(MPFlashError, match="Debug probe"): + _select_flash_method(mcu, FlashMethod.PYOCD, Path("fw.hex")) + + +@patch("mpflash.flash.is_debug_programming_available") +@patch("mpflash.flash.is_pyocd_supported_from_mcu") +def test_select_flash_method_pyocd_unsupported_target(mock_is_supported, mock_is_available): + """Test _select_flash_method raises when target not supported (line 184-185).""" + mock_is_available.return_value = True + mock_is_supported.return_value = False + + mcu = _make_mcu(port="stm32") + mcu.cpu = "UNKNOWN_CPU" + with pytest.raises(MPFlashError, match="pyOCD does not support"): + _select_flash_method(mcu, FlashMethod.PYOCD, Path("fw.hex")) + + +def test_select_flash_method_uf2_valid(): + """Test _select_flash_method returns UF2 for rp2 with .uf2 file.""" + mcu = _make_mcu(port="rp2") + result = _select_flash_method(mcu, FlashMethod.UF2, Path("fw.uf2")) + assert result == FlashMethod.UF2 + + +def test_select_flash_method_uf2_invalid_port(): + """Test _select_flash_method raises for UF2 with wrong port (line 187).""" + mcu = _make_mcu(port="esp32") + with pytest.raises(MPFlashError, match="UF2 method not suitable"): + _select_flash_method(mcu, FlashMethod.UF2, Path("fw.uf2")) + + +def test_select_flash_method_dfu_valid(): + """Test _select_flash_method returns DFU for stm32 (line 190).""" + mcu = _make_mcu(port="stm32") + result = _select_flash_method(mcu, FlashMethod.DFU, Path("fw.dfu")) + assert result == FlashMethod.DFU + + +def test_select_flash_method_dfu_invalid_port(): + """Test _select_flash_method raises DFU with non-stm32 (line 191-192).""" + mcu = _make_mcu(port="esp32") + with pytest.raises(MPFlashError, match="DFU method not suitable"): + _select_flash_method(mcu, FlashMethod.DFU, Path("fw.dfu")) + + +def test_select_flash_method_esptool_valid(): + """Test _select_flash_method returns ESPTOOL for esp32 (line 194).""" + mcu = _make_mcu(port="esp32") + result = _select_flash_method(mcu, FlashMethod.ESPTOOL, Path("fw.bin")) + assert result == FlashMethod.ESPTOOL + + +def test_select_flash_method_esptool_invalid_port(): + """Test _select_flash_method raises ESPTOOL with non-esp port (lines 195-196).""" + mcu = _make_mcu(port="stm32") + with pytest.raises(MPFlashError, match="esptool method not suitable"): + _select_flash_method(mcu, FlashMethod.ESPTOOL, Path("fw.bin")) + + +@patch("mpflash.flash._select_serial_method") +def test_select_flash_method_serial_delegates(mock_serial): + """Test _select_flash_method with SERIAL delegates to _select_serial_method (line 198).""" + mock_serial.return_value = FlashMethod.ESPTOOL + mcu = _make_mcu(port="esp32") + result = _select_flash_method(mcu, FlashMethod.SERIAL, Path("fw.bin")) + assert result == FlashMethod.ESPTOOL + mock_serial.assert_called_once_with(mcu, Path("fw.bin")) + + +def test_select_flash_method_auto_delegates_to_auto(): + """Test _select_flash_method AUTO mode returns auto selection.""" + mcu = _make_mcu(port="esp32") + result = _select_flash_method(mcu, FlashMethod.AUTO, Path("fw.bin")) + assert result == FlashMethod.ESPTOOL + + +# --------------------------------------------------------------------------- +# Tests for _auto_select_flash_method and _select_serial_method +# --------------------------------------------------------------------------- + + +def test_auto_select_uf2_for_rp2(): + mcu = _make_mcu(port="rp2") + result = _auto_select_flash_method(mcu, Path("fw.uf2")) + assert result == FlashMethod.UF2 + + +def test_auto_select_dfu_for_stm32(): + mcu = _make_mcu(port="stm32") + result = _auto_select_flash_method(mcu, Path("fw.dfu")) + assert result == FlashMethod.DFU + + +def test_auto_select_esptool_for_esp32(): + mcu = _make_mcu(port="esp32") + result = _auto_select_flash_method(mcu, Path("fw.bin")) + assert result == FlashMethod.ESPTOOL + + +def test_auto_select_esptool_for_esp8266(): + mcu = _make_mcu(port="esp8266") + result = _auto_select_flash_method(mcu, Path("fw.bin")) + assert result == FlashMethod.ESPTOOL + + +def test_select_serial_method_unknown_raises(): + """Test _select_serial_method raises for unknown platform.""" + mcu = _make_mcu(port="unknown_port") + with pytest.raises(MPFlashError, match="Don't know how to flash"): + _select_serial_method(mcu, Path("fw.bin")) + + @patch("mpflash.download.jid.find_downloaded_firmware") @patch("mpflash.download.jid.download") @patch("mpflash.download.jid.alternate_board_names") diff --git a/tests/flash/test_worklist_refactored.py b/tests/flash/test_worklist_refactored.py index 26da0316..a8ce31a0 100644 --- a/tests/flash/test_worklist_refactored.py +++ b/tests/flash/test_worklist_refactored.py @@ -4,18 +4,24 @@ import pytest +from mpflash.common import FlashMethod from mpflash.db.models import Firmware from mpflash.errors import MPFlashError from mpflash.flash.worklist import ( FlashTask, WorklistConfig, _create_flash_task, + _create_manual_board, _find_firmware_for_board, + auto_update_worklist, create_auto_worklist, create_filtered_worklist, create_manual_worklist, create_single_board_worklist, create_worklist, + manual_board, + manual_worklist, + select_firmware_for_method, ) from mpflash.mpremoteboard import MPRemoteBoard @@ -572,4 +578,183 @@ def test_create_single_board_worklist(self, mock_create_auto): assert called_boards[0].serialport == "COM1" +class TestSelectFirmwareForMethod: + """Test select_firmware_for_method function (lines 162-189).""" + + def test_empty_firmware_list_raises(self): + """Test that empty firmware list raises MPFlashError.""" + with pytest.raises(MPFlashError, match="No firmware files available"): + select_firmware_for_method([], FlashMethod.AUTO) + + def test_single_firmware_returned_directly(self): + """Test that a single firmware is returned without selection logic.""" + fw = Firmware(board_id="ESP32_GENERIC", version="1.22.0", port="esp32", firmware_file="fw.bin") + result = select_firmware_for_method([fw], FlashMethod.AUTO) + assert result is fw + + def test_selects_preferred_extension_for_pyocd(self): + """Test PYOCD prefers .hex over .bin over .elf.""" + fw_bin = Firmware(board_id="B", version="1.22.0", port="stm32", firmware_file="fw.bin") + fw_hex = Firmware(board_id="B", version="1.22.0", port="stm32", firmware_file="fw.hex") + result = select_firmware_for_method([fw_bin, fw_hex], FlashMethod.PYOCD) + assert result is fw_hex + + def test_selects_preferred_extension_for_dfu(self): + """Test DFU prefers .dfu.""" + fw_bin = Firmware(board_id="B", version="1.22.0", port="stm32", firmware_file="fw.bin") + fw_dfu = Firmware(board_id="B", version="1.22.0", port="stm32", firmware_file="fw.dfu") + result = select_firmware_for_method([fw_bin, fw_dfu], FlashMethod.DFU) + assert result is fw_dfu + + def test_selects_preferred_extension_for_uf2(self): + """Test UF2 prefers .uf2.""" + fw_bin = Firmware(board_id="B", version="1.22.0", port="rp2", firmware_file="fw.bin") + fw_uf2 = Firmware(board_id="B", version="1.22.0", port="rp2", firmware_file="fw.uf2") + result = select_firmware_for_method([fw_bin, fw_uf2], FlashMethod.UF2) + assert result is fw_uf2 + + def test_selects_preferred_extension_for_esptool(self): + """Test ESPTOOL prefers .bin.""" + fw_uf2 = Firmware(board_id="B", version="1.22.0", port="esp32", firmware_file="fw.uf2") + fw_bin = Firmware(board_id="B", version="1.22.0", port="esp32", firmware_file="fw.bin") + result = select_firmware_for_method([fw_uf2, fw_bin], FlashMethod.ESPTOOL) + assert result is fw_bin + + def test_fallback_to_last_when_no_preferred(self): + """Test fallback to last firmware when no preferred extension matches.""" + fw1 = Firmware(board_id="B", version="1.22.0", port="esp32", firmware_file="fw.xyz") + fw2 = Firmware(board_id="B", version="1.22.0", port="esp32", firmware_file="fw2.xyz") + result = select_firmware_for_method([fw1, fw2], FlashMethod.PYOCD) + assert result is fw2 + + +class TestAutoUpdateWorklist: + """Test auto_update_worklist function (lines 210-234).""" + + @patch("mpflash.flash.worklist.find_downloaded_firmware") + @patch("mpflash.flash.worklist.log") + def test_skips_non_micropython(self, mock_log, mock_find_fw): + """Test non-MicroPython boards are skipped.""" + board = MPRemoteBoard("COM1") + board.family = "arduino" + board.port = "avr" + board.board = "UNO" + + result = auto_update_worklist([board], "1.22.0") + + assert result == [] + mock_find_fw.assert_not_called() + + @patch("mpflash.flash.worklist.find_downloaded_firmware") + @patch("mpflash.flash.worklist.log") + def test_no_firmware_found_appends_none(self, mock_log, mock_find_fw): + """Test that boards with no firmware are added as (board, None).""" + mock_find_fw.return_value = [] + board = MPRemoteBoard("COM1") + board.family = "micropython" + board.board = "ESP32_GENERIC" + board.port = "esp32" + + result = auto_update_worklist([board], "1.22.0") + + assert len(result) == 1 + assert result[0] == (board, None) + + @patch("mpflash.flash.worklist.find_downloaded_firmware") + @patch("mpflash.flash.worklist.log") + def test_firmware_found_selects_best(self, mock_log, mock_find_fw): + """Test that when firmware is found the best is selected.""" + fw = Firmware(board_id="ESP32_GENERIC", version="1.22.0", port="esp32", firmware_file="fw.bin") + mock_find_fw.return_value = [fw] + board = MPRemoteBoard("COM1") + board.family = "micropython" + board.board = "ESP32_GENERIC" + board.port = "esp32" + + result = auto_update_worklist([board], "1.22.0") + + assert len(result) == 1 + assert result[0] == (board, fw) + + @patch("mpflash.flash.worklist.find_downloaded_firmware") + @patch("mpflash.flash.worklist.log") + def test_multiple_firmwares_warns(self, mock_log, mock_find_fw): + """Test warning when multiple firmwares found.""" + fw1 = Firmware(board_id="ESP32_GENERIC", version="1.22.0", port="esp32", firmware_file="fw1.bin") + fw2 = Firmware(board_id="ESP32_GENERIC", version="1.22.0", port="esp32", firmware_file="fw2.bin") + mock_find_fw.return_value = [fw1, fw2] + board = MPRemoteBoard("COM1") + board.family = "micropython" + board.board = "ESP32_GENERIC" + board.port = "esp32" + + result = auto_update_worklist([board], "1.22.0") + + assert len(result) == 1 + mock_log.warning.assert_called() # Warning about multiple firmwares + + +class TestManualWorklist: + """Test manual_worklist function (lines 246-251).""" + + @patch("mpflash.flash.worklist.manual_board") + @patch("mpflash.flash.worklist.log") + def test_creates_worklist_for_each_port(self, mock_log, mock_manual_board): + """Test manual_worklist calls manual_board for each serial port.""" + fw = Firmware(board_id="ESP32_GENERIC", version="1.22.0", port="esp32", firmware_file="fw.bin") + board1 = MPRemoteBoard("COM1") + board2 = MPRemoteBoard("COM2") + mock_manual_board.side_effect = [(board1, fw), (board2, fw)] + + result = manual_worklist(["COM1", "COM2"], board_id="ESP32_GENERIC", version="1.22.0") + + assert len(result) == 2 + assert mock_manual_board.call_count == 2 + + @patch("mpflash.flash.worklist.manual_board") + def test_empty_serial_list_returns_empty(self, mock_manual_board): + """Test that empty serial list returns empty worklist.""" + result = manual_worklist([], board_id="ESP32_GENERIC", version="1.22.0") + assert result == [] + mock_manual_board.assert_not_called() + + +class TestManualBoard: + """Test manual_board function (lines 264-278).""" + + @patch("mpflash.flash.worklist.find_known_board") + @patch("mpflash.flash.worklist._find_firmware_for_board") + @patch("mpflash.flash.worklist.log") + def test_board_not_found_returns_task_with_no_firmware(self, mock_log, mock_find_fw, mock_find_board): + """Test that LookupError from find_known_board results in task with None firmware.""" + mock_find_board.side_effect = LookupError("Board not found") + + result = manual_board("COM1", board_id="UNKNOWN_BOARD", version="1.22.0") + + assert isinstance(result, FlashTask) + assert result.firmware is None + mock_log.error.assert_called_once() + mock_find_fw.assert_not_called() + + @patch("mpflash.flash.worklist.find_known_board") + @patch("mpflash.flash.worklist._find_firmware_for_board") + @patch("mpflash.flash.worklist.log") + def test_successful_manual_board(self, mock_log, mock_find_fw, mock_find_board): + """Test successful manual_board creates correct flash task.""" + board_info = Mock() + board_info.port = "esp32" + board_info.mcu = "ESP32" + mock_find_board.return_value = board_info + + fw = Firmware(board_id="ESP32_GENERIC", version="1.22.0", port="esp32", firmware_file="fw.bin") + mock_find_fw.return_value = fw + + result = manual_board("COM1", board_id="ESP32_GENERIC", version="1.22.0") + + assert isinstance(result, FlashTask) + assert result.firmware is fw + assert result.board.port == "esp32" + assert result.board.board == "ESP32_GENERIC" + + # End of tests diff --git a/tests/integration/test_cli_integration.py b/tests/integration/test_cli_integration.py new file mode 100644 index 00000000..09462062 --- /dev/null +++ b/tests/integration/test_cli_integration.py @@ -0,0 +1,408 @@ +""" +Integration tests for CLI functionality with pyOCD support. + +Tests the CLI flash command with pyOCD method selection, +parameter parsing, and error handling. +""" + +from pathlib import Path +from unittest.mock import MagicMock, Mock, patch + +import pytest +from click.testing import CliRunner + +# Import CLI functions and related modules +from mpflash.cli_flash import cli_flash_board +from mpflash.common import BootloaderMethod, FlashMethod +from mpflash.errors import MPFlashError + +# Import test fixtures +from tests.fixtures.mock_pyocd_data import MOCK_MCUS, MOCK_PROBES + + +class TestCLIFlashCommandPyOCD: + """Test CLI flash command with pyOCD integration.""" + + def setup_method(self): + """Set up CLI runner for testing.""" + self.runner = CliRunner() + self._ask_patcher = patch("mpflash.cli_flash.ask_missing_params", side_effect=lambda p: p) + self._ask_patcher.start() + self._show_patcher = patch("mpflash.cli_flash.show_mcus") + self._show_patcher.start() + + def teardown_method(self): + self._ask_patcher.stop() + self._show_patcher.stop() + + @patch("mpflash.cli_flash.flash_tasks") + @patch("mpflash.cli_flash.create_worklist") + @patch("mpflash.cli_flash.connected_ports_boards_variants") + @patch("mpflash.cli_flash.jid.ensure_firmware_downloaded_tasks") + def test_flash_with_pyocd_method(self, mock_download, mock_connected, mock_create_worklist, mock_flash_tasks): + """Test flash command with explicit pyOCD method.""" + # Mock board detection + mock_board = MOCK_MCUS["stm32wb55"] + mock_connected.return_value = (["COM1"], ["NUCLEO_WB55"], [""], [mock_board]) + mock_create_worklist.return_value = [] + # Mock successful flashing + mock_flash_tasks.return_value = [mock_board] + + result = self.runner.invoke( + cli_flash_board, ["--method", "pyocd", "--version", "stable", "--probe-id", "066CFF", "--auto-install-packs"] + ) + + assert result.exit_code == 0 + + # Verify flash_list was called with correct parameters + mock_flash_tasks.assert_called_once() + call_args = mock_flash_tasks.call_args + + assert call_args[1]["method"] == FlashMethod.PYOCD + assert call_args[1]["probe_id"] == "066CFF" + assert call_args[1]["auto_install_packs"] is True + + @patch("mpflash.cli_flash.flash_tasks") + @patch("mpflash.cli_flash.create_worklist") + @patch("mpflash.cli_flash.connected_ports_boards_variants") + @patch("mpflash.cli_flash.jid.ensure_firmware_downloaded_tasks") + def test_flash_with_pyocd_no_auto_install(self, mock_download, mock_connected, mock_create_worklist, mock_flash_tasks): + """Test flash command with pyOCD and disabled pack installation.""" + mock_board = MOCK_MCUS["stm32wb55"] + mock_connected.return_value = (["COM1"], ["NUCLEO_WB55"], [""], [mock_board]) + mock_create_worklist.return_value = [] + mock_flash_tasks.return_value = [mock_board] + + result = self.runner.invoke(cli_flash_board, ["--method", "pyocd", "--version", "stable", "--no-auto-install-packs"]) + + assert result.exit_code == 0 + + call_args = mock_flash_tasks.call_args + assert call_args[1]["auto_install_packs"] is False + + @patch("mpflash.cli_flash.flash_tasks") + @patch("mpflash.cli_flash.create_worklist") + @patch("mpflash.cli_flash.connected_ports_boards_variants") + def test_flash_with_auto_method_excludes_pyocd(self, mock_connected, mock_create_worklist, mock_flash_tasks): + """Test that auto method selection excludes pyOCD by default.""" + mock_board = MOCK_MCUS["stm32wb55"] + mock_connected.return_value = (["COM1"], ["NUCLEO_WB55"], [""], [mock_board]) + mock_create_worklist.return_value = [] + mock_flash_tasks.return_value = [mock_board] + + result = self.runner.invoke( + cli_flash_board, + [ + "--method", + "auto", # Should not use pyOCD + "--version", + "stable", + ], + ) + + assert result.exit_code == 0 + + call_args = mock_flash_tasks.call_args + assert call_args[1]["method"] == FlashMethod.AUTO + + @patch("mpflash.cli_flash.flash_tasks") + @patch("mpflash.cli_flash.create_worklist") + @patch("mpflash.cli_flash.connected_ports_boards_variants") + def test_flash_command_parameter_extraction(self, mock_connected, mock_create_worklist, mock_flash_tasks): + """Test that pyOCD parameters are correctly extracted from CLI args.""" + mock_board = MOCK_MCUS["stm32wb55"] + mock_connected.return_value = (["COM1"], ["NUCLEO_WB55"], [""], [mock_board]) + mock_create_worklist.return_value = [] + mock_flash_tasks.return_value = [mock_board] + + result = self.runner.invoke( + cli_flash_board, + ["--method", "pyocd", "--probe-id", "066CFF505750827567154312", "--version", "stable", "--erase", "--auto-install-packs"], + ) + + assert result.exit_code == 0 + + call_args = mock_flash_tasks.call_args + assert call_args[1]["method"] == FlashMethod.PYOCD + assert call_args[1]["probe_id"] == "066CFF505750827567154312" + assert call_args[1]["auto_install_packs"] is True + assert call_args[0][1] is True # erase parameter + + def test_invalid_flash_method(self): + """Test error handling for invalid flash method.""" + result = self.runner.invoke(cli_flash_board, ["--method", "invalid_method", "--version", "stable"]) + + assert result.exit_code != 0 + assert "Invalid value" in result.output + + @patch("mpflash.cli_flash.flash_tasks") + @patch("mpflash.cli_flash.create_worklist") + @patch("mpflash.cli_flash.connected_ports_boards_variants") + def test_flash_failure_handling(self, mock_connected, mock_create_worklist, mock_flash_tasks): + """Test handling of flash operation failures.""" + mock_board = MOCK_MCUS["stm32wb55"] + mock_connected.return_value = (["COM1"], ["NUCLEO_WB55"], [""], [mock_board]) + mock_create_worklist.return_value = [] + # Mock flash failure + mock_flash_tasks.return_value = [] # No boards flashed + + result = self.runner.invoke(cli_flash_board, ["--method", "pyocd", "--version", "stable"]) + + assert result.exit_code == 1 + # note: log.error message goes to loguru, not result.output + + +class TestCLIParameterValidation: + """Test CLI parameter validation and error handling.""" + + def setup_method(self): + self.runner = CliRunner() + self._ask_patcher = patch("mpflash.cli_flash.ask_missing_params", side_effect=lambda p: p) + self._ask_patcher.start() + self._show_patcher = patch("mpflash.cli_flash.show_mcus") + self._show_patcher.start() + + def teardown_method(self): + self._ask_patcher.stop() + self._show_patcher.stop() + + def test_probe_id_parameter_validation(self): + """Test probe ID parameter accepts various formats.""" + with patch("mpflash.cli_flash.flash_tasks") as mock_flash: + with patch("mpflash.cli_flash.create_worklist") as mock_worklist: + with patch("mpflash.cli_flash.connected_ports_boards_variants") as mock_connected: + mock_board = MOCK_MCUS["stm32wb55"] + mock_connected.return_value = (["COM1"], ["NUCLEO_WB55"], [""], [mock_board]) + mock_worklist.return_value = [] + mock_flash.return_value = [mock_board] + + # Test short probe ID + result = self.runner.invoke(cli_flash_board, ["--method", "pyocd", "--probe-id", "066C", "--version", "stable"]) + + assert result.exit_code == 0 + + # Test full probe ID + result = self.runner.invoke( + cli_flash_board, ["--method", "pyocd", "--probe-id", "066CFF505750827567154312", "--version", "stable"] + ) + + assert result.exit_code == 0 + + def test_auto_install_packs_default_true(self): + """Test that auto-install-packs defaults to True.""" + with patch("mpflash.cli_flash.flash_tasks") as mock_flash: + with patch("mpflash.cli_flash.create_worklist") as mock_worklist: + with patch("mpflash.cli_flash.connected_ports_boards_variants") as mock_connected: + mock_board = MOCK_MCUS["stm32wb55"] + mock_connected.return_value = (["COM1"], ["NUCLEO_WB55"], [""], [mock_board]) + mock_worklist.return_value = [] + mock_flash.return_value = [mock_board] + + result = self.runner.invoke( + cli_flash_board, + [ + "--method", + "pyocd", + "--version", + "stable", + # No explicit --auto-install-packs flag + ], + ) + + assert result.exit_code == 0 + + call_args = mock_flash.call_args + assert call_args[1]["auto_install_packs"] is True # Default value + + def test_multiple_versions_error(self): + """Test error when multiple versions specified.""" + result = self.runner.invoke( + cli_flash_board, + [ + "--version", + "stable", + "--version", + "1.20.0", # Multiple versions not allowed + "--method", + "pyocd", + ], + ) + + # Should fail during parameter processing + assert result.exit_code != 0 + + +class TestCLIWorkflowIntegration: + """Test complete CLI workflows with pyOCD.""" + + def setup_method(self): + self.runner = CliRunner() + self._ask_patcher = patch("mpflash.cli_flash.ask_missing_params", side_effect=lambda p: p) + self._ask_patcher.start() + + def teardown_method(self): + self._ask_patcher.stop() + + @patch("mpflash.cli_flash.flash_tasks") + @patch("mpflash.cli_flash.create_worklist") + @patch('mpflash.cli_flash.connected_ports_boards_variants') + @patch("mpflash.cli_flash.jid.ensure_firmware_downloaded_tasks") + @patch('mpflash.cli_flash.show_mcus') + def test_complete_pyocd_workflow_success(self, mock_show, mock_download, mock_connected, mock_create_worklist, mock_flash_tasks): + """Test complete successful pyOCD flash workflow.""" + # Setup mocks + mock_board = MOCK_MCUS["stm32wb55"] + mock_connected.return_value = (["COM1"], ["NUCLEO_WB55"], [""], [mock_board]) + mock_create_worklist.return_value = [] + mock_flash_tasks.return_value = [mock_board] # Successful flash + + result = self.runner.invoke(cli_flash_board, [ + "--method", "pyocd", + "--version", "stable", + "--probe-id", "066CFF", + "--erase", + "--auto-install-packs" + ]) + + assert result.exit_code == 0 + # note: log.info message goes to loguru, not result.output + + # Verify all steps were called + mock_download.assert_called_once() # Firmware downloaded + mock_flash_tasks.assert_called_once() # Flash operation + mock_show.assert_called_once() # Results displayed + + @patch("mpflash.cli_flash.flash_tasks") + @patch("mpflash.cli_flash.create_worklist") + @patch("mpflash.cli_flash.show_mcus") + @patch('mpflash.cli_flash.connected_ports_boards_variants') + @patch("mpflash.cli_flash.jid.ensure_firmware_downloaded_tasks") + def test_custom_firmware_pyocd_workflow(self, mock_download, mock_connected, mock_show, mock_create_worklist, mock_flash_tasks): + """Test pyOCD workflow with custom firmware.""" + mock_board = MOCK_MCUS["stm32wb55"] + mock_connected.return_value = (["COM1"], ["NUCLEO_WB55"], [""], [mock_board]) + mock_create_worklist.return_value = [] + mock_flash_tasks.return_value = [mock_board] + + result = self.runner.invoke(cli_flash_board, [ + "--method", "pyocd", + "--version", "stable", + "--custom" # Custom firmware flag + ]) + + assert result.exit_code == 0 + + # Custom firmware should skip download + mock_download.assert_not_called() + mock_flash_tasks.assert_called_once() + + @patch('mpflash.cli_flash.connected_ports_boards_variants') + @patch('mpflash.cli_flash.ask_missing_params') + def test_interactive_parameter_prompting(self, mock_ask, mock_connected): + """Test interactive parameter prompting with pyOCD method.""" + # No boards detected initially + mock_connected.return_value = ([], [], [], []) + + # Mock user cancellation + mock_ask.return_value = None # User cancelled + + result = self.runner.invoke(cli_flash_board, [ + "--method", "pyocd", + "--version", "stable" + ]) + + assert result.exit_code == 2 # User cancellation exit code + mock_ask.assert_called_once() + + +class TestCLIErrorScenarios: + """Test CLI error handling scenarios.""" + + def setup_method(self): + self.runner = CliRunner() + self._ask_patcher = patch("mpflash.cli_flash.ask_missing_params", side_effect=lambda p: p) + self._ask_patcher.start() + self._show_patcher = patch("mpflash.cli_flash.show_mcus") + self._show_patcher.start() + + def teardown_method(self): + self._ask_patcher.stop() + self._show_patcher.stop() + + @patch("mpflash.cli_flash.flash_tasks") + @patch("mpflash.cli_flash.create_worklist") + @patch("mpflash.cli_flash.connected_ports_boards_variants") + def test_flash_method_error_propagation(self, mock_connected, mock_create_worklist, mock_flash_tasks): + """Test that flash method errors are properly propagated.""" + mock_board = MOCK_MCUS["stm32wb55"] + mock_connected.return_value = (["COM1"], ["NUCLEO_WB55"], [""], [mock_board]) + mock_create_worklist.return_value = [] + mock_flash_tasks.side_effect = MPFlashError("pyOCD programming failed") + + result = self.runner.invoke(cli_flash_board, ["--method", "pyocd", "--version", "stable"]) + + assert result.exit_code != 0 + + @patch("mpflash.cli_flash.flash_tasks") + @patch("mpflash.cli_flash.create_worklist") + @patch("mpflash.cli_flash.connected_ports_boards_variants") + def test_no_boards_detected_workflow(self, mock_connected, mock_create_worklist, mock_flash_tasks): + """Test workflow when no boards are detected.""" + mock_connected.return_value = ([], [], [], []) + mock_create_worklist.return_value = [] + mock_flash_tasks.return_value = [] + + result = self.runner.invoke(cli_flash_board, ["--method", "pyocd", "--version", "stable"]) + + assert result.exit_code == 1 # No boards flashed + + @patch("mpflash.cli_flash.flash_tasks") + @patch("mpflash.cli_flash.create_worklist") + @patch("mpflash.cli_flash.connected_ports_boards_variants") + def test_missing_required_parameters(self, mock_connected, mock_create_worklist, mock_flash_tasks): + """Test behavior with missing required parameters (version uses default 'stable').""" + mock_connected.return_value = ([], [], [], []) + mock_create_worklist.return_value = [] + mock_flash_tasks.return_value = [] + + # No version specified - should use default "stable" + result = self.runner.invoke(cli_flash_board, ["--method", "pyocd"]) + + # No boards detected -> exit 1; valid Click parse -> not 2 + assert result.exit_code in (0, 1, 2) + + +class TestCLIHelpAndDocumentation: + """Test CLI help text and documentation for pyOCD options.""" + + def setup_method(self): + self.runner = CliRunner() + + def test_cli_help_includes_pyocd_options(self): + """Test that CLI help includes pyOCD-specific options.""" + result = self.runner.invoke(cli_flash_board, ["--help"]) + + assert result.exit_code == 0 + assert "--method" in result.output + assert "pyocd" in result.output + assert "--probe-id" in result.output + assert "--auto-install-packs" in result.output + + def test_method_choice_validation(self): + """Test that method parameter validates choices correctly.""" + # Valid method + result = self.runner.invoke(cli_flash_board, [ + "--method", "pyocd", + "--version", "stable", + "--help" # Just show help, don't execute + ]) + + assert "pyocd" in result.output + + # Should include all valid methods in help + assert "auto" in result.output + assert "serial" in result.output + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/tests/test_ask_input.py b/tests/test_ask_input.py index c13963de..040c911d 100644 --- a/tests/test_ask_input.py +++ b/tests/test_ask_input.py @@ -1,3 +1,4 @@ +import os from unittest.mock import MagicMock, Mock import pytest @@ -195,6 +196,7 @@ def test_ask_missing_params_with_interactivity( params = FlashParams(**input) # make sure we can be interactive during testing, even in CI + mocker.patch.dict(os.environ, {"GITHUB_ACTIONS": ""}) _config = MPFlashConfig() _config.interactive = True mocker.patch("mpflash.ask_input.config", _config) diff --git a/tests/unit/test_probe_management.py b/tests/unit/test_probe_management.py new file mode 100644 index 00000000..0d9f5701 --- /dev/null +++ b/tests/unit/test_probe_management.py @@ -0,0 +1,435 @@ +""" +Unit tests for debug probe management and PyOCD probe implementation. + +Tests probe discovery, connection handling, and flash programming +without requiring actual hardware. +""" + +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, Mock, call, patch + +import pytest +from mpflash.errors import MPFlashError +# Import modules under test +from mpflash.flash.debug_probe import ( + DebugProbe, + _probe_implementations, + find_debug_probe, + get_debug_probes, + is_debug_programming_available, + register_probe_implementation, +) +from mpflash.flash.pyocd_flash import PyOCDFlash, PyOCDProbe, flash_pyocd + +# Import test fixtures +from tests.fixtures.mock_pyocd_data import ERROR_SCENARIOS, MOCK_MCUS, MOCK_PROBES + + +class MockPyOCDProbe(DebugProbe): + """Mock PyOCD probe for testing without pyOCD dependency.""" + + def __init__(self, unique_id: str, description: str): + super().__init__(unique_id, description) + self.connected = False + self.programming_success = True + + def program_flash(self, firmware_path: Path, target_type: str, **options) -> bool: + if not self.connected: + raise MPFlashError("Probe not connected") + return self.programming_success + + @classmethod + def is_implementation_available(cls) -> bool: + return True + + @classmethod + def discover(cls) -> list: + return [ + cls(probe["unique_id"], probe["description"]) + for probe in MOCK_PROBES + ] + + +class TestDebugProbeRegistry: + """Test the debug probe registration system.""" + + def setup_method(self): + """Clear registry before each test.""" + _probe_implementations.clear() + + def test_register_probe_implementation(self): + """Test registering a probe implementation.""" + register_probe_implementation("mock", MockPyOCDProbe) + + assert "mock" in _probe_implementations + assert _probe_implementations["mock"] == MockPyOCDProbe + + def test_register_invalid_probe_class(self): + """Test error when registering invalid probe class.""" + class InvalidProbe: + pass + + with pytest.raises(ValueError, match="must inherit from DebugProbe"): + register_probe_implementation("invalid", InvalidProbe) + + def test_auto_registration_pyocd(self): + """Test that pyOCD probe can be registered.""" + # Clear first + _probe_implementations.clear() + + # Test direct registration (simpler than module reload) + register_probe_implementation("pyocd", MockPyOCDProbe) + + assert "pyocd" in _probe_implementations + assert _probe_implementations["pyocd"] == MockPyOCDProbe + + +class TestProbeDiscovery: + """Test probe discovery functionality.""" + + def setup_method(self): + """Set up mock probe for testing.""" + _probe_implementations.clear() + register_probe_implementation("mock", MockPyOCDProbe) + + def test_get_debug_probes_success(self): + """Test successful probe discovery.""" + probes = get_debug_probes() + + assert len(probes) == 2 # From MOCK_PROBES + assert all(isinstance(p, MockPyOCDProbe) for p in probes) + assert probes[0].unique_id == "066CFF505750827567154312" + assert probes[1].unique_id == "0D28C20417A04C1D" + + def test_get_debug_probes_no_implementations(self): + """Test probe discovery with no implementations.""" + _probe_implementations.clear() + + probes = get_debug_probes() + assert probes == [] + + def test_get_debug_probes_implementation_unavailable(self): + """Test probe discovery when implementation is unavailable.""" + class UnavailableProbe(DebugProbe): + @classmethod + def is_implementation_available(cls): + return False + + @classmethod + def discover(cls): + return [] + + def program_flash(self, firmware_path, target_type, **options): + pass + + register_probe_implementation("unavailable", UnavailableProbe) + probes = get_debug_probes() + + # Should only return mock probes, not unavailable ones + assert len(probes) == 2 + assert all(isinstance(p, MockPyOCDProbe) for p in probes) + + def test_get_debug_probes_discovery_exception(self): + """Test graceful handling of discovery exceptions.""" + class FaultyProbe(DebugProbe): + @classmethod + def is_implementation_available(cls): + return True + + @classmethod + def discover(cls): + raise Exception("Discovery failed") + + def program_flash(self, firmware_path, target_type, **options): + pass + + register_probe_implementation("faulty", FaultyProbe) + probes = get_debug_probes() + + # Should return mock probes despite faulty probe throwing exception + assert len(probes) == 2 + + +class TestProbeFinding: + """Test probe finding functionality.""" + + def setup_method(self): + _probe_implementations.clear() + register_probe_implementation("mock", MockPyOCDProbe) + + def test_find_debug_probe_no_id(self): + """Test finding first available probe when no ID specified.""" + probe = find_debug_probe() + + assert probe is not None + assert isinstance(probe, MockPyOCDProbe) + assert probe.unique_id == "066CFF505750827567154312" # First probe + + def test_find_debug_probe_exact_match(self): + """Test finding probe by exact ID match.""" + probe_id = "0D28C20417A04C1D" + probe = find_debug_probe(probe_id) + + assert probe is not None + assert probe.unique_id == probe_id + + def test_find_debug_probe_partial_match(self): + """Test finding probe by partial ID match.""" + probe = find_debug_probe("066CFF") # Partial match + + assert probe is not None + assert probe.unique_id == "066CFF505750827567154312" + + def test_find_debug_probe_ambiguous_match(self): + """Test error on ambiguous partial match.""" + # Both probe IDs start with "0" - should be ambiguous + with pytest.raises(MPFlashError, match="Ambiguous probe ID"): + find_debug_probe("0") + + def test_find_debug_probe_no_match(self): + """Test no match found.""" + probe = find_debug_probe("NONEXISTENT") + assert probe is None + + def test_find_debug_probe_no_probes_available(self): + """Test behavior when no probes are available.""" + _probe_implementations.clear() + + probe = find_debug_probe() + assert probe is None + + +class TestPyOCDProbeIntegration: + """Test PyOCD probe implementation details.""" + + @patch("mpflash.flash.pyocd_flash._ensure_pyocd") + def test_pyocd_probe_is_available(self, mock_ensure): + """Test checking if pyOCD is available.""" + mock_ensure.return_value = {"ConnectHelper": Mock()} + + available = PyOCDProbe.is_implementation_available() + assert available is True + + @patch("mpflash.flash.pyocd_flash._ensure_pyocd") + def test_pyocd_probe_not_available(self, mock_ensure): + """Test behavior when pyOCD is not available.""" + mock_ensure.side_effect = MPFlashError("pyOCD not installed") + + available = PyOCDProbe.is_implementation_available() + assert available is False + + @patch("mpflash.flash.pyocd_flash._ensure_pyocd") + def test_pyocd_probe_discovery(self, mock_ensure): + """Test PyOCD probe discovery.""" + # Mock pyOCD ConnectHelper + mock_helper = Mock() + mock_probe_info = Mock() + mock_probe_info.unique_id = "TEST123" + mock_probe_info.description = "Test Probe" + mock_helper.get_all_connected_probes.return_value = [mock_probe_info] + + mock_ensure.return_value = {"ConnectHelper": mock_helper} + + probes = PyOCDProbe.discover() + + assert len(probes) == 1 + assert probes[0].unique_id == "TEST123" + assert probes[0].description == "Test Probe" + + +class TestPyOCDFlash: + """Test PyOCDFlash class functionality.""" + + def setup_method(self): + """Set up mocks for testing.""" + self.mock_mcu = MOCK_MCUS["stm32wb55"] + self.test_firmware = Path(tempfile.mktemp(suffix=".bin")) + + @patch("mpflash.flash.pyocd_flash.detect_pyocd_target") + @patch("mpflash.flash.pyocd_flash.is_pyocd_available") + def test_pyocd_flash_init_success(self, mock_available, mock_get_target): + """Test successful PyOCDFlash initialization.""" + mock_available.return_value = True + mock_get_target.return_value = "stm32wb55xg" + + flasher = PyOCDFlash(self.mock_mcu) + + assert flasher.mcu == self.mock_mcu + assert flasher.target_type == "stm32wb55xg" + + @patch("mpflash.flash.pyocd_flash.detect_pyocd_target") + @patch("mpflash.flash.pyocd_flash.is_pyocd_available") + def test_pyocd_flash_init_no_debug_support(self, mock_available, mock_get_target): + """Test PyOCDFlash initialization when debug programming unavailable.""" + mock_get_target.return_value = "stm32wb55xg" # target found but pyocd not available + mock_available.return_value = False + + with pytest.raises(MPFlashError, match="No debug probe support available"): + PyOCDFlash(self.mock_mcu) + + @patch("mpflash.flash.pyocd_flash.get_unsupported_reason") + @patch("mpflash.flash.pyocd_flash.detect_pyocd_target") + @patch("mpflash.flash.pyocd_flash.is_pyocd_available") + def test_pyocd_flash_init_unsupported_target(self, mock_available, mock_get_target, mock_reason): + """Test PyOCDFlash initialization with unsupported target.""" + mock_available.return_value = True + mock_get_target.return_value = None # No target found + mock_reason.return_value = "not in pyOCD target database" + + with pytest.raises(MPFlashError, match="not supported by pyOCD"): + PyOCDFlash(self.mock_mcu) + + @patch("mpflash.flash.pyocd_flash.detect_pyocd_target") + @patch("mpflash.flash.pyocd_flash.is_pyocd_available") + @patch("mpflash.flash.pyocd_flash.find_pyocd_probe") + def test_flash_firmware_success(self, mock_find_probe, mock_available, mock_get_target): + """Test successful firmware flashing.""" + # Setup mocks + mock_available.return_value = True + mock_get_target.return_value = "stm32wb55xg" + + mock_probe = MagicMock() + mock_probe.description = "Test Probe" + mock_probe.program_flash.return_value = True + mock_find_probe.return_value = mock_probe + + # Create temporary firmware file + self.test_firmware.touch() + + try: + flasher = PyOCDFlash(self.mock_mcu) + result = flasher.flash_firmware(self.test_firmware) + + assert result is True + mock_probe.program_flash.assert_called_once() + finally: + self.test_firmware.unlink(missing_ok=True) + + @patch("mpflash.flash.pyocd_flash.detect_pyocd_target") + @patch("mpflash.flash.pyocd_flash.is_pyocd_available") + def test_flash_firmware_file_not_found(self, mock_available, mock_get_target): + """Test error when firmware file doesn't exist.""" + mock_available.return_value = True + mock_get_target.return_value = "stm32wb55xg" + + flasher = PyOCDFlash(self.mock_mcu) + + with pytest.raises(MPFlashError, match="Firmware file not found"): + flasher.flash_firmware(Path("/nonexistent/firmware.bin")) + + @patch("mpflash.flash.pyocd_flash.detect_pyocd_target") + @patch("mpflash.flash.pyocd_flash.is_pyocd_available") + @patch("mpflash.flash.pyocd_flash.find_pyocd_probe") + def test_flash_firmware_no_probe(self, mock_find_probe, mock_available, mock_get_target): + """Test error when no probe is found.""" + mock_available.return_value = True + mock_get_target.return_value = "stm32wb55xg" + mock_find_probe.return_value = None + + self.test_firmware.touch() + + try: + flasher = PyOCDFlash(self.mock_mcu) + + with pytest.raises(MPFlashError, match="No PyOCD debug probes available"): + flasher.flash_firmware(self.test_firmware) + finally: + self.test_firmware.unlink(missing_ok=True) + + +class TestFlashPyOCDFunction: + """Test the flash_pyocd convenience function.""" + + def setup_method(self): + self.mock_mcu = MOCK_MCUS["stm32wb55"] + self.test_firmware = Path(tempfile.mktemp(suffix=".bin")) + + @patch("mpflash.flash.pyocd_flash.is_pyocd_supported") + @patch('mpflash.flash.pyocd_flash.PyOCDFlash') + def test_flash_pyocd_success(self, mock_flasher_class, mock_supported): + """Test successful flash_pyocd function call.""" + mock_supported.return_value = True + + mock_flasher = Mock() + mock_flasher.flash_firmware.return_value = True + mock_flasher_class.return_value = mock_flasher + + self.test_firmware.touch() + + try: + result = flash_pyocd(self.mock_mcu, self.test_firmware) + + assert result is True + mock_flasher_class.assert_called_once() + mock_flasher.flash_firmware.assert_called_once_with( + self.test_firmware, erase=False + ) + finally: + self.test_firmware.unlink(missing_ok=True) + + @patch("mpflash.flash.pyocd_flash.is_pyocd_supported") + def test_flash_pyocd_unsupported(self, mock_supported): + """Test flash_pyocd with unsupported MCU.""" + mock_supported.return_value = False + + with patch("mpflash.flash.pyocd_flash.get_unsupported_reason") as mock_reason: + mock_reason.return_value = "ESP32 not supported" + + with pytest.raises(MPFlashError, match="PyOCD flash not supported"): + flash_pyocd(self.mock_mcu, self.test_firmware) + + @patch("mpflash.flash.pyocd_flash.is_pyocd_supported") + @patch("mpflash.flash.pyocd_flash.PyOCDFlash") + def test_flash_pyocd_no_probe(self, mock_flasher_class, mock_supported): + """Test flash_pyocd when no suitable probe found.""" + mock_supported.return_value = True + mock_flasher = Mock() + mock_flasher.flash_firmware.side_effect = MPFlashError("No PyOCD debug probes available") + mock_flasher_class.return_value = mock_flasher + + self.test_firmware.touch() + try: + with pytest.raises(MPFlashError, match="No PyOCD debug probes available"): + flash_pyocd(self.mock_mcu, self.test_firmware) + finally: + self.test_firmware.unlink(missing_ok=True) + + +class TestProbeAvailability: + """Test availability checking functions.""" + + def setup_method(self): + _probe_implementations.clear() + + def test_is_debug_programming_available_true(self): + """Test debug programming availability when probes available.""" + register_probe_implementation("mock", MockPyOCDProbe) + + assert is_debug_programming_available() is True + + def test_is_debug_programming_available_false(self): + """Test debug programming availability when no probes available.""" + class UnavailableProbe(DebugProbe): + @classmethod + def is_implementation_available(cls): + return False + + @classmethod + def discover(cls): + return [] + + def program_flash(self, firmware_path, target_type, **options): + pass + + register_probe_implementation("unavailable", UnavailableProbe) + + assert is_debug_programming_available() is False + + def test_is_debug_programming_available_no_implementations(self): + """Test debug programming availability with no implementations.""" + assert is_debug_programming_available() is False + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/tests/unit/test_target_detection.py b/tests/unit/test_target_detection.py new file mode 100644 index 00000000..dc2cb8ca --- /dev/null +++ b/tests/unit/test_target_detection.py @@ -0,0 +1,384 @@ +""" +Unit tests for pyOCD target detection and fuzzy matching. + +Tests the core business logic without external dependencies by mocking +pyOCD APIs and subprocess calls. +""" + +from pathlib import Path +from unittest.mock import MagicMock, Mock, patch + +import pytest +from mpflash.errors import MPFlashError + +# Import the modules under test +from mpflash.flash.pyocd_core import ( + MCUIdentifier, + auto_install_pack_for_target, + cached_target_lookup, + detect_pyocd_target, + fuzzy_match_target, + get_pyocd_targets, + parse_mcu_info, +) + +# Import test fixtures +from tests.fixtures.mock_pyocd_data import ( + ALL_PYOCD_TARGETS, + BUILTIN_PYOCD_TARGETS, + ERROR_SCENARIOS, + EXPECTED_FUZZY_MATCHES, + MOCK_MCUS, + MOCK_SUBPROCESS_OUTPUTS, + PACK_PYOCD_TARGETS, + SAMPLE_MCU_DESCRIPTIONS, +) + + +class TestMCUInfoParsing: + """Test MCU information parsing from device descriptions.""" + + def test_parse_stm32_with_variant(self): + """Test parsing STM32 description with board and variant.""" + mcu = MOCK_MCUS["stm32wb55"] + info = parse_mcu_info(mcu) + + assert info["chip_family"] == "STM32WB55" + assert info["chip_variant"] == "RGV6" + assert info["board_name"] == "NUCLEO-WB55" + assert info["port"] == "stm32" + assert info["cpu"] == "STM32WB55RGV6" + + def test_parse_stm32_f429(self): + """Test parsing STM32F429 description.""" + mcu = MOCK_MCUS["stm32f429"] + info = parse_mcu_info(mcu) + + assert info["chip_family"] == "STM32F429" + assert info["chip_variant"] == "ZI" + assert info["board_name"] == "NUCLEO-F429ZI" + + def test_parse_rp2040(self): + """Test parsing RP2040 description.""" + mcu = MOCK_MCUS["rp2040"] + info = parse_mcu_info(mcu) + + assert info["chip_family"] == "RP2040" + assert info["board_name"] == "Raspberry Pi Pico" + assert info["port"] == "rp2" + + def test_parse_samd51(self): + """Test parsing SAMD51 description.""" + mcu = MOCK_MCUS["samd51"] + info = parse_mcu_info(mcu) + + assert info["chip_family"] == "SAMD51J19A" + assert info["chip_variant"] == "" + assert info["board_name"] == "Adafruit Metro M4" + + def test_parse_esp32(self): + """Test parsing ESP32 description (should work but won't match pyOCD).""" + mcu = MOCK_MCUS["esp32"] + info = parse_mcu_info(mcu) + + # ESP32 parsing should extract chip info but won't match pyOCD targets + assert "ESP32" in info["chip_family"] + assert info["port"] == "esp32" + + def test_parse_malformed_description(self): + """Test handling of malformed MCU descriptions.""" + mcu = MOCK_MCUS["malformed"] + info = parse_mcu_info(mcu) + + # Should fall back to CPU and board_id + assert info["board_name"] == "UNKNOWN" + assert info["chip_family"] != "" # Should have fallback + + +class TestFuzzyMatching: + """Test fuzzy matching algorithm for target detection.""" + + def test_exact_family_matches(self): + """Test exact chip family matches get high scores.""" + for chip_family, expected_target in EXPECTED_FUZZY_MATCHES.items(): + if expected_target is None: + continue + + mcu_info = {"chip_family": chip_family, "chip_variant": "", "port": "stm32"} + result = fuzzy_match_target(mcu_info, ALL_PYOCD_TARGETS) + + assert result == expected_target, f"Expected {expected_target} for {chip_family}, got {result}" + + def test_no_match_for_unsupported_chips(self): + """Test that unsupported chips return None.""" + mcu_info = {"chip_family": "ESP32", "chip_variant": "", "port": "esp32"} + result = fuzzy_match_target(mcu_info, ALL_PYOCD_TARGETS) + + assert result is None + + def test_port_matching_bonus(self): + """Test that matching port gives score bonus.""" + # STM32 on stm32 port should score higher than on unknown port + mcu_info_stm32_port = {"chip_family": "STM32F429", "chip_variant": "", "port": "stm32"} + mcu_info_unknown_port = {"chip_family": "STM32F429", "chip_variant": "", "port": "unknown"} + + result_stm32 = fuzzy_match_target(mcu_info_stm32_port, ALL_PYOCD_TARGETS) + result_unknown = fuzzy_match_target(mcu_info_unknown_port, ALL_PYOCD_TARGETS) + + # Both should find the target, but port matching should be considered + assert result_stm32 == result_unknown == "stm32f429xi" + + def test_empty_chip_family(self): + """Test handling of empty chip family.""" + mcu_info = {"chip_family": "", "chip_variant": "", "port": "unknown"} + result = fuzzy_match_target(mcu_info, ALL_PYOCD_TARGETS) + + assert result is None + + def test_case_insensitive_matching(self): + """Test that matching is case insensitive.""" + mcu_info = {"chip_family": "stm32f429", "chip_variant": "", "port": "stm32"} # lowercase + result = fuzzy_match_target(mcu_info, ALL_PYOCD_TARGETS) + + assert result == "stm32f429xi" + + def test_threshold_filtering(self): + """Test that low-scoring matches are filtered out.""" + # Use a completely unrelated chip name + mcu_info = {"chip_family": "COMPLETELY_DIFFERENT", "chip_variant": "", "port": "unknown"} + result = fuzzy_match_target(mcu_info, ALL_PYOCD_TARGETS) + + assert result is None + + +class TestPyOCDTargetDiscovery: + """Test pyOCD target discovery functionality.""" + + @patch("subprocess.run") + def test_get_pyocd_targets_success(self, mock_subprocess): + """Test target discovery via subprocess.""" + # Mock subprocess success + mock_result = Mock() + mock_result.returncode = 0 + mock_result.stdout = MOCK_SUBPROCESS_OUTPUTS["pyocd_list_targets"] + mock_subprocess.return_value = mock_result + + # Mock API to force subprocess path + with patch("mpflash.flash.pyocd_core.get_pyocd_targets") as mock_get_targets: + with patch('mpflash.flash.pyocd_core._ensure_pyocd'): + pass # Simplified test - subprocess path tested separately + + def test_pyocd_not_available(self): + """Test behavior when pyOCD is not installed.""" + with patch('mpflash.flash.pyocd_core._ensure_pyocd', side_effect=MPFlashError("pyOCD not installed")): + with pytest.raises(MPFlashError, match="pyOCD not installed"): + get_pyocd_targets() + + +class TestDynamicTargetDetection: + """Test the main dynamic target detection function.""" + + def setup_method(self): + """Clear target cache before each test to avoid inter-test contamination.""" + import mpflash.flash.pyocd_core as pyocd_core + + pyocd_core._target_cache.clear() + pyocd_core.get_pyocd_targets.cache_clear() + + @patch("mpflash.flash.pyocd_core.get_pyocd_targets") + def test_successful_fuzzy_match(self, mock_get_targets): + """Test successful target detection via fuzzy matching.""" + mock_get_targets.return_value = ALL_PYOCD_TARGETS + + mcu = MOCK_MCUS["stm32wb55"] + result = detect_pyocd_target(mcu, auto_install_packs=False) + + assert result == "stm32wb55xg" + + @patch("mpflash.flash.pyocd_core.get_pyocd_targets") + def test_no_match_without_pack_install(self, mock_get_targets): + """Test no match found when pack installation disabled.""" + # Only return builtin targets (no H563 support) + mock_get_targets.return_value = BUILTIN_PYOCD_TARGETS + + mcu = MOCK_MCUS["stm32h563"] # Not in builtin targets + result = detect_pyocd_target(mcu, auto_install_packs=False) + + # May find a similar STM32 target due to fuzzy matching + # The important thing is that H563 specific target isn't found + if result: + assert "h563" not in result.lower() # Should not find H563 specific target + + @patch("mpflash.flash.pyocd_core.get_pyocd_targets") + @patch("mpflash.flash.pyocd_core.auto_install_pack_for_target") + def test_successful_pack_installation(self, mock_install_pack, mock_get_targets): + """Test successful target detection after pack installation.""" + # First call returns empty targets to force pack installation + mock_get_targets.side_effect = [{}, ALL_PYOCD_TARGETS] + mock_install_pack.return_value = True + + mcu = MOCK_MCUS["stm32h563"] + result = detect_pyocd_target(mcu, auto_install_packs=True) + + # After pack installation should find H563 target + assert result == "stm32h563zitx" + mock_install_pack.assert_called_once_with("STM32H563") + + @patch("mpflash.flash.pyocd_core.get_pyocd_targets") + @patch("mpflash.flash.pyocd_core.auto_install_pack_for_target") + def test_failed_pack_installation(self, mock_install_pack, mock_get_targets): + """Test behavior when pack installation fails.""" + mock_get_targets.return_value = {} # No targets available + mock_install_pack.return_value = False + + mcu = MOCK_MCUS["stm32h563"] + result = detect_pyocd_target(mcu, auto_install_packs=True) + + # With failed pack installation and no targets, should return None + assert result is None + mock_install_pack.assert_called_once_with("STM32H563") + + +class TestPackInstallation: + """Test automatic CMSIS pack installation.""" + + @patch("subprocess.run") + def test_successful_pack_search_and_install(self, mock_subprocess): + """Test successful pack search and installation.""" + # Mock pack find command + find_result = Mock() + find_result.returncode = 0 + find_result.stdout = MOCK_SUBPROCESS_OUTPUTS["pyocd_pack_find_stm32h563"] + + # Mock pack install command + install_result = Mock() + install_result.returncode = 0 + install_result.stdout = MOCK_SUBPROCESS_OUTPUTS["pyocd_pack_install_success"] + + mock_subprocess.side_effect = [find_result, install_result] + + with patch("mpflash.flash.pyocd_core.get_pyocd_targets") as mock_cache: + mock_cache.cache_clear = Mock() + result = auto_install_pack_for_target("STM32H563") + + assert result is True + assert mock_subprocess.call_count == 2 + + # Verify commands called + find_call = mock_subprocess.call_args_list[0] + install_call = mock_subprocess.call_args_list[1] + + assert find_call[0][0] == ["pyocd", "pack", "find", "STM32H563"] + assert install_call[0][0] == ["pyocd", "pack", "install", "STM32H563"] + + @patch("subprocess.run") + def test_pack_search_failure(self, mock_subprocess): + """Test pack installation when search fails.""" + mock_result = Mock() + mock_result.returncode = 1 + mock_result.stderr = "No packs found" + mock_subprocess.return_value = mock_result + + result = auto_install_pack_for_target("NONEXISTENT_CHIP") + + assert result is False + + @patch("subprocess.run") + def test_pack_install_timeout(self, mock_subprocess): + """Test pack installation timeout handling.""" + from subprocess import TimeoutExpired + + mock_subprocess.side_effect = TimeoutExpired("pyocd", 300) + + result = auto_install_pack_for_target("STM32H563") + + assert result is False + + @patch("mpflash.flash.pyocd_core._run_pyocd_command") + def test_no_packs_to_install(self, mock_run_command): + """Test when all packs are already installed.""" + # Mock output showing all packs installed + installed_output = """ +Part Number Vendor Pack Version Installed +------------------------------------------------------------------------------- +STM32H563ZI STMicroelectronics Keil.STM32H5xx_DFP 1.0.0 true +""" + mock_result = Mock() + mock_result.returncode = 0 + mock_result.stdout = installed_output + mock_run_command.return_value = mock_result + + result = auto_install_pack_for_target("STM32H563") + + assert result is False # No packs to install + + +class TestCaching: + """Test caching functionality.""" + + def test_mcu_identifier_creation(self): + """Test MCUIdentifier creation from MCU.""" + mcu = MOCK_MCUS["stm32wb55"] + mcu_id = MCUIdentifier.from_mcu(mcu) + + assert mcu_id.board_id == "NUCLEO_WB55" + assert mcu_id.cpu == "STM32WB55RGV6" + assert mcu_id.description == "NUCLEO-WB55 with STM32WB55RGV6" + assert mcu_id.port == "stm32" + + def test_cached_lookup_same_results(self): + """Test that cached lookup returns consistent results.""" + mcu_id = MCUIdentifier("TEST_BOARD", "STM32F429", "Test MCU", "stm32") + + with patch("mpflash.flash.pyocd_core.detect_pyocd_target") as mock_dynamic: + mock_dynamic.return_value = "stm32f429xi" + + result1 = cached_target_lookup(mcu_id) + result2 = cached_target_lookup(mcu_id) + + assert result1 == result2 == "stm32f429xi" + # Should only call the underlying function once due to caching + assert mock_dynamic.call_count == 1 + + +class TestErrorHandling: + """Test error handling scenarios.""" + + def setup_method(self): + """Clear target cache before each test to avoid inter-test contamination.""" + import mpflash.flash.pyocd_core as pyocd_core + + pyocd_core._target_cache.clear() + pyocd_core.get_pyocd_targets.cache_clear() + + def test_graceful_exception_handling(self): + """Test that exceptions in target detection are handled gracefully.""" + mcu = MOCK_MCUS["stm32wb55"] + + with patch('mpflash.flash.pyocd_core.get_pyocd_targets', side_effect=Exception("API Error")): + result = detect_pyocd_target(mcu) + assert result is None # Should not crash + + def test_empty_target_list(self): + """Test behavior with empty target list.""" + mcu_info = {"chip_family": "STM32F429", "chip_variant": "", "port": "stm32"} + result = fuzzy_match_target(mcu_info, {}) # Empty targets + + assert result is None + + def test_malformed_subprocess_output(self): + """Test handling of malformed subprocess output.""" + with patch("mpflash.flash.pyocd_core._ensure_pyocd"): + with patch("mpflash.flash.pyocd_core.subprocess.run") as mock_subprocess: + mock_result = Mock() + mock_result.returncode = 0 + mock_result.stdout = "Malformed output\nNot a proper table" + mock_subprocess.return_value = mock_result + + # Should not crash with malformed output - simplified test + result = get_pyocd_targets() + assert isinstance(result, dict) # At minimum should return dict + + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file diff --git a/uv.lock b/uv.lock index 31e07b7f..44f8f0db 100644 --- a/uv.lock +++ b/uv.lock @@ -2,11 +2,30 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, +] + [[package]] name = "appnope" version = "0.1.4" @@ -218,6 +237,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, ] +[[package]] +name = "capstone" +version = "5.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/9c/28b11f64e2425774efb21c206a6e952cfce6e3e2ef3e4b63cdae32ccd8a5/capstone-5.0.7.tar.gz", hash = "sha256:796bdd69b05fa124fc2aa2e74b9a0b3d4c4e7f3e02add5e583cf2f3bca282ede", size = 2945245, upload-time = "2026-02-09T22:51:56.392Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/91/98fdb020106469d2354920ffb989507f8c494d847eb9135202a0b20afe8d/capstone-5.0.7-py3-none-macosx_10_9_universal2.whl", hash = "sha256:388af4ddb9224d3b4f9269673ee575b3f94f77774d48b3f1a283ad13c29a106a", size = 2192836, upload-time = "2026-02-09T22:51:42.782Z" }, + { url = "https://files.pythonhosted.org/packages/b7/6c/f02847b3385651eb00a6a27049bd12982cdd61e981b248e7f0dc8ed15756/capstone-5.0.7-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:a9f64e3d75d8c4d7b3d26bba153b2992aadcf6b8d57674b4ef176b4ecdd9822f", size = 1188995, upload-time = "2026-02-09T22:51:44.653Z" }, + { url = "https://files.pythonhosted.org/packages/8f/43/29cd1dbdb2b55bf339bca36444809503b8311dcc04898726a7e35b47ac86/capstone-5.0.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:acb89f5bf6f625745a104a3a44819d3acea173228055c1eadc60d2282ae490bb", size = 1200084, upload-time = "2026-02-09T22:51:45.668Z" }, + { url = "https://files.pythonhosted.org/packages/7d/32/084419edcd9a3efaadf5c22166de625090fae833aad0672ddd9fa436d1e2/capstone-5.0.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c58546c814567c95e4b9a63bdb8624c960cb8508855c7c767d5f108d7bc09ce2", size = 1458867, upload-time = "2026-02-09T22:51:46.8Z" }, + { url = "https://files.pythonhosted.org/packages/15/3e/3a4f45dd4eda6fc8051f76bf3ed50ead6040d827000371211fbaaf057625/capstone-5.0.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b809a9654844ce0d35099121a851ddd2ab2689df1ff6687037babcedcaae6391", size = 1482184, upload-time = "2026-02-09T22:51:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a8/6b4df60c822aaeb42954ea6c9fdbf3410feaa6bea6dc3f18f6c363fd1ed2/capstone-5.0.7-py3-none-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b0f1b93fc703c419fda8cf84cfa017fd8909be62a4e88024273126ab16f006", size = 1481169, upload-time = "2026-02-09T22:51:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/05/24/4162bbc2591091dfa6c161b4a9ef1a6eccb739c20798e58f59f917a8a3d1/capstone-5.0.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:467716e6555d50cb3526b290f0dbdccb5f961839b1f1e299b484fb5d814173e6", size = 1457760, upload-time = "2026-02-09T22:51:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/e1/51/e3913f53ed03a8f7311b1efacbc1052811e129baa37b4f14e79da3cbc0c6/capstone-5.0.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e551311d4b6dc344fe5518ef6decf4c2dfafe37bba9ad027a53a406930bc5c63", size = 1484528, upload-time = "2026-02-09T22:51:52.483Z" }, + { url = "https://files.pythonhosted.org/packages/0b/98/a7d631f7bca9de02357637cfab1fb90a67992905f2041357ac5732c8a4cd/capstone-5.0.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a13437b28b136c886600e88bee192d25adf56ba1db5597ff5a0bec758bb9c533", size = 1486041, upload-time = "2026-02-09T22:51:53.729Z" }, + { url = "https://files.pythonhosted.org/packages/70/39/2138d890a8e827636b9de9924fbc8527fe83e38b6d26605b30ac55e30ebe/capstone-5.0.7-py3-none-win_amd64.whl", hash = "sha256:4ab8bcb7da8f221ff45926ca168ca33e76f7237d06fbf3c10780002faa2670e1", size = 1272204, upload-time = "2026-02-09T22:51:54.829Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -426,6 +463,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "cmsis-pack-manager" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "cffi" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/df/07336875bb9a51053eb671b3d6046b23552eb9e9301b917336b0f392a82b/cmsis_pack_manager-0.6.0.tar.gz", hash = "sha256:94913a3db9695f8d0676a4a74916a5626984e2b46f923ada61881e4f5064079e", size = 67773, upload-time = "2025-06-27T02:42:59.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/9b/a9eafbafc92d56902b963d10e4c72c2b23598fd609caaf0087ed39a9b12f/cmsis_pack_manager-0.6.0-py3-none-linux_armv6l.whl", hash = "sha256:2c540ae648479ca91487585ca7cbda830fa7a1b9244a7b20765510231cd3c91a", size = 3484862, upload-time = "2025-06-27T02:36:14.167Z" }, + { url = "https://files.pythonhosted.org/packages/de/b2/970a9ddaebd82712d496ae2ba98176edf531be16b1b6abb46e3088ceebdb/cmsis_pack_manager-0.6.0-py3-none-macosx_10_12_universal2.whl", hash = "sha256:4b912d77b5a13146c936a87673a840ccdbf7305fa0a21414cde74709c246c052", size = 4122951, upload-time = "2025-06-27T02:36:15.893Z" }, + { url = "https://files.pythonhosted.org/packages/46/85/66f9839456e1c240a1f55594faf7efced1054bad5c2137326f4bc6f7ef5e/cmsis_pack_manager-0.6.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f734bf40b19103222716ab4920da78e5af37777a19769e920472218146f7f2e3", size = 3770533, upload-time = "2025-06-27T02:36:17.335Z" }, + { url = "https://files.pythonhosted.org/packages/78/a1/217310c633609bfde6a8553222295b08e6f50c99f347cb3bb6d556a74ae0/cmsis_pack_manager-0.6.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c073b93db44c86cb27b60dc98d42b54c3fd84be479979657def094c5da342c36", size = 3413476, upload-time = "2025-06-27T02:36:18.827Z" }, + { url = "https://files.pythonhosted.org/packages/a2/87/83a3e0bcd0a75110488842526637f22fadcb7dae6b8a9afb848115141280/cmsis_pack_manager-0.6.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3b48ea644034acda9bd2a6afe9f89f4d6b67ee28fe5800a25dbb51e179310b5c", size = 3462120, upload-time = "2025-06-27T02:36:20.337Z" }, + { url = "https://files.pythonhosted.org/packages/75/29/c65da965f9b60f2d470f01020a5cab8e8abe5113f4b22ecaadcfba22fa44/cmsis_pack_manager-0.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8c0a2cb8790168496df493eb178215a8b638d5d9c2176289764da0686ec7fd", size = 3666217, upload-time = "2025-06-27T02:36:21.882Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c4/619a3e979666fa640bdb5333f6782bf709962c39da0770fdadb4f8d51652/cmsis_pack_manager-0.6.0-py3-none-win32.whl", hash = "sha256:8e3830566ee7b2f596f538b58e42500b7dffdfe18ce0b543b07c2715ad7734f5", size = 1520643, upload-time = "2025-06-27T02:36:24.766Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7e/547624bf371eeaeae6370ed754bebbafbbf114a2d7dfc372c4e5a7ff3ded/cmsis_pack_manager-0.6.0-py3-none-win_amd64.whl", hash = "sha256:53fc43ae474905d107889681c5829ea90b6211d139794fa3f8691c9b0da3bb85", size = 1795914, upload-time = "2025-06-27T02:36:23.25Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -690,7 +757,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -715,6 +782,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/ac/e5d886f892666d2d1e5cb8c1a41146e1d79ae8896477b1153a21711d3b44/fasteners-0.20-py3-none-any.whl", hash = "sha256:9422c40d1e350e4259f509fb2e608d6bc43c0136f79a00db1b49046029d0b3b7", size = 18702, upload-time = "2025-08-11T10:19:35.716Z" }, ] +[[package]] +name = "hidapi" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/f6/caad9ed701fbb9223eb9e0b41a5514390769b4cb3084a2704ab69e9df0fe/hidapi-0.15.0.tar.gz", hash = "sha256:ecbc265cbe8b7b88755f421e0ba25f084091ec550c2b90ff9e8ddd4fcd540311", size = 184995, upload-time = "2025-12-09T09:48:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/5a/46620fc194f3fa728dce1966ce977334b080fc33b8b525018ad0e0324b91/hidapi-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b0e1781f7fb8b4015e318d839d66fa79e98900d53900e31d04edb336e0103846", size = 70517, upload-time = "2025-12-09T09:44:47.751Z" }, + { url = "https://files.pythonhosted.org/packages/2d/97/bcbcb89f9461c29d3b12dd32affd29e6312fd521154ee7f394496d0039a9/hidapi-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fa3e792987d4b7ed66d785491307e23d4f09d3636f8a23665a9694c43e92409", size = 70181, upload-time = "2025-12-09T09:44:49.321Z" }, + { url = "https://files.pythonhosted.org/packages/26/cc/03f7d56b82a9dc2abcfcd8d55915504e156b6558fb66926629836e551595/hidapi-0.15.0-cp310-cp310-win32.whl", hash = "sha256:81de6b5fcb4fbbbfc71c6d201a2ac6914d1d86e51930ffde5f96faf50e922473", size = 60160, upload-time = "2025-12-09T09:45:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/35/20/a39e33a9088d76f98f80d9320f8413bdaeaa0458b42470e63a47ac4ccf94/hidapi-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:c36895abaef3a4004af6c5020ca214fcdacb2a491e4cde5576afbb1dad903548", size = 67063, upload-time = "2025-12-09T09:45:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c5/3cfa157e7d1fd6af5ad52ea6ea031b1c8da141c61a2506ad5cb3420afa7f/hidapi-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53d018e6ac639a217c019c6dcd79a1d30c2401ac7a8147eedd11f2aa29307661", size = 70578, upload-time = "2025-12-09T09:45:05.497Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7e/a43efc66b3b0a68058e76ad0cb2267ce9b30d9006dce10824f3c8b314b08/hidapi-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a11ef4b5ab0a2f36e35f44af394bff41cb645b03ce36c5b2f2c00873f112661f", size = 70233, upload-time = "2025-12-09T09:45:06.826Z" }, + { url = "https://files.pythonhosted.org/packages/53/40/6a45375a52027d8142e7acdaeab182a44ff6b818df41384019662eb4f351/hidapi-0.15.0-cp311-cp311-win32.whl", hash = "sha256:2c35bd9cc62227ec91047e36b260f75bdbf50814f3cf3c3b28648ac3ffabd9d7", size = 60070, upload-time = "2025-12-09T09:45:24.745Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a9/8ad4e6423c7416eb8dd765327f3be67f083c985b41b9b48ef3061a64c5f2/hidapi-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c5f7ee9ce8e3373fdb7002497f16bc652d9d4acf20c91275877f55165caf6f0c", size = 67296, upload-time = "2025-12-09T09:45:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/b3/fd/9076aba0736f339b71bda5ee26cb678c02420bf96c7ea5bd9ebcbaf0aa3c/hidapi-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b7eb950214191c936b5bdabaeaf1cab99c0dfc3ae3edc220e3c6d4547296053", size = 70157, upload-time = "2025-12-09T09:45:25.77Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/9335957cb7e1ca1be997e97afe850f62a9ff0708015048b3871b553eed5c/hidapi-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27933e7f3da7007e5e8d0b25bcf48a79a74e802555e496bba2c7d87f8a77cd61", size = 69597, upload-time = "2025-12-09T09:45:26.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a7/9b7fe9acd09ed90d702f8cc01190ab53e01d5022c21808b079144bc9017d/hidapi-0.15.0-cp312-cp312-win32.whl", hash = "sha256:ce6f99554dae15c48cd89a12d5aede77a92a8bd184b45d0b257a17c97e053cdb", size = 60136, upload-time = "2025-12-09T09:45:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/7f/35/f9e2d3ead60b573140546b041dd41c78a48c0e6b573d61a03130adccd32e/hidapi-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:9abc71a62bf8a8f1d70a6fd3613f86feb37ddb67e05ed934e734df79e27bf4f6", size = 67073, upload-time = "2025-12-09T09:45:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/d1/19/bda2dce4af8b8028e6d611d5caf60e223a4d32a638d1155eefdc6d8f2462/hidapi-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f336dd2dc308928d54a6fdce137814a941a3633ad6722919d7a3142dd99005c2", size = 69045, upload-time = "2025-12-09T09:45:41.534Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/9b7d6b6a7561654b9b0d7b22fba9c5c971b36f5fc541b04e02d71928349d/hidapi-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8017f2213810c3e57d9ab64c2328c593a1129b25804240108a6451fe35cde193", size = 68755, upload-time = "2025-12-09T09:45:42.464Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c7/52f2d903a607f024086829349383dcc7f0c22533fc242f26f9754eba2f06/hidapi-0.15.0-cp313-cp313-win32.whl", hash = "sha256:e4ddb57e71e2b8aca2c685b94b8587dc7d7cfae3c529a7c4076ba0775eb28d4a", size = 59749, upload-time = "2025-12-09T09:45:53.882Z" }, + { url = "https://files.pythonhosted.org/packages/d9/00/7dd3f866a748b0c9eb4adedaa025ec8c533ed1946e9e918661c948a5c0f4/hidapi-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:04e0c1ed6d742bc6e1d00599394bec6b2afb8ee2fc5a0a183d3c4c2d4e315c34", size = 66362, upload-time = "2025-12-09T09:45:52.797Z" }, + { url = "https://files.pythonhosted.org/packages/0a/65/e9c70deb396f5baea92cd2ea7803914a55d455c3982367bae4390483e6cc/hidapi-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a6bb079bcd5152dd8968893aecdcbde3954b849896078f54df2254c0d1f301e", size = 69532, upload-time = "2025-12-09T09:45:55.269Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/d006a7e28ec63b5db76340e4aca6ff077ad336f8fbe64ac804b968927011/hidapi-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d80c745d285dfc9889e856226daa8eee6f9e83025ef2cb97e5fe0b1396f7818a", size = 69350, upload-time = "2025-12-09T09:45:56.695Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d6/c0b9d8568fdb38eebb9caaa56643a81d92cccb5d59a5b44e0d975a0f3808/hidapi-0.15.0-cp314-cp314-win32.whl", hash = "sha256:901c03817306eac7edd589f09e0e07467871b45f665949cc4caa645b54abc97b", size = 61089, upload-time = "2025-12-09T09:46:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/cb/14/3b27516b2867e53545164ebd8ef45d0c36857d2a062036e788e56e2e34ce/hidapi-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:df0dd435562654e27ad0c7d045074eb1e1e016b09167dd48a258dec1ce4b9127", size = 67710, upload-time = "2025-12-09T09:46:08.677Z" }, + { url = "https://files.pythonhosted.org/packages/97/8f/ff034661faf99acaabe8954fb7db87c5242c79bf763dd972b23b1be5f104/hidapi-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2f16e9ea8a1d24ac7a6bf316905823eafd58f78ec815a93118d4710faf95a224", size = 70893, upload-time = "2025-12-09T09:46:10.625Z" }, + { url = "https://files.pythonhosted.org/packages/d8/44/617666a0919d06521d732ef07998306b8ddcbd96038e19348c831acd76e8/hidapi-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:77f0965ca81a9be44694e84aa18e501d5cda49e372202a38a52d22acc38deadd", size = 71430, upload-time = "2025-12-09T09:46:11.648Z" }, + { url = "https://files.pythonhosted.org/packages/bd/eb/c0676ff1f7e9ec9d99eaa428c66745cdab5b1742808f0c16061c85bd0b18/hidapi-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:005c84c74c940a314b211674edc763a931ace0b63685ace88148496f563e25f9", size = 66288, upload-time = "2025-12-09T09:46:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5b/f22dbf886824559d3e66d84710c5fb48c09856e6816885da97f2f51700eb/hidapi-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e384430363d400c4ffb33fe65f22a61c9f288a870158dc5a9f5c9d917b7250c7", size = 74723, upload-time = "2025-12-09T09:46:24.36Z" }, +] + [[package]] name = "humanfriendly" version = "10.0" @@ -741,13 +840,22 @@ name = "importlib-metadata" version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.12'" }, + { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, ] +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -766,6 +874,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/78/79461288da2b13ed0a13deb65c4ad1428acb674b95278fa9abf1cefe62a2/intelhex-2.3.0-py2.py3-none-any.whl", hash = "sha256:87cc5225657524ec6361354be928adfd56bcf2a3dcc646c40f8f094c39c07db4", size = 50914, upload-time = "2020-10-20T20:35:50.162Z" }, ] +[[package]] +name = "intervaltree" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/c3/b2afa612aa0373f3e6bb190e6de35f293b307d1537f109e3e25dbfcdf212/intervaltree-3.2.1.tar.gz", hash = "sha256:f3f7e8baeb7dd75b9f7a6d33cf3ec10025984a8e66e3016d537e52130c73cfe2", size = 1231531, upload-time = "2025-12-24T04:25:06.773Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/7f/8a80a1c7c2ed05822b5a2b312d2995f30c533641f8198366ba2e26a7bb03/intervaltree-3.2.1-py2.py3-none-any.whl", hash = "sha256:a8a8381bbd35d48ceebee932c77ffc988492d22fb1d27d0ba1d74a7694eb8f0b", size = 25929, upload-time = "2025-12-24T04:25:05.298Z" }, +] + [[package]] name = "ipykernel" version = "7.2.0" @@ -847,7 +967,8 @@ name = "ipython" version = "9.12.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.12'", + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, @@ -935,6 +1056,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "jsonlines" version = "4.0.0" @@ -1006,6 +1139,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + [[package]] name = "libusb" version = "1.0.29.post7" @@ -1020,6 +1162,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/15/e882ac7820ea0f2bf08ae19cfe79fb07e94659924476a83664a8c850ef21/libusb-1.0.29.post7-py3-none-any.whl", hash = "sha256:7917691dbf0db1031e8b274fc761659281fb9d5681820fb24f8aba8f34c71139", size = 745298, upload-time = "2026-02-16T08:22:39.495Z" }, ] +[[package]] +name = "libusb-package" +version = "1.0.26.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-resources" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/27/c989cd606683102170c2dc2e89771a22dc71b9d88c4d20c3a97f6d23a0a1/libusb_package-1.0.26.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ffa01d1db3ef9e7faa62b03f409cd077232885ae3fab6f95912db78035a41db", size = 63863, upload-time = "2025-04-01T12:59:12.09Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e4/663a37d23b3a47a641f74556bb42c04b26c46b95fb8a65c11421cb0ccb0d/libusb_package-1.0.26.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:95bbf9872674318a23450cd92053eee01683eeae6b6aa76eba30ee5f37c3765b", size = 59502, upload-time = "2025-04-01T12:59:13.489Z" }, + { url = "https://files.pythonhosted.org/packages/a7/93/c99ea3b13539c501c41e605645693346e08cfcb7747025ee640502f7460d/libusb_package-1.0.26.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36a1c48779c8763fc6bc0331fda668c93b58d55934236d0393d3ec026875f7cd", size = 70247, upload-time = "2025-04-01T14:53:00.28Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c4/84c7af13453e840ba2e3e9f247c9855035077a7214c1f0f273e1df5a845f/libusb_package-1.0.26.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:209efb9a78ac652afc2332b0a63ef2e423202fa3a1bebe5fe3c499e0922afc03", size = 74537, upload-time = "2025-04-01T14:53:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/933f70e9a1e05e0d25fb7fb6a5a4512ba7845203b11afc163cfdc98e9b88/libusb_package-1.0.26.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:433e89dd1f9f9a4149b975247cf1d493170454945fec54b4db9fe61c9e6b861f", size = 70652, upload-time = "2025-04-01T14:53:03.346Z" }, + { url = "https://files.pythonhosted.org/packages/ae/96/77f873c2a3a84b93439a973c70ecc53d2b9ae14cb45b4ba710b89d822228/libusb_package-1.0.26.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7e6fcad72db04b30c8495ac0df6a9b1a4ec8705930bfa2160cc9b018f14101a1", size = 71861, upload-time = "2025-04-01T14:53:04.507Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6e/64f83de4274e4a10ad28a03ff7879d3765c5d7efe0e5c833938318a7de20/libusb_package-1.0.26.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:65ee502c4999ded1c71e38769b0a89152c1e03e43b0d35919f3e32a8cbc7cd99", size = 76476, upload-time = "2025-04-01T14:53:05.511Z" }, + { url = "https://files.pythonhosted.org/packages/ad/56/3792e6a41776d85a4113e75c256879355261b5dd1ed22eb55fd8bc924125/libusb_package-1.0.26.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1f6d012df4942c91e6833dd251bd90c1242496a30c81020e43b98df85c66fa30", size = 71037, upload-time = "2025-04-01T14:53:06.642Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f2/99091fdc38e916cc9e254912d1131496ac56ab82506548f6e1fc2eea8429/libusb_package-1.0.26.3-cp310-cp310-win32.whl", hash = "sha256:55c3988f622745a4874ac4face117da19969b82d51250e5334cd176f516bcb57", size = 77642, upload-time = "2025-04-01T12:58:00.114Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9a/7bc9f60e563e535bf80b125d0d7541ad07ecb0160965d48cb8b6dccc2cf6/libusb_package-1.0.26.3-cp310-cp310-win_amd64.whl", hash = "sha256:ba5e87e70833e5fff977d7bf12b7107df427ee21a8021d59520e1fdf14a32368", size = 90594, upload-time = "2025-04-01T12:58:01.799Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bf/3fe9d322e2dcd0437ae2bd6a039117965702ed473ca59d2d6a1c39838009/libusb_package-1.0.26.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:60e15d7d3e4aab31794da95641bc28c4ffec9e24f50891ce33f75794b8f531f3", size = 63864, upload-time = "2025-04-01T12:59:14.567Z" }, + { url = "https://files.pythonhosted.org/packages/bc/70/df0348c11e6aaead4a66cc59840e102ddf64baf8e4b2c1ad5cff1ca83554/libusb_package-1.0.26.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d93a6137609cf72dc5db69bc337ddf96520231e395beeff69fa77a923090003", size = 59502, upload-time = "2025-04-01T12:59:15.863Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/6c84eebc9fcdf7f26704b5d32b51b3ee5bf4e9090d61286941257bdc8702/libusb_package-1.0.26.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fafb69c5fd42b241fbd20493d014328c507d34e1b7ceb883a20ef14565b26898", size = 70247, upload-time = "2025-04-01T14:53:07.606Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b4/cbcc42ca4b3d8778bf081b96e6e6288a437d82a4cc4e9b982bef40a88856/libusb_package-1.0.26.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c206cd8a30565a0cede3ba426929e70a37e7b769e41a5ac7f00ca6737dc5d", size = 74537, upload-time = "2025-04-01T14:53:08.61Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/fd203fb1fa5eda1d446f345d84205f23533767e6ef837a7c77a2599d5783/libusb_package-1.0.26.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80a2041331c087d5887969405837f86c8422120fe9ba3e6faa44bf4810f07b71", size = 70653, upload-time = "2025-04-01T14:53:09.576Z" }, + { url = "https://files.pythonhosted.org/packages/79/ef/dcc682cb4b29c4d4cdb23df65825c6276753184f6a7b4338c54a59a54c20/libusb_package-1.0.26.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:48b536a1279ee0dbf70b898cffd16cd661774d2c8bbec8ff7178a5bc20196af3", size = 71859, upload-time = "2025-04-01T14:53:10.987Z" }, + { url = "https://files.pythonhosted.org/packages/62/4d/323d5ac4ba339362e4b148c291fbc6e7ee04c6395d5fec967b32432db5c5/libusb_package-1.0.26.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3f273e33ff1810242f81ea3a0286e25887d99d99019ba83e08be0d1ca456cc05", size = 76476, upload-time = "2025-04-01T14:53:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3b/506db7f6cbe5dc2f38c14b272b8faf4d43e5559ac99d4dce1a41026ec925/libusb_package-1.0.26.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67476093601e1ea58a6130426795b906acd8d18d51c84e29a3a69548a5dfcf5d", size = 71037, upload-time = "2025-04-01T14:53:13.42Z" }, + { url = "https://files.pythonhosted.org/packages/3e/40/2538763c06e07bbbe0a5c8830779ef1ed1cea845264a91973bf31b9ecce5/libusb_package-1.0.26.3-cp311-cp311-win32.whl", hash = "sha256:8f3eed2852ee4f08847a221749a98d0f4f3962f8bed967e2253327db1171ba60", size = 77642, upload-time = "2025-04-01T12:58:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/a4/46/0cd5ea91c5bbe6293c0936c96915051e31750f72e9556718af666af3fe45/libusb_package-1.0.26.3-cp311-cp311-win_amd64.whl", hash = "sha256:b48b5f5b17c7ac5e315e233f9ee801f730aac6183eb53a3226b01245d7bcfe00", size = 90592, upload-time = "2025-04-01T12:58:04.103Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f6/83e13936b5799360eae8f0e31b5b298dd092451b91136d7cd13852777954/libusb_package-1.0.26.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c9404298485762a4e73b416e8a3208d33aa3274fb9b870c2a1cacba7e2918f19", size = 62045, upload-time = "2025-04-01T12:59:16.817Z" }, + { url = "https://files.pythonhosted.org/packages/33/97/86ed73880b6734c9383be5f34061b541e8fe5bd0303580b1f5abe2962d58/libusb_package-1.0.26.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8126f6711318dad4cb2805ea20cd47b895a847207087d8fdb032e082dd7a2e24", size = 59502, upload-time = "2025-04-01T12:59:17.72Z" }, + { url = "https://files.pythonhosted.org/packages/95/f7/27b67b8fe63450abf0b0b66aacf75d5d64cdf30317e214409ceb534f34b4/libusb_package-1.0.26.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11c219366e4a2368117b9a9807261f3506b5623531f8b8ce41af5bbaec8156a0", size = 70247, upload-time = "2025-04-01T14:53:14.387Z" }, + { url = "https://files.pythonhosted.org/packages/8c/11/613543f9c6dab5a82eefd0c78d52d08b5d9eb93a0362151fbedf74b32541/libusb_package-1.0.26.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8809a50d8ab84297344c54e862027090c0d73b14abef843a8b5f783313f49457", size = 74537, upload-time = "2025-04-01T14:53:15.345Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/5a2331615693b56221a902869fb2094d9a0b9a764a8706c8ba16e915f77c/libusb_package-1.0.26.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a83067c3dfdbb3856badb4532eaea22e8502b52ce4245f5ab46acf93d7fbd471", size = 70652, upload-time = "2025-04-01T14:53:16.319Z" }, + { url = "https://files.pythonhosted.org/packages/44/1a/186d4ec86421b69feb45e214edb5301fbcb9e8dc9df963678aeff1a447d5/libusb_package-1.0.26.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b56be087ea9cde8e50fb02740a4f0cefb6f63c61ac2e7812a9244487614a3973", size = 71860, upload-time = "2025-04-01T14:53:17.87Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3c/8cebdad822d7bfcb683a77d5fd113fbc6f72516cfb7c1c3a274fefafa8e9/libusb_package-1.0.26.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea0f6bf40e54b1671e763e40c9dbed46bf7f596a4cd98b7c827e147f176d8c97", size = 76476, upload-time = "2025-04-01T14:53:19.202Z" }, + { url = "https://files.pythonhosted.org/packages/49/5f/30c625b6c4ecd14871644c1d16e97d7c971f82a0f87a9cfa81022f85bcfc/libusb_package-1.0.26.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b40f77df991c6db8621de9575504886eca03a00277e521a4d64b66cbef8f6997", size = 71037, upload-time = "2025-04-01T14:53:21.359Z" }, + { url = "https://files.pythonhosted.org/packages/7f/e9/3aa3ff3242867b7f22ee3ce28d0e93ff88547f170ca1b8a6edc59660d974/libusb_package-1.0.26.3-cp312-cp312-win32.whl", hash = "sha256:6eee99c9fde137443869c8604d0c01b2127a9545ebc59d06a3376cf1d891e786", size = 77642, upload-time = "2025-04-01T12:58:05.471Z" }, + { url = "https://files.pythonhosted.org/packages/15/0e/913ddb1849f828fc385438874c34541939d9b06c0e5616f48f24cddd24de/libusb_package-1.0.26.3-cp312-cp312-win_amd64.whl", hash = "sha256:5e09c0b6b3cd475841cffe78e46e91df58f0c6c02ea105ea1a4d0755a07c8006", size = 90593, upload-time = "2025-04-01T12:58:06.798Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b8/23bc7f3f53b4a5b1027c721ec3eb42324ca1ec56355f0d0851307adc7c6c/libusb_package-1.0.26.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:04c4505e2ca68d3dc6938f116ff9bf82daffb06c1a97aba08293a84715a998da", size = 62045, upload-time = "2025-04-01T12:59:18.698Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f8/e3be96d0604070488ddc5ce5af1976992e1f4a00e6441c94edf807f274d5/libusb_package-1.0.26.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4961cdb3c622aa9f858d3e4f99a58ce5e822a97c22abc77040fd806cb5fa4c66", size = 59502, upload-time = "2025-04-01T12:59:19.632Z" }, + { url = "https://files.pythonhosted.org/packages/24/d5/df1508df5e6776ac8a09a2858991df29bc96ea6a0d1f90240b1c4d59b45d/libusb_package-1.0.26.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16182670e0c23235521b447054c5a01600bd8f1eed3bb08eedbb0d9f8a43249f", size = 70247, upload-time = "2025-04-01T14:53:22.328Z" }, + { url = "https://files.pythonhosted.org/packages/65/01/4cc9eed12b9214c088cfa8055ece3b1db970404400be9d7e3dda68d198f2/libusb_package-1.0.26.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75ea57b2cc903d28ec1d4b909902df442cbf21949d80d5b3d8b9dac36ac45d1a", size = 74537, upload-time = "2025-04-01T14:53:23.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/83/9eb317f706f588f4b6679bddb8abee3b115ce53dc3fa560cca59910f8807/libusb_package-1.0.26.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d30b51b128ef5112fff73268b4696fea00b5676b3f39a5ee859bd76cb3ace5", size = 70651, upload-time = "2025-04-01T14:53:24.33Z" }, + { url = "https://files.pythonhosted.org/packages/22/49/85d3b307b4a20cf0150ab381e6e0385e5b78cb5dede8bade0a2d655d3fd3/libusb_package-1.0.26.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5c098dcfcfa8000cab42f33e19628c8fdb16111670db381048b2993651f2413b", size = 71860, upload-time = "2025-04-01T14:53:25.752Z" }, + { url = "https://files.pythonhosted.org/packages/da/7a/2271a5ae542d9036d9254415ae745d5c5d01a08d56d13054b2439bf9d392/libusb_package-1.0.26.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:93169aeab0657255fe6c9f757cf408f559db13827a1d122fc89239994d7d51f1", size = 76477, upload-time = "2025-04-01T14:53:27.564Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9d/d06d53994bb164564ec142ef631a4afa31e324994cf223f169ecca127f3a/libusb_package-1.0.26.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:63257653ee1ee06aa836e942f4bb89a1d7a0c6ae3d6183647a9011e585ffa1e3", size = 71036, upload-time = "2025-04-01T14:53:29.011Z" }, + { url = "https://files.pythonhosted.org/packages/32/3d/97f775a1d582548b1eb2a42444c58813e5fd93d568fc3b9ace59f64df527/libusb_package-1.0.26.3-cp313-cp313-win32.whl", hash = "sha256:05db4cc801db2e6373a808725748a701509f9450fecf393fbebab61c45d50b50", size = 77642, upload-time = "2025-04-01T12:58:07.774Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c4/d5234607697ca60593fbef88428a154317ac31f5c58ee23337b8a9360e91/libusb_package-1.0.26.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cd4aec825dac2b4fa5d23b37f6d72e63a1127987e5a073dabeb7b73528623a3", size = 90593, upload-time = "2025-04-01T12:58:08.676Z" }, + { url = "https://files.pythonhosted.org/packages/9f/20/f5293a167b4e910badc64272131a8bb8dbd80f10dfd843eb07846aafaef2/libusb_package-1.0.26.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6a62bf7fa20fe704ed0413e74d620b37bdfe6b084478d23cc85b1f10708f2596", size = 62194, upload-time = "2025-04-01T12:59:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/28/e5/1ceae06e6c965847d89be36de58908354c35faf641cd4c6071c9f06a7e9b/libusb_package-1.0.26.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab40d6b295bcfbe37f280268ea0d0a1ef4b1d795025fe41b3dda48e07eb0fc8e", size = 59506, upload-time = "2025-04-01T12:59:27.081Z" }, + { url = "https://files.pythonhosted.org/packages/6e/74/8afb1a05fda665abebac3bb44a7738f23437cac11081e44a929b51afee6a/libusb_package-1.0.26.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a125d72cca545950ae357aa6d7f0f33dfb39f16b895691cf3f8c9b772bc7e31", size = 70255, upload-time = "2025-04-01T14:53:48.956Z" }, + { url = "https://files.pythonhosted.org/packages/35/46/5e6be05f302e887055a277bbb5cc1db6be9af01319b35f1a9663211b075c/libusb_package-1.0.26.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:229d9f26af5828512828154d6bae4214ef5016a9401dd022477e06d0df5153e7", size = 74543, upload-time = "2025-04-01T14:53:49.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/e4/51a81cc69ba4eefdd9a291cc5e6596a8f7d8c7f2378273917bf64465412d/libusb_package-1.0.26.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b8b862f190c244f29699d783f044e3d816fed84e39bca9a3c3731140f0b1b39", size = 70658, upload-time = "2025-04-01T14:53:51.132Z" }, + { url = "https://files.pythonhosted.org/packages/15/14/2c85379880d475f12ee74a27b02a2ffe435d863f8045fe80e5c246c30f23/libusb_package-1.0.26.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fadaad1181784713948f9cbb7ad1cab8f2b307e784e2e162ed80ba5d2f745901", size = 90602, upload-time = "2025-04-01T12:58:16.828Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -1045,6 +1243,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + [[package]] name = "matplotlib-inline" version = "0.2.1" @@ -1122,6 +1405,12 @@ dev = [ { name = "keyring" }, { name = "tornado" }, ] +perf = [ + { name = "scalene" }, +] +pyocd = [ + { name = "pyocd" }, +] test = [ { name = "coverage" }, { name = "distro" }, @@ -1166,6 +1455,7 @@ requires-dist = [ { name = "platformdirs", specifier = ">=4.2.0" }, { name = "psutil", specifier = ">=7.0.0,<8.0.0" }, { name = "pygithub", specifier = ">=2.1.1" }, + { name = "pyocd", marker = "extra == 'pyocd'", specifier = ">=0.36.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=7.1.2,<9.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=6.0.0" }, @@ -1178,12 +1468,13 @@ requires-dist = [ { name = "questionary", specifier = ">=2.0.0" }, { name = "requests", specifier = ">=2.31.0" }, { name = "rich-click", specifier = ">=1.8.1" }, + { name = "scalene", marker = "extra == 'perf'", specifier = ">=1.5.51" }, { name = "tenacity", specifier = "==9.0.0" }, { name = "tomli", specifier = ">=2.2.1" }, { name = "tomli-w", specifier = ">=1.2.0" }, { name = "tornado", marker = "extra == 'dev'", specifier = ">=6.5" }, ] -provides-extras = ["dev", "test"] +provides-extras = ["dev", "test", "perf", "pyocd"] [package.metadata.requires-dev] dev = [ @@ -1205,6 +1496,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/bf/cb9a7f38015c0fec0295b2cd014b830561f224d264f2f303c2ec15d8ef2f/mpremote-1.27.0-py3-none-any.whl", hash = "sha256:11d134c69b21b487dae3d03eed54c8ccbf84c916c8732a3e069a97cae47be3d4", size = 36094, upload-time = "2025-12-09T14:51:53.759Z" }, ] +[[package]] +name = "natsort" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, +] + [[package]] name = "nest-asyncio" version = "1.6.0" @@ -1214,6 +1514,164 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] +[[package]] +name = "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.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + +[[package]] +name = "nvidia-ml-py" +version = "13.595.45" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/49/c29f6e30d8662d2e94fef17739ea7309cc76aba269922ae999e4cc07f268/nvidia_ml_py-13.595.45.tar.gz", hash = "sha256:c9f34897fe0441ff35bc8f35baf80f830a20b0f4e6ce71e0a325bc0e66acf079", size = 50780, upload-time = "2026-03-19T16:59:44.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/24/fc256107d23597fa33d319505ce77160fa1a2349c096d01901ffc7cb7fc4/nvidia_ml_py-13.595.45-py3-none-any.whl", hash = "sha256:b65a7977f503d56154b14d683710125ef93594adb63fbf7e559336e3318f1376", size = 51776, upload-time = "2026-03-19T16:59:43.603Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -1286,6 +1744,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "prettytable" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/45/b0847d88d6cfeb4413566738c8bbf1e1995fad3d42515327ff32cc1eb578/prettytable-3.17.0.tar.gz", hash = "sha256:59f2590776527f3c9e8cf9fe7b66dd215837cca96a9c39567414cbc632e8ddb0", size = 67892, upload-time = "2025-11-14T17:33:20.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl", hash = "sha256:aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287", size = 34433, upload-time = "2025-11-14T17:33:19.093Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -1367,6 +1837,139 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + [[package]] name = "pyelftools" version = "0.32" @@ -1418,6 +2021,19 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pylink-square" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psutil" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/26/12865d2b7784d2ff687d6dc29eea38013688d47a5499df60010e1f13ba4f/pylink_square-1.7.0.tar.gz", hash = "sha256:9a4c4b1cf0cffedd15e00f82c3e23745252a5094c96b27560daac25a0d08aa36", size = 173502, upload-time = "2025-08-28T15:56:47.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/b8/db67ffbb71881b02c169b638bc53c9a1bf0d8f41828e3b48974e77f34419/pylink_square-1.7.0-py2.py3-none-any.whl", hash = "sha256:f418db3479b3b86e45f821f88cea5397357beed382eb441ab2fd11e1d4d9e1e3", size = 86908, upload-time = "2025-08-28T15:56:46.171Z" }, +] + [[package]] name = "pynacl" version = "1.6.2" @@ -1453,6 +2069,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, ] +[[package]] +name = "pyocd" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "capstone" }, + { name = "cmsis-pack-manager" }, + { name = "colorama" }, + { name = "hidapi", marker = "sys_platform != 'linux'" }, + { name = "importlib-metadata" }, + { name = "importlib-resources" }, + { name = "intelhex" }, + { name = "intervaltree" }, + { name = "lark" }, + { name = "libusb-package" }, + { name = "natsort" }, + { name = "prettytable" }, + { name = "pyelftools" }, + { name = "pylink-square" }, + { name = "pyusb" }, + { name = "pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/eb/f779689bae5ba430e725f9559301e82140c658c8598d356cfd1076002578/pyocd-0.44.0.tar.gz", hash = "sha256:9e7b8f8364c7d3d8db1d37360e7d749e3f68aa7dc94ddfe62863eb973ef53f47", size = 16356706, upload-time = "2026-04-01T11:50:09.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c5/9481e436573537b929a0bd5a7b8732861c006b4c014505626a52274e17bd/pyocd-0.44.0-py3-none-any.whl", hash = "sha256:5772c20c496168049986516dfcd4f2c2fb1023a508c06c327e24687dd9690c73", size = 14783170, upload-time = "2026-04-01T11:50:04.999Z" }, +] + [[package]] name = "pyproject-hooks" version = "1.2.0" @@ -1814,6 +2458,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/e5/d708d262b600a352abe01c2ae360d8ff75b0af819b78e9af293191d928e6/rich_click-1.9.7-py3-none-any.whl", hash = "sha256:2f99120fca78f536e07b114d3b60333bc4bb2a0969053b1250869bcdc1b5351b", size = 71491, upload-time = "2026-01-31T04:29:26.777Z" }, ] +[[package]] +name = "scalene" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "jinja2" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-ml-py", marker = "sys_platform != 'darwin'" }, + { name = "psutil" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/fc/ed550649058e9ad4bc262039c2c146c92e4d5d0b9d305c7d33d709c151f5/scalene-2.2.1.tar.gz", hash = "sha256:642f809e34d84cae5712bbe8cca76450af81f2dc4c02762b714d1710160dd791", size = 9483401, upload-time = "2026-03-22T14:49:36.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/06/a8e0c2f44536d2efb24a199849a7548c5e06ef4d8c380c360642b47e323e/scalene-2.2.1-cp310-cp310-macosx_15_0_universal2.whl", hash = "sha256:c292a31228eba3f79d82e1be53677a0319eb8b72631d58235fc912d41f73ee6d", size = 1225137, upload-time = "2026-03-22T14:50:27.548Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0c/a570acd34ae64f3fb68133e497b411e408af93b5cc00119e1bc24dc815db/scalene-2.2.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:444a5a9eab1ac35925363b4103acfeaf9f8d248f6a29edec31868798c43719c1", size = 1526264, upload-time = "2026-03-22T14:42:37.536Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2c/c20b56e2d30b3ffa689400007223810ffb72b7b75f3c87cac306eda6680d/scalene-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9198a1fe46b4d5d230a0642e45028a0c6eaa39333af654108085fcc6f97f812b", size = 1158148, upload-time = "2026-03-22T14:44:31.545Z" }, + { url = "https://files.pythonhosted.org/packages/a4/16/79de96af863a9df1ac9a3b30fc0c9bd51fe4754c137207b2ab3a1f7ebe2f/scalene-2.2.1-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:87be9856bd8b13d87e023ab5e96feae36fd6611912a5433e996e5c34288fe2a4", size = 1225210, upload-time = "2026-03-22T14:49:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/9118db832a7b8bf4eacf20273c5d9e4fe2554bda103712df35273ef193ad/scalene-2.2.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ff0507f7f176703cf4d3f51b4778e9282910699d8de717040cd4ac070aff97b", size = 1526990, upload-time = "2026-03-22T14:42:40.539Z" }, + { url = "https://files.pythonhosted.org/packages/95/3a/10e260f5465d77191affc6ef27c04a59451a47ef7d947957c85dc12ca346/scalene-2.2.1-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:e9625d2bf3ab1fef827f836e38134d3f56fc319513ac7632e45c36fd6c00d872", size = 1224979, upload-time = "2026-03-22T14:44:08.652Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b4/6765587712d20353f0d96951bee397e3e6303bc00f376d2561982b081efc/scalene-2.2.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eb22ef9c1bb8fb3df95365b6263a57e94eb81e777f550af1c513d7f32a50721", size = 1527444, upload-time = "2026-03-22T14:42:33.537Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/7c5b47a3ae695ab7110919b98d60f98c0ca1e6001f3a2e2a9c18bee5c711/scalene-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:de06f01ec97cae6939e5ff3a31e6ab0d96cfe3cf8a2146bea536a998ddac434c", size = 1158170, upload-time = "2026-03-22T14:44:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/85/f0/5697e94cf16a82999e6c5d456b457fd037ce406399b75070dafbe10173ea/scalene-2.2.1-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:6f94c608d7186ce3d6311bf5b7270c2ba69350f96141c8e913e5fcd272d00ef2", size = 1229689, upload-time = "2026-03-22T14:49:47.403Z" }, + { url = "https://files.pythonhosted.org/packages/ec/43/8e9bb41288713df00a8b1d0c9eabe00272cf4a93370992cbbfb26ef660f7/scalene-2.2.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6a72a3a55c3672779bcbd0d8278d9557ff99ef23d8d1b99b9cdcef3138c25b9", size = 1537230, upload-time = "2026-03-22T14:42:36.301Z" }, + { url = "https://files.pythonhosted.org/packages/a8/86/08024979ec47aed7fa26c8c5c644539c84c935cf06657c34f745a31defb7/scalene-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6adf5db81cc83b644fe0d60640b0b3097cfa4ff36911bc1e9f1833023930c99a", size = 1159794, upload-time = "2026-03-22T14:44:46.863Z" }, + { url = "https://files.pythonhosted.org/packages/4b/38/6ee3bbd6eb983bf6f25427b4110b2f2d34359e78fbb02b79b4e9c11ca565/scalene-2.2.1-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:7e13ceaaea670c385825b9b21b58596f86f4d0b6e2a0927eefa9dcbf1444380b", size = 1229918, upload-time = "2026-03-22T14:49:37.607Z" }, + { url = "https://files.pythonhosted.org/packages/94/1a/0e7a49db2440c31e66479c93567cc4e4f0198dbe413699a2de9d2095a8e3/scalene-2.2.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:edb511df0097d3091b4967c70023698306e5fa2c75e9532216904b882e3bd02e", size = 1537041, upload-time = "2026-03-22T14:42:40.067Z" }, + { url = "https://files.pythonhosted.org/packages/c8/13/a11f6b081c7235a66da71c5230adf5a7a87ef92a6cd98c659494ec20d22e/scalene-2.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:8af3834504d3a4f4ebaa489bb9266563be818972c83a826337936b6809c145e7", size = 1165782, upload-time = "2026-03-22T14:45:13.804Z" }, +] + [[package]] name = "secretstorage" version = "3.5.0" @@ -1836,6 +2513,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "soupsieve" version = "2.8.3" @@ -2004,6 +2690,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + [[package]] name = "typish" version = "1.9.3"