From 3c11528c3cdf4ea4fc09ceaf9617070aa22046e1 Mon Sep 17 00:00:00 2001 From: Bryn Date: Thu, 23 Jul 2026 22:39:11 -0500 Subject: [PATCH] Switch Windows packaging from Nuitka to PyInstaller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Nuitka standalone build shipped an Add Source dialog whose OK/Cancel buttons were not visible — the frozen app rendered the dialog differently than running from source (a DPI/manifest difference in the freeze, not a bug in the dialog code, which lays out correctly from source and under PyInstaller). PyInstaller freezes the same code without that problem. While here, this also fixes native binaries never being bundled: Nuitka's --include-data-dir silently skips .dll/.exe files, so audio_hook32.dll, audio_hook64.dll, and injector32.exe never shipped and LegacyCapture was broken in every release. PyInstaller's --add-data copies them into native/dist/ where capture_legacy.py looks. - build.py: rewritten to drive PyInstaller (one-dir, windowed by default, --console for debugging); bundles data dirs, native binaries, and the packages PyInstaller's hooks miss. - CI: build with PyInstaller; artifact/release now from dist/PubStreamer/. - pyproject: swap the nuitka dev dep for pyinstaller. - README: document the PyInstaller build. Co-Authored-By: Claude Fable 5 --- .github/workflows/build.yml | 28 ++------ .gitignore | 1 - README.md | 10 ++- build.py | 123 +++++++++++++++--------------------- pyproject.toml | 2 +- uv.lock | 98 +++++++++++++++++++++++++--- 6 files changed, 153 insertions(+), 109 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0fba68e..98a2534 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,12 +13,6 @@ jobs: build: runs-on: windows-latest - env: - # Nuitka stores the downloaded MinGW toolchain and its ccache results - # here; caching this dir is the single biggest CI speedup. Forward - # slashes are fine on Windows for Nuitka. - NUITKA_CACHE_DIR: ${{ github.workspace }}/nuitka-cache - steps: - uses: actions/checkout@v4 @@ -33,11 +27,10 @@ jobs: run: uv python install 3.12 - name: Install dependencies + # Installs the dev group too (PyInstaller), which build.py imports. run: uv sync --all-extras # Caches the native CMake build trees so unchanged C isn't recompiled. - # Marginal next to Nuitka; drop this step if it ever causes a stale - # "compiler changed" CMake error (just bump/clear the key). - name: Cache native build trees uses: actions/cache@v4 with: @@ -57,33 +50,20 @@ jobs: MSBUILDDISABLENODEREUSE: "1" run: ./build_native.ps1 - # ccache + the downloaded MinGW toolchain live under NUITKA_CACHE_DIR. - # The SHA in the key makes every run save a fresh cache; restore-keys - # fall back to the most recent one so ccache stays warm even when - # dependencies change. - - name: Cache Nuitka build cache - uses: actions/cache@v4 - with: - path: ${{ env.NUITKA_CACHE_DIR }} - key: nuitka-${{ runner.os }}-${{ hashFiles('uv.lock') }}-${{ github.sha }} - restore-keys: | - nuitka-${{ runner.os }}-${{ hashFiles('uv.lock') }}- - nuitka-${{ runner.os }}- - - - name: Build with Nuitka + - name: Build with PyInstaller run: uv run python build.py - name: Upload build artifact uses: actions/upload-artifact@v4 with: name: PubStreamer-windows - path: dist/main.dist/ + path: dist/PubStreamer/ if-no-files-found: error # Create a GitHub Release and attach the build when a version tag is pushed. - name: Zip release folder if: startsWith(github.ref, 'refs/tags/') - run: Compress-Archive -Path dist/main.dist/* -DestinationPath dist/PubStreamer-${{ github.ref_name }}-windows.zip + run: Compress-Archive -Path dist/PubStreamer/* -DestinationPath dist/PubStreamer-${{ github.ref_name }}-windows.zip - name: Create release if: startsWith(github.ref, 'refs/tags/') diff --git a/.gitignore b/.gitignore index 9e682d0..3d55faa 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ native/**/Makefile /test_*.py /*.wav C*Temptest*.txt -nuitka-crash-report.xml hook_audio.obj *.obj diff --git a/README.md b/README.md index c2767be..3146cbd 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,19 @@ Releases are built on GitHub Actions. Push a `v*` tag to trigger a build and cre git tag v1.0.0 && git push origin v1.0.0 ``` -The workflow uses Nuitka with MinGW on `windows-latest` and produces a standalone `PubStreamer.dist/` folder. MSVC is not used because certain generated C files (pyasn1, requests) exceed its internal heap limit on constrained machines. +The workflow uses PyInstaller on `windows-latest` and produces a one-dir standalone under `dist/PubStreamer/`, with the launcher at `dist/PubStreamer/PubStreamer.exe`. -To build locally, install MinGW and run: +To build locally: ``` uv run python build.py ``` -Set `GITHUB_ACTIONS=true` in the environment or edit `build.py` to pass `--mingw64` explicitly. +Build the native components first (see below) so the audio hook DLLs and `injector32.exe` get bundled. For a build with a console attached (so tracebacks are visible while debugging), pass `--console`: + +``` +uv run python build.py --console +``` ## Audio sources diff --git a/build.py b/build.py index a8a3901..e4b6db7 100644 --- a/build.py +++ b/build.py @@ -1,90 +1,69 @@ -"""Build Pub-Streamer into a standalone Windows executable using Nuitka. +"""Build Pub-Streamer into a standalone Windows app using PyInstaller. Usage (from project root): - uv run python build.py + uv run python build.py # windowed release build (no console) + uv run python build.py --console # debug build with a console for tracebacks -On CI the GITHUB_ACTIONS env var is set automatically, which switches the -compiler to MinGW (GCC) — it handles the large generated C files that cause -MSVC to run out of heap on constrained machines. +Produces a one-dir standalone under dist/PubStreamer/ with the launcher at +dist/PubStreamer/PubStreamer.exe. + +Note: build the native components first (see build_native.ps1) so the audio +hook DLLs and injector32.exe get bundled — LegacyCapture needs them. """ import os -import subprocess -import sys import pathlib +import sys + +import PyInstaller.__main__ ROOT = pathlib.Path(__file__).parent DIST = ROOT / "dist" -LOCALE = ROOT / "locale" -SOUNDS = ROOT / "sounds" +WORK = ROOT / "build" -ON_CI = os.environ.get("GITHUB_ACTIONS") == "true" - -# Data directories to bundle as-is -data_dirs = [] -if LOCALE.exists(): - data_dirs.append(f"--include-data-dir={LOCALE}=locale") -if SOUNDS.exists(): - data_dirs.append(f"--include-data-dir={SOUNDS}=sounds") -native_dir = ROOT / "native" -if native_dir.exists(): - data_dirs.append(f"--include-data-dir={native_dir}=native") -plugins_dir = ROOT / "plugins" -if plugins_dir.exists(): - data_dirs.append(f"--include-data-dir={plugins_dir}=plugins") +CONSOLE = "--console" in sys.argv icon = ROOT / "pubstreamer" / "ui" / "icon.ico" -cmd = [ - sys.executable, "-m", "nuitka", - "--standalone", - "--windows-console-mode=disable", - "--assume-yes-for-downloads", - - # MinGW on CI — avoids MSVC heap exhaustion on large generated C files. - # Remove this line to force MSVC locally if you have enough RAM. - "--mingw64" if ON_CI else "", - - f"--windows-icon-from-ico={icon}" if icon.exists() else "", - - # Compile everything — no frozen-bytecode shortcuts. - "--follow-import-to=pubstreamer", - "--include-package=wx", - "--include-package=numpy", - "--include-package=pedalboard", - "--include-package=win32com", - "--include-package=win32api", - "--include-package=win32con", - "--include-package=pywintypes", - "--include-package=comtypes", - "--include-package=httpx", - "--include-package=httpcore", - "--include-package=certifi", - "--include-package=pyaudiowpatch", - "--include-package=edge_tts", - "--include-package=gtts", - "--include-package=google", - "--include-package=grpc", - "--include-package=pyasn1", - "--include-package=pyasn1_modules", - "--include-package=piper", - "--include-package=onnxruntime", - "--include-package=cffi", - "--include-package=aiohttp", - - f"--output-dir={DIST}", - "--output-filename=PubStreamer", - - *data_dirs, - +args = [ "main.py", + "--name=PubStreamer", + "--noconfirm", + "--onedir", + "--console" if CONSOLE else "--windowed", + f"--distpath={DIST}", + f"--workpath={WORK}", + f"--specpath={WORK}", ] -cmd = [c for c in cmd if c] - -print("Running Nuitka...") -print(" ".join(cmd)) +if icon.exists(): + args.append(f"--icon={icon}") + +# Data directories bundled as-is (srcdest). +for d in ("locale", "sounds", "plugins"): + p = ROOT / d + if p.exists(): + args.append(f"--add-data={p}{os.pathsep}{d}") + +# Native binaries — land at native/dist/ where capture_legacy.py looks for them. +# PyInstaller's --add-data copies these .dll/.exe files as-is. +native_dist = ROOT / "native" / "dist" +for binary in ("audio_hook32.dll", "audio_hook64.dll", "injector32.exe"): + src = native_dist / binary + if src.exists(): + args.append(f"--add-data={src}{os.pathsep}native/dist") + +# Packages that ship data/binaries/submodules PyInstaller's hooks won't +# fully discover on their own. +for pkg in ("onnxruntime", "grpc", "piper", "pedalboard", "edge_tts", "gtts", + "google", "comtypes", "pyaudiowpatch", "aiohttp", "certifi", + "pyasn1", "pyasn1_modules", "httpx", "httpcore"): + args.append(f"--collect-all={pkg}") + +for m in ("win32com", "win32api", "win32con", "pywintypes"): + args.append(f"--hidden-import={m}") + +print("Running PyInstaller...") +print("\n".join(args)) print() - -result = subprocess.run(cmd, cwd=ROOT) -sys.exit(result.returncode) +PyInstaller.__main__.run(args) diff --git a/pyproject.toml b/pyproject.toml index de2b9d5..db9dfad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,5 +23,5 @@ package = false [dependency-groups] dev = [ - "nuitka>=4.1.1", + "pyinstaller>=6.10", ] diff --git a/uv.lock b/uv.lock index e396bc5..427f7db 100644 --- a/uv.lock +++ b/uv.lock @@ -58,6 +58,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "altgraph" +version = "0.17.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" }, +] + [[package]] name = "anyio" version = "4.13.0" @@ -411,6 +420,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] +[[package]] +name = "macholib" +version = "1.16.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altgraph" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" }, +] + [[package]] name = "multidict" version = "6.7.1" @@ -438,12 +459,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] -[[package]] -name = "nuitka" -version = "4.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d2/a6/8cf2a663402c057005f0328bdc691a653fa14848248358ef051f86a21880/nuitka-4.1.1.tar.gz", hash = "sha256:777e2c84d6b168f9bf78c3242b3779958865ae5815d26c76170eb830995368cc", size = 4558061, upload-time = "2026-05-19T08:44:16.716Z" } - [[package]] name = "numpy" version = "2.4.5" @@ -517,6 +532,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/76/618da9185591a516f65dbc5c7b4631446efaca2af0ff55b7904b47da6bb7/pedalboard-0.9.23-cp312-cp312-win_amd64.whl", hash = "sha256:2a2b9a37a9fe31a2751f121fe53771e56daf850532b9c4bcb6d960d6bbfffb0a", size = 3594700, upload-time = "2026-05-15T23:43:22.164Z" }, ] +[[package]] +name = "pefile" +version = "2024.8.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, +] + [[package]] name = "piper-tts" version = "1.4.2" @@ -608,7 +632,7 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "nuitka" }, + { name = "pyinstaller" }, ] [package.metadata] @@ -628,7 +652,7 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "nuitka", specifier = ">=4.1.1" }] +dev = [{ name = "pyinstaller", specifier = ">=6.10" }] [[package]] name = "pyasn1" @@ -669,6 +693,46 @@ 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 = "pyinstaller" +version = "6.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altgraph" }, + { name = "macholib", marker = "sys_platform == 'darwin'" }, + { name = "packaging" }, + { name = "pefile", marker = "sys_platform == 'win32'" }, + { name = "pyinstaller-hooks-contrib" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/4d/ec706c3fcf39e26888c35b39615ff4d5865d184069666c47492cff1fbe50/pyinstaller-6.21.0.tar.gz", hash = "sha256:bb9fab705983e393a2d1cac77d6972513057ad800215fd861dc15ff5272e98fd", size = 4061519, upload-time = "2026-06-13T14:15:06.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/4a/53cf98bf66daed012dc9cd78c8203f19a675d696f2fc12afcf8c5049a0e0/pyinstaller-6.21.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:327d132389f37912609e01be62810cf96b5aa95b613903e4b8692e0d12fb0eda", size = 1052350, upload-time = "2026-06-13T14:13:55.88Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/b591295c352ef464c50b4c6ffff1c4f771d875c9e833f578d1b9f564f6b3/pyinstaller-6.21.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7071d4b094d5b40deeef5fa3d3b98a1b846087f7562b49209663d5f9281fe251", size = 748477, upload-time = "2026-06-13T14:14:00.327Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8f/88fff4e403873b1e22286911350e75ff00db014aa08e57045da9d4328993/pyinstaller-6.21.0-py3-none-manylinux2014_i686.whl", hash = "sha256:6b6374d652107dd4a2eeece903ff82bb4045bb5e1006c5a158a6dcdbefe84bf2", size = 760877, upload-time = "2026-06-13T14:14:04.836Z" }, + { url = "https://files.pythonhosted.org/packages/8a/13/f0e48fbdfd1d05d948157121cea8b1b823dcb89efe6934b71fdd8bdb3f0f/pyinstaller-6.21.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4e3108b3f02384560da70e39b8bf22b0ad597d02bd68a40d76ea91c1cfa00cad", size = 759194, upload-time = "2026-06-13T14:14:10.61Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d5/ea7878cf9924ed30d946d8288777424e6d069d94f5bde56b4d0890069664/pyinstaller-6.21.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:697532279f535ad572bda613db4f821540e235c7854ca6da4d3bf0373f4415ee", size = 754979, upload-time = "2026-06-13T14:14:15.226Z" }, + { url = "https://files.pythonhosted.org/packages/9f/09/51b8905714b733bac66dbc041a7821372d70d888d273ae474c4037d4202d/pyinstaller-6.21.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:605169523a6b5ace39f13dfbff21add9f2bc43df99c7daf9394fefb2c45e8b6f", size = 754812, upload-time = "2026-06-13T14:14:20.264Z" }, + { url = "https://files.pythonhosted.org/packages/4b/43/d77779439d8c6c2e27a77bcfbd1d5cc0f568ebb611bb472b11af81b5f177/pyinstaller-6.21.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5fa56746c1e76f93634d018502301378a2d0c382553d37d8c3c34ff436c12dd1", size = 753887, upload-time = "2026-06-13T14:14:25.268Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/c22df1f6837784ac349057ba693f08e7b1ca7a0e06f9c33c63bc6280007b/pyinstaller-6.21.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:42395ec76df8e8120c36b13339d9db8cab83e316a12839ee303cc00fc941bb74", size = 753779, upload-time = "2026-06-13T14:14:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/c9/76/1ce8a27ce62ba8cf3a87c9ce6d575610f4e55d7cb0123e7512fc3f4b921a/pyinstaller-6.21.0-py3-none-win32.whl", hash = "sha256:c6b28d30d8fd99ce162ff3aab5013ed44dbfb747566b1f01b9bed7964d7c14e9", size = 1336462, upload-time = "2026-06-13T14:14:35.785Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/ca1d7e5257dd8566a9dfc0dfb02f8a8075eeb53d4b2d3c579f1276759042/pyinstaller-6.21.0-py3-none-win_amd64.whl", hash = "sha256:7fae06c494ce0ebfe6bd3055c0e409def884f63af2e3705d06bd431ad9237fc7", size = 1397487, upload-time = "2026-06-13T14:14:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/dc/75/21b51523ce8d96629b71311775a0a65f5f5a872124ab0de33e5c848f8bff/pyinstaller-6.21.0-py3-none-win_arm64.whl", hash = "sha256:f13c95c9c03fb567217135919f93815c305813126780b0ed6e0123cb8acaf025", size = 1346094, upload-time = "2026-06-13T14:14:48.914Z" }, +] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2026.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/5b/c9fe0db5e83ee1c39b2258fa21d23b15e1a60786b6c5990ee5074ead8bb6/pyinstaller_hooks_contrib-2026.6.tar.gz", hash = "sha256:bef5002c32f4f50bd55b005da12cff64eca8783e7eaf86a06a62410164bab725", size = 173354, upload-time = "2026-06-08T22:37:16.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/31/f2d7343d8ed5f7c4678377886f6ce533e6eaaa131b252ce950114c2a7efa/pyinstaller_hooks_contrib-2026.6-py3-none-any.whl", hash = "sha256:fd13b8ac126b35361175edacd41a0d97080b75dd5f4b594ecefefff969509dd3", size = 457159, upload-time = "2026-06-08T22:37:14.722Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -679,6 +743,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "requests" version = "2.34.2" @@ -694,6 +767,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, +] + [[package]] name = "tabulate" version = "0.10.0"