diff --git a/.github/workflows/ports_windows.yml b/.github/workflows/ports_windows.yml index 5318a85198153..3a7a38df3226f 100644 --- a/.github/workflows/ports_windows.yml +++ b/.github/workflows/ports_windows.yml @@ -25,23 +25,24 @@ jobs: platform: [x86, x64] configuration: [Debug, Release] variant: [dev, standard] - visualstudio: ['2017', '2019', '2022'] + visualstudio: ['2019', '2022'] include: - - visualstudio: '2017' - vs_version: '[15, 16)' - custom_vs_install: true - visualstudio: '2019' - vs_version: '[16, 17)' - custom_vs_install: true + # The v142 toolset (VS 2019 compiler) is pre-installed on + # windows-2022 as a component of VS 2022. Use VS 2022's + # MSBuild and select the v142 toolset via PlatformToolset. + vs_version: '[17, 18)' + platform_toolset: v142 - visualstudio: '2022' vs_version: '[17, 18)' + platform_toolset: v143 # trim down the number of jobs in the matrix exclude: - variant: standard configuration: Debug - visualstudio: '2019' configuration: Debug - runs-on: windows-latest + runs-on: windows-2022 env: CI_BUILD_CONFIGURATION: ${{ matrix.configuration }} steps: @@ -51,31 +52,20 @@ jobs: uses: actions/setup-python@v6 with: python-version: '3.11' - - name: Install Visual Studio ${{ matrix.visualstudio }} - if: matrix.custom_vs_install - shell: bash - # Shell functions in this block are to retry intermittent corrupt - # downloads (with a clean download dir) before failing the job outright - run: | - try () { ($@) || ($@) || ($@) || ($@) } - clean_install () ( rm -rf $TEMP/chocolatey; choco install $1 ) - try clean_install visualstudio${{ matrix.visualstudio }}buildtools - try clean_install visualstudio${{ matrix.visualstudio }}-workload-vctools - try clean_install windows-sdk-8.1 - uses: microsoft/setup-msbuild@v2 with: vs-version: ${{ matrix.vs_version }} - uses: actions/checkout@v6 - name: Build mpy-cross.exe - run: msbuild mpy-cross\mpy-cross.vcxproj -maxcpucount -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} + run: msbuild mpy-cross\mpy-cross.vcxproj -maxcpucount -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PlatformToolset=${{ matrix.platform_toolset }} - name: Update submodules run: git submodule update --init lib/micropython-lib - name: Build micropython.exe - run: msbuild ports\windows\micropython.vcxproj -maxcpucount -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PyVariant=${{ matrix.variant }} + run: msbuild ports\windows\micropython.vcxproj -maxcpucount -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PyVariant=${{ matrix.variant }} -property:PlatformToolset=${{ matrix.platform_toolset }} - name: Get micropython.exe path id: get_path run: | - $exePath="$(msbuild ports\windows\micropython.vcxproj -nologo -v:m -t:ShowTargetPath -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PyVariant=${{ matrix.variant }})" + $exePath="$(msbuild ports\windows\micropython.vcxproj -nologo -v:m -t:ShowTargetPath -property:Configuration=${{ matrix.configuration }} -property:Platform=${{ matrix.platform }} -property:PyVariant=${{ matrix.variant }} -property:PlatformToolset=${{ matrix.platform_toolset }})" echo ("micropython=" + $exePath.Trim()) >> $env:GITHUB_OUTPUT - name: Run tests id: test diff --git a/tools/mpy-triage/.gitignore b/tools/mpy-triage/.gitignore new file mode 100644 index 0000000000000..547d4be83364f --- /dev/null +++ b/tools/mpy-triage/.gitignore @@ -0,0 +1,6 @@ +*.egg-info/ +__pycache__/ +.venv/ +*.pyc +data/*.db +.pytest_cache/ diff --git a/tools/mpy-triage/data/schema.sql b/tools/mpy-triage/data/schema.sql new file mode 100644 index 0000000000000..7c05d023bd3e1 --- /dev/null +++ b/tools/mpy-triage/data/schema.sql @@ -0,0 +1,31 @@ +-- MicroPython Issue Triage Database Schema + +-- Issues and PRs +CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY, + number INTEGER NOT NULL, + repo TEXT NOT NULL DEFAULT 'micropython/micropython', + type TEXT NOT NULL, -- 'issue' or 'pr' + title TEXT, + body TEXT, + author TEXT, + state TEXT, -- open, closed + labels TEXT, -- JSON array of label names + created_at TEXT, + closed_at TEXT, + updated_at TEXT, + UNIQUE(repo, number) +); + +-- Sync state for incremental updates +CREATE TABLE IF NOT EXISTS sync_state ( + key TEXT PRIMARY KEY, + value TEXT +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_items_repo ON items(repo); +CREATE INDEX IF NOT EXISTS idx_items_type ON items(type); +CREATE INDEX IF NOT EXISTS idx_items_state ON items(state); +CREATE INDEX IF NOT EXISTS idx_items_created ON items(created_at); +CREATE INDEX IF NOT EXISTS idx_items_repo_number ON items(repo, number); diff --git a/tools/mpy-triage/pyproject.toml b/tools/mpy-triage/pyproject.toml new file mode 100644 index 0000000000000..9ea9ddbd2e302 --- /dev/null +++ b/tools/mpy-triage/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "mpy-triage" +version = "0.1.0" +description = "CLI tool for detecting duplicate/related MicroPython issues and PRs" +requires-python = ">=3.10" +license = {text = "MIT"} + +dependencies = [ + "sqlite-vec>=0.1.6", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "ruff>=0.4.0", +] + +[project.scripts] +mpy-triage = "mpy_triage.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.ruff] +line-length = 99 + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/tools/mpy-triage/src/mpy_triage/__init__.py b/tools/mpy-triage/src/mpy_triage/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tools/mpy-triage/src/mpy_triage/cli.py b/tools/mpy-triage/src/mpy_triage/cli.py new file mode 100644 index 0000000000000..a4bee8ae8bf50 --- /dev/null +++ b/tools/mpy-triage/src/mpy_triage/cli.py @@ -0,0 +1,29 @@ +"""CLI entry point for mpy-triage.""" + +import argparse +import logging + + +def main() -> None: + """Main entry point for the mpy-triage CLI.""" + parser = argparse.ArgumentParser( + prog="mpy-triage", + description="Detect duplicate and related MicroPython issues/PRs using semantic search.", + ) + parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging.") + parser.add_argument( + "--db", + default="data/triage.db", + help="Path to the SQLite database (default: data/triage.db).", + ) + + _args = parser.parse_args() + + logging.basicConfig( + level=logging.DEBUG if _args.verbose else logging.INFO, + format="%(levelname)s: %(message)s", + ) + + +if __name__ == "__main__": + main() diff --git a/tools/mpy-triage/src/mpy_triage/db.py b/tools/mpy-triage/src/mpy_triage/db.py new file mode 100644 index 0000000000000..08c8d65ae51ef --- /dev/null +++ b/tools/mpy-triage/src/mpy_triage/db.py @@ -0,0 +1,85 @@ +"""Database layer for mpy-triage. + +Manages SQLite connections, schema initialization, sync state, and +sqlite-vec extension loading. +""" + +import logging +import sqlite3 +from pathlib import Path + +log = logging.getLogger(__name__) + + +def get_connection(db_path: Path) -> sqlite3.Connection: + """Open an SQLite connection with Row factory and WAL mode. + + Args: + db_path: Path to the SQLite database file. + + Returns: + An open sqlite3.Connection. + """ + conn = sqlite3.connect(str(db_path)) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + return conn + + +def init_db(conn: sqlite3.Connection, schema_path: Path) -> None: + """Read and execute a schema SQL file against the connection. + + Safe to call multiple times (uses IF NOT EXISTS in schema). + + Args: + conn: An open sqlite3 connection. + schema_path: Path to the .sql schema file. + """ + schema_path = Path(schema_path) + schema_sql = schema_path.read_text() + conn.executescript(schema_sql) + conn.commit() + + +def get_sync_state(conn: sqlite3.Connection, key: str) -> str | None: + """Retrieve a value from the sync_state table. + + Args: + conn: An open sqlite3 connection. + key: The sync state key to look up. + + Returns: + The stored value string, or None if the key is not found. + """ + cursor = conn.execute("SELECT value FROM sync_state WHERE key = ?", (key,)) + row = cursor.fetchone() + return row[0] if row else None + + +def set_sync_state(conn: sqlite3.Connection, key: str, value: str) -> None: + """Insert or update a value in the sync_state table. + + Args: + conn: An open sqlite3 connection. + key: The sync state key. + value: The value to store. + """ + conn.execute( + "INSERT OR REPLACE INTO sync_state (key, value) VALUES (?, ?)", + (key, value), + ) + conn.commit() + + +def load_vec_extension(conn: sqlite3.Connection) -> None: + """Load the sqlite-vec extension into the connection. + + Args: + conn: An open sqlite3 connection. + """ + import sqlite_vec + + conn.enable_load_extension(True) + sqlite_vec.load(conn) + conn.enable_load_extension(False) + log.info("sqlite-vec extension loaded") diff --git a/tools/mpy-triage/src/mpy_triage/gh.py b/tools/mpy-triage/src/mpy_triage/gh.py new file mode 100644 index 0000000000000..1330310be4db9 --- /dev/null +++ b/tools/mpy-triage/src/mpy_triage/gh.py @@ -0,0 +1,167 @@ +"""GitHub API wrapper for mpy-triage. + +Uses the ``gh`` CLI tool for authenticated access. Handles pagination, +rate-limit back-off, and search queries. +""" + +import json +import logging +import re +import subprocess +import time +from urllib.parse import quote + +log = logging.getLogger(__name__) + +# Rate limiting: 5000 requests/hour for authenticated users. +REQUESTS_PER_HOUR = 5000 +REQUEST_DELAY: float = 3600 / REQUESTS_PER_HOUR # ~0.72s +_last_request_time: float = 0.0 + + +def gh_api( + endpoint: str, + *, + paginate: bool = False, + accept: str = "application/vnd.github+json", + method: str | None = None, +) -> list | dict | str | None: + """Call the GitHub API via the ``gh`` CLI. + + Args: + endpoint: API endpoint path (e.g. ``repos/owner/repo/issues``). + paginate: If True, pass ``--paginate`` and merge concatenated JSON arrays. + accept: Value for the Accept header. + method: HTTP method override (e.g. ``GET``, ``POST``). Omitted when None. + + Returns: + Parsed JSON (list or dict), raw text for non-JSON accept types, or + None on error. + """ + global _last_request_time + + cmd = ["gh", "api", "-H", f"Accept: {accept}"] + if paginate: + cmd.append("--paginate") + if method is not None: + cmd.extend(["--method", method]) + cmd.append(endpoint) + + while True: + # Enforce minimum delay between requests to stay under rate limit. + elapsed = time.time() - _last_request_time + if elapsed < REQUEST_DELAY: + time.sleep(REQUEST_DELAY - elapsed) + + result = subprocess.run(cmd, capture_output=True, text=True) + _last_request_time = time.time() + + if result.returncode != 0: + # Check for rate limiting (HTTP 403 or 429). + stderr = result.stderr + if "403" in stderr or "429" in stderr: + reset_ts = _parse_rate_limit_reset(stderr) + if reset_ts is not None: + wait = max(0, reset_ts - time.time()) + 1 + log.warning("Rate limited. Sleeping %.0f seconds until reset.", wait) + time.sleep(wait) + continue + log.error("gh api error (rc=%d): %s", result.returncode, result.stderr) + return None + + break + + if not result.stdout.strip(): + return [] if paginate else None + + # Non-JSON responses (e.g. diff). + if "json" not in accept: + return result.stdout + + # Paginated responses: gh --paginate concatenates multiple JSON arrays. + if paginate: + return _parse_concatenated_json(result.stdout) + + return json.loads(result.stdout) + + +def gh_search(query: str, *, date_range: tuple[str, str] | None = None) -> list[dict]: + """Search GitHub issues/PRs via the search API. + + Args: + query: Search query string (e.g. ``repo:micropython/micropython is:issue``). + date_range: Optional (start, end) date strings in ``YYYY-MM-DD`` format. + Adds a ``created:start..end`` qualifier to the query. + + Returns: + List of item dicts from the search results. + """ + q = query + if date_range is not None: + start, end = date_range + q += f" created:{start}..{end}" + + endpoint = f"search/issues?q={quote(q)}&per_page=100&sort=updated&order=desc" + result = gh_api(endpoint) + if result is None or not isinstance(result, dict): + return [] + return result.get("items", []) + + +def gh_diff(repo: str, pr_number: int) -> str | None: + """Fetch the diff for a pull request. + + Args: + repo: Repository in ``owner/name`` format. + pr_number: PR number. + + Returns: + The diff as a string, or None on error. + """ + result = gh_api( + f"repos/{repo}/pulls/{pr_number}", + accept="application/vnd.github.diff", + ) + if isinstance(result, str): + return result + return None + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + + +def _parse_concatenated_json(text: str) -> list: + """Parse potentially concatenated JSON arrays from ``gh --paginate``. + + When paginating, ``gh`` concatenates raw JSON responses without a + separator. Two consecutive arrays like ``[1,2][3,4]`` must be merged + into ``[1,2,3,4]``. + """ + data: list = [] + decoder = json.JSONDecoder() + text = text.strip() + pos = 0 + while pos < len(text): + try: + obj, end = decoder.raw_decode(text, pos) + if isinstance(obj, list): + data.extend(obj) + else: + data.append(obj) + pos = end + # Skip whitespace between concatenated values. + while pos < len(text) and text[pos] in " \t\n\r": + pos += 1 + except json.JSONDecodeError: + break + return data + + +def _parse_rate_limit_reset(stderr: str) -> float | None: + """Extract ``X-RateLimit-Reset`` epoch timestamp from gh stderr output.""" + match = re.search(r"X-RateLimit-Reset:\s*(\d+)", stderr, re.IGNORECASE) + if match: + return float(match.group(1)) + return None diff --git a/tools/mpy-triage/tests/__init__.py b/tools/mpy-triage/tests/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tools/mpy-triage/tests/test_db.py b/tools/mpy-triage/tests/test_db.py new file mode 100644 index 0000000000000..8a1f8f003b15e --- /dev/null +++ b/tools/mpy-triage/tests/test_db.py @@ -0,0 +1,69 @@ +"""Tests for the database layer.""" + +import sqlite3 +from pathlib import Path + +import pytest + +from mpy_triage.db import get_connection, get_sync_state, init_db, set_sync_state + +SCHEMA_PATH = Path(__file__).resolve().parent.parent / "data" / "schema.sql" + + +@pytest.fixture() +def tmp_db(tmp_path: Path) -> sqlite3.Connection: + """Create a temporary database with schema applied.""" + db_path = tmp_path / "test.db" + conn = get_connection(db_path) + init_db(conn, SCHEMA_PATH) + return conn + + +def test_schema_creates_tables(tmp_db: sqlite3.Connection) -> None: + """init_db should create the expected tables.""" + cursor = tmp_db.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") + tables = {row[0] for row in cursor.fetchall()} + assert "items" in tables + assert "sync_state" in tables + + +def test_wal_mode(tmp_path: Path) -> None: + """get_connection should enable WAL journal mode.""" + conn = get_connection(tmp_path / "wal.db") + mode = conn.execute("PRAGMA journal_mode").fetchone()[0] + assert mode == "wal" + + +def test_row_factory(tmp_path: Path) -> None: + """get_connection should set row_factory to sqlite3.Row.""" + conn = get_connection(tmp_path / "row.db") + assert conn.row_factory is sqlite3.Row + + +def test_sync_state_roundtrip(tmp_db: sqlite3.Connection) -> None: + """set_sync_state then get_sync_state should return the stored value.""" + set_sync_state(tmp_db, "last_updated", "2024-01-15") + assert get_sync_state(tmp_db, "last_updated") == "2024-01-15" + + +def test_sync_state_overwrite(tmp_db: sqlite3.Connection) -> None: + """set_sync_state should overwrite an existing key.""" + set_sync_state(tmp_db, "cursor", "abc") + set_sync_state(tmp_db, "cursor", "def") + assert get_sync_state(tmp_db, "cursor") == "def" + + +def test_get_sync_state_missing(tmp_db: sqlite3.Connection) -> None: + """get_sync_state should return None for a key that does not exist.""" + assert get_sync_state(tmp_db, "nonexistent") is None + + +def test_idempotent_init_db(tmp_path: Path) -> None: + """Calling init_db twice should not raise an error.""" + db_path = tmp_path / "idem.db" + conn = get_connection(db_path) + init_db(conn, SCHEMA_PATH) + init_db(conn, SCHEMA_PATH) # second call should be fine + cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") + tables = {row[0] for row in cursor.fetchall()} + assert "sync_state" in tables diff --git a/tools/mpy-triage/tests/test_gh.py b/tools/mpy-triage/tests/test_gh.py new file mode 100644 index 0000000000000..e6a9309f58d13 --- /dev/null +++ b/tools/mpy-triage/tests/test_gh.py @@ -0,0 +1,194 @@ +"""Tests for the GitHub API wrapper.""" + +import json +import subprocess +import time +from unittest.mock import patch + +from mpy_triage import gh as gh_mod +from mpy_triage.gh import ( + _parse_concatenated_json, + _parse_rate_limit_reset, + gh_api, + gh_diff, + gh_search, +) + + +class TestParseConcatenatedJson: + """Unit tests for the JSON multiparse helper.""" + + def test_single_array(self) -> None: + assert _parse_concatenated_json("[1, 2, 3]") == [1, 2, 3] + + def test_two_arrays_concatenated(self) -> None: + text = '[{"a":1},{"a":2}][{"a":3}]' + result = _parse_concatenated_json(text) + assert result == [{"a": 1}, {"a": 2}, {"a": 3}] + + def test_arrays_with_whitespace(self) -> None: + text = "[1, 2]\n\n[3, 4]\n" + assert _parse_concatenated_json(text) == [1, 2, 3, 4] + + def test_single_object(self) -> None: + text = '{"key": "val"}' + assert _parse_concatenated_json(text) == [{"key": "val"}] + + def test_empty_string(self) -> None: + assert _parse_concatenated_json("") == [] + + +class TestParseRateLimitReset: + """Unit tests for rate-limit header parsing.""" + + def test_extracts_timestamp(self) -> None: + stderr = "HTTP 403\nX-RateLimit-Reset: 1700000000\nsome other text" + assert _parse_rate_limit_reset(stderr) == 1700000000.0 + + def test_returns_none_when_missing(self) -> None: + assert _parse_rate_limit_reset("HTTP 500 Internal Server Error") is None + + +def _make_completed_process( + stdout: str = "", stderr: str = "", returncode: int = 0 +) -> subprocess.CompletedProcess: + return subprocess.CompletedProcess( + args=["gh", "api"], stdout=stdout, stderr=stderr, returncode=returncode + ) + + +class TestGhApi: + """Tests for gh_api using mocked subprocess.run.""" + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("subprocess.run") + def test_simple_get(self, mock_run) -> None: + payload = {"id": 1, "title": "test"} + mock_run.return_value = _make_completed_process(stdout=json.dumps(payload)) + + result = gh_api("repos/owner/repo/issues/1") + + assert result == payload + mock_run.assert_called_once() + cmd = mock_run.call_args[0][0] + assert "repos/owner/repo/issues/1" in cmd + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("subprocess.run") + def test_paginate_merges_arrays(self, mock_run) -> None: + page1 = [{"n": 1}] + page2 = [{"n": 2}] + stdout = json.dumps(page1) + json.dumps(page2) + mock_run.return_value = _make_completed_process(stdout=stdout) + + result = gh_api("repos/owner/repo/issues", paginate=True) + + assert result == [{"n": 1}, {"n": 2}] + cmd = mock_run.call_args[0][0] + assert "--paginate" in cmd + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("subprocess.run") + def test_error_returns_none(self, mock_run) -> None: + mock_run.return_value = _make_completed_process(returncode=1, stderr="Not Found") + assert gh_api("repos/owner/repo/nonexistent") is None + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("subprocess.run") + def test_empty_response_paginate(self, mock_run) -> None: + mock_run.return_value = _make_completed_process(stdout=" ") + assert gh_api("repos/owner/repo/issues", paginate=True) == [] + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("subprocess.run") + def test_empty_response_no_paginate(self, mock_run) -> None: + mock_run.return_value = _make_completed_process(stdout=" ") + assert gh_api("repos/owner/repo/issues") is None + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("time.sleep") + @patch("subprocess.run") + def test_rate_limit_retry(self, mock_run, mock_sleep) -> None: + """On 403 with rate-limit header, gh_api should sleep and retry.""" + reset_time = int(time.time()) + 5 + rate_stderr = f"HTTP 403\nX-RateLimit-Reset: {reset_time}\n" + mock_run.side_effect = [ + _make_completed_process(returncode=1, stderr=rate_stderr), + _make_completed_process(stdout='{"ok":true}'), + ] + + result = gh_api("repos/owner/repo/issues") + + assert result == {"ok": True} + assert mock_run.call_count == 2 + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("subprocess.run") + def test_custom_accept_header(self, mock_run) -> None: + mock_run.return_value = _make_completed_process(stdout="diff --git a/f b/f\n") + + result = gh_api("repos/owner/repo/pulls/1", accept="application/vnd.github.diff") + + assert result == "diff --git a/f b/f\n" + cmd = mock_run.call_args[0][0] + assert any("application/vnd.github.diff" in arg for arg in cmd) + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("subprocess.run") + def test_method_override(self, mock_run) -> None: + mock_run.return_value = _make_completed_process(stdout="{}") + gh_api("repos/owner/repo/issues", method="POST") + cmd = mock_run.call_args[0][0] + assert "--method" in cmd + assert "POST" in cmd + + +class TestGhSearch: + """Tests for gh_search.""" + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("subprocess.run") + def test_returns_items(self, mock_run) -> None: + payload = {"items": [{"number": 1}, {"number": 2}], "total_count": 2} + mock_run.return_value = _make_completed_process(stdout=json.dumps(payload)) + + result = gh_search("repo:micropython/micropython is:issue") + assert len(result) == 2 + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("subprocess.run") + def test_date_range_added(self, mock_run) -> None: + payload = {"items": [], "total_count": 0} + mock_run.return_value = _make_completed_process(stdout=json.dumps(payload)) + + gh_search("repo:micropython/micropython", date_range=("2024-01-01", "2024-06-30")) + cmd_str = " ".join(mock_run.call_args[0][0]) + assert ( + "created%3A2024-01-01..2024-06-30" in cmd_str + or "created:2024-01-01..2024-06-30" in cmd_str + ) + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("subprocess.run") + def test_error_returns_empty(self, mock_run) -> None: + mock_run.return_value = _make_completed_process(returncode=1, stderr="err") + assert gh_search("bad query") == [] + + +class TestGhDiff: + """Tests for gh_diff.""" + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("subprocess.run") + def test_returns_diff_string(self, mock_run) -> None: + diff_text = "diff --git a/file.c b/file.c\n+hello\n" + mock_run.return_value = _make_completed_process(stdout=diff_text) + + result = gh_diff("micropython/micropython", 42) + assert result == diff_text + + @patch.object(gh_mod, "REQUEST_DELAY", 0) + @patch("subprocess.run") + def test_error_returns_none(self, mock_run) -> None: + mock_run.return_value = _make_completed_process(returncode=1, stderr="err") + assert gh_diff("micropython/micropython", 999) is None diff --git a/tools/mpy-triage/uv.lock b/tools/mpy-triage/uv.lock new file mode 100644 index 0000000000000..b2acebb31aef7 --- /dev/null +++ b/tools/mpy-triage/uv.lock @@ -0,0 +1,198 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + +[[package]] +name = "mpy-triage" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "sqlite-vec" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4.0" }, + { name = "sqlite-vec", specifier = ">=0.1.6" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 }, +] + +[[package]] +name = "ruff" +version = "0.15.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037 }, + { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433 }, + { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302 }, + { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625 }, + { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743 }, + { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536 }, + { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292 }, + { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981 }, + { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422 }, + { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158 }, + { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861 }, + { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310 }, + { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752 }, + { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961 }, + { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538 }, + { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839 }, + { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304 }, +] + +[[package]] +name = "sqlite-vec" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/50/7ad59cfd3003a2110cc366e526293de4c2520486f5ddaa8dc78b265f8d3e/sqlite_vec-0.1.7-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:c34a136caecff4ae17d4c0cc268fcda89764ee870039caa21431e8e3fb2f4d48", size = 131171 }, + { url = "https://files.pythonhosted.org/packages/e6/c9/1cd2f59b539096cd2ce6b540247b2dfe3c47ba04d9368b5e8e3dc86498d4/sqlite_vec-0.1.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d272593d1b45ec7ea289b160ee6e5fafbaa6e1f5ba15f1305c012b0bda43653", size = 165434 }, + { url = "https://files.pythonhosted.org/packages/75/91/30c3c382140dcc7bc6e3a07eac7ca610a2b5b70eb9bc7066dc3e7f748d58/sqlite_vec-0.1.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d27746d8e254a390bd15574aed899a0b9bb915b5321eb130a9c09722898cc03", size = 160076 }, + { url = "https://files.pythonhosted.org/packages/59/56/6ff304d917ee79da769708dad0aed5fd34c72cbd0ae5e38bcc56cdc652a4/sqlite_vec-0.1.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:ad654283cb9c059852ce2d82018c757b06a705ada568f8b126022a131189818e", size = 163388 }, + { url = "https://files.pythonhosted.org/packages/8b/27/fb1b6e3f9072854fe405f7aa99c46d4b465e84c9cec2ff7778edf29ecbbd/sqlite_vec-0.1.7-py3-none-win_amd64.whl", hash = "sha256:0c67877a87cb49426237b950237e82dbeb77778ab2ba89bea859f391fd169382", size = 292804 }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704 }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454 }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561 }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824 }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227 }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859 }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204 }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084 }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285 }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924 }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018 }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948 }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341 }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159 }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290 }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141 }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847 }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088 }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866 }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887 }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704 }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628 }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180 }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674 }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976 }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755 }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265 }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726 }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859 }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713 }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084 }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973 }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223 }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973 }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082 }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490 }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263 }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736 }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717 }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461 }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855 }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144 }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683 }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196 }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393 }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +]