diff --git a/.python-version b/.python-version deleted file mode 100644 index 6324d40..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.14 diff --git a/CHANGELOG.md b/CHANGELOG.md index a6d7de6..1256925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,70 +1,18 @@ # Changelog -## [1.2.0] — 2026-06-06 +## [1.0.0] — 2026-10-13 ### Features -- Background threading — library refresh, update checks and downloads now run off the GUI thread, so the app no longer freezes during network or disk I/O -- Parallel product-info fetching — up to 16 concurrent requests dramatically speed up library refresh and update checks (previously one request per game, sequentially) -- Download progress — per-file byte progress bar with live transfer speed, plus a Cancel button to abort an in-progress download - -### Improvements - -- "Scan Existing Installers" now matches sanitized folder names and marks detected games as managed, so they appear checked in the Library tab -- In-session caching of product info and owned game IDs in the GOG API client, cleared at the start of each manual refresh -- Atomic writes for `config.json`, metadata and auth tokens — a crash mid-write can no longer corrupt them -- `User-Agent` header now reports the real application version -- File logging to `~/.config/gogupdater/gogupdater.log` (2 MB rotation) plus verbose diagnostics and logging of uncaught exceptions raised inside Qt slots +- Unified **Games** tab — the former Library and Status tabs are merged into a single view: tick a game to manage it and see its current/latest version, update status and bonus progress in the same list. A single "Refresh && Check" fetches the library and computes status in one pass; per-game settings and version history are reachable from the right-click menu. Owned-but-unmanaged games are shown on demand via "Show all owned games". ### Fixes -- Background workers were garbage-collected before running, so "Refresh Library" and "Check for Updates" did nothing — workers are now kept alive until their thread finishes -- Download resume no longer corrupts files when the server ignores the HTTP `Range` request (falls back to a full re-download) +- Fixed games getting **permanently stuck on "update available"** after GOG renamed them. Installer folders were derived from the game's (changeable) title, so a rename sent new downloads to a new folder while metadata still pointed at the old one — verification then wiped the newer version on every check. Game records now pin a stable `folder`, and verify/prune/version-history look across every folder a game may occupy. +- Added a **"Repair Metadata"** button (Settings) that re-syncs tracked games with the folders actually on disk — fixes existing split/stuck records (merging versions spread across renamed folders) without moving any files. +- Fixed a **crash (SIGSEGV)** during parallel library/update fetching caused by sharing one HTTP session across threads; each thread now uses its own session, with token refresh serialized. +- Removed a leaked-semaphore warning on shutdown by dropping loguru's multiprocessing-backed queued logging (logging remains thread-safe). -### Tests +### Build -- Added unit tests for folder-name sanitization, version comparison, prune strategies, atomic writes and the threading helpers; rewrote the stale constants test to match the current module - -## [1.1.0] — 2026-05-13 - -### Features - -- Dedicated Pruning tab — global keep-count setting with four strategies: latest N only, latest N + oldest, latest N + one per year, latest N + one per year + oldest -- Per-game pruning overrides — exclude individual games from global pruning or set a custom keep count, configurable via the existing game settings dialog -- Prune confirmation dialog shows a per-platform preview (Windows and Linux pruned independently — no cross-platform version comparison) - -### Changes - -- Pruning controls removed from Status tab and moved to the new Pruning tab -- Fixed bug in `prune_old_versions()`: installer list was reassigned inside the loop and `latest_version` was not updated after pruning - -## [1.0.1] — 2026-05-12 - -### Improvements - -- Version decision cache — ambiguous version comparisons are stored in `config.json`; the dialog is shown only once per version pair, answer is reused on subsequent checks -- Library → Status propagation — after "Refresh Library", the Status tab is automatically populated for both platforms without additional API calls; product data fetched during library refresh is reused directly -- Download logic refactored into `_run_download_for_platform()` shared helper - -## [1.0.0] — 2026-04-09 - -### Features - -- GOG OAuth2 authentication — browser-based login, paste authorization code -- Game library browser with checkboxes for management, DLCs as sub-items -- Windows / Linux platform switcher in Library and Status tabs (games shown only for platforms where installers exist) -- Per-language installer management — each language tracked independently, version comparison per language -- Language selection tab — choose which languages to download globally -- English-only mode — single toggle to skip language subfolders entirely -- Bonus content support — soundtracks, wallpapers, manuals etc. stored in `GameName/Bonus/` -- Per-game settings override — double-click any game in Library to configure languages, english-only and bonus per game -- Folder name sanitization — strips ®, ™, replaces `:` with ` - `, collapses spaces -- Version comparison — numeric dot-separated comparison with GOG suffix stripping; ambiguous versions ask user via dialog -- Status tab — per-language version display, color-coded status (up to date, update available, not downloaded, unversioned) -- Download with resume support (HTTP Range), real filename extracted from CDN URL -- Prune old versions — keep N most recent, configurable per platform -- Metadata integrity check — stale entries (manually deleted files) cleaned automatically on each check -- Scan existing installers — detect already-downloaded files from correct folder structure and import into metadata -- Version history dialog — double-click game in Status tab to see all downloaded versions with delete options -- DLC installers stored in parent game's folder, not separate folders -- Windows-only games do not download bonus content to Linux path and vice versa +- Migrated project tooling from Poetry to **uv** (`uv.lock`, `uv_build` backend); VS Code configured to use uv as the environment and package manager. diff --git a/GOGUpdater.py b/GOGUpdater.py index 6339f77..cb8951c 100644 --- a/GOGUpdater.py +++ b/GOGUpdater.py @@ -7,7 +7,7 @@ from PySide6.QtWidgets import QApplication from src.api import GogApi from src.auth import AuthManager -from src.config import AppConfig, DEFAULT_CONFIG_DIR +from src.config import DEFAULT_CONFIG_DIR, AppConfig from src.constants import APP_TITLE from src.ui.main_window import MainWindow @@ -15,6 +15,8 @@ from src.ui.main_window import MainWindow def _setup_logging(config_dir) -> None: """Log DEBUG to stderr and to a rotating file for diagnostics.""" log_file = config_dir / "gogupdater.log" + # loguru is already thread-safe (per-sink lock); avoid enqueue=True, which + # spins up a multiprocessing queue and leaks semaphores on a hard crash. logger.add( log_file, level="DEBUG", @@ -22,7 +24,6 @@ def _setup_logging(config_dir) -> None: retention=3, backtrace=True, diagnose=True, - enqueue=True, # thread-safe logging from worker threads ) logger.info(f"Logging to {log_file}") diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index b4c1032..0000000 --- a/poetry.lock +++ /dev/null @@ -1,717 +0,0 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. - -[[package]] -name = "certifi" -version = "2026.4.22" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"}, - {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.7" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"}, - {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"}, - {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} - -[[package]] -name = "idna" -version = "3.15" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, - {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, -] - -[package.extras] -all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "iniconfig" -version = "2.3.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, -] - -[[package]] -name = "librt" -version = "0.11.0" -description = "Mypyc runtime library" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "platform_python_implementation != \"PyPy\"" -files = [ - {file = "librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f"}, - {file = "librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45"}, - {file = "librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c"}, - {file = "librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33"}, - {file = "librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884"}, - {file = "librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0"}, - {file = "librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89"}, - {file = "librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4"}, - {file = "librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29"}, - {file = "librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9"}, - {file = "librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5"}, - {file = "librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b"}, - {file = "librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89"}, - {file = "librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412"}, - {file = "librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d"}, - {file = "librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73"}, - {file = "librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c"}, - {file = "librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46"}, - {file = "librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3"}, - {file = "librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67"}, - {file = "librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a"}, - {file = "librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a"}, - {file = "librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8"}, - {file = "librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a"}, - {file = "librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9"}, - {file = "librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c"}, - {file = "librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894"}, - {file = "librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c"}, - {file = "librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea"}, - {file = "librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230"}, - {file = "librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2"}, - {file = "librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e"}, - {file = "librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e"}, - {file = "librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47"}, - {file = "librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44"}, - {file = "librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd"}, - {file = "librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4"}, - {file = "librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8"}, - {file = "librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b"}, - {file = "librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175"}, - {file = "librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe"}, - {file = "librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f"}, - {file = "librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7"}, - {file = "librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1"}, - {file = "librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72"}, - {file = "librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd"}, - {file = "librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8"}, - {file = "librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c"}, - {file = "librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253"}, - {file = "librt-0.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd72d903911d995ab666dbd1871f8b1e80925a699af8063fbf50053329fb05f"}, - {file = "librt-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ef69ac715f3cd8e5cd252cb2aebfa72c015492aacc339d5d7bf8fef3c62c677"}, - {file = "librt-0.11.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:624a40c4a4ad7773315c287276cd024509b2c66ff5904f504bfc08d2c70293ab"}, - {file = "librt-0.11.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:41dc19fe150b69716c8ece4f76773a9e8813fe3e35e032a58b4d46423fb8d7c0"}, - {file = "librt-0.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8bd98ea9c47ae90b319a087ab28dac493f1ffbc1ecd1f28fcdbf3b7e1108d1"}, - {file = "librt-0.11.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84308fc49423ce6475d1c5d1985cd69a8ca9f0325fc7d5f81bb690a3f3625d4e"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff0fbaf5f44a21beeb0110f2ab64f45135a9536a834b79c0d1ef018f2786bbfa"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9c028a9442a18e266955d364ce42259136e79a7ba14d773e0d778d5f70cd56f1"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9f1692105a02bcf853f355032a5fdc5494358ef83d8fd22d16de375c85cec3f5"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a80a71e1fda83cc752a9141e87aae7fef279538597564d670e9ce513f286192"}, - {file = "librt-0.11.0-cp39-cp39-win32.whl", hash = "sha256:140695816ddf3c86eb972981a26f35efd871c44b0c3aed44c8cd01749386617f"}, - {file = "librt-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f7ff819c197fc30473190a12c2856f325ac90aabfccbeb2072d28cc2e234e3"}, - {file = "librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1"}, -] - -[[package]] -name = "loguru" -version = "0.7.3" -description = "Python logging made (stupidly) simple" -optional = false -python-versions = "<4.0,>=3.5" -groups = ["main"] -files = [ - {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, - {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} -win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} - -[package.extras] -dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==0.910) ; python_version < \"3.6\"", "mypy (==0.971) ; python_version == \"3.6\"", "mypy (==1.13.0) ; python_version >= \"3.8\"", "mypy (==1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] - -[[package]] -name = "mypy" -version = "1.20.2" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4"}, - {file = "mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997"}, - {file = "mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14"}, - {file = "mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99"}, - {file = "mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c"}, - {file = "mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd"}, - {file = "mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2"}, - {file = "mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c"}, - {file = "mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3"}, - {file = "mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254"}, - {file = "mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98"}, - {file = "mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac"}, - {file = "mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67"}, - {file = "mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100"}, - {file = "mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b"}, - {file = "mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4"}, - {file = "mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6"}, - {file = "mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066"}, - {file = "mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102"}, - {file = "mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9"}, - {file = "mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58"}, - {file = "mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026"}, - {file = "mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943"}, - {file = "mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517"}, - {file = "mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15"}, - {file = "mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee"}, - {file = "mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f"}, - {file = "mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330"}, - {file = "mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30"}, - {file = "mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924"}, - {file = "mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb"}, - {file = "mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc"}, - {file = "mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558"}, - {file = "mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8"}, - {file = "mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3"}, - {file = "mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609"}, - {file = "mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2"}, - {file = "mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c"}, - {file = "mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744"}, - {file = "mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6"}, - {file = "mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec"}, - {file = "mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382"}, - {file = "mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563"}, - {file = "mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665"}, -] - -[package.dependencies] -librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""} -mypy_extensions = ">=1.0.0" -pathspec = ">=1.0.0" -typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.15\""} - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -native-parser = ["ast-serialize (>=0.1.1,<1.0.0)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "packaging" -version = "26.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, - {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, -] - -[[package]] -name = "pathspec" -version = "1.1.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, - {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, -] - -[package.extras] -hyperscan = ["hyperscan (>=0.7)"] -optional = ["typing-extensions (>=4)"] -re2 = ["google-re2 (>=1.1)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "pygments" -version = "2.20.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, - {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyside6" -version = "6.11.1" -description = "Python bindings for the Qt cross-platform application and UI framework" -optional = false -python-versions = "<3.15,>=3.10" -groups = ["main"] -files = [ - {file = "pyside6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:537682c3b7530817203e667c1f5a2f00486b37bf52c52eeab438544c7a0917f6"}, - {file = "pyside6-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b1fc521ba2bb5109425ab8add06bddbdd524abcad06cfa012cc39a22a189feb2"}, - {file = "pyside6-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:75f0005c3eb95c07cfb65522ec50d0815ac007a96482c21dc3cb4b4c04895d84"}, - {file = "pyside6-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:0968877ab1fb4ef3587a284da6fe05e8647ada56a6a3750b6395188e01f4aba6"}, - {file = "pyside6-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:acee467cb5f256cc47ebb9d815a054c1d8416da380c191b247a76d164aa3f805"}, -] - -[package.dependencies] -PySide6_Addons = "6.11.1" -PySide6_Essentials = "6.11.1" -shiboken6 = "6.11.1" - -[[package]] -name = "pyside6-addons" -version = "6.11.1" -description = "Python bindings for the Qt cross-platform application and UI framework (Addons)" -optional = false -python-versions = "<3.15,>=3.10" -groups = ["main"] -files = [ - {file = "pyside6_addons-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:54733c77f789bef5f03c6aff4ad3bec8b2eff021f0cfcbc53d5e6c250ded24f9"}, - {file = "pyside6_addons-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6c65fbd73a512d6f72cda8d8277444a85a34dc99dd1dae9c21d35b8671bb1f"}, - {file = "pyside6_addons-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:bf1c6c4e954e5eba3d2a7c661ad4b9689e8f09c7f4a16bdf29713371d11af993"}, - {file = "pyside6_addons-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:0d13c4dfd671b050a48e4f8d8ddc724b7248f9c0437e7fc47fdf316278572923"}, - {file = "pyside6_addons-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:3494f480dee92f415be2f2d989c0b3f4755ac332b28045cbf4ba0f5c5a22ba37"}, -] - -[package.dependencies] -PySide6_Essentials = "6.11.1" -shiboken6 = "6.11.1" - -[[package]] -name = "pyside6-essentials" -version = "6.11.1" -description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)" -optional = false -python-versions = "<3.15,>=3.10" -groups = ["main"] -files = [ - {file = "pyside6_essentials-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:228de53c2bc26b07e5021fbe3614fc44ca08e4dab9999af08c2b389d2c239957"}, - {file = "pyside6_essentials-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e3ef7027b41e4e55fadb56e3b3257dc8ee92154b639fe67fc4c8e05e9d976c60"}, - {file = "pyside6_essentials-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:a039b6da68a3a4b9d243217b2b98d475eed3f617159ef6be925badab53c11b0d"}, - {file = "pyside6_essentials-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:63311bd48e32c584599ab04b9ef7c324082374cd2c9fa533f978fb893bb47e40"}, - {file = "pyside6_essentials-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:11253ea52aabecefe9febddbbe78b43a824129e3af1cec98431028fba7fa954f"}, -] - -[package.dependencies] -shiboken6 = "6.11.1" - -[[package]] -name = "pytest" -version = "9.0.3" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, - {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1.0.1" -packaging = ">=22" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-mock" -version = "3.15.1" -description = "Thin-wrapper around the mock package for easier use with pytest" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, - {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, -] - -[package.dependencies] -pytest = ">=6.2.5" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - -[[package]] -name = "python-dotenv" -version = "1.2.2" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, - {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "requests" -version = "2.34.2" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, - {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, -] - -[package.dependencies] -certifi = ">=2023.5.7" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.26,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] - -[[package]] -name = "ruff" -version = "0.15.13" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8"}, - {file = "ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7"}, - {file = "ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca"}, - {file = "ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd"}, - {file = "ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6"}, - {file = "ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51"}, - {file = "ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2"}, - {file = "ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b"}, - {file = "ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41"}, - {file = "ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4"}, - {file = "ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21"}, - {file = "ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7"}, -] - -[[package]] -name = "shiboken6" -version = "6.11.1" -description = "Python/C++ bindings helper module" -optional = false -python-versions = "<3.15,>=3.10" -groups = ["main"] -files = [ - {file = "shiboken6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:1a16867f103ef1c662a5f09dfed03273a9f81688b174555162c58e83650a3f02"}, - {file = "shiboken6-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9a8bccfafc8805254cabcfa1edfaf55cd52889f4998c91ad0d9a4433fb1bcdbe"}, - {file = "shiboken6-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:1bd2f4314414df2d122d9f646e03b731bc6d6b5f77a5f53f99a4fe4e97d84e6f"}, - {file = "shiboken6-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:c2c6863aa80ec18c0f82cea3417837b279cdc60024ac17123461dc9042577df7"}, - {file = "shiboken6-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:7c8d9af17db4495d4fa5b1c393f218311c4855546b9dfa6a0bd21bcd66b55e9d"}, -] - -[[package]] -name = "tqdm" -version = "4.67.3" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf"}, - {file = "tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "types-requests" -version = "2.33.0.20260513" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "types_requests-2.33.0.20260513-py3-none-any.whl", hash = "sha256:d5a965f9d18b6e06b72039a69565de9027e58f36a7f709857da747fbe7521122"}, - {file = "types_requests-2.33.0.20260513.tar.gz", hash = "sha256:bd845450e954e751373d5d33526742592f298808a3ee3bda7e858e46b839b57f"}, -] - -[package.dependencies] -urllib3 = ">=2" - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] - -[[package]] -name = "urllib3" -version = "2.7.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, - {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, -] - -[package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] - -[[package]] -name = "win32-setctime" -version = "1.2.0" -description = "A small Python utility to set file creation time on Windows" -optional = false -python-versions = ">=3.5" -groups = ["main"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, - {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, -] - -[package.extras] -dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.14,<3.15" -content-hash = "feafc9d9aeea619528d2ca9e876b943ca4c42ba21799f580ce473e4f32e2a38c" diff --git a/prebuild.py b/prebuild.py index 84de3ae..177d48c 100644 --- a/prebuild.py +++ b/prebuild.py @@ -1,7 +1,9 @@ import os import sys from pathlib import Path + from dotenv import load_dotenv + from src.constants import VERSION load_dotenv() diff --git a/pyproject.toml b/pyproject.toml index 382e9d4..5061131 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,31 +1,33 @@ [project] name = "gogupdater" -version = "1.2.0" -description = "" +version = "1.0.0" +description = "Add your description here" +readme = "README.md" authors = [ - {name = "Jan Doubravský",email = "jan.doubravsky@gmail.com"} + { name = "Jan Doubravský", email = "jan.doubravsky@gmail.com" } ] -requires-python = ">=3.14,<3.15" +requires-python = ">=3.14" dependencies = [ - "pyside6 (>=6.11.0,<7.0.0)", - "loguru (>=0.7.3,<0.8.0)", - "python-dotenv (>=1.2.2,<2.0.0)", - "requests (>=2.33.0,<3.0.0)", - "tqdm (>=4.67.3,<5.0.0)" + "loguru>=0.7.3", + "pyside6>=6.12.0", + "python-dotenv>=1.2.4", + "requests>=2.34.2", + "tqdm>=4.70.1", ] -[tool.poetry] -package-mode = false +[project.scripts] +gogupdater = "gogupdater:main" [build-system] -requires = ["poetry-core>=2.0.0,<3.0.0"] -build-backend = "poetry.core.masonry.api" +requires = ["uv_build>=0.13.0,<0.14.0"] +build-backend = "uv_build" [dependency-groups] dev = [ - "ruff (>=0.15.7,<0.16.0)", - "mypy (>=1.19.1,<2.0.0)", - "pytest (>=9.0.2,<10.0.0)", - "pytest-mock (>=3.15.1,<4.0.0)", - "types-requests (>=2.32.4.20260324,<3.0.0.0)" + "ast-serialize<0.13", + "mypy>=2.4.0", + "pytest>=9.1.1", + "pytest-mock>=3.16.0", + "ruff>=0.17.0", + "types-requests>=2.33.0.20261006", ] diff --git a/src/_version.py b/src/_version.py index 0e78e23..e922c7e 100644 --- a/src/_version.py +++ b/src/_version.py @@ -1,2 +1,2 @@ """Auto-generated — do not edit manually.""" -__version__ = "1.2.0" +__version__ = "1.0.0" diff --git a/src/api.py b/src/api.py index e763026..e2dc052 100644 --- a/src/api.py +++ b/src/api.py @@ -1,5 +1,6 @@ """GOG API client for fetching game library and installer info.""" +import threading from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor, as_completed from urllib.parse import unquote, urlparse @@ -9,7 +10,13 @@ from loguru import logger from src.auth import AuthManager from src.constants import APP_NAME, VERSION -from src.models import BonusContent, InstallerInfo, InstallerPlatform, InstallerType, OwnedGame +from src.models import ( + BonusContent, + InstallerInfo, + InstallerPlatform, + InstallerType, + OwnedGame, +) GOG_API = "https://api.gog.com" GOG_EMBED = "https://embed.gog.com" @@ -22,26 +29,37 @@ class GogApi: def __init__(self, auth: AuthManager) -> None: self.auth = auth - self.session = requests.Session() - self.session.headers.update({"User-Agent": f"{APP_NAME}/{VERSION}"}) - # Larger connection pool so concurrent product fetches don't block each other. - adapter = requests.adapters.HTTPAdapter( - pool_connections=self.PRODUCT_FETCH_WORKERS, - pool_maxsize=self.PRODUCT_FETCH_WORKERS, - ) - self.session.mount("https://", adapter) - self.session.mount("http://", adapter) + # requests.Session is NOT thread-safe; concurrent TLS requests over one + # shared session can segfault in OpenSSL. Give every thread its own session. + self._local = threading.local() + self._auth_lock = threading.Lock() # Caches to avoid refetching the same data repeatedly within a session. self._product_cache: dict[str, dict] = {} self._owned_ids_cache: list[int] | None = None + @property + def session(self) -> requests.Session: + """Per-thread requests.Session (created lazily).""" + session = getattr(self._local, "session", None) + if session is None: + session = requests.Session() + session.headers.update({"User-Agent": f"{APP_NAME}/{VERSION}"}) + adapter = requests.adapters.HTTPAdapter(pool_connections=4, pool_maxsize=4) + session.mount("https://", adapter) + session.mount("http://", adapter) + self._local.session = session + return session + def clear_cache(self) -> None: """Drop cached product info and owned IDs (call before a fresh refresh).""" self._product_cache.clear() self._owned_ids_cache = None def _ensure_auth(self) -> bool: - token = self.auth.access_token + # Serialize token access so concurrent threads don't trigger overlapping + # refreshes on the shared AuthManager session. + with self._auth_lock: + token = self.auth.access_token if not token: logger.error("Not authenticated") return False diff --git a/src/config.py b/src/config.py index 2068197..c7edc09 100644 --- a/src/config.py +++ b/src/config.py @@ -6,7 +6,17 @@ from pathlib import Path from loguru import logger from src.fileutil import atomic_write_text -from src.models import DownloadedInstaller, GameRecord, GameSettings, InstallerType, LANGUAGE_NAMES, PruneStrategy, language_folder_name +from src.models import ( + LANGUAGE_NAMES, + DownloadedBonus, + DownloadedInstaller, + GameRecord, + GameSettings, + InstallerType, + PruneStrategy, + language_folder_name, + sanitize_folder_name, +) DEFAULT_CONFIG_DIR = Path.home() / ".config" / "gogupdater" METADATA_FILENAME = "gogupdater.json" @@ -146,6 +156,40 @@ class MetadataStore: def get_game(self, game_id: str) -> GameRecord | None: return self.games.get(game_id) + def primary_dir(self, record: GameRecord) -> Path: + """Folder new downloads for this game go into (pinned, title-independent).""" + return self.base_path / (record.folder or sanitize_folder_name(record.name)) + + def candidate_dirs(self, record: GameRecord) -> list[Path]: + """All folders this game may occupy on disk: pinned folder + legacy sanitized name. + + A game renamed on GOG can have versions/bonuses spread across more than one + folder; files are considered present if found under any of these. + """ + names: list[str] = [] + for name in (record.folder, sanitize_folder_name(record.name)): + if name and name not in names: + names.append(name) + return [self.base_path / name for name in names] + + def find_installer_file(self, record: GameRecord, inst: DownloadedInstaller) -> Path | None: + """Locate an installer file across all candidate folders (lang or direct layout).""" + for game_dir in self.candidate_dirs(record): + lang_path = game_dir / inst.version / language_folder_name(inst.language) / inst.filename + direct_path = game_dir / inst.version / inst.filename + if lang_path.exists(): + return lang_path + if direct_path.exists(): + return direct_path + return None + + def find_bonus_file(self, record: GameRecord, bonus: DownloadedBonus) -> Path | None: + for game_dir in self.candidate_dirs(record): + path = game_dir / "Bonus" / bonus.filename + if path.exists(): + return path + return None + def update_game(self, record: GameRecord) -> None: self.games[record.game_id] = record self.save() @@ -179,11 +223,12 @@ class MetadataStore: deleted: list[Path] = [] for version in versions_to_remove: - version_dir = self.base_path / record.name / version - if version_dir.exists(): - shutil.rmtree(version_dir) - deleted.append(version_dir) - logger.info(f"Pruned {version_dir}") + for game_dir in self.candidate_dirs(record): + version_dir = game_dir / version + if version_dir.exists(): + shutil.rmtree(version_dir) + deleted.append(version_dir) + logger.info(f"Pruned {version_dir}") record.installers = [i for i in record.installers if i.version not in versions_to_remove] remaining = sorted({i.version for i in record.installers}) @@ -194,7 +239,7 @@ class MetadataStore: @staticmethod def _select_versions_to_keep( - record: "GameRecord", + record: GameRecord, all_versions: list[str], keep_latest: int, strategy: PruneStrategy, @@ -213,7 +258,7 @@ class MetadataStore: return keep @staticmethod - def _yearly_representatives(record: "GameRecord", all_versions: list[str]) -> set[str]: + def _yearly_representatives(record: GameRecord, all_versions: list[str]) -> set[str]: """Return one version per calendar year (the latest in that year by sorted order).""" version_year: dict[str, int] = {} for inst in record.installers: @@ -244,33 +289,28 @@ class MetadataStore: game_ids_to_delete: list[str] = [] for game_id, record in self.games.items(): - game_dir = self.base_path / record.name + candidates = self.candidate_dirs(record) - # If the entire game folder is gone, remove the record - if not game_dir.exists(): + # If none of the game's possible folders exist, remove the record + if not any(d.exists() for d in candidates): game_ids_to_delete.append(game_id) removed += len(record.installers) + len(record.bonuses) - logger.info(f"Verify: game folder missing for '{record.name}', removing record") + logger.info(f"Verify: no folder on disk for '{record.name}', removing record") continue - # Check individual installer files + # Check individual installer files (across all candidate folders) valid_installers: list[DownloadedInstaller] = [] for inst in record.installers: - # Try with language subfolder first, then without - lang_path = game_dir / inst.version / language_folder_name(inst.language) / inst.filename - direct_path = game_dir / inst.version / inst.filename - - if lang_path.exists() or direct_path.exists(): + if self.find_installer_file(record, inst): valid_installers.append(inst) else: removed += 1 logger.info(f"Verify: missing installer '{inst.filename}' for '{record.name}'") - # Check bonus files + # Check bonus files (across all candidate folders) valid_bonuses = [] for bonus in record.bonuses: - bonus_path = game_dir / "Bonus" / bonus.filename - if bonus_path.exists(): + if self.find_bonus_file(record, bonus): valid_bonuses.append(bonus) else: removed += 1 @@ -299,6 +339,40 @@ class MetadataStore: return removed + @staticmethod + def _installers_in_dir(game_dir: Path) -> list[DownloadedInstaller]: + """Parse installer files under a game folder (base/Game/version/[Language/]file).""" + lang_name_to_code: dict[str, str] = {v: k for k, v in LANGUAGE_NAMES.items()} + installers: list[DownloadedInstaller] = [] + + for version_dir in sorted(game_dir.iterdir()): + if not version_dir.is_dir() or version_dir.name == "Bonus": + continue + version = version_dir.name + + sub_entries = list(version_dir.iterdir()) + has_lang_subdirs = any(e.is_dir() and e.name in lang_name_to_code for e in sub_entries) + + if has_lang_subdirs: + for lang_dir in version_dir.iterdir(): + if not lang_dir.is_dir(): + continue + lang_code = lang_name_to_code.get(lang_dir.name, lang_dir.name) + for f in lang_dir.iterdir(): + if f.is_file() and not f.name.startswith("."): + installers.append(DownloadedInstaller( + filename=f.name, size=f.stat().st_size, version=version, + language=lang_code, installer_type=InstallerType.GAME, downloaded_at="", + )) + else: + for f in version_dir.iterdir(): + if f.is_file() and not f.name.startswith("."): + installers.append(DownloadedInstaller( + filename=f.name, size=f.stat().st_size, version=version, + language="en", installer_type=InstallerType.GAME, downloaded_at="", + )) + return installers + def scan_existing_installers(self, title_to_id: dict[str, str]) -> list[str]: """Scan directory for existing installers and populate metadata. @@ -310,9 +384,6 @@ class MetadataStore: logger.warning(f"Scan path does not exist: {self.base_path}") return [] - # Reverse language map: folder name -> code - lang_name_to_code: dict[str, str] = {v: k for k, v in LANGUAGE_NAMES.items()} - detected_ids: list[str] = [] for game_dir in sorted(self.base_path.iterdir()): if not game_dir.is_dir() or game_dir.name.startswith("."): @@ -331,62 +402,14 @@ class MetadataStore: logger.debug(f"Scan: '{game_name}' already in metadata, skipping") continue - installers: list[DownloadedInstaller] = [] - latest_version = "" - - for version_dir in sorted(game_dir.iterdir()): - if not version_dir.is_dir(): - continue - - version = version_dir.name - if not latest_version: - latest_version = version - - # Check if subdirs are language folders or direct installer files - sub_entries = list(version_dir.iterdir()) - has_lang_subdirs = any( - e.is_dir() and e.name in lang_name_to_code for e in sub_entries - ) - - if has_lang_subdirs: - for lang_dir in version_dir.iterdir(): - if not lang_dir.is_dir(): - continue - lang_code = lang_name_to_code.get(lang_dir.name, lang_dir.name) - for f in lang_dir.iterdir(): - if f.is_file() and not f.name.startswith("."): - installers.append(DownloadedInstaller( - filename=f.name, - size=f.stat().st_size, - version=version, - language=lang_code, - installer_type=InstallerType.GAME, - downloaded_at="", - )) - else: - # No language subfolder — files directly in version dir - for f in version_dir.iterdir(): - if f.is_file() and not f.name.startswith("."): - installers.append(DownloadedInstaller( - filename=f.name, - size=f.stat().st_size, - version=version, - language="en", - installer_type=InstallerType.GAME, - downloaded_at="", - )) - + installers = self._installers_in_dir(game_dir) if installers: - # Latest version = last sorted version directory all_versions = sorted({i.version for i in installers}) latest_version = all_versions[-1] if all_versions else "" record = GameRecord( - game_id=game_id, - name=game_name, - latest_version=latest_version, - managed=True, - installers=installers, + game_id=game_id, name=game_name, folder=game_name, + latest_version=latest_version, managed=True, installers=installers, ) self.games[game_id] = record detected_ids.append(game_id) @@ -395,3 +418,76 @@ class MetadataStore: if detected_ids: self.save() return detected_ids + + def repair_folders(self, id_to_titles: dict[str, list[str]]) -> int: + """Re-sync existing records with the folders actually on disk. + + GOG renaming a game splits its files across folders (old title + new title). + For each record, look at every candidate folder (its current name plus the + given current titles), rebuild installers from all of them, pin `folder` to + the one holding the newest version (where future downloads go), and drop + bonus entries whose files are gone. Returns the number of records changed. + """ + if not self.base_path.is_dir(): + return 0 + + changed = 0 + for game_id, record in self.games.items(): + names: list[str] = [] + for name in [record.folder, sanitize_folder_name(record.name)] + [ + sanitize_folder_name(t) for t in id_to_titles.get(game_id, []) + ]: + if name and name not in names: + names.append(name) + dirs = [self.base_path / n for n in names if (self.base_path / n).is_dir()] + if not dirs: + continue + + # Rebuild installers from every candidate folder. + merged: dict[tuple[str, str, str], DownloadedInstaller] = {} + dir_latest: dict[str, str] = {} # folder name -> its newest version + for game_dir in dirs: + insts = self._installers_in_dir(game_dir) + for inst in insts: + merged[(inst.version, inst.language, inst.filename)] = inst + versions = sorted({i.version for i in insts}) + if versions: + dir_latest[game_dir.name] = versions[-1] + + if not merged and not dir_latest: + continue + + all_installers = list(merged.values()) + all_versions = sorted({i.version for i in all_installers}) + new_latest = all_versions[-1] if all_versions else "" + # Pin folder to the directory whose newest version is the overall newest. + new_folder = record.folder or sanitize_folder_name(record.name) + if dir_latest: + new_folder = max(dir_latest, key=lambda d: dir_latest[d]) + + orig_folder = record.folder + orig_latest = record.latest_version + orig_inst_count = len(record.installers) + orig_bonus_count = len(record.bonuses) + + # Pin the folder first so candidate_dirs (and the bonus check) sees it. + record.folder = new_folder + record.installers = all_installers + record.latest_version = new_latest + # Keep only bonus entries whose files still exist in some candidate folder. + record.bonuses = [b for b in record.bonuses if self.find_bonus_file(record, b)] + + if (new_folder != orig_folder or new_latest != orig_latest + or len(all_installers) != orig_inst_count + or len(record.bonuses) != orig_bonus_count): + changed += 1 + logger.info( + f"Repair: '{record.name}' folder '{orig_folder}'->'{new_folder}', " + f"version '{orig_latest}'->'{new_latest}', " + f"installers {orig_inst_count}->{len(all_installers)}, " + f"bonuses {orig_bonus_count}->{len(record.bonuses)}" + ) + + if changed: + self.save() + return changed diff --git a/src/downloader.py b/src/downloader.py index 9728e2b..7a25807 100644 --- a/src/downloader.py +++ b/src/downloader.py @@ -1,7 +1,7 @@ """Installer file downloader with progress reporting and resume support.""" import time -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Protocol import requests @@ -51,7 +51,7 @@ class InstallerDownloader: If single_language is True, the language subfolder is skipped (game only available in one language). """ - game_name = sanitize_folder_name(game_name) + folder = self._folder_for(installer.game_id, game_name) # Resolve actual download URL and real filename resolved = self.api.resolve_download_url(installer.download_url) if not resolved: @@ -64,10 +64,10 @@ class InstallerDownloader: version = installer.version if installer.version else "unknown" if single_language: - target_dir = self.metadata.base_path / game_name / version + target_dir = self.metadata.base_path / folder / version else: lang_folder = language_folder_name(installer.language) - target_dir = self.metadata.base_path / game_name / version / lang_folder + target_dir = self.metadata.base_path / folder / version / lang_folder target_dir.mkdir(parents=True, exist_ok=True) target_file = target_dir / real_filename @@ -88,7 +88,7 @@ class InstallerDownloader: if response.status_code == 416: logger.info(f"Already fully downloaded: {real_filename}") - self._record_download(installer, game_name, real_filename) + self._record_download(installer, folder, real_filename) if callback: callback.on_finished(True, "Already downloaded") return True @@ -141,7 +141,7 @@ class InstallerDownloader: callback.on_finished(False, f"Write error: {e}") return False - self._record_download(installer, game_name, real_filename) + self._record_download(installer, folder, real_filename) logger.info(f"Downloaded {real_filename} ({downloaded} bytes)") if callback: callback.on_finished(True, "Download complete") @@ -154,7 +154,7 @@ class InstallerDownloader: callback: DownloadProgressCallback | None = None, ) -> bool: """Download a bonus content file to GameName/Bonus/ directory.""" - game_name = sanitize_folder_name(game_name) + folder = self._folder_for(bonus.game_id, game_name) resolved = self.api.resolve_download_url(bonus.download_url) if not resolved: logger.error(f"Could not resolve download URL for bonus '{bonus.name}'") @@ -164,14 +164,14 @@ class InstallerDownloader: actual_url, real_filename = resolved - target_dir = self.metadata.base_path / game_name / "Bonus" + target_dir = self.metadata.base_path / folder / "Bonus" target_dir.mkdir(parents=True, exist_ok=True) target_file = target_dir / real_filename # Skip if already downloaded with same size if target_file.exists() and target_file.stat().st_size == bonus.size: logger.info(f"Bonus already downloaded: {real_filename}") - self._record_bonus(bonus, game_name, real_filename) + self._record_bonus(bonus, folder, real_filename) if callback: callback.on_finished(True, "Already downloaded") return True @@ -192,7 +192,7 @@ class InstallerDownloader: if response.status_code == 416: logger.info(f"Bonus already fully downloaded: {real_filename}") - self._record_bonus(bonus, game_name, real_filename) + self._record_bonus(bonus, folder, real_filename) if callback: callback.on_finished(True, "Already downloaded") return True @@ -244,45 +244,54 @@ class InstallerDownloader: callback.on_finished(False, f"Write error: {e}") return False - self._record_bonus(bonus, game_name, real_filename) + self._record_bonus(bonus, folder, real_filename) logger.info(f"Downloaded bonus {real_filename} ({downloaded} bytes)") if callback: callback.on_finished(True, "Download complete") return True - def _record_bonus(self, bonus: BonusContent, game_name: str, real_filename: str) -> None: + def _folder_for(self, game_id: str, game_name: str) -> str: + """Pinned download folder for a game: its recorded folder, else the sanitized title.""" + record = self.metadata.get_game(game_id) + if record and record.folder: + return record.folder + return sanitize_folder_name(game_name) + + def _record_bonus(self, bonus: BonusContent, folder: str, real_filename: str) -> None: """Record the bonus download in metadata store.""" - game_name = sanitize_folder_name(game_name) record = self.metadata.get_game(bonus.game_id) if not record: record = GameRecord( game_id=bonus.game_id, - name=game_name, + name=folder, ) + if not record.folder: + record.folder = folder downloaded = DownloadedBonus( filename=real_filename, name=bonus.name, bonus_type=bonus.bonus_type, size=bonus.size, - downloaded_at=datetime.now(timezone.utc).isoformat(), + downloaded_at=datetime.now(UTC).isoformat(), ) record.bonuses = [b for b in record.bonuses if b.filename != real_filename] record.bonuses.append(downloaded) - record.last_checked = datetime.now(timezone.utc).isoformat() + record.last_checked = datetime.now(UTC).isoformat() self.metadata.update_game(record) - def _record_download(self, installer: InstallerInfo, game_name: str, real_filename: str) -> None: + def _record_download(self, installer: InstallerInfo, folder: str, real_filename: str) -> None: """Record the download in metadata store.""" - game_name = sanitize_folder_name(game_name) record = self.metadata.get_game(installer.game_id) if not record: record = GameRecord( game_id=installer.game_id, - name=game_name, + name=folder, latest_version=installer.version, ) + if not record.folder: + record.folder = folder downloaded = DownloadedInstaller( filename=real_filename, @@ -290,12 +299,12 @@ class InstallerDownloader: version=installer.version, language=installer.language, installer_type=installer.installer_type, - downloaded_at=datetime.now(timezone.utc).isoformat(), + downloaded_at=datetime.now(UTC).isoformat(), ) # Replace existing entry for same file, or add new record.installers = [i for i in record.installers if i.filename != real_filename] record.installers.append(downloaded) record.latest_version = installer.version - record.last_checked = datetime.now(timezone.utc).isoformat() + record.last_checked = datetime.now(UTC).isoformat() self.metadata.update_game(record) diff --git a/src/gogupdater/__init__.py b/src/gogupdater/__init__.py new file mode 100644 index 0000000..aa101fb --- /dev/null +++ b/src/gogupdater/__init__.py @@ -0,0 +1,2 @@ +def main() -> None: + print("Hello from gogupdater!") diff --git a/src/models.py b/src/models.py index 82e6edc..a22c94f 100644 --- a/src/models.py +++ b/src/models.py @@ -4,7 +4,6 @@ import re from dataclasses import dataclass, field from enum import Enum - LANGUAGE_NAMES: dict[str, str] = { "en": "English", "fr": "French", @@ -122,7 +121,7 @@ class InstallerInfo: } @classmethod - def from_dict(cls, data: dict) -> "InstallerInfo": + def from_dict(cls, data: dict) -> InstallerInfo: return cls( installer_id=data["installer_id"], filename=data["filename"], @@ -158,7 +157,7 @@ class DownloadedInstaller: } @classmethod - def from_dict(cls, data: dict) -> "DownloadedInstaller": + def from_dict(cls, data: dict) -> DownloadedInstaller: return cls( filename=data["filename"], size=data["size"], @@ -175,6 +174,10 @@ class GameRecord: game_id: str name: str + # Physical subfolder under base_path. Pinned once downloaded so GOG renaming + # the game's title later does not desync tracking from the files on disk. + # Empty = fall back to sanitize_folder_name(name). + folder: str = "" latest_version: str = "" managed: bool = True installers: list[DownloadedInstaller] = field(default_factory=list) @@ -182,7 +185,7 @@ class GameRecord: last_checked: str = "" def to_dict(self) -> dict: - return { + data = { "name": self.name, "latest_version": self.latest_version, "managed": self.managed, @@ -190,12 +193,16 @@ class GameRecord: "bonuses": [b.to_dict() for b in self.bonuses], "last_checked": self.last_checked, } + if self.folder: + data["folder"] = self.folder + return data @classmethod - def from_dict(cls, game_id: str, data: dict) -> "GameRecord": + def from_dict(cls, game_id: str, data: dict) -> GameRecord: return cls( game_id=game_id, name=data["name"], + folder=data.get("folder", ""), latest_version=data.get("latest_version", ""), managed=data.get("managed", True), installers=[DownloadedInstaller.from_dict(i) for i in data.get("installers", [])], @@ -235,7 +242,7 @@ class BonusContent: } @classmethod - def from_dict(cls, data: dict) -> "BonusContent": + def from_dict(cls, data: dict) -> BonusContent: return cls( bonus_id=data["bonus_id"], name=data["name"], @@ -266,7 +273,7 @@ class DownloadedBonus: } @classmethod - def from_dict(cls, data: dict) -> "DownloadedBonus": + def from_dict(cls, data: dict) -> DownloadedBonus: return cls( filename=data["filename"], name=data["name"], @@ -301,7 +308,7 @@ class GameSettings: return data @classmethod - def from_dict(cls, data: dict) -> "GameSettings": + def from_dict(cls, data: dict) -> GameSettings: return cls( languages=data.get("languages"), english_only=data.get("english_only"), @@ -343,7 +350,7 @@ class GameStatusInfo: language_versions: list[LanguageVersionInfo] = field(default_factory=list) last_checked: str = "" parent_name: str = "" - dlcs: list["GameStatusInfo"] = field(default_factory=list) + dlcs: list[GameStatusInfo] = field(default_factory=list) bonus_available: int = 0 bonus_downloaded: int = 0 diff --git a/src/ui/dialog_game_settings.py b/src/ui/dialog_game_settings.py index ce5df1e..0cd5d81 100644 --- a/src/ui/dialog_game_settings.py +++ b/src/ui/dialog_game_settings.py @@ -1,5 +1,6 @@ """Per-game settings dialog — override global language, bonus, and english_only settings.""" +from loguru import logger from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QCheckBox, @@ -15,10 +16,8 @@ from PySide6.QtWidgets import ( QWidget, ) -from loguru import logger - from src.config import AppConfig -from src.models import GameSettings, LANGUAGE_NAMES +from src.models import LANGUAGE_NAMES, GameSettings class GameSettingsDialog(QDialog): diff --git a/src/ui/dialog_game_versions.py b/src/ui/dialog_game_versions.py index 0071178..7d58a0b 100644 --- a/src/ui/dialog_game_versions.py +++ b/src/ui/dialog_game_versions.py @@ -1,8 +1,8 @@ """Dialog showing downloaded versions of a game with management actions.""" import shutil -from pathlib import Path +from loguru import logger from PySide6.QtCore import Qt from PySide6.QtGui import QFont from PySide6.QtWidgets import ( @@ -18,10 +18,9 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from loguru import logger from src.config import MetadataStore -from src.models import DownloadedInstaller, language_folder_name, sanitize_folder_name +from src.models import DownloadedInstaller, language_folder_name COL_FILE = 0 COL_VERSION = 1 @@ -38,13 +37,6 @@ def _fmt_size(size: int) -> str: return f"{size:.1f} GB" -def _installer_path(base_path: Path, game_name: str, installer: DownloadedInstaller) -> Path: - """Reconstruct the path where an installer file lives.""" - lang_path = base_path / game_name / installer.version / language_folder_name(installer.language) / installer.filename - direct_path = base_path / game_name / installer.version / installer.filename - return lang_path if lang_path.exists() else direct_path - - class GameVersionsDialog(QDialog): """Shows all downloaded versions/files for a game and lets user manage them.""" @@ -60,7 +52,6 @@ class GameVersionsDialog(QDialog): self.game_title = game_title self.metadata = metadata self.base_path = metadata.base_path - self.game_folder = self.base_path / sanitize_folder_name(game_title) self.setWindowTitle(f"Downloaded versions — {game_title}") self.setMinimumSize(700, 450) @@ -171,8 +162,7 @@ class GameVersionsDialog(QDialog): file_node.setData(COL_FILE, Qt.ItemDataRole.UserRole, ("installer", inst)) # Grey out if file no longer on disk - path = _installer_path(self.base_path, sanitize_folder_name(self.game_title), inst) - if not path.exists(): + if not self.metadata.find_installer_file(record, inst): for col in range(5): file_node.setForeground(col, self.palette().color(self.foregroundRole()).darker(150)) file_node.setToolTip(COL_FILE, "File not found on disk") @@ -192,8 +182,7 @@ class GameVersionsDialog(QDialog): node.setText(3, _fmt_size(bonus.size)) node.setData(0, Qt.ItemDataRole.UserRole, bonus) - bonus_path = self.base_path / sanitize_folder_name(self.game_title) / "Bonus" / bonus.filename - if not bonus_path.exists(): + if not self.metadata.find_bonus_file(record, bonus): for col in range(4): node.setForeground(col, self.palette().color(self.foregroundRole()).darker(150)) node.setToolTip(0, "File not found on disk") @@ -226,8 +215,8 @@ class GameVersionsDialog(QDialog): deleted = 0 for item in selected: inst: DownloadedInstaller = item.data(COL_FILE, Qt.ItemDataRole.UserRole)[1] - path = _installer_path(self.base_path, sanitize_folder_name(self.game_title), inst) - if path.exists(): + path = self.metadata.find_installer_file(record, inst) + if path: path.unlink() logger.info(f"Deleted {path}") deleted += 1 @@ -266,19 +255,20 @@ class GameVersionsDialog(QDialog): version = preselected_version or versions[-1] + version_dirs = [d / version for d in self.metadata.candidate_dirs(record) if (d / version).exists()] + reply = QMessageBox.question( self, "Delete version folder", f"Delete entire version folder '{version}' for '{self.game_title}'?\n\n" - f"Path: {self.base_path / sanitize_folder_name(self.game_title) / version}\n\n" + f"Path: {version_dirs[0] if version_dirs else '(not found on disk)'}\n\n" "This will remove all files in that folder from disk.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply != QMessageBox.StandardButton.Yes: return - version_dir = self.base_path / sanitize_folder_name(self.game_title) / version - if version_dir.exists(): + for version_dir in version_dirs: shutil.rmtree(version_dir) logger.info(f"Deleted version folder {version_dir}") @@ -313,8 +303,8 @@ class GameVersionsDialog(QDialog): deleted = 0 for item in selected: bonus = item.data(0, Qt.ItemDataRole.UserRole) - path = self.base_path / sanitize_folder_name(self.game_title) / "Bonus" / bonus.filename - if path.exists(): + path = self.metadata.find_bonus_file(record, bonus) + if path: path.unlink() logger.info(f"Deleted bonus {path}") deleted += 1 diff --git a/src/ui/main_window.py b/src/ui/main_window.py index bfd29f4..932f12c 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -8,15 +8,14 @@ from src.auth import AuthManager from src.config import AppConfig from src.constants import APP_TITLE from src.ui.tab_auth import AuthTab +from src.ui.tab_games import GamesTab from src.ui.tab_languages import LanguagesTab -from src.ui.tab_library import LibraryTab from src.ui.tab_prune import PruneTab from src.ui.tab_settings import SettingsTab -from src.ui.tab_status import StatusTab class MainWindow(QMainWindow): - """Main GOGUpdater window with 5 tabs.""" + """Main GOGUpdater window. The Games tab merges the former Library + Status tabs.""" def __init__(self, auth: AuthManager, api: GogApi, config: AppConfig) -> None: super().__init__() @@ -35,17 +34,15 @@ class MainWindow(QMainWindow): self.setCentralWidget(self.tabs) self.auth_tab = AuthTab(self.auth) - self.library_tab = LibraryTab(self.api, self.config) + self.games_tab = GamesTab(self.api, self.config) self.languages_tab = LanguagesTab(self.config) self.settings_tab = SettingsTab(self.api, self.config) - self.status_tab = StatusTab(self.api, self.config) self.prune_tab = PruneTab(self.config) self.tabs.addTab(self.auth_tab, "Login") - self.tabs.addTab(self.library_tab, "Library") + self.tabs.addTab(self.games_tab, "Games") self.tabs.addTab(self.languages_tab, "Languages") self.tabs.addTab(self.settings_tab, "Settings") - self.tabs.addTab(self.status_tab, "Status") self.tabs.addTab(self.prune_tab, "Pruning") self._update_tab_states() @@ -53,14 +50,12 @@ class MainWindow(QMainWindow): def _connect_signals(self) -> None: self.auth_tab.login_state_changed.connect(self._on_login_changed) self.settings_tab.english_only_changed.connect(self._on_english_only_changed) - self.library_tab.library_refreshed.connect(self.status_tab.check_updates_from_cache) def _on_login_changed(self, logged_in: bool) -> None: logger.info(f"Login state changed: logged_in={logged_in}") self._update_tab_states() def _on_english_only_changed(self, english_only: bool) -> None: - """Enable/disable Languages tab based on english_only setting.""" logger.info(f"English only changed: {english_only}") self._update_tab_states() @@ -69,6 +64,7 @@ class MainWindow(QMainWindow): logged_in = self.auth.is_logged_in for i in range(1, self.tabs.count()): self.tabs.setTabEnabled(i, logged_in) - # Languages tab (index 2) disabled when english_only + # Languages tab is meaningless in English-only mode. + languages_index = self.tabs.indexOf(self.languages_tab) if logged_in and self.config.english_only: - self.tabs.setTabEnabled(2, False) + self.tabs.setTabEnabled(languages_index, False) diff --git a/src/ui/tab_games.py b/src/ui/tab_games.py new file mode 100644 index 0000000..6bfbcf7 --- /dev/null +++ b/src/ui/tab_games.py @@ -0,0 +1,695 @@ +"""Games tab — unified library management + update status in one view. + +Merges the former Library and Status tabs: one tree per platform shows every +owned game with a checkbox (managed on/off) alongside its version/update status. +A single "Refresh & Check" fetches the library and computes status in one pass; +"Download Updates" fetches everything pending for the current platform. +""" + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import UTC, datetime +from typing import cast + +from loguru import logger +from PySide6.QtCore import QPoint, Qt, Signal +from PySide6.QtGui import QAction, QBrush, QColor +from PySide6.QtWidgets import ( + QCheckBox, + QHBoxLayout, + QHeaderView, + QLabel, + QMenu, + QMessageBox, + QProgressBar, + QPushButton, + QTabWidget, + QTreeWidget, + QTreeWidgetItem, + QVBoxLayout, + QWidget, +) + +from src.api import GogApi +from src.config import AppConfig, MetadataStore +from src.models import ( + GameStatus, + GameStatusInfo, + InstallerPlatform, + LanguageVersionInfo, + language_folder_name, +) +from src.ui.dialog_game_settings import GameSettingsDialog +from src.ui.dialog_game_versions import GameVersionsDialog +from src.ui.workers import DownloadWorker, FetchWorker, run_on_thread +from src.version_compare import ( + CompareResult, + compare_versions, + format_comparison_question, + normalize_version, +) + +STATUS_COLORS: dict[GameStatus, str] = { + GameStatus.UP_TO_DATE: "#2e7d32", + GameStatus.UPDATE_AVAILABLE: "#e65100", + GameStatus.NOT_DOWNLOADED: "#1565c0", + GameStatus.UNKNOWN: "#757575", + GameStatus.UNVERSIONED: "#f9a825", +} + +COL_TITLE = 0 +COL_CURRENT = 1 +COL_LATEST = 2 +COL_STATUS = 3 +COL_LANG = 4 +COL_BONUS = 5 +COL_OVERRIDES = 6 + +# Qt user-data roles on COL_TITLE +ROLE_GAME_ID = Qt.ItemDataRole.UserRole +ROLE_IS_DLC = Qt.ItemDataRole.UserRole + 1 + + +@dataclass +class GameMeta: + """Structural info about an owned game (independent of managed state).""" + + game_id: str + title: str + platforms: set[str] + dlcs: list[tuple[str, str]] # (dlc_id, dlc_title) for owned DLCs + + +def _make_tree() -> QTreeWidget: + tree = QTreeWidget() + tree.setColumnCount(7) + tree.setHeaderLabels( + ["Game", "Current", "Latest", "Status", "Language", "Bonus", "Overrides"] + ) + tree.header().setSectionResizeMode(COL_TITLE, QHeaderView.ResizeMode.Stretch) + tree.setRootIsDecorated(True) + tree.setSortingEnabled(True) + tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + return tree + + +class GamesTab(QWidget): + """Unified games view: manage which games to track and see/download updates.""" + + managed_games_changed = Signal() + + def __init__(self, api: GogApi, config: AppConfig, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.api = api + self.config = config + + self._active_threads: list = [] + self._download_worker: DownloadWorker | None = None + self._updating = False + self._dl_label = "" + self._dl_result: tuple[int, int] = (0, 0) + + # Fetched data (all owned games). + self._products_cache: dict[str, dict] = {} + self._owned_set: set[str] = set() + self._games_meta: list[GameMeta] = [] + self._meta_by_id: dict[str, GameMeta] = {} + # Computed status for managed games, per platform. + self.status_items: dict[InstallerPlatform, list[GameStatusInfo]] = { + InstallerPlatform.WINDOWS: [], + InstallerPlatform.LINUX: [], + } + self._status_by_id: dict[InstallerPlatform, dict[str, GameStatusInfo]] = { + InstallerPlatform.WINDOWS: {}, + InstallerPlatform.LINUX: {}, + } + + self._setup_ui() + + # ------------------------------------------------------------------ UI + def _setup_ui(self) -> None: + layout = QVBoxLayout(self) + + controls = QHBoxLayout() + self.refresh_btn = QPushButton("Refresh && Check") + self.refresh_btn.clicked.connect(self.refresh_and_check) + controls.addWidget(self.refresh_btn) + + self.download_btn = QPushButton("Download Updates") + self.download_btn.clicked.connect(self.download_updates) + self.download_btn.setEnabled(False) + controls.addWidget(self.download_btn) + + self.cancel_btn = QPushButton("Cancel") + self.cancel_btn.clicked.connect(self._cancel_download) + self.cancel_btn.setVisible(False) + controls.addWidget(self.cancel_btn) + + self.show_all_cb = QCheckBox("Show all owned games") + self.show_all_cb.setChecked(False) + self.show_all_cb.toggled.connect(self._repopulate_current) + controls.addWidget(self.show_all_cb) + + self.show_unknown_cb = QCheckBox("Show unversioned/unknown") + self.show_unknown_cb.setChecked(False) + self.show_unknown_cb.toggled.connect(self._repopulate_current) + controls.addWidget(self.show_unknown_cb) + + controls.addStretch() + layout.addLayout(controls) + + self.status_label = QLabel("Click 'Refresh && Check' to load your games and their status.") + layout.addWidget(self.status_label) + + self.progress_bar = QProgressBar() + self.progress_bar.setVisible(False) + layout.addWidget(self.progress_bar) + + self.platform_tabs = QTabWidget() + self.tree_windows = _make_tree() + self.tree_linux = _make_tree() + self.platform_tabs.addTab(self.tree_windows, "Windows") + self.platform_tabs.addTab(self.tree_linux, "Linux") + layout.addWidget(self.platform_tabs) + + for tree in (self.tree_windows, self.tree_linux): + tree.itemChanged.connect(self._on_item_changed) + tree.itemDoubleClicked.connect(self._on_item_double_clicked) + tree.customContextMenuRequested.connect( + lambda pos, t=tree: self._on_context_menu(t, pos) + ) + + self._update_platform_tab_visibility() + + def _update_platform_tab_visibility(self) -> None: + self.platform_tabs.setTabVisible(0, bool(self.config.windows_path)) + self.platform_tabs.setTabVisible(1, bool(self.config.linux_path)) + + def _current_platform(self) -> InstallerPlatform: + return InstallerPlatform.WINDOWS if self.platform_tabs.currentIndex() == 0 else InstallerPlatform.LINUX + + def _tree_for(self, platform: InstallerPlatform) -> QTreeWidget: + return self.tree_windows if platform == InstallerPlatform.WINDOWS else self.tree_linux + + def _base_path_for(self, platform: InstallerPlatform) -> str: + return self.config.windows_path if platform == InstallerPlatform.WINDOWS else self.config.linux_path + + # -------------------------------------------------------- refresh + check + def refresh_and_check(self) -> None: + """Fetch the full library and compute update status in one background pass.""" + if any(thread.isRunning() for thread, _ in self._active_threads): + return + self.api.clear_cache() + self.refresh_btn.setEnabled(False) + self.download_btn.setEnabled(False) + self.status_label.setText("Loading library...") + self._update_platform_tab_visibility() + self.progress_bar.setVisible(True) + self.progress_bar.setRange(0, 0) + + worker = FetchWorker(self._fetch_all) + worker.result.connect(self._on_fetched) + worker.failed.connect(self._on_fetch_failed) + worker.progress.connect(self._on_fetch_progress) + run_on_thread(self, worker) + + def _fetch_all(self, progress: Callable[[int, int], None]) -> object: + """Worker thread: fetch owned IDs + all product info, verify metadata on disk.""" + logger.debug("GamesTab._fetch_all: begin") + owned_ids = self.api.get_owned_game_ids() + if not owned_ids: + logger.warning("GamesTab._fetch_all: no owned IDs") + return None + products_cache = self.api.get_products(owned_ids, progress=progress) + self._verify_metadata() + owned_set = {str(gid) for gid in owned_ids} + return owned_ids, products_cache, owned_set + + def _on_fetch_progress(self, done: int, total: int) -> None: + self.status_label.setText(f"Loading game info {done}/{total}...") + + def _on_fetch_failed(self, message: str) -> None: + logger.error(f"Refresh & Check failed: {message}") + self.progress_bar.setVisible(False) + self.refresh_btn.setEnabled(True) + self.status_label.setText("Failed to load library.") + + def _on_fetched(self, result: object) -> None: + """Main thread: build meta + status and render trees (may show dialogs).""" + self.progress_bar.setVisible(False) + self.refresh_btn.setEnabled(True) + if result is None: + self.status_label.setText("No games found or not logged in.") + return + + owned_ids, products_cache, owned_set = cast( + tuple[list[int], dict[str, dict], set[str]], result + ) + self._products_cache = products_cache + self._owned_set = owned_set + self._build_meta(owned_ids) + self._rebuild_status() + self._populate_tree(InstallerPlatform.WINDOWS) + self._populate_tree(InstallerPlatform.LINUX) + self.download_btn.setEnabled(self._has_downloadable_items()) + + managed = len(self.config.managed_game_ids) + self.status_label.setText( + f"{len(self._games_meta)} owned games, {managed} managed. " + "Tick a game to manage it; double-click for version history." + ) + + def _build_meta(self, owned_ids: list[int]) -> None: + """Build structural metadata (title, platforms, owned DLCs) for every owned game.""" + dlc_ids: set[str] = set() + metas: list[GameMeta] = [] + for gid in owned_ids: + key = str(gid) + product = self._products_cache.get(key) + if not product: + metas.append(GameMeta(key, f"Game {gid}", set(), [])) + continue + title = product.get("title", f"Game {gid}") + dlcs: list[tuple[str, str]] = [] + for dlc in product.get("expanded_dlcs", []): + dlc_id = str(dlc.get("id", "")) + if dlc_id in self._owned_set: + dlc_ids.add(dlc_id) + dlcs.append((dlc_id, dlc.get("title", f"DLC {dlc_id}"))) + installers = product.get("downloads", {}).get("installers", []) + platforms = {inst.get("os", "") for inst in installers} + metas.append(GameMeta(key, title, platforms, dlcs)) + + # Drop DLCs from the top-level list; they render as children. + metas = [m for m in metas if m.game_id not in dlc_ids] + metas.sort(key=lambda m: m.title.lower()) + self._games_meta = metas + self._meta_by_id = {m.game_id: m for m in metas} + + # -------------------------------------------------------- status building + def _verify_metadata(self) -> None: + for platform in InstallerPlatform: + base_path = self._base_path_for(platform) + if base_path: + metadata = MetadataStore(base_path) + stale = metadata.verify_metadata() + if stale: + logger.info(f"Cleaned {stale} stale metadata entries from {base_path}") + + def _rebuild_status(self) -> None: + """Recompute status for all managed games from the cached products (no network).""" + managed_set = set(self.config.managed_game_ids) + for items in self.status_items.values(): + items.clear() + for index in self._status_by_id.values(): + index.clear() + + for platform in InstallerPlatform: + base_path = self._base_path_for(platform) + if not base_path: + continue + metadata = MetadataStore(base_path) + items_list = self.status_items[platform] + index = self._status_by_id[platform] + + for meta in self._games_meta: + if meta.game_id not in managed_set: + continue + product = self._products_cache.get(meta.game_id) + if not product: + gsi = GameStatusInfo( + game_id=meta.game_id, name=meta.title, + current_version="", latest_version="", status=GameStatus.UNKNOWN, + ) + items_list.append(gsi) + index[meta.game_id] = gsi + continue + + game_status = self._build_game_status(meta.game_id, meta.title, product, platform, metadata) + for dlc in product.get("expanded_dlcs", []): + dlc_id = str(dlc.get("id", "")) + if dlc_id not in self._owned_set or dlc_id not in managed_set: + continue + dlc_name = dlc.get("title", f"DLC {dlc_id}") + dlc_status = self._build_game_status(dlc_id, dlc_name, dlc, platform, metadata, parent_name=meta.title) + game_status.dlcs.append(dlc_status) + index[dlc_id] = dlc_status + + items_list.append(game_status) + index[meta.game_id] = game_status + + def _build_game_status( + self, game_id: str, game_name: str, product: dict, + platform: InstallerPlatform, metadata: MetadataStore, parent_name: str = "", + ) -> GameStatusInfo: + platform_key = "windows" if platform == InstallerPlatform.WINDOWS else "linux" + installers = product.get("downloads", {}).get("installers", []) + + lang_versions: dict[str, str] = {} + for inst in installers: + if inst.get("os") == platform_key: + lang = inst.get("language", "en") + version = inst.get("version") or "" + effective_langs = self.config.get_effective_languages(game_id) + if lang in effective_langs or not effective_langs: + lang_versions[lang] = version + + record = metadata.get_game(game_id) + current_version = record.latest_version if record else "" + + language_infos: list[LanguageVersionInfo] = [] + for lang, latest_ver in lang_versions.items(): + lang_status = self._determine_status_with_compare(current_version, latest_ver, game_name, lang) + language_infos.append(LanguageVersionInfo( + language=lang, current_version=current_version, + latest_version=latest_ver, status=lang_status, + )) + + if language_infos: + overall_status = self._worst_status(language_infos) + representative_latest = language_infos[0].latest_version + else: + overall_status = GameStatus.UNKNOWN + representative_latest = "" + + all_same_version = len({lv.latest_version for lv in language_infos}) <= 1 + + bonus_available = 0 + bonus_downloaded = 0 + if self.config.get_effective_include_bonus(game_id) and language_infos: + bonus_items = self.api.get_bonus_content(game_id, product) + bonus_available = len(bonus_items) + if record: + bonus_downloaded = len(record.bonuses) + + return GameStatusInfo( + game_id=game_id, name=game_name, current_version=current_version, + latest_version=representative_latest if all_same_version else "", + status=overall_status, language_versions=language_infos, + last_checked=datetime.now(UTC).isoformat(), + parent_name=parent_name, bonus_available=bonus_available, bonus_downloaded=bonus_downloaded, + ) + + @staticmethod + def _is_non_version(value: str) -> bool: + return value.strip().lower() in {"bonus", "n/a", "na", "none", ""} + + def _determine_status_with_compare(self, current: str, latest: str, game_name: str, language: str) -> GameStatus: + if not latest or self._is_non_version(latest): + return GameStatus.UNVERSIONED + if not current or self._is_non_version(current): + return GameStatus.NOT_DOWNLOADED + + result = compare_versions(current, latest) + if result == CompareResult.EQUAL: + return GameStatus.UP_TO_DATE + elif result == CompareResult.OLDER: + return GameStatus.UPDATE_AVAILABLE + elif result == CompareResult.NEWER: + return GameStatus.UP_TO_DATE + else: + cache_key = f"{normalize_version(current)}|||{normalize_version(latest)}" + if cache_key in self.config.version_decisions: + return GameStatus.UPDATE_AVAILABLE if self.config.version_decisions[cache_key] else GameStatus.UP_TO_DATE + question = format_comparison_question(game_name, language, current, latest) + reply = QMessageBox.question( + self, "Version Comparison", question, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + ) + is_newer = reply == QMessageBox.StandardButton.Yes + self.config.version_decisions[cache_key] = is_newer + self.config.save() + return GameStatus.UPDATE_AVAILABLE if is_newer else GameStatus.UP_TO_DATE + + def _worst_status(self, lang_infos: list[LanguageVersionInfo]) -> GameStatus: + priority = [ + GameStatus.UPDATE_AVAILABLE, GameStatus.NOT_DOWNLOADED, + GameStatus.UNVERSIONED, GameStatus.UNKNOWN, GameStatus.UP_TO_DATE, + ] + for status in priority: + if any(lv.status == status for lv in lang_infos): + return status + return GameStatus.UNKNOWN + + def _has_downloadable_items(self) -> bool: + downloadable = {GameStatus.UPDATE_AVAILABLE, GameStatus.NOT_DOWNLOADED} + for items in self.status_items.values(): + for item in items: + if item.status in downloadable: + return True + if any(dlc.status in downloadable for dlc in item.dlcs): + return True + if item.bonus_available > item.bonus_downloaded: + return True + return False + + # -------------------------------------------------------------- rendering + def _repopulate_current(self) -> None: + self._populate_tree(self._current_platform()) + + def _managed_visible(self, gsi: GameStatusInfo) -> bool: + if gsi.status in (GameStatus.UNKNOWN, GameStatus.UNVERSIONED): + return self.show_unknown_cb.isChecked() + return True + + def _populate_tree(self, platform: InstallerPlatform) -> None: + tree = self._tree_for(platform) + self._updating = True + tree.setSortingEnabled(False) + tree.clear() + + managed_set = set(self.config.managed_game_ids) + show_all = self.show_all_cb.isChecked() + index = self._status_by_id[platform] + base_path = self._base_path_for(platform) + + for meta in self._games_meta: + if platform.value not in meta.platforms or not base_path: + continue + is_managed = meta.game_id in managed_set + + if is_managed: + gsi = index.get(meta.game_id) + # Respect the unversioned/unknown filter for managed games. + if gsi and not self._managed_visible(gsi): + # Still show if a DLC child is visible. + visible_dlcs = [d for d in gsi.dlcs if self._managed_visible(d)] + if not visible_dlcs: + continue + elif not show_all: + continue + + node = self._create_game_node(meta.game_id, meta.title, is_managed, index.get(meta.game_id), is_dlc=False) + tree.addTopLevelItem(node) + + # DLC children: show owned DLCs (managed reflect status). + for dlc_id, dlc_title in meta.dlcs: + dlc_managed = dlc_id in managed_set + if not dlc_managed and not show_all: + continue + dlc_gsi = index.get(dlc_id) + child = self._create_game_node(dlc_id, dlc_title, dlc_managed, dlc_gsi, is_dlc=True) + node.addChild(child) + + if node.childCount(): + node.setExpanded(True) + + tree.setSortingEnabled(True) + self._updating = False + + def _create_game_node( + self, game_id: str, title: str, managed: bool, + gsi: GameStatusInfo | None, is_dlc: bool, + ) -> QTreeWidgetItem: + node = QTreeWidgetItem() + node.setText(COL_TITLE, title) + node.setData(COL_TITLE, ROLE_GAME_ID, game_id) + node.setData(COL_TITLE, ROLE_IS_DLC, is_dlc) + node.setFlags(node.flags() | Qt.ItemFlag.ItemIsUserCheckable) + node.setCheckState(COL_TITLE, Qt.CheckState.Checked if managed else Qt.CheckState.Unchecked) + self._set_overrides_label(node, game_id) + + if managed and gsi is not None: + self._fill_status_columns(node, gsi) + return node + + def _fill_status_columns(self, node: QTreeWidgetItem, gsi: GameStatusInfo) -> None: + has_different_versions = len({lv.latest_version for lv in gsi.language_versions}) > 1 + node.setText(COL_CURRENT, gsi.current_version or "-") + node.setText(COL_STATUS, gsi.status_text) + node.setForeground(COL_STATUS, QBrush(QColor(STATUS_COLORS.get(gsi.status, "#000000")))) + + if has_different_versions: + node.setText(COL_LATEST, "(per language)") + for lv in gsi.language_versions: + lang_node = QTreeWidgetItem() + lang_node.setText(COL_TITLE, f" {language_folder_name(lv.language)}") + lang_node.setText(COL_CURRENT, lv.current_version or "-") + lang_node.setText(COL_LATEST, lv.latest_version or "-") + lang_node.setText(COL_STATUS, GameStatusInfo( + game_id="", name="", current_version="", latest_version="", status=lv.status, + ).status_text) + lang_node.setText(COL_LANG, lv.language) + lang_node.setForeground(COL_STATUS, QBrush(QColor(STATUS_COLORS.get(lv.status, "#000000")))) + node.addChild(lang_node) + node.setExpanded(True) + else: + latest = gsi.language_versions[0].latest_version if gsi.language_versions else "-" + node.setText(COL_LATEST, latest or "-") + node.setText(COL_LANG, ", ".join(language_folder_name(lv.language) for lv in gsi.language_versions)) + + if gsi.bonus_available > 0: + node.setText(COL_BONUS, f"{gsi.bonus_downloaded}/{gsi.bonus_available}") + color = "#e65100" if gsi.bonus_downloaded < gsi.bonus_available else "#2e7d32" + node.setForeground(COL_BONUS, QBrush(QColor(color))) + + def _set_overrides_label(self, node: QTreeWidgetItem, game_id: str) -> None: + gs = self.config.get_game_settings(game_id) + if gs.is_default(): + node.setText(COL_OVERRIDES, "") + return + parts: list[str] = [] + if gs.english_only is not None: + parts.append("EN only" if gs.english_only else "multilang") + if gs.languages is not None: + parts.append(f"{len(gs.languages)} lang(s)") + if gs.include_bonus is not None: + parts.append("bonus" if gs.include_bonus else "no bonus") + node.setText(COL_OVERRIDES, ", ".join(parts)) + + # ---------------------------------------------------------- managed toggle + def _on_item_changed(self, item: QTreeWidgetItem, column: int) -> None: + if self._updating or column != COL_TITLE: + return + game_id = item.data(COL_TITLE, ROLE_GAME_ID) + if not game_id: + return # language detail row (not checkable / no id) + + managed = item.checkState(COL_TITLE) == Qt.CheckState.Checked + self.config.set_game_managed(game_id, managed) + + # Toggling a parent game cascades to its owned DLCs. + meta = self._meta_by_id.get(game_id) + if meta: + for dlc_id, _ in meta.dlcs: + self.config.set_game_managed(dlc_id, managed) + + # Recompute status from cache (fast, no network) and re-render. + self._rebuild_status() + self._populate_tree(InstallerPlatform.WINDOWS) + self._populate_tree(InstallerPlatform.LINUX) + self.download_btn.setEnabled(self._has_downloadable_items()) + self.managed_games_changed.emit() + + # ------------------------------------------------------ double-click / menu + def _on_item_double_clicked(self, item: QTreeWidgetItem, column: int) -> None: + game_id = item.data(COL_TITLE, ROLE_GAME_ID) + if not game_id: + return + self._open_versions(game_id, item.text(COL_TITLE)) + + def _on_context_menu(self, tree: QTreeWidget, pos: QPoint) -> None: + item = tree.itemAt(pos) + if item is None: + return + game_id = item.data(COL_TITLE, ROLE_GAME_ID) + if not game_id: + return + title = item.text(COL_TITLE) + + menu = QMenu(self) + settings_action = QAction("Per-game settings…", self) + settings_action.triggered.connect(lambda: self._open_settings(game_id, title)) + menu.addAction(settings_action) + versions_action = QAction("Version history…", self) + versions_action.triggered.connect(lambda: self._open_versions(game_id, title)) + menu.addAction(versions_action) + menu.exec(tree.viewport().mapToGlobal(pos)) + + def _open_settings(self, game_id: str, title: str) -> None: + dialog = GameSettingsDialog(game_id, title, self.config, self) + if dialog.exec(): + self._rebuild_status() + self._populate_tree(InstallerPlatform.WINDOWS) + self._populate_tree(InstallerPlatform.LINUX) + + def _open_versions(self, game_id: str, title: str) -> None: + base_path = self._base_path_for(self._current_platform()) + if not base_path: + return + metadata = MetadataStore(base_path) + GameVersionsDialog(game_id, title, metadata, self).exec() + + # -------------------------------------------------------------- downloads + def download_updates(self) -> None: + if self._download_worker is not None: + return + platform = self._current_platform() + base_path = self._base_path_for(platform) + if not base_path: + self.status_label.setText("No path configured for this platform.") + return + + downloadable = {GameStatus.UPDATE_AVAILABLE, GameStatus.NOT_DOWNLOADED} + has_items = any( + item.status in downloadable or any(dlc.status in downloadable for dlc in item.dlcs) + for item in self.status_items[platform] + ) + has_bonus = any(item.bonus_available > item.bonus_downloaded for item in self.status_items[platform]) + if not has_items and not has_bonus: + self.status_label.setText("Nothing to download.") + return + + self.download_btn.setEnabled(False) + self.refresh_btn.setEnabled(False) + self.cancel_btn.setVisible(True) + self.cancel_btn.setEnabled(True) + self.progress_bar.setVisible(True) + self.progress_bar.setRange(0, 0) + self._dl_result = (0, 0) + self._dl_label = "" + + worker = DownloadWorker( + self.api, self.config, platform, list(self.status_items[platform]), base_path + ) + self._download_worker = worker + worker.byte_progress.connect(self._on_download_bytes) + worker.file_progress.connect(self._on_download_file) + worker.result.connect(self._on_download_result) + worker.done.connect(self._on_download_done) + run_on_thread(self, worker) + + def _cancel_download(self) -> None: + if self._download_worker is not None: + self.cancel_btn.setEnabled(False) + self.status_label.setText("Cancelling after current file...") + self._download_worker.cancel() + + def _on_download_file(self, index: int, total: int, name: str) -> None: + self._dl_label = f"Downloading ({index}/{total}): {name}" + self.status_label.setText(self._dl_label) + self.progress_bar.setRange(0, 0) + + def _on_download_bytes(self, downloaded: int, total: int, speed: float) -> None: + if total > 0: + self.progress_bar.setRange(0, total) + self.progress_bar.setValue(downloaded) + self.status_label.setText(f"{self._dl_label} — {speed / (1024 * 1024):.1f} MB/s") + + def _on_download_result(self, completed: int, bonus_count: int) -> None: + self._dl_result = (completed, bonus_count) + + def _on_download_done(self) -> None: + cancelled = self._download_worker is not None and self._download_worker.is_cancelled() + self._download_worker = None + self.progress_bar.setVisible(False) + self.progress_bar.setRange(0, 100) + self.cancel_btn.setVisible(False) + self.refresh_btn.setEnabled(True) + + completed, bonus_count = self._dl_result + parts = [f"Downloaded {completed} items"] + if bonus_count: + parts.append(f"{bonus_count} bonus files") + prefix = "Cancelled. " if cancelled else "" + self.status_label.setText(f"{prefix}{', '.join(parts)}. Click 'Refresh && Check' to refresh.") diff --git a/src/ui/tab_languages.py b/src/ui/tab_languages.py index a73441d..00122f1 100644 --- a/src/ui/tab_languages.py +++ b/src/ui/tab_languages.py @@ -1,5 +1,6 @@ """Languages tab — select which languages to manage installers for.""" +from loguru import logger from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QLabel, @@ -10,8 +11,6 @@ from PySide6.QtWidgets import ( QWidget, ) -from loguru import logger - from src.config import AppConfig from src.models import LANGUAGE_NAMES diff --git a/src/ui/tab_library.py b/src/ui/tab_library.py deleted file mode 100644 index 90c0002..0000000 --- a/src/ui/tab_library.py +++ /dev/null @@ -1,264 +0,0 @@ -"""Library tab — owned games with management checkboxes, DLCs as sub-items.""" - -from PySide6.QtCore import Qt, Signal -from PySide6.QtWidgets import ( - QHeaderView, - QLabel, - QPushButton, - QTabWidget, - QTreeWidget, - QTreeWidgetItem, - QVBoxLayout, - QWidget, -) -from loguru import logger - -from collections.abc import Callable -from typing import cast - -from src.api import GogApi -from src.config import AppConfig -from src.models import InstallerPlatform -from src.ui.dialog_game_settings import GameSettingsDialog -from src.ui.workers import FetchWorker, run_on_thread - -COL_TITLE = 0 -COL_ID = 1 -COL_OVERRIDES = 2 - - -def _make_tree() -> QTreeWidget: - tree = QTreeWidget() - tree.setColumnCount(3) - tree.setHeaderLabels(["Game Title", "Game ID", "Overrides"]) - tree.header().setSectionResizeMode(COL_TITLE, QHeaderView.ResizeMode.Stretch) - tree.setRootIsDecorated(True) - tree.setSortingEnabled(True) - return tree - - -class LibraryTab(QWidget): - """Tab showing owned games with checkboxes. DLCs appear as children of their parent game.""" - - managed_games_changed = Signal() - library_refreshed = Signal(object, object) # products_cache: dict[str, dict], owned_set: set[str] - - def __init__(self, api: GogApi, config: AppConfig, parent: QWidget | None = None) -> None: - super().__init__(parent) - self.api = api - self.config = config - self._updating = False - self._active_threads: list = [] - # store games_data per platform for re-populating after settings change - self._games_data: dict[InstallerPlatform, list[tuple[str, str, list[tuple[str, str]]]]] = { - InstallerPlatform.WINDOWS: [], - InstallerPlatform.LINUX: [], - } - self._setup_ui() - - def _setup_ui(self) -> None: - layout = QVBoxLayout(self) - - top_row = QPushButton("Refresh Library") - top_row.setFixedWidth(200) - top_row.clicked.connect(self.refresh_library) - layout.addWidget(top_row) - - self.status_label = QLabel("Click 'Refresh Library' to load your games.") - layout.addWidget(self.status_label) - - self.platform_tabs = QTabWidget() - self.tree_windows = _make_tree() - self.tree_linux = _make_tree() - self.platform_tabs.addTab(self.tree_windows, "Windows") - self.platform_tabs.addTab(self.tree_linux, "Linux") - layout.addWidget(self.platform_tabs) - - self.tree_windows.itemChanged.connect(self._on_item_changed) - self.tree_linux.itemChanged.connect(self._on_item_changed) - self.tree_windows.itemDoubleClicked.connect(self._on_item_double_clicked) - self.tree_linux.itemDoubleClicked.connect(self._on_item_double_clicked) - - self._update_platform_tab_visibility() - - def _update_platform_tab_visibility(self) -> None: - self.platform_tabs.setTabVisible(0, bool(self.config.windows_path)) - self.platform_tabs.setTabVisible(1, bool(self.config.linux_path)) - - def refresh_library(self) -> None: - """Fetch owned games from GOG API (in a background thread) and populate trees.""" - if any(thread.isRunning() for thread, _ in self._active_threads): - return # a refresh is already in progress - self.status_label.setText("Loading library...") - self.tree_windows.clear() - self.tree_linux.clear() - self._update_platform_tab_visibility() - logger.info("Refreshing game library") - self.api.clear_cache() - - worker = FetchWorker(self._fetch_library_data) - worker.result.connect(self._on_library_fetched) - worker.failed.connect(self._on_library_failed) - worker.progress.connect(self._on_library_progress) - run_on_thread(self, worker) - logger.debug("refresh_library: worker dispatched") - - def _fetch_library_data(self, progress: Callable[[int, int], None]) -> object: - """Network fetch — runs on a worker thread. Returns None if not logged in.""" - logger.debug("_fetch_library_data: begin (worker thread)") - owned_ids = self.api.get_owned_game_ids() - if not owned_ids: - logger.warning("_fetch_library_data: no owned IDs returned") - return None - - logger.info(f"Fetching product info for {len(owned_ids)} games (parallel)") - products_cache = self.api.get_products(owned_ids, progress=progress) - logger.debug(f"_fetch_library_data: got {len(products_cache)} products, building tree data") - - owned_set = {str(gid) for gid in owned_ids} - dlc_ids: set[str] = set() - # (game_id, title, [(dlc_id, dlc_title)], available_platforms) - games_data: list[tuple[str, str, list[tuple[str, str]], set[str]]] = [] - - for gid in owned_ids: - key = str(gid) - product = products_cache.get(key) - if not product: - games_data.append((key, f"Game {gid}", [], set())) - continue - - title = product.get("title", f"Game {gid}") - dlcs: list[tuple[str, str]] = [] - for dlc in product.get("expanded_dlcs", []): - dlc_id = str(dlc.get("id", "")) - if dlc_id in owned_set: - dlc_ids.add(dlc_id) - dlcs.append((dlc_id, dlc.get("title", f"DLC {dlc_id}"))) - - installers = product.get("downloads", {}).get("installers", []) - platforms: set[str] = {inst.get("os", "") for inst in installers} - games_data.append((key, title, dlcs, platforms)) - - games_data = [(gid, title, dlcs, plats) for gid, title, dlcs, plats in games_data if gid not in dlc_ids] - games_data.sort(key=lambda x: x[1].lower()) - return games_data, products_cache, owned_set - - def _on_library_progress(self, done: int, total: int) -> None: - self.status_label.setText(f"Loading library {done}/{total}...") - - def _on_library_failed(self, message: str) -> None: - logger.error(f"Library refresh failed: {message}") - self.status_label.setText("Failed to load library.") - - def _on_library_fetched(self, result: object) -> None: - """Populate trees on the main thread from fetched data.""" - logger.debug("_on_library_fetched: populating trees (main thread)") - if result is None: - logger.warning("No owned games found or not logged in") - self.status_label.setText("No games found or not logged in.") - return - - games_data, products_cache, owned_set = cast( - tuple[list[tuple[str, str, list[tuple[str, str]], set[str]]], dict[str, dict], set[str]], - result, - ) - - self._updating = True - self.tree_windows.setSortingEnabled(False) - self.tree_linux.setSortingEnabled(False) - - win_count = 0 - lin_count = 0 - - for game_id, title, dlcs, platforms in games_data: - managed = self.config.is_game_managed(game_id) - check = Qt.CheckState.Checked if managed else Qt.CheckState.Unchecked - - if "windows" in platforms and self.config.windows_path: - node = self._make_node(game_id, title, check) - self.tree_windows.addTopLevelItem(node) - for dlc_id, dlc_title in dlcs: - child = self._make_node(dlc_id, dlc_title, check) - node.addChild(child) - win_count += 1 - - if "linux" in platforms and self.config.linux_path: - node = self._make_node(game_id, title, check) - self.tree_linux.addTopLevelItem(node) - for dlc_id, dlc_title in dlcs: - child = self._make_node(dlc_id, dlc_title, check) - node.addChild(child) - lin_count += 1 - - self.tree_windows.setSortingEnabled(True) - self.tree_linux.setSortingEnabled(True) - self._updating = False - - logger.info(f"Library loaded: {win_count} Windows, {lin_count} Linux games") - self.status_label.setText( - f"Loaded {win_count} Windows / {lin_count} Linux games. Double-click to configure per-game settings." - ) - self.library_refreshed.emit(products_cache, owned_set) - - def _make_node(self, game_id: str, title: str, check: Qt.CheckState) -> QTreeWidgetItem: - node = QTreeWidgetItem() - node.setText(COL_TITLE, title) - node.setText(COL_ID, game_id) - node.setFlags(node.flags() | Qt.ItemFlag.ItemIsUserCheckable) - node.setCheckState(COL_TITLE, check) - self._update_override_label(node, game_id) - return node - - def _on_item_changed(self, item: QTreeWidgetItem, column: int) -> None: - if self._updating or column != COL_TITLE: - return - - game_id = item.text(COL_ID) - managed = item.checkState(COL_TITLE) == Qt.CheckState.Checked - - self._updating = True - if item.parent() is None: - logger.debug(f"Game {game_id} managed={managed}") - self.config.set_game_managed(game_id, managed) - for i in range(item.childCount()): - child = item.child(i) - if child: - child.setCheckState(COL_TITLE, item.checkState(COL_TITLE)) - self.config.set_game_managed(child.text(COL_ID), managed) - else: - logger.debug(f"DLC {game_id} managed={managed}") - self.config.set_game_managed(game_id, managed) - self._updating = False - - self.managed_games_changed.emit() - - def _on_item_double_clicked(self, item: QTreeWidgetItem, column: int) -> None: - game_id = item.text(COL_ID) - game_title = item.text(COL_TITLE) - dialog = GameSettingsDialog(game_id, game_title, self.config, self) - if dialog.exec(): - # Update override label in both trees - for tree in (self.tree_windows, self.tree_linux): - for i in range(tree.topLevelItemCount()): - top = tree.topLevelItem(i) - if top and top.text(COL_ID) == game_id: - self._update_override_label(top, game_id) - if top: - for j in range(top.childCount()): - child = top.child(j) - if child and child.text(COL_ID) == game_id: - self._update_override_label(child, game_id) - - def _update_override_label(self, item: QTreeWidgetItem, game_id: str) -> None: - gs = self.config.get_game_settings(game_id) - if gs.is_default(): - item.setText(COL_OVERRIDES, "") - return - parts: list[str] = [] - if gs.english_only is not None: - parts.append("EN only" if gs.english_only else "multilang") - if gs.languages is not None: - parts.append(f"{len(gs.languages)} lang(s)") - if gs.include_bonus is not None: - parts.append("bonus" if gs.include_bonus else "no bonus") - item.setText(COL_OVERRIDES, ", ".join(parts)) diff --git a/src/ui/tab_prune.py b/src/ui/tab_prune.py index 447206c..a5a65ab 100644 --- a/src/ui/tab_prune.py +++ b/src/ui/tab_prune.py @@ -1,5 +1,6 @@ """Pruning tab — manage how many old installer versions to keep.""" +from loguru import logger from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QButtonGroup, @@ -16,7 +17,6 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from loguru import logger from src.config import AppConfig, MetadataStore from src.models import PruneStrategy diff --git a/src/ui/tab_settings.py b/src/ui/tab_settings.py index 28670e0..a9a3857 100644 --- a/src/ui/tab_settings.py +++ b/src/ui/tab_settings.py @@ -1,5 +1,6 @@ """Settings tab — installer paths configuration.""" +from loguru import logger from PySide6.QtCore import Signal from PySide6.QtWidgets import ( QCheckBox, @@ -14,8 +15,6 @@ from PySide6.QtWidgets import ( QWidget, ) -from loguru import logger - from src.api import GogApi from src.config import AppConfig, MetadataStore from src.models import sanitize_folder_name @@ -88,6 +87,15 @@ class SettingsTab(QWidget): scan_btn.clicked.connect(self._scan_existing) btn_layout.addWidget(scan_btn) + repair_btn = QPushButton("Repair Metadata") + repair_btn.setFixedWidth(200) + repair_btn.setToolTip( + "Re-sync tracked games with the folders on disk. Fixes games that keep " + "showing an update after a GOG rename split their files across folders." + ) + repair_btn.clicked.connect(self._repair_metadata) + btn_layout.addWidget(repair_btn) + btn_layout.addStretch() layout.addLayout(btn_layout) @@ -177,5 +185,39 @@ class SettingsTab(QWidget): "Scan Complete", f"Found {total_detected} game(s) with existing installers.\n\n" "Their metadata has been recorded and they have been marked as managed.\n" - "Check the Library and Status tabs for details.", + "Check the Games tab for details.", ) + + def _repair_metadata(self) -> None: + """Re-sync tracked games with the folders on disk (handles GOG renames).""" + paths = [p for p in (self.config.windows_path, self.config.linux_path) if p] + if not paths: + self.status_label.setText("No paths configured. Set Windows/Linux paths first.") + return + + # Collect tracked game IDs across stores, then fetch their current titles. + stores = [MetadataStore(p) for p in paths] + game_ids = {gid for store in stores for gid in store.games} + if not game_ids: + self.status_label.setText("No tracked games to repair. Run 'Scan Existing Installers' first.") + return + + self.status_label.setText(f"Fetching current titles for {len(game_ids)} games...") + id_to_titles: dict[str, list[str]] = {} + for gid in game_ids: + product = self.api.get_product_info(gid) + if product and product.get("title"): + id_to_titles[gid] = [product["title"]] + + total_changed = 0 + for store in stores: + total_changed += store.repair_folders(id_to_titles) + + msg = f"Repair complete. Re-synced {total_changed} game record(s) with disk." + self.status_label.setText(msg) + logger.info(msg) + QMessageBox.information( + self, + "Repair Metadata", + f"{msg}\n\nRun 'Refresh && Check' on the Games tab to see updated status.", + ) diff --git a/src/ui/tab_status.py b/src/ui/tab_status.py deleted file mode 100644 index f383703..0000000 --- a/src/ui/tab_status.py +++ /dev/null @@ -1,561 +0,0 @@ -"""Status tab — installer status overview, update checks, downloads, pruning.""" - -from collections.abc import Callable -from datetime import datetime, timezone -from typing import cast - -from PySide6.QtCore import Qt -from PySide6.QtGui import QBrush, QColor -from PySide6.QtWidgets import ( - QCheckBox, - QHBoxLayout, - QHeaderView, - QLabel, - QMessageBox, - QProgressBar, - QPushButton, - QTabWidget, - QTreeWidget, - QTreeWidgetItem, - QVBoxLayout, - QWidget, -) -from loguru import logger - -from src.api import GogApi -from src.config import AppConfig, MetadataStore -from src.ui.dialog_game_versions import GameVersionsDialog -from src.ui.workers import DownloadWorker, FetchWorker, run_on_thread -from src.models import ( - GameStatus, - GameStatusInfo, - InstallerPlatform, - LanguageVersionInfo, - language_folder_name, -) -from src.version_compare import CompareResult, compare_versions, format_comparison_question, normalize_version - -STATUS_COLORS: dict[GameStatus, str] = { - GameStatus.UP_TO_DATE: "#2e7d32", - GameStatus.UPDATE_AVAILABLE: "#e65100", - GameStatus.NOT_DOWNLOADED: "#1565c0", - GameStatus.UNKNOWN: "#757575", - GameStatus.UNVERSIONED: "#f9a825", -} - -COL_NAME = 0 -COL_CURRENT = 1 -COL_LATEST = 2 -COL_STATUS = 3 -COL_LANGS = 4 -COL_BONUS = 5 - - -def _make_tree() -> QTreeWidget: - tree = QTreeWidget() - tree.setColumnCount(6) - tree.setHeaderLabels(["Game", "Current Version", "Latest Version", "Status", "Language", "Bonus"]) - tree.header().setSectionResizeMode(COL_NAME, QHeaderView.ResizeMode.Stretch) - tree.setRootIsDecorated(True) - tree.setSortingEnabled(True) - return tree - - -class StatusTab(QWidget): - """Tab showing installer status with check/download/prune controls.""" - - def __init__(self, api: GogApi, config: AppConfig, parent: QWidget | None = None) -> None: - super().__init__(parent) - self.api = api - self.config = config - # per-platform status items - self.status_items: dict[InstallerPlatform, list[GameStatusInfo]] = { - InstallerPlatform.WINDOWS: [], - InstallerPlatform.LINUX: [], - } - self._active_threads: list = [] - self._download_worker: DownloadWorker | None = None - self._dl_label = "" - self._dl_result: tuple[int, int] = (0, 0) - self._setup_ui() - - def _setup_ui(self) -> None: - layout = QVBoxLayout(self) - - # Controls - controls = QHBoxLayout() - - self.check_btn = QPushButton("Check for Updates") - self.check_btn.clicked.connect(self.check_updates) - controls.addWidget(self.check_btn) - - self.download_btn = QPushButton("Download Updates") - self.download_btn.clicked.connect(self.download_updates) - self.download_btn.setEnabled(False) - controls.addWidget(self.download_btn) - - self.cancel_btn = QPushButton("Cancel") - self.cancel_btn.clicked.connect(self._cancel_download) - self.cancel_btn.setVisible(False) - controls.addWidget(self.cancel_btn) - - self.show_unknown_cb = QCheckBox("Show unversioned/unknown") - self.show_unknown_cb.setChecked(False) - self.show_unknown_cb.toggled.connect(self._repopulate_current) - controls.addWidget(self.show_unknown_cb) - - controls.addStretch() - layout.addLayout(controls) - - # Status label - self.status_label = QLabel("Click 'Check for Updates' to scan managed games.") - layout.addWidget(self.status_label) - - # Progress bar - self.progress_bar = QProgressBar() - self.progress_bar.setVisible(False) - layout.addWidget(self.progress_bar) - - # Platform sub-tabs - self.platform_tabs = QTabWidget() - self.tree_windows = _make_tree() - self.tree_linux = _make_tree() - self.platform_tabs.addTab(self.tree_windows, "Windows") - self.platform_tabs.addTab(self.tree_linux, "Linux") - self.tree_windows.itemDoubleClicked.connect(self._on_item_double_clicked) - self.tree_linux.itemDoubleClicked.connect(self._on_item_double_clicked) - layout.addWidget(self.platform_tabs) - - self._update_platform_tab_visibility() - - def _update_platform_tab_visibility(self) -> None: - """Show/hide platform tabs based on configured paths.""" - has_win = bool(self.config.windows_path) - has_lin = bool(self.config.linux_path) - self.platform_tabs.setTabVisible(0, has_win) - self.platform_tabs.setTabVisible(1, has_lin) - - def _current_platform(self) -> InstallerPlatform: - idx = self.platform_tabs.currentIndex() - return InstallerPlatform.WINDOWS if idx == 0 else InstallerPlatform.LINUX - - def _tree_for(self, platform: InstallerPlatform) -> QTreeWidget: - return self.tree_windows if platform == InstallerPlatform.WINDOWS else self.tree_linux - - def _base_path_for(self, platform: InstallerPlatform) -> str: - return self.config.windows_path if platform == InstallerPlatform.WINDOWS else self.config.linux_path - - def check_updates(self) -> None: - """Check update status for all managed games (fetch in a background thread).""" - if any(thread.isRunning() for thread, _ in self._active_threads): - return # a check is already in progress - managed_ids = list(self.config.managed_game_ids) - if not managed_ids: - self.status_label.setText("No managed games. Go to Library tab to select games.") - return - - self.api.clear_cache() - self.check_btn.setEnabled(False) - self.download_btn.setEnabled(False) - self.status_label.setText("Checking updates...") - self._update_platform_tab_visibility() - self.progress_bar.setVisible(True) - self.progress_bar.setRange(0, 0) - - def fetch(progress: Callable[[int, int], None]) -> object: - self._verify_metadata() - owned_set = self.api.get_owned_ids_set() - products_cache = self.api.get_products(managed_ids, progress=progress) - return products_cache, owned_set - - worker = FetchWorker(fetch) - worker.result.connect(self._on_check_fetched) - worker.failed.connect(self._on_check_failed) - worker.progress.connect(self._on_check_progress) - run_on_thread(self, worker) - - def _on_check_progress(self, done: int, total: int) -> None: - self.status_label.setText(f"Fetching game info {done}/{total}...") - - def _on_check_failed(self, message: str) -> None: - logger.error(f"Update check failed: {message}") - self.progress_bar.setVisible(False) - self.check_btn.setEnabled(True) - self.status_label.setText("Failed to check updates.") - - def _on_check_fetched(self, result: object) -> None: - """Build status and populate trees on the main thread (may show dialogs).""" - self.progress_bar.setVisible(False) - products_cache, owned_set = cast(tuple[dict[str, dict], set[str]], result) - self._run_check(products_cache, owned_set) - - def check_updates_from_cache(self, products_cache: dict[str, dict], owned_set: set[str]) -> None: - """Populate status using products already fetched by Library refresh (no API calls).""" - if not self.config.managed_game_ids: - return - self.check_btn.setEnabled(False) - self.status_label.setText("Checking updates...") - self._update_platform_tab_visibility() - self._verify_metadata() - self._run_check(products_cache, owned_set) - - def _verify_metadata(self) -> None: - for platform in InstallerPlatform: - base_path = self._base_path_for(platform) - if base_path: - metadata = MetadataStore(base_path) - stale = metadata.verify_metadata() - if stale: - logger.info(f"Cleaned {stale} stale metadata entries from {base_path}") - - def _run_check(self, products_cache: dict[str, dict], owned_set: set[str]) -> None: - managed_ids = self.config.managed_game_ids - managed_set = set(managed_ids) - - for items in self.status_items.values(): - items.clear() - self.tree_windows.clear() - self.tree_linux.clear() - - dlc_ids: set[str] = set() - for game_id in managed_ids: - product = products_cache.get(game_id) - if product: - for dlc in product.get("expanded_dlcs", []): - dlc_ids.add(str(dlc.get("id", ""))) - - for platform in InstallerPlatform: - base_path = self._base_path_for(platform) - if not base_path: - continue - - metadata = MetadataStore(base_path) - items_list = self.status_items[platform] - - for game_id in managed_ids: - if game_id in dlc_ids: - continue - - product = products_cache.get(game_id) - if not product: - items_list.append(GameStatusInfo( - game_id=game_id, - name=f"Game {game_id}", - current_version="", - latest_version="", - status=GameStatus.UNKNOWN, - )) - continue - - game_name = product.get("title", f"Game {game_id}") - game_status = self._build_game_status(game_id, game_name, product, platform, metadata) - - for dlc in product.get("expanded_dlcs", []): - dlc_id = str(dlc.get("id", "")) - if dlc_id not in owned_set or dlc_id not in managed_set: - continue - dlc_name = dlc.get("title", f"DLC {dlc_id}") - dlc_status = self._build_game_status(dlc_id, dlc_name, dlc, platform, metadata, parent_name=game_name) - game_status.dlcs.append(dlc_status) - - items_list.append(game_status) - - self._populate_tree(InstallerPlatform.WINDOWS) - self._populate_tree(InstallerPlatform.LINUX) - self.download_btn.setEnabled(self._has_downloadable_items()) - self.check_btn.setEnabled(True) - - total = sum(len(items) for items in self.status_items.values()) - self.status_label.setText(f"Checked {total} game entries across platforms.") - - def _build_game_status( - self, - game_id: str, - game_name: str, - product: dict, - platform: InstallerPlatform, - metadata: MetadataStore, - parent_name: str = "", - ) -> GameStatusInfo: - """Build GameStatusInfo with per-language version tracking.""" - platform_key = "windows" if platform == InstallerPlatform.WINDOWS else "linux" - installers = product.get("downloads", {}).get("installers", []) - - lang_versions: dict[str, str] = {} - for inst in installers: - if inst.get("os") == platform_key: - lang = inst.get("language", "en") - version = inst.get("version") or "" - effective_langs = self.config.get_effective_languages(game_id) - if lang in effective_langs or not effective_langs: - lang_versions[lang] = version - - record = metadata.get_game(game_id) - current_version = record.latest_version if record else "" - - language_infos: list[LanguageVersionInfo] = [] - for lang, latest_ver in lang_versions.items(): - lang_status = self._determine_status_with_compare(current_version, latest_ver, game_name, lang) - language_infos.append(LanguageVersionInfo( - language=lang, - current_version=current_version, - latest_version=latest_ver, - status=lang_status, - )) - - if language_infos: - overall_status = self._worst_status(language_infos) - representative_latest = language_infos[0].latest_version - else: - overall_status = GameStatus.UNKNOWN - representative_latest = "" - - all_same_version = len({lv.latest_version for lv in language_infos}) <= 1 - - bonus_available = 0 - bonus_downloaded = 0 - if self.config.get_effective_include_bonus(game_id) and language_infos: - bonus_items = self.api.get_bonus_content(game_id, product) - bonus_available = len(bonus_items) - if record: - bonus_downloaded = len(record.bonuses) - - return GameStatusInfo( - game_id=game_id, - name=game_name, - current_version=current_version, - latest_version=representative_latest if all_same_version else "", - status=overall_status, - language_versions=language_infos, - last_checked=datetime.now(timezone.utc).isoformat(), - parent_name=parent_name, - bonus_available=bonus_available, - bonus_downloaded=bonus_downloaded, - ) - - @staticmethod - def _is_non_version(value: str) -> bool: - return value.strip().lower() in {"bonus", "n/a", "na", "none", ""} - - def _determine_status_with_compare( - self, current: str, latest: str, game_name: str, language: str, - ) -> GameStatus: - if not latest or self._is_non_version(latest): - return GameStatus.UNVERSIONED - if not current: - return GameStatus.NOT_DOWNLOADED - if self._is_non_version(current): - return GameStatus.NOT_DOWNLOADED - - result = compare_versions(current, latest) - if result == CompareResult.EQUAL: - return GameStatus.UP_TO_DATE - elif result == CompareResult.OLDER: - return GameStatus.UPDATE_AVAILABLE - elif result == CompareResult.NEWER: - return GameStatus.UP_TO_DATE - else: - cache_key = f"{normalize_version(current)}|||{normalize_version(latest)}" - if cache_key in self.config.version_decisions: - logger.debug(f"Version decision cache hit: '{current}' vs '{latest}'") - return GameStatus.UPDATE_AVAILABLE if self.config.version_decisions[cache_key] else GameStatus.UP_TO_DATE - question = format_comparison_question(game_name, language, current, latest) - reply = QMessageBox.question( - self, - "Version Comparison", - question, - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - ) - is_newer = reply == QMessageBox.StandardButton.Yes - self.config.version_decisions[cache_key] = is_newer - self.config.save() - return GameStatus.UPDATE_AVAILABLE if is_newer else GameStatus.UP_TO_DATE - - def _worst_status(self, lang_infos: list[LanguageVersionInfo]) -> GameStatus: - priority = [ - GameStatus.UPDATE_AVAILABLE, - GameStatus.NOT_DOWNLOADED, - GameStatus.UNVERSIONED, - GameStatus.UNKNOWN, - GameStatus.UP_TO_DATE, - ] - for status in priority: - if any(lv.status == status for lv in lang_infos): - return status - return GameStatus.UNKNOWN - - def _has_downloadable_items(self) -> bool: - downloadable = {GameStatus.UPDATE_AVAILABLE, GameStatus.NOT_DOWNLOADED} - for items in self.status_items.values(): - for item in items: - if item.status in downloadable: - return True - if any(dlc.status in downloadable for dlc in item.dlcs): - return True - if item.bonus_available > item.bonus_downloaded: - return True - return False - - def _should_show(self, info: GameStatusInfo) -> bool: - if info.status in (GameStatus.UNKNOWN, GameStatus.UNVERSIONED): - return self.show_unknown_cb.isChecked() - return True - - def _repopulate_current(self) -> None: - """Repopulate only the currently visible platform tree.""" - self._populate_tree(self._current_platform()) - - def _populate_tree(self, platform: InstallerPlatform) -> None: - tree = self._tree_for(platform) - tree.setSortingEnabled(False) - tree.clear() - - for item in self.status_items[platform]: - visible_dlcs = [dlc for dlc in item.dlcs if self._should_show(dlc)] - if not self._should_show(item) and not visible_dlcs: - continue - - game_node = self._create_game_tree_item(item) - tree.addTopLevelItem(game_node) - - for dlc in visible_dlcs: - dlc_node = self._create_game_tree_item(dlc) - game_node.addChild(dlc_node) - - if visible_dlcs: - game_node.setExpanded(True) - - tree.setSortingEnabled(True) - - def _create_game_tree_item(self, info: GameStatusInfo) -> QTreeWidgetItem: - tree_item = QTreeWidgetItem() - tree_item.setText(COL_NAME, info.name) - tree_item.setData(COL_STATUS, Qt.ItemDataRole.UserRole, info.game_id) - - has_different_versions = len({lv.latest_version for lv in info.language_versions}) > 1 - - if has_different_versions: - tree_item.setText(COL_CURRENT, info.current_version or "-") - tree_item.setText(COL_LATEST, "(per language)") - tree_item.setText(COL_STATUS, info.status_text) - tree_item.setText(COL_LANGS, "") - tree_item.setForeground(COL_STATUS, QBrush(QColor(STATUS_COLORS.get(info.status, "#000000")))) - - for lv in info.language_versions: - lang_node = QTreeWidgetItem() - lang_node.setText(COL_NAME, f" {language_folder_name(lv.language)}") - lang_node.setText(COL_CURRENT, lv.current_version or "-") - lang_node.setText(COL_LATEST, lv.latest_version or "-") - lang_node.setText(COL_STATUS, GameStatusInfo( - game_id="", name="", current_version="", latest_version="", status=lv.status, - ).status_text) - lang_node.setText(COL_LANGS, lv.language) - lang_node.setForeground(COL_STATUS, QBrush(QColor(STATUS_COLORS.get(lv.status, "#000000")))) - tree_item.addChild(lang_node) - - tree_item.setExpanded(True) - else: - tree_item.setText(COL_CURRENT, info.current_version or "-") - latest = info.language_versions[0].latest_version if info.language_versions else "-" - tree_item.setText(COL_LATEST, latest or "-") - tree_item.setText(COL_STATUS, info.status_text) - langs = ", ".join(language_folder_name(lv.language) for lv in info.language_versions) - tree_item.setText(COL_LANGS, langs) - tree_item.setForeground(COL_STATUS, QBrush(QColor(STATUS_COLORS.get(info.status, "#000000")))) - - if info.bonus_available > 0: - tree_item.setText(COL_BONUS, f"{info.bonus_downloaded}/{info.bonus_available}") - color = "#e65100" if info.bonus_downloaded < info.bonus_available else "#2e7d32" - tree_item.setForeground(COL_BONUS, QBrush(QColor(color))) - - return tree_item - - def download_updates(self) -> None: - """Download updates for the currently visible platform (in a background thread).""" - if self._download_worker is not None: - return # a download is already running - - platform = self._current_platform() - base_path = self._base_path_for(platform) - if not base_path: - self.status_label.setText("No path configured for this platform.") - return - - downloadable = {GameStatus.UPDATE_AVAILABLE, GameStatus.NOT_DOWNLOADED} - has_items = any( - item.status in downloadable or any(dlc.status in downloadable for dlc in item.dlcs) - for item in self.status_items[platform] - ) - has_bonus = any(item.bonus_available > item.bonus_downloaded for item in self.status_items[platform]) - if not has_items and not has_bonus: - self.status_label.setText("Nothing to download.") - return - - self.download_btn.setEnabled(False) - self.check_btn.setEnabled(False) - self.cancel_btn.setVisible(True) - self.cancel_btn.setEnabled(True) - self.progress_bar.setVisible(True) - self.progress_bar.setRange(0, 0) # busy indicator until first bytes arrive - self._dl_result = (0, 0) - self._dl_label = "" - - worker = DownloadWorker( - self.api, self.config, platform, list(self.status_items[platform]), base_path - ) - self._download_worker = worker - worker.byte_progress.connect(self._on_download_bytes) - worker.file_progress.connect(self._on_download_file) - worker.result.connect(self._on_download_result) - worker.done.connect(self._on_download_done) - run_on_thread(self, worker) - - def _cancel_download(self) -> None: - if self._download_worker is not None: - self.cancel_btn.setEnabled(False) - self.status_label.setText("Cancelling after current file...") - self._download_worker.cancel() - - def _on_download_file(self, index: int, total: int, name: str) -> None: - self._dl_label = f"Downloading ({index}/{total}): {name}" - self.status_label.setText(self._dl_label) - self.progress_bar.setRange(0, 0) # busy until byte progress for this file - - def _on_download_bytes(self, downloaded: int, total: int, speed: float) -> None: - if total > 0: - self.progress_bar.setRange(0, total) - self.progress_bar.setValue(downloaded) - speed_mb = speed / (1024 * 1024) - self.status_label.setText(f"{self._dl_label} — {speed_mb:.1f} MB/s") - - def _on_download_result(self, completed: int, bonus_count: int) -> None: - self._dl_result = (completed, bonus_count) - - def _on_download_done(self) -> None: - cancelled = self._download_worker is not None and self._download_worker.is_cancelled() - self._download_worker = None - self.progress_bar.setVisible(False) - self.progress_bar.setRange(0, 100) - self.cancel_btn.setVisible(False) - self.check_btn.setEnabled(True) - - completed, bonus_count = self._dl_result - parts = [f"Downloaded {completed} items"] - if bonus_count: - parts.append(f"{bonus_count} bonus files") - prefix = "Cancelled. " if cancelled else "" - self.status_label.setText(f"{prefix}{', '.join(parts)}. Run 'Check for Updates' to refresh.") - - def _on_item_double_clicked(self, item: QTreeWidgetItem, column: int) -> None: - """Open version management dialog for the clicked game.""" - game_id = item.data(COL_STATUS, Qt.ItemDataRole.UserRole) - if not game_id: - return # language sub-row, no game_id stored - game_name = item.text(COL_NAME) - base_path = self._base_path_for(self._current_platform()) - if not base_path: - return - metadata = MetadataStore(base_path) - dialog = GameVersionsDialog(game_id, game_name, metadata, self) - dialog.exec() - diff --git a/src/ui/workers.py b/src/ui/workers.py index 498dbe5..e216ecf 100644 --- a/src/ui/workers.py +++ b/src/ui/workers.py @@ -7,8 +7,8 @@ back via signals. from collections.abc import Callable -from PySide6.QtCore import QObject, QThread, Signal from loguru import logger +from PySide6.QtCore import QObject, QThread, Signal from src.api import GogApi from src.config import AppConfig, MetadataStore diff --git a/tests/test_repair.py b/tests/test_repair.py new file mode 100644 index 0000000..420f5a0 --- /dev/null +++ b/tests/test_repair.py @@ -0,0 +1,98 @@ +"""Tests for folder persistence and metadata repair (GOG rename handling).""" + +from pathlib import Path + +from src.config import MetadataStore +from src.models import DownloadedBonus, DownloadedInstaller, GameRecord, InstallerType + + +def _installer(version: str, filename: str) -> DownloadedInstaller: + return DownloadedInstaller( + filename=filename, size=1, version=version, language="en", + installer_type=InstallerType.GAME, downloaded_at="", + ) + + +def _make_version(base: Path, folder: str, version: str, *filenames: str) -> None: + d = base / folder / version + d.mkdir(parents=True, exist_ok=True) + for f in filenames: + (d / f).write_text("x") + + +def _make_bonus(base: Path, folder: str, *filenames: str) -> None: + d = base / folder / "Bonus" + d.mkdir(parents=True, exist_ok=True) + for f in filenames: + (d / f).write_text("x") + + +class TestFolderField: + def test_roundtrip_persists_folder(self) -> None: + rec = GameRecord(game_id="1", name="Old Name", folder="Pinned Folder") + restored = GameRecord.from_dict("1", rec.to_dict()) + assert restored.folder == "Pinned Folder" + + def test_empty_folder_not_written(self) -> None: + rec = GameRecord(game_id="1", name="Game") + assert "folder" not in rec.to_dict() + + +class TestCandidateLookup: + def test_finds_files_across_folders(self, tmp_path: Path) -> None: + # Game renamed: v1 under old folder, v2 under new (pinned) folder. + _make_version(tmp_path, "New Title", "2.0", "setup.exe") + _make_version(tmp_path, "Old Title", "1.0", "setup.exe") + store = MetadataStore(str(tmp_path)) + rec = GameRecord( + game_id="1", name="Old Title", folder="New Title", latest_version="2.0", + installers=[_installer("2.0", "setup.exe"), _installer("1.0", "setup.exe")], + ) + store.games["1"] = rec + store.save = lambda: None # type: ignore[method-assign] + + removed = store.verify_metadata() + assert removed == 0 # both installers found despite living in different folders + assert rec.latest_version == "2.0" + assert len(rec.installers) == 2 + + +class TestRepairFolders: + def test_repoints_to_newest_version_folder_and_merges(self, tmp_path: Path) -> None: + # Split data: old folder has 1.0 + a bonus, new-title folder has 2.0. + _make_version(tmp_path, "Witcher Complete", "1.0", "setup_1.exe") + _make_version(tmp_path, "Witcher Remastered", "2.0", "setup_2.exe") + _make_bonus(tmp_path, "Witcher Complete", "ost.zip") + + store = MetadataStore(str(tmp_path)) + # Metadata still points at the old folder and (wrongly) a single version. + rec = GameRecord( + game_id="1", name="Witcher Complete", folder="", latest_version="2.0", + installers=[_installer("1.0", "setup_1.exe"), _installer("2.0", "setup_2.exe")], + bonuses=[DownloadedBonus(filename="ost.zip", name="OST", bonus_type="audio", size=1, downloaded_at="")], + ) + store.games["1"] = rec + saved: dict[str, bool] = {} + store.save = lambda: saved.update(saved=True) # type: ignore[method-assign] + + changed = store.repair_folders({"1": ["Witcher Remastered"]}) + + assert changed == 1 + assert rec.folder == "Witcher Remastered" # pinned to newest-version folder + assert rec.latest_version == "2.0" + assert {i.version for i in rec.installers} == {"1.0", "2.0"} # merged from both folders + assert len(rec.bonuses) == 1 # bonus in old folder still found + assert saved.get("saved") + + def test_drops_bonus_with_missing_file(self, tmp_path: Path) -> None: + _make_version(tmp_path, "Game", "1.0", "setup.exe") + store = MetadataStore(str(tmp_path)) + rec = GameRecord( + game_id="1", name="Game", installers=[_installer("1.0", "setup.exe")], + bonuses=[DownloadedBonus(filename="gone.zip", name="X", bonus_type="", size=1, downloaded_at="")], + ) + store.games["1"] = rec + store.save = lambda: None # type: ignore[method-assign] + + store.repair_folders({"1": ["Game"]}) + assert rec.bonuses == [] # file not on disk -> dropped diff --git a/tests/test_workers.py b/tests/test_workers.py index f1cff50..ff7608d 100644 --- a/tests/test_workers.py +++ b/tests/test_workers.py @@ -5,10 +5,10 @@ import time os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") -from PySide6.QtCore import QObject # noqa: E402 -from PySide6.QtWidgets import QApplication # noqa: E402 +from PySide6.QtCore import QObject +from PySide6.QtWidgets import QApplication -from src.ui.workers import FetchWorker, run_on_thread # noqa: E402 +from src.ui.workers import FetchWorker, run_on_thread def _app() -> QApplication: diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a34e193 --- /dev/null +++ b/uv.lock @@ -0,0 +1,630 @@ +version = 1 +revision = 5 +requires-python = ">=3.14" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "ast-serialize" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/1c/7257e6ec9382843915ce475558ce4492ccb5ed39122c256bb369c27e2ebf/ast_serialize-0.12.1.tar.gz", hash = "sha256:5285a390caf1c44368ae270f037f797b91427d138b7d43cad0f1fda4c83518d9", size = 954408, upload-time = "2026-10-03T12:25:00.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/f7/e976169da322c009bb083a52d21e88fbfe5f071e1806e8c8361ab4ac477a/ast_serialize-0.12.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e73255c9227fd74eac8a9b55c4049e8ad7b66d1f690bf827c98a86b2e594def7", size = 897232, upload-time = "2026-10-03T12:23:21.945Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/5545f6f4d38dd41b4e2a20050967ccd722508bc90fab0dfba463d8c8b994/ast_serialize-0.12.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4655ef993e69e01bb47d2d99647de9bbb74af03938438656832cd010d95de348", size = 1235397, upload-time = "2026-10-03T12:23:23.835Z" }, + { url = "https://files.pythonhosted.org/packages/22/19/e9b839ef9b57626e15e20dd7cf764a9a6b50f9750f86d0a49bc3a971fb72/ast_serialize-0.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bac5a99a2c91dd823be9b8c44645694fccbb0750773cc5b27889a9f6f22fce89", size = 1216331, upload-time = "2026-10-03T12:23:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/26/2a/d054d4ff8ba42472a22e3da6eb6dee0e69a32c477b5077eefdbada99f554/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6485e681625ed7a094221f16a7ff2ef154946112266a05cf83bde50c959ef345", size = 1281194, upload-time = "2026-10-03T12:23:27.349Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/c73eddfa180a7a4c1613c0f3d3ef020b05dca9b922ac08212463c33ad11f/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a513bc6f60980d01767f7cbe39b17ce0373e722824a74ce28d6cea49ee3c8460", size = 1287170, upload-time = "2026-10-03T12:23:29.333Z" }, + { url = "https://files.pythonhosted.org/packages/94/77/39dc75d8b718844859b64a9067c9df0cfce218ca45ea215fb24a1fda3cf7/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82866f3523d53ffca8d2a69a750bec52908b69f728012e40959bebce2620453c", size = 1531238, upload-time = "2026-10-03T12:23:30.954Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c0/6a6a6f94f45a288c4bac2eb8379a3d9654574a0f9249380ce3b07f6d64bb/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243054a05a5190f5d087b5c8b16423e7f1cefa4991bac26e9c8ace18074b75b6", size = 1307154, upload-time = "2026-10-03T12:23:32.645Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/80f843892bd0f7c0d95ec5422ba3dc315c1ce011e6f08b06d5f71bd82c25/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9fbe5a3e8acddfc2fddff3dbbc7ea0e9798b3df3428f851b8abc52a3806f31", size = 1301506, upload-time = "2026-10-03T12:23:34.63Z" }, + { url = "https://files.pythonhosted.org/packages/df/cc/49a5fe852706f545e3e005584c5be89456bc637a8c9179aeaa8b9f26e8e4/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d3d516da3463071d27e64caf54d88cba25cf4ad4afcc807e0bcf67743719f03e", size = 1297616, upload-time = "2026-10-03T12:23:36.377Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/1208c91d6e00cc43cc276bd6233c40c9b4ec3ef8537c83281dd5372cbdb8/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d6711adf11136c77e3a35517de9488a5081d1012874fae99c2876b64f4daace", size = 1354884, upload-time = "2026-10-03T12:23:38.035Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/2b5fc912ff0be64d8d61ff5dc7dc405c6311297a0e2039b848b7d14333f2/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cbe239bee4bd609186daf60b95b7b0f47146c7f7f55f6da83807d747d6fe753f", size = 1457911, upload-time = "2026-10-03T12:23:39.679Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f8/d720429bf8933efbd0cc2038c0a50b6267a585d503500845c44bc6c8ff66/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5bbf582286c9dc6b4c544ef645dc99e4b3aa09db28892bc60344141f6926641f", size = 1563023, upload-time = "2026-10-03T12:23:41.585Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0a/99e6cc92bdbae5db60f84a14a0fb1ae77b6087e451d77808d87558162c9a/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99e33c93efb5254a70c525b46038212371dfe5693d48eb2d0d5f17d936a263d7", size = 1556601, upload-time = "2026-10-03T12:23:43.361Z" }, + { url = "https://files.pythonhosted.org/packages/7a/05/59de9e16a2e333da534f30776d0f5e426034b64c67c17843425e3cc827d1/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:aa6c17a2b7f07e81fa8cfcc4aa7c832b3e57733853aebea113ab502f9b0963db", size = 1665039, upload-time = "2026-10-03T12:23:45.257Z" }, + { url = "https://files.pythonhosted.org/packages/33/83/35ed67a127167b484b42a071df440f84b14c0d20ea8f69dbed5cc96bfd98/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f896fa38e0af38821e1ab1425c5dee89e359623e165765bdeae7d0eb6909e76d", size = 1472263, upload-time = "2026-10-03T12:23:46.811Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b0/3ab8613bbb690297f1bb687d780a248c486df0f4131b6a82044fcb49e438/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af699e81fd7ce80b8b03945826d8ea23dd36d072f10d4613402da597ba4ee9c6", size = 1499242, upload-time = "2026-10-03T12:23:48.531Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a0/a28894d3b06f8775cea8989f371bd9f72ce562c32bc807770e7fb920ce17/ast_serialize-0.12.1-cp314-cp314t-win32.whl", hash = "sha256:10b59afc108eb285146acb23d1b5ec0fc58bb3c09cb2ab8876402df06c373c3b", size = 1119488, upload-time = "2026-10-03T12:23:50.419Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b2/0c44952f4ba4e14bb7f60a5858e2960dfefb9884e6ff007aaf64337dba5c/ast_serialize-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:72e871f6995a066c1b19104f8a6b5832b1163adb9a8267c2aa4711fbb0f4d1f3", size = 1157434, upload-time = "2026-10-03T12:23:52.383Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1e/cb594c63f46a01d53629af1c4f9e42cd02afcea1c2fe483e12f22743ebac/ast_serialize-0.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:3398e458047d21c9bc1b323fe5aab77c608dc9ddb65b2d44deebaff503a1f1eb", size = 1129094, upload-time = "2026-10-03T12:23:54.133Z" }, + { url = "https://files.pythonhosted.org/packages/16/05/ca16884f9498386f3646bb18be59f0e31d44e992d252d7d6f5e4f8ae1ee2/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:410233de149ab8414cb27c6fc73e9d2baa35d6f971672d540d752060d980ffb4", size = 1235686, upload-time = "2026-10-03T12:23:55.863Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/34e87ed30e292cf365523712c4bcfef1967d9c3c2749de21b1f93b1fe0f3/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9a2310845302f1a6bd45ae8a67d5760211103a8d66410b854bfa440d107e093", size = 1215598, upload-time = "2026-10-03T12:23:57.48Z" }, + { url = "https://files.pythonhosted.org/packages/f8/dc/c498f41c957b6ff31b97ed8ceccf3a84f85af7debca1125183cab95bb58b/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff65f40f49d5e1a1a043ba366081d59a4e26a9f5c1b07eb1170e172115da7ca", size = 1281911, upload-time = "2026-10-03T12:23:58.954Z" }, + { url = "https://files.pythonhosted.org/packages/dc/60/70ccefae9d88058c4c234bf0aed93f54aca36eb74087736e76e9515aee96/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:536d783c4d91331f094e0a892221e619be5ffbe6fb6640885f14d7f226ec90ca", size = 1287296, upload-time = "2026-10-03T12:24:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/08/e9/4fc697879c7128e29f9dab2ed19a9b586a56b621e5ea4aee2ae28c18e116/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c42d2d65f388d1960c5796231eb9bf5a988c46228633eb489605c4549ad16c52", size = 1532339, upload-time = "2026-10-03T12:24:02.053Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/9e2bd489731602a94dbd0c576ebe1cc487a2d0f6127be743f44711166f0d/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5628a12acc875fe7a167910f18d101dd101c2a7b1e6c2b6f7289ffaff25805c", size = 1308988, upload-time = "2026-10-03T12:24:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/3f/69/e9cae837bd766a66db6953ffb5fc7f04b1945e02b0a9e4c6a0b6acb08f17/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e855adfa5bb982b2e6fe09056b2d584f6dd4fce085d91a07d1155683751b6b5", size = 1303412, upload-time = "2026-10-03T12:24:05.62Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1e/5ef8c62d5031d93187ed0d8dade5d942de9920c3fbd678c7652362b9a7a2/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:fafe1471e8aca6c87b4913b7b54ff97197adf702fbe28692284b929dfa62ff96", size = 1298466, upload-time = "2026-10-03T12:24:07.242Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/25ded60844a1a437edc840e597b6f81daf91dc4a26035416e14298d3a091/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a5cac246474d2a703147d1605a6ac5ba0fa9e0a443cf1cf42513adf4df02686f", size = 1355617, upload-time = "2026-10-03T12:24:09.125Z" }, + { url = "https://files.pythonhosted.org/packages/05/68/a0d3cc8d8042208a2cbef7b26483f4941b44dd5dd717bb19f20e4a4c0d66/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6e25cd319fb0d7b39fcac666784ec86708ccbc78d07a698b1400cf5ed40c045b", size = 1458949, upload-time = "2026-10-03T12:24:10.772Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/5e4d355c48a9f125b8bec7b1b98d4d2dcd8324ff1d4dfcb03678a03c1414/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:657a7354ea16ed4d29f8127ed477c6fee3915c111d135f020ca835a991438e90", size = 1562328, upload-time = "2026-10-03T12:24:12.404Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/3d40f6a7908200f2f0ed9ce1bad130d00e06d0405baa7918d4a4299b25dd/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:9eb9de7e59621acdb3e66984f374272b33d56d15a04763e2fd0604211e1c8303", size = 1557751, upload-time = "2026-10-03T12:24:14.455Z" }, + { url = "https://files.pythonhosted.org/packages/3f/13/d53e5a7e299d6dbaeab23a424eea2c98c821b46ed7b05abbe14743beeb63/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:09cc4d3103c1fc97f6845ba307af1db9cde5226bef47f8843220dde83f2276ba", size = 1666238, upload-time = "2026-10-03T12:24:16.171Z" }, + { url = "https://files.pythonhosted.org/packages/37/5b/7638ee3ae35a64e4467160a37dc7565cddfe2a87f06cef2fd07c93cfd503/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:c9e2a592706fd791c2271ce9c8f4e38c98d3ea0b4a86b511e09a4fe3ac44ab37", size = 1472620, upload-time = "2026-10-03T12:24:18.053Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e7/6e9e621e0e4a3be5a9a5f8b6961982964d2367013dbe64feb5adaa43e56d/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ac042e95a575432c1730ca4cb9183066a2074886a599f46c1f1b0955fb8198", size = 1500141, upload-time = "2026-10-03T12:24:20.036Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4f/3217da5b671711c09cc6be580095839cad539983662a6599405bd75c1a19/ast_serialize-0.12.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:b3cd105995942cc6163a229674a86161ba1305f646493efd59bd6723a357ee14", size = 1118382, upload-time = "2026-10-03T12:24:21.599Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/6074cf29dca8ceff27d50e845c88e7a2eaaa7f0b6f3972909e878d844737/ast_serialize-0.12.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:a9cd24a26126088693ca054547ea0a391398a29cf1a3a2bec1009b4b6acc8b82", size = 1158034, upload-time = "2026-10-03T12:24:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/92/a0/81ce428f9f3f1ca45f8b62c9711c30452bf8190476e8685cea0f72d8d008/ast_serialize-0.12.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:9649cd903db0dc047906c6dd740784a2ba665d54f7e43ba31457edbce76c9493", size = 1129437, upload-time = "2026-10-03T12:24:25.044Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d9/1c08adb90728607d0d07d188df4558ae863d688b4458087efe9fafeca458/ast_serialize-0.12.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5ef62601db3ce5c23445132262a193075e211fb2fc87b46b7550dd351fac0976", size = 897419, upload-time = "2026-10-03T12:24:26.699Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/1eabd2c0673283054468b1c6cb539aeb877636d6c84b280279f2d7a177a9/ast_serialize-0.12.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98d91cd3a6cb76a39512ee090a539d1e3206b732ad8150eb38918cffa1ddf515", size = 1241515, upload-time = "2026-10-03T12:24:28.493Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d7/c56955934a431a0fa3e4e9aa7af4a53ceab2a61241005427545208945eb4/ast_serialize-0.12.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8a32f184ce3e4b1d0b06d642a1243281cf55b99e0323680b1b8f904029fd7700", size = 1228048, upload-time = "2026-10-03T12:24:30.556Z" }, + { url = "https://files.pythonhosted.org/packages/1e/4e/2b2ca4602baf92f842316ea617423402089df4fbd2ea42571ba28725ba46/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e62126ac2be2d9340ac1b3ee7a0466a883ecbed634cff0929c88ca0b671483b7", size = 1292087, upload-time = "2026-10-03T12:24:32.45Z" }, + { url = "https://files.pythonhosted.org/packages/d9/49/9ebd05218a87ca31f4f855d5e3df14239bba3c58f2aed9d02c7cba5d94f5/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f93a70fa9826c04ea9f2c3a880f87f4cca09a828144ed5084682abd28110980", size = 1295355, upload-time = "2026-10-03T12:24:34.423Z" }, + { url = "https://files.pythonhosted.org/packages/dc/09/6db7c4327e7a56aba805f7190d377a159fc0bf6bdefb410dc7860624dfa3/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1858887be56a64a2aea899423dfe43787c34c75c18b0d7497de8e618d54b2790", size = 1541231, upload-time = "2026-10-03T12:24:36.05Z" }, + { url = "https://files.pythonhosted.org/packages/ed/85/7ab6097e5fe23cd4657b0e5a2fabb4f789f91e441a3ee40b3ca8b79be238/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89a2bc39a820bc7785b60c53a5742b4e8dd4c1a599294e2dd68fae545883d44a", size = 1317324, upload-time = "2026-10-03T12:24:37.737Z" }, + { url = "https://files.pythonhosted.org/packages/c0/60/58961e7fd129e226ce36788fe328d20034f3105f5d3380df690050517737/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a9eaedf1db4857dad7cc47dd757ed70bfcc40b89d44d516c4a2f0d5033bd76", size = 1311809, upload-time = "2026-10-03T12:24:39.306Z" }, + { url = "https://files.pythonhosted.org/packages/15/c7/09d973db87d4575cba470fd80a3fa489322f7d6882546e43a4b21012468e/ast_serialize-0.12.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:c30b609e8fea426b310543126de876592236a25aa8ebd59f1e2b323dd52a4085", size = 1306678, upload-time = "2026-10-03T12:24:40.891Z" }, + { url = "https://files.pythonhosted.org/packages/84/27/84f69c22bcdaa5256b4fe43ff972fc117668fff8e68495807d5792eadcce/ast_serialize-0.12.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b1ad06513022cfa1337744959255af0ef16119d2beb1e547b67f37ad9433d4a", size = 1362009, upload-time = "2026-10-03T12:24:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/76ee342ef22cd6d82ccd6089d5e2f7163246de73d1816ccb4b6ec0550db6/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6add54b495e37ae3cf3a1f0d5eaba364814eb72e93026adc41b7791e4b0d45d3", size = 1467959, upload-time = "2026-10-03T12:24:44.496Z" }, + { url = "https://files.pythonhosted.org/packages/31/4d/18e48154bbf6058eed8d9b54fcebb2e130f8a380db1a2a202b4faac48626/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5fc136cd08001b817ad0b3e7426f50a7d2b8982dc7c6491f0af78af4c3dd8672", size = 1571157, upload-time = "2026-10-03T12:24:46.156Z" }, + { url = "https://files.pythonhosted.org/packages/34/76/6b16ddf0510e713613a5f5441b13407c1bde6158df04946b9f1fdc65add3/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:45a9e6b700bbd973d49942668a5cdbeffa693f2e250b8a5409abd1fa9d351854", size = 1566873, upload-time = "2026-10-03T12:24:48.542Z" }, + { url = "https://files.pythonhosted.org/packages/75/33/9f6169ae7f60c2da4baec03450073d3f1edb39e95ab538be0d25a7d2f72e/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:a1d8267f83c613ea0a31f2518df074bd62e98a4b3a4892f6a529d74e08e02dba", size = 1673030, upload-time = "2026-10-03T12:24:50.566Z" }, + { url = "https://files.pythonhosted.org/packages/11/51/0d78755bd61d6cf8980f0cfdc7fa8ede38df46a5423c9f7a3da0cff587ec/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a0cbab9796e6ce841197feeba4008faa96b4cc7741129542fd81c882d7a4f01", size = 1480052, upload-time = "2026-10-03T12:24:52.277Z" }, + { url = "https://files.pythonhosted.org/packages/01/ae/ad4c0e5129991f2761f388420c5ded37cb134ec5882e3e59043d33c1ad87/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b4282695f1d51a3c6ef76560351bad5af880eff7d755aefea325bffb9bf68c25", size = 1508940, upload-time = "2026-10-03T12:24:53.846Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/3299182794d38815ae6e9c7ede9bb8f2e4aa93c3578bed201c1ea746643a/ast_serialize-0.12.1-cp39-abi3-win32.whl", hash = "sha256:119d1b0cadaba4a6e475f9bbe79eecc79351e373142eabe7c353f0250d70aebb", size = 1125618, upload-time = "2026-10-03T12:24:55.405Z" }, + { url = "https://files.pythonhosted.org/packages/86/14/5d4fb733c18a1d69e237c067b183842f3a7ea1c999a51ddc87093a281c88/ast_serialize-0.12.1-cp39-abi3-win_amd64.whl", hash = "sha256:3d6ed63d4fc1ec867b8cb522d58c36df0e8f05e487bea0ffd102043a37636d72", size = 1165798, upload-time = "2026-10-03T12:24:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f4/b54123680025c0b7253117418f023d1b2487f1102552acbdd9d8ee96b622/ast_serialize-0.12.1-cp39-abi3-win_arm64.whl", hash = "sha256:610a41351de68199de9a1434499083b4256c0df7658ec1cfc0a0a7b20b08d317", size = 1136560, upload-time = "2026-10-03T12:24:58.689Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/ca/7aa91362a2f77ac8e9e28a9b902a74f7d0e11a851ef0d27a74308da8cd90/charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", size = 226010, upload-time = "2026-09-30T04:36:27.669Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/ac8878d0322cf88a1aad4c7b147db32ca0bd806eb0060957b2e31486dbe6/charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", size = 239266, upload-time = "2026-09-30T04:36:29.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6d/9a08d7e0b29b7208e2c6c01dc56c8e0520e7c7beadbbfb024b58fd69c8a5/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", size = 206783, upload-time = "2026-09-30T04:36:30.872Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/b0aa350280e6ff5a5492d17cf10460dd39d5ee848f872f7ba2df10607f60/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", size = 210449, upload-time = "2026-09-30T04:36:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/40db9aa9f5907bb0e6f8b6d64064bf8852fb33d4b813ff9414911df7647c/charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", size = 367815, upload-time = "2026-09-30T04:36:34.197Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/9c5e7707b57c8ddfa9ddf7b0b1d009d7fbab9e9e887d5b721060f37e307d/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", size = 247095, upload-time = "2026-09-30T04:36:35.803Z" }, + { url = "https://files.pythonhosted.org/packages/83/09/71e453691e927de4ddf792770cfaab3f49d494e222f66ea5e404bbd5e39c/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", size = 233942, upload-time = "2026-09-30T04:36:37.407Z" }, + { url = "https://files.pythonhosted.org/packages/9f/86/85c84e4da8b27dd409577d9437926ff581c5f9d3c66038dc68c1a526de51/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", size = 273762, upload-time = "2026-09-30T04:36:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/564955a4b5f2ccb410ab480bbe8c6a18063ff27f2d35458731c4a5335df9/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", size = 269514, upload-time = "2026-09-30T04:36:40.469Z" }, + { url = "https://files.pythonhosted.org/packages/18/24/bad3ac4271589df29cf5ce2f5ae490518a5739358052bd0d61209e6fea54/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", size = 255056, upload-time = "2026-09-30T04:36:42.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3e/350d89ad49916b86554d6f5f2d03ec1152148f87e5ff735106c6a03b1a36/charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", size = 251311, upload-time = "2026-09-30T04:36:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/5b/4970a2d154df502e133402906dd04e3ae7cada7b3011283c88d0479a2585/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", size = 250190, upload-time = "2026-09-30T04:36:45.185Z" }, + { url = "https://files.pythonhosted.org/packages/88/8c/f1a91bddc8fb47c2889e29ea7ea49a194eb0d9868675d786806519c00d76/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", size = 237098, upload-time = "2026-09-30T04:36:46.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/0e/bb5dace3cc7e79068425386a6589c19b5a2ab5fefc2a46abea6919683332/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", size = 274228, upload-time = "2026-09-30T04:36:48.31Z" }, + { url = "https://files.pythonhosted.org/packages/9d/79/b849ad523017ea9f5a45581bbebed91439e0cf42fd2860a6f64e358eb5a6/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", size = 252401, upload-time = "2026-09-30T04:36:50.091Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/75469d690cf47200bce8f6cad7655724fc23148e147abfc5ce78b5f65863/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", size = 271846, upload-time = "2026-09-30T04:36:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/26/cd/6d52d3c7437cdcf2e310ce9f28f282e733d4ef60ed19105d1819c356255f/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", size = 258151, upload-time = "2026-09-30T04:36:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4c/070b38bdb5f49a70199fce923ec0726a49536a63ab262abbfcaaf351110b/charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", size = 143570, upload-time = "2026-09-30T04:36:54.816Z" }, + { url = "https://files.pythonhosted.org/packages/81/84/9ebfc8ed6c8c4fcd8e726ff6bf220cc8deb3966e31dce9be8dd8aa017e64/charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", size = 186412, upload-time = "2026-09-30T04:36:56.643Z" }, + { url = "https://files.pythonhosted.org/packages/d1/78/5ed86f743d4bc350db307e7636419a0a5ee1d91806d30c7f667bd5c80dae/charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", size = 210965, upload-time = "2026-09-30T04:36:58.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/94/a3a7698e9b1a395e1eb99ccd9a324be9347973bff4e72db2a06496d7cd27/charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", size = 200406, upload-time = "2026-09-30T04:36:59.764Z" }, + { url = "https://files.pythonhosted.org/packages/c1/48/c5dd00d5ef7791f02666de250a5bb6071e29b7e133cf4b835800b6d3bc27/charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", size = 396871, upload-time = "2026-09-30T04:37:01.543Z" }, + { url = "https://files.pythonhosted.org/packages/12/c8/8379554b42e8368161d898476686947a0fdbd3e8865170d7909dcabfdee8/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", size = 249711, upload-time = "2026-09-30T04:37:03.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/eb/2ddb1035d17320caa9f41682935123a9a250277b261c3efc86b2d2a21343/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", size = 231146, upload-time = "2026-09-30T04:37:04.721Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/2ecb4bde104322cd7859d6594fcfa74649f8d90b3221c9feecbef149875b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", size = 271552, upload-time = "2026-09-30T04:37:06.295Z" }, + { url = "https://files.pythonhosted.org/packages/3f/98/9d5f6ebc3aee9fef5d30b4aff11fb2ab7a1222b4064f8ef2c7c87cde217a/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", size = 269810, upload-time = "2026-09-30T04:37:07.905Z" }, + { url = "https://files.pythonhosted.org/packages/09/e1/a3b06a10461b1b7628853c934c644e03bc28e42767116afb52f19a56519b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", size = 259845, upload-time = "2026-09-30T04:37:09.554Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/6f561f74a296cf27d61775a1dc665ad13f3bff6a798810ca05907f37a7c4/charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", size = 252825, upload-time = "2026-09-30T04:37:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/69e13ca3b18f43e0eafcd34c04a45b732ae22a43b54a5fc9e119103356eb/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f", size = 252659, upload-time = "2026-09-30T04:37:12.941Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/ace29806a0dae18939919c76ba526472d83214afa101105fabff2cf30625/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", size = 238819, upload-time = "2026-09-30T04:37:14.659Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c1/6116d52a2e3311ec80f21f5fb5e17b27405f10b9608af8f6e69516841a1b/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", size = 272973, upload-time = "2026-09-30T04:37:16.346Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/9955c7e93bba10a9c7e8f7a5031b7ced66f3a1883a55c00712b8d5850ff3/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", size = 253692, upload-time = "2026-09-30T04:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/bb/33/2a6ae7fdc1b10cb581cef91addd8cdfc5f40d50abb5702309369d5834579/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", size = 268757, upload-time = "2026-09-30T04:37:19.877Z" }, + { url = "https://files.pythonhosted.org/packages/a2/22/80992720a0282cd39bba1db35868e6b9c22f41281160143a836544bc1d8a/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", size = 262220, upload-time = "2026-09-30T04:37:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/92/9f/181fd07e1bffea1d95cd80c84ac537354f50699c22cfc4d3c02b6fc16208/charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", size = 197797, upload-time = "2026-09-30T04:37:23.235Z" }, + { url = "https://files.pythonhosted.org/packages/49/1c/25d8415ec1c4f2f41f1680435e4c87cfb378ff2f677d950946f2a45d0632/charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", size = 222765, upload-time = "2026-09-30T04:37:24.891Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b4/46b48f013dadfc0d0d33b375438e31bdf5a989dc68389c6bf627054d4df9/charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", size = 210985, upload-time = "2026-09-30T04:37:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26", size = 367676, upload-time = "2026-09-30T04:37:28.329Z" }, + { url = "https://files.pythonhosted.org/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364", size = 248414, upload-time = "2026-09-30T04:37:30.167Z" }, + { url = "https://files.pythonhosted.org/packages/26/79/e697f77464748a3ee3cf490c83d592459400d4898380d66c38366b03080c/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253", size = 244775, upload-time = "2026-09-30T04:37:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/3d42a42e18ea066e2513936fd678a00696e77878b5ae04528976abdbcb83/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0", size = 273877, upload-time = "2026-09-30T04:37:33.661Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/8a28136f3938ba9836f84280ce0c4d61ed1cf15a036b2034900c62634162/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc", size = 269455, upload-time = "2026-09-30T04:37:35.573Z" }, + { url = "https://files.pythonhosted.org/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229", size = 257336, upload-time = "2026-09-30T04:37:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a2/8b50aa320adb880ad579518e6f718f24944804b42a88b83d267d5d444125/charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5", size = 257584, upload-time = "2026-09-30T04:37:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/50e3fed84e175f40349bd0da7a4fce94c87f0378f52d74f511d89e0bdc20/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98", size = 251854, upload-time = "2026-09-30T04:37:41.23Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/f7fbb3493c9f49091213b9c2d6dd65800696f1ce1a3f196a4205f50417b1/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3", size = 248767, upload-time = "2026-09-30T04:37:42.883Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/b3a6385acc5a1e45b39ae9c90bfb9cf838a09b9dd37ef2740ab4c6b4a2eb/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2", size = 274479, upload-time = "2026-09-30T04:37:44.658Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/809913e2cfd279e635a9294fdbbfb1b1dc62a8189d473d561f649fce98d8/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf", size = 258310, upload-time = "2026-09-30T04:37:46.529Z" }, + { url = "https://files.pythonhosted.org/packages/af/a2/f28400ab13359d91bd39179df8e149376b9bf36588e739a3a4f9de2b84b2/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95", size = 271100, upload-time = "2026-09-30T04:37:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/e9/89/9bab37955edf0adb3b66f8a3a6617d9f2f487e0d56f295a6a286cb640aa6/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d", size = 259936, upload-time = "2026-09-30T04:37:50.023Z" }, + { url = "https://files.pythonhosted.org/packages/23/b5/4459e08d45a679f903d50fea08bc52cfa728cca4d7bd02c757b5e5abda2e/charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847", size = 186301, upload-time = "2026-09-30T04:37:51.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8", size = 210858, upload-time = "2026-09-30T04:37:53.427Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a", size = 200309, upload-time = "2026-09-30T04:37:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f9/00ee0195db1013d8f7c416fd770fbeb560bb46eb2e36b054d05cb56f6cfa/charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1", size = 395343, upload-time = "2026-09-30T04:37:56.743Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/c00b50e94c964cf934c7899cd47c97952fc11dad71cc5884b3c61795b09b/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b", size = 250114, upload-time = "2026-09-30T04:37:58.607Z" }, + { url = "https://files.pythonhosted.org/packages/50/27/d102dc880bbcffd0479ab64dfc1fb96777a854355a55e2bda72a71efadcb/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f", size = 239997, upload-time = "2026-09-30T04:38:00.511Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4a/bf7ef45794dd293fab5f98a9309817977fbb845b9998f171b8cc5d8437a3/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3", size = 272313, upload-time = "2026-09-30T04:38:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ee/008a2837737991474c5754bb3191010007663860979701990982a502cbaf/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e", size = 269986, upload-time = "2026-09-30T04:38:04.435Z" }, + { url = "https://files.pythonhosted.org/packages/93/ad/bd74a283940dc910c5b14f8e4f80a248082bc9c0fcbe1f54530cb6d9cc5e/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9", size = 260202, upload-time = "2026-09-30T04:38:06.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7b/ed341c66f69f688723501fac752be3d63c7159ca0d0d4174fc611e5710bb/charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a", size = 259083, upload-time = "2026-09-30T04:38:08.311Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9d/e41588b777965e5031a43128a1e96173ebb35ac75fc53ec3b517e7c21cd4/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115", size = 253081, upload-time = "2026-09-30T04:38:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/81/35/b761eb6d8c1eb218b9b42b9b4d5ac902afdc399fb6dac6f9a9aac7bda589/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c", size = 248712, upload-time = "2026-09-30T04:38:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2c/147169a041b747759f37405c0a97157e8e92de967968373101ff14915cba/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d", size = 274176, upload-time = "2026-09-30T04:38:14.138Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2d/0ff8db0d373ba8538db686db11cd7e8912031490b9e4f383b41912e8d594/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d", size = 259413, upload-time = "2026-09-30T04:38:15.841Z" }, + { url = "https://files.pythonhosted.org/packages/8a/8e/b4a085fb47c9d3a7e43576a4784fdd8fe23f907514a972de8086edaf7a48/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4", size = 269434, upload-time = "2026-09-30T04:38:17.626Z" }, + { url = "https://files.pythonhosted.org/packages/83/1c/d8d8d7322a7c3eecdf3237a4a419cf41d2eaad8e006ce7dfdd9d4c8fa2eb/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b", size = 262716, upload-time = "2026-09-30T04:38:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/16/0e4c6ba9b44e97a2da150e52d331e8f9c968b21b358fbffa6c856cebcd89/charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800", size = 197159, upload-time = "2026-09-30T04:38:21.037Z" }, + { url = "https://files.pythonhosted.org/packages/be/33/e90bc2b1374f7f36ef106f56620de5a783907e19ca857efe2277e31cac3e/charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21", size = 221322, upload-time = "2026-09-30T04:38:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/66/89/dfa6dcb08c200b7830ab56439e8c1890f2971d51aafbb3937894a2e7fcfc/charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58", size = 209192, upload-time = "2026-09-30T04:38:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" }, + { url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" }, + { url = "https://files.pythonhosted.org/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" }, +] + +[[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, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "gogupdater" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "loguru" }, + { name = "pyside6" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "tqdm" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ast-serialize" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-mock" }, + { name = "ruff" }, + { name = "types-requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "loguru", specifier = ">=0.7.3" }, + { name = "pyside6", specifier = ">=6.12.0" }, + { name = "python-dotenv", specifier = ">=1.2.4" }, + { name = "requests", specifier = ">=2.34.2" }, + { name = "tqdm", specifier = ">=4.70.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "ast-serialize", specifier = "<0.13" }, + { name = "mypy", specifier = ">=2.4.0" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "pytest-mock", specifier = ">=3.16.0" }, + { name = "ruff", specifier = ">=0.17.0" }, + { name = "types-requests", specifier = ">=2.33.0.20261006" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "librt" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" }, + { url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" }, + { url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" }, + { url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" }, + { url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" }, + { url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" }, + { url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" }, + { url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" }, + { url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" }, + { url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" }, + { url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" }, + { url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" }, + { url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" }, + { url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + +[[package]] +name = "mypy" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" }, + { url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" }, + { url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" }, + { url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" }, + { url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" }, + { url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" }, + { url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" }, + { url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" }, + { url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[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, upload-time = "2025-05-15T12:30:07.975Z" } +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 = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyside6" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-addons" }, + { name = "pyside6-essentials" }, + { name = "pyside6-pdf" }, + { name = "pyside6-webengine" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/db/0722bbb81c3515dd0847fd66a7fb4760b13ad59c89e40b27b362ab5624e2/pyside6-6.12.0-cp310-abi3-macosx_14_0_universal2.whl", hash = "sha256:efea4c2f2aa54403c45de825865a902a1960a1cc9d5aaa92d4ebb8f4c4cd0708", size = 577475, upload-time = "2026-10-08T09:11:15.222Z" }, + { url = "https://files.pythonhosted.org/packages/2f/53/db0cabed1e70d0bc4666b8cbc2611b72bff2f9d2c4813820182621a8c6ee/pyside6-6.12.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d13ed4d0053cc20f3e4e0fdbf12790a9b7f958d7e22c0843411ecfa8c799e2a9", size = 577709, upload-time = "2026-10-08T09:11:17.345Z" }, + { url = "https://files.pythonhosted.org/packages/4d/8c/dfd4f339285d2e6c31067789b9b7dd2cee26c098ecc78ac7200fb04aa344/pyside6-6.12.0-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:8920e163f60493e18822d20f1e01c7c4bcf9c1c5cfbc68284788896f692594f8", size = 577708, upload-time = "2026-10-08T09:11:18.705Z" }, + { url = "https://files.pythonhosted.org/packages/a1/da/8e8514fc29ea916c3cd71a524480b77409e19ed12d709e8a5d73be089b81/pyside6-6.12.0-cp310-abi3-win_amd64.whl", hash = "sha256:31a8d2893f2f28ab483b1534dde0438f27af6e234593de30425cb4729052d8f8", size = 578958, upload-time = "2026-10-08T09:11:20.179Z" }, + { url = "https://files.pythonhosted.org/packages/5f/2e/3ebafaf7f4ccea6f6ea9d017f7fc7d9484b79e6b31d1cb92f5f5262055f0/pyside6-6.12.0-cp310-abi3-win_arm64.whl", hash = "sha256:22e2ec6407f32b61955a28f91ab55676e60017210285c28f0c381bfeed389414", size = 562743, upload-time = "2026-10-08T09:11:21.552Z" }, +] + +[[package]] +name = "pyside6-addons" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-essentials" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/16/c401d6455dbf49df37a8b142883e12d446abf51919c3e0ef5d9570e29160/pyside6_addons-6.12.0-cp310-abi3-macosx_14_0_universal2.whl", hash = "sha256:1d9bcd294ce1c9829194c7d13b01f771a2254b86cb3462cc3399321d2797fdac", size = 90545550, upload-time = "2026-10-08T09:08:14.778Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3e/0d6c5934de575286ac0699a701d4155048185035b8da0ee78d57a38bc7ef/pyside6_addons-6.12.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4ab38c0017f0453671a9313d3a53ca1c0e378924f54b07e596bcea94a9642391", size = 59192702, upload-time = "2026-10-08T09:08:21.13Z" }, + { url = "https://files.pythonhosted.org/packages/94/14/b6c5683614601448c3ea8f19613ca378db75158ba594b662a8a8041e3e5f/pyside6_addons-6.12.0-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:48cd0b639c06c9f79685f340ccae4319614b66d6efcdf06c2ce3836790d8c46e", size = 63646547, upload-time = "2026-10-08T09:08:26.921Z" }, + { url = "https://files.pythonhosted.org/packages/02/41/ba3d52fe2acd942ebf71eb02b3bdf0897f7f6f4ce8f52289cb50228e48d3/pyside6_addons-6.12.0-cp310-abi3-win_amd64.whl", hash = "sha256:98d8d62010da6605344cee83e5c96fb846170a1f2d924523f10f5d92078b02b0", size = 40522402, upload-time = "2026-10-08T09:08:31.678Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/860ab764368cd59b1a09cb891a9a4519425395ad1f6c47d83fde908fe603/pyside6_addons-6.12.0-cp310-abi3-win_arm64.whl", hash = "sha256:3c079cbaad2ad28bc5bfa92fd6bac4ba28d622b825825e3b4220f0682599b2db", size = 35940682, upload-time = "2026-10-08T09:08:35.424Z" }, +] + +[[package]] +name = "pyside6-essentials" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/90/d528f8dd3f9f20712bc719e10c698d27c998e1dd76ed3f33b6fb606d0972/pyside6_essentials-6.12.0-cp310-abi3-macosx_14_0_universal2.whl", hash = "sha256:0867b709a724db28b161227c7d6ecb5ed10cc09226a53542417ac6dee1308239", size = 116002775, upload-time = "2026-10-08T09:09:54.453Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3c/7597d8dcb36aa8315427635ec6ec9d850ffbad4ce78b9118ac854889f2d3/pyside6_essentials-6.12.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:16f4b5e41daf49235ae048458243da9efe5f3d8040f9cc5aa454ce69893d3bd9", size = 84346433, upload-time = "2026-10-08T09:10:02.825Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4e/f4fd4c016719be832ec49d3c87940459e87fd666179a0a199a3998d07916/pyside6_essentials-6.12.0-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:08c5841063fc1df69b7fa1ee405f539e66576a83fc48004be3e2c5e35ccbfa69", size = 83810102, upload-time = "2026-10-08T09:10:09.13Z" }, + { url = "https://files.pythonhosted.org/packages/03/61/9a7e32a37b224f68506785bfb0f288d516098ec71681782d72b695a6c95d/pyside6_essentials-6.12.0-cp310-abi3-win_amd64.whl", hash = "sha256:c9a95102aa23c1f86516a30d5a9be37552d324eee7e567eb7508973f1218cb42", size = 82153795, upload-time = "2026-10-08T09:10:15.104Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/43afb3d0e7c25dd09f0eb7e44f1283dadb5fa8d8b517249fe3fde02e6702/pyside6_essentials-6.12.0-cp310-abi3-win_arm64.whl", hash = "sha256:c76aa689989bf9bb9b45535e0a81c6733294df720b0eed1308fa30d2606b98cc", size = 62073456, upload-time = "2026-10-08T09:10:20.202Z" }, +] + +[[package]] +name = "pyside6-pdf" +version = "6.12.0.140" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-essentials" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/33/23949c3b48af4b96b618e0502b7313ac52a782615db8dceae5bc2c516831/pyside6_pdf-6.12.0.140-cp310-abi3-macosx_14_0_universal2.whl", hash = "sha256:74287989dd24a82ac1604e27a2d4530a0ab71ff03cdf878c16fdbcf5144373fb", size = 8644803, upload-time = "2026-10-08T09:07:36.716Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f0/9b4f3cb0506641ea53210c7b635703f1c2c1e81c27074438105ff118f8e7/pyside6_pdf-6.12.0.140-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0c94370b6b9d65fbfe54f9d6d25df0677db0cdcfd0a842a63958a3603a42007c", size = 2843877, upload-time = "2026-10-08T09:07:39.208Z" }, + { url = "https://files.pythonhosted.org/packages/39/cb/4a8cf3edb3f1b2396935573c02255be75f5605dcc77e9e3a201a6d6f26d7/pyside6_pdf-6.12.0.140-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:2b63ee1f8ce3d3b2b648be2ebda483dd0fbca8d44c92a6c6f924033ac7fbde8c", size = 2837165, upload-time = "2026-10-08T09:07:40.756Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8a/3f5eab9eb9e6486f0334b100322bbd0aff432dd90ad7977a3910979a163f/pyside6_pdf-6.12.0.140-cp310-abi3-win_amd64.whl", hash = "sha256:4971ba1a0ca15e7c29cfd808e9600aeb46b2305dad01456eb09fa3197166268e", size = 2983516, upload-time = "2026-10-08T09:07:42.312Z" }, + { url = "https://files.pythonhosted.org/packages/b2/87/5c29bdaf47bf0b0cee7f467e86ffeb07be5bf8aa2afd2ff6f0785d0515b4/pyside6_pdf-6.12.0.140-cp310-abi3-win_arm64.whl", hash = "sha256:ce6e4d858df1eb4e2adcbd8ba212f3611ce60f9978881fe10d417f36013a6ff9", size = 2758117, upload-time = "2026-10-08T09:07:43.936Z" }, +] + +[[package]] +name = "pyside6-webengine" +version = "6.12.0.140" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-addons" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/37/3b50067bdcba5c22e20a8d7a029c919850c6d94f8398175b330c3b8765ad/pyside6_webengine-6.12.0.140-cp310-abi3-macosx_14_0_universal2.whl", hash = "sha256:9a1de2da360d1817c08e9400a2a2fc5234beb3022a22123e15355169e183074e", size = 249778242, upload-time = "2026-10-08T09:06:03.602Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e0/cb8b68b962afc253709ed61555d0a4f297fd6786c0e77921d2e951504210/pyside6_webengine-6.12.0.140-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c041c9921d7c91c9e51a2c027f434d05c3c59baa2c8f17b71dc8194e77791f06", size = 116741921, upload-time = "2026-10-08T09:06:13.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/34/75944ed126293868fcbcc589fa2254e3310016920e5d32d21c29be021524/pyside6_webengine-6.12.0.140-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:66a842ccb7f06fa7e357f6d4d63bc2739e3cbc1f93583f314e38c43024fe5f74", size = 114053128, upload-time = "2026-10-08T09:06:21.275Z" }, + { url = "https://files.pythonhosted.org/packages/21/0d/260c7c286f2d4e8a49c099152a385414a872f022117f937824ca0a3f38e5/pyside6_webengine-6.12.0.140-cp310-abi3-win_amd64.whl", hash = "sha256:f9b61b1d457a591ee590a6b80515f25a3e61e2576a2a0a70566380f9d9de4ab0", size = 131697708, upload-time = "2026-10-08T09:06:29.606Z" }, + { url = "https://files.pythonhosted.org/packages/76/34/7a2d1d7b95c96adf173c77d1862c2587789f7afb8754430fcfc2c9e4e47a/pyside6_webengine-6.12.0.140-cp310-abi3-win_arm64.whl", hash = "sha256:44d1fac2a057a71b8f947fc5177a95fab8a0ea4e2fda068f223a00fd0193a8c7", size = 65723, upload-time = "2026-10-08T09:06:32.559Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/7f/6ed29931d5c8cd396e7c0a55412e6cc88020373365c8685985dea53d26d7/pytest_mock-3.16.0.tar.gz", hash = "sha256:5a8395528b8f498205f3718f575228d0edaed7425fff638f87d1a6c3e0383636", size = 35362, upload-time = "2026-09-27T14:57:55.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/5b/b83a9bf1a3b4ec222f9fa083147ff6816245223da0ab92370e7e056f113f/pytest_mock-3.16.0-py3-none-any.whl", hash = "sha256:007cfeb257801d88d9c0b2a7b5a15a15e73b71968dfd72e7bf8c4a2f8393aec8", size = 10016, upload-time = "2026-09-27T14:57:54.283Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +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 = "ruff" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/a7/70debb024dfacda67b8e560cc7511f52b34b9348a8cc8c1ec23036dcd51d/ruff-0.17.0.tar.gz", hash = "sha256:5cd03240d8208a557c2a9655a5cb07ebe36aa6bb35065f97d48c1f6adef5a322", size = 5320554, upload-time = "2026-10-09T19:47:29.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/f8/ee5ab9da6089eae2a33e6008b01deb1eda19992c1c8e10661e98cee1640f/ruff-0.17.0-py3-none-linux_armv6l.whl", hash = "sha256:0e271826af9a20d18c6cfae8c51e82959167c24859686ddd3eb9a7f0842ce81e", size = 10148907, upload-time = "2026-10-09T19:46:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d9/2f81fb5a9d580afbb11b1c8ff915233a11f2a1b27405d7991f183c5e1976/ruff-0.17.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5f0ca4a40f81403689c04f12966e22f44e329ae362072d8f1587b7bda87f603b", size = 10224208, upload-time = "2026-10-09T19:46:41.711Z" }, + { url = "https://files.pythonhosted.org/packages/a7/20/643f3c8f75594f937b2bf74801241c56a2e2b8e139d24dff8b66b28cdd7f/ruff-0.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbf7149e0927dc3295d5d64679a4765576eef71b00782b2ae969ef82274d6bb9", size = 9273324, upload-time = "2026-10-09T19:46:44.323Z" }, + { url = "https://files.pythonhosted.org/packages/ec/91/627700b233d367736cb274f1bd0b47d1f2b12f68878192812bd875adadc3/ruff-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ee90156522998c3037059d8f66885c8adeeaf7643bdce2caceee196ecd23e0", size = 9953190, upload-time = "2026-10-09T19:46:47.155Z" }, + { url = "https://files.pythonhosted.org/packages/cd/92/91f7b5ed39490f89d6cbf56e1f543c383667a725efa8e2c2dee0f01f5591/ruff-0.17.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d8e4a002a94cd9d0dc48b51dc69d807a172b5b9bf2b668e656424dc5b55ead1", size = 9961746, upload-time = "2026-10-09T19:46:50.098Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/7310f9fc63ce11ff6394edbd5e85433dfb0e14c9fbf6ccc97f1538491bc7/ruff-0.17.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b8a60c06a218c337e1161638d34757f83449243e2db161483ddf948e53ad14", size = 10625483, upload-time = "2026-10-09T19:46:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8d/97443f0dca4a03a0bc7629fd396fd494a1cb6666121e38c5075acb217d8f/ruff-0.17.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a330178bdffc4205dbf3bda11d93e059e388fd6546f8cdd304501a9160363c0d", size = 11350129, upload-time = "2026-10-09T19:46:56.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/c525efd9777be4b6b012e6969a3012648468e7e6c4b3e5b46af69f46e8eb/ruff-0.17.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bb08489e234876fa2da67ae3ea938e9a2156da80293e0e4365abd6973d98329", size = 10906223, upload-time = "2026-10-09T19:47:00.263Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/4a01195d93420cad1175bedad13a515dc8a56f95a6e39789b92e582682f5/ruff-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7c133e82d55b5f15897b2a442d72c0cb4a0c886c46801ce3c247150b60c", size = 10386314, upload-time = "2026-10-09T19:47:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/c7/72/1a3951665485a921f6375f91e754a1854d5a645d41acc3642668064ff64d/ruff-0.17.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:db4f74c533403ab70fe4007873f6ae0c9f94a8b03158cf48d78788e47cdbe399", size = 10565464, upload-time = "2026-10-09T19:47:05.831Z" }, + { url = "https://files.pythonhosted.org/packages/90/8c/539b4d8c082f57e18db8ae2be85a460d77861c79dcd5798e32b536a6a06f/ruff-0.17.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3d8cc360e666d1914e47b0777c6906d70cf18891a55532bd0a16844195d70859", size = 10079468, upload-time = "2026-10-09T19:47:08.617Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b8/84286966db79434e8c26b585b0a0f6897cb3ab1c51a4aa4df10c28488b62/ruff-0.17.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d66de796b726c4801e05fa99a2a8d7a780e107be222486c304ab61765561e866", size = 9958556, upload-time = "2026-10-09T19:47:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/27b6eed27b83fcdd5bfa0d52b83231e29094754374698da404d094487ae3/ruff-0.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c3f268baf004aea944f040623327119527ea231af15f7fb7890e82cea0679589", size = 10352173, upload-time = "2026-10-09T19:47:14.188Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/955399fd13044cd827862044117d784a59e3196f6cce7424908ac9a7f914/ruff-0.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:864b6c1acb6b0bccf94b5a3938a1531fd09aaca5e5659a2e7bf0f3cf2a685540", size = 10791761, upload-time = "2026-10-09T19:47:16.931Z" }, + { url = "https://files.pythonhosted.org/packages/ae/bf/024e01e1f6aec87768696725e648ed5b438941341eea8f8100beb681961f/ruff-0.17.0-py3-none-win32.whl", hash = "sha256:5e50aa5b84decd9fe5b0bb0e6f71c3b592f1767ed09faa4b7207d933961e35cd", size = 9904112, upload-time = "2026-10-09T19:47:19.75Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/1ee73df41dcc8d1cdb686ee4bc46ea29704ea175feb6b95c78420f631ab8/ruff-0.17.0-py3-none-win_amd64.whl", hash = "sha256:8ab76bcda86dfd28e13776cb5de3c7bcdcf1ae3d37ed761113d1a5a415dc134c", size = 10115585, upload-time = "2026-10-09T19:47:22.698Z" }, + { url = "https://files.pythonhosted.org/packages/fd/71/eb4f0ccc844aece56963e8578df9c95d4c00f580547d52329d4035d3af18/ruff-0.17.0-py3-none-win_arm64.whl", hash = "sha256:c154c73ff43f9854395e24cac507af13078962e53d2b511605058d22af1fdb88", size = 9872035, upload-time = "2026-10-09T19:47:26.306Z" }, +] + +[[package]] +name = "shiboken6" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/45/11536fba5f50a57671eb14441c6098096f3e57074e54c9d7d66039a5831e/shiboken6-6.12.0-cp310-abi3-macosx_14_0_universal2.whl", hash = "sha256:47105e05baf57d35453e07d240bbe59cb4b8d9565377ea5134ff9fb37d4f5c02", size = 479653, upload-time = "2026-10-08T09:10:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/49/c2/a04fa003b4360e99d8e3fc2829abacf37396ace63ea1cd616402209ec7c6/shiboken6-6.12.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bde565bb0890044b5c7f63808adddede5abc241e9395a2bfed61919884d6240", size = 279268, upload-time = "2026-10-08T09:10:42.982Z" }, + { url = "https://files.pythonhosted.org/packages/97/ca/a4415e6218e0dff764fe2b2d30db2fc8f499a08ace07634232ade3f73adc/shiboken6-6.12.0-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ff72a72b3277b418902d562b987b33a6f6818bed9fe40a49d634d09bc67f620e", size = 276562, upload-time = "2026-10-08T09:10:44.25Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b6/bee97e7a2edeab72aafabb46ee5adc939790a638d107860b4cad7a20e407/shiboken6-6.12.0-cp310-abi3-win_amd64.whl", hash = "sha256:a1906cb8116869178c64b6bab52623bbc8b4ceed7b30bfc3c9aa0f6e67f6438e", size = 1265476, upload-time = "2026-10-08T09:10:45.627Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d5/5894d35f8a470ca8acff0d1cd8f15b1c94cbfdfe0ce4a6ec0fcf10b95bd1/shiboken6-6.12.0-cp310-abi3-win_arm64.whl", hash = "sha256:da382e68f0815b31b6dc2bce11a465b148c4f42c37b91546a361de1576567d10", size = 1838851, upload-time = "2026-10-08T09:10:47.123Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/ea/b2a5bd54b28a324dae8211928b2d730b6547500342c7e6c6dea08bd0a485/tqdm-4.70.1.tar.gz", hash = "sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4", size = 171846, upload-time = "2026-09-11T07:25:16.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl", hash = "sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73", size = 80199, upload-time = "2026-09-11T07:25:14.599Z" }, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20261006" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/15/9b7e2e2e7c87d01366185b198b3febc6bc0c973f2bf21a62da4ab7d3495e/types_requests-2.33.0.20261006.tar.gz", hash = "sha256:0652999e9306aea345f40732d58fa49a7f6cade6a0d74d92119c5c8d82eddaf0", size = 25316, upload-time = "2026-10-06T08:15:57.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/72/b82789207b3d360ce9f5a4372c790c52cc3dd928aeb7d4faeec652b651b2/types_requests-2.33.0.20261006-py3-none-any.whl", hash = "sha256:26cc8146505cab33cda9737991929e4144c559bebe05078ccc6998f27c4ca2c1", size = 21445, upload-time = "2026-10-06T08:15:56.658Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "urllib3" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +]