Merge Library and Status into a unified Games tab
This commit is contained in:
@@ -1 +0,0 @@
|
||||
3.14
|
||||
+8
-60
@@ -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.
|
||||
|
||||
+3
-2
@@ -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}")
|
||||
|
||||
|
||||
Generated
-717
@@ -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"
|
||||
@@ -1,7 +1,9 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from src.constants import VERSION
|
||||
|
||||
load_dotenv()
|
||||
|
||||
+19
-17
@@ -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" }
|
||||
]
|
||||
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",
|
||||
]
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
"""Auto-generated — do not edit manually."""
|
||||
__version__ = "1.2.0"
|
||||
__version__ = "1.0.0"
|
||||
|
||||
+28
-10
@@ -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,25 +29,36 @@ 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:
|
||||
# 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")
|
||||
|
||||
+167
-71
@@ -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,7 +223,8 @@ class MetadataStore:
|
||||
|
||||
deleted: list[Path] = []
|
||||
for version in versions_to_remove:
|
||||
version_dir = self.base_path / record.name / version
|
||||
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)
|
||||
@@ -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
|
||||
|
||||
+30
-21
@@ -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)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
def main() -> None:
|
||||
print("Hello from gogupdater!")
|
||||
+16
-9
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
+7
-11
@@ -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)
|
||||
|
||||
@@ -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.")
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
+1
-1
@@ -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
|
||||
|
||||
+45
-3
@@ -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.",
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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" },
|
||||
]
|
||||
Reference in New Issue
Block a user