Compare commits

...

15 Commits

Author SHA1 Message Date
tretrauit 0140969e4b
game: more refactoring
Signed-off-by: tretrauit <tretrauit@gmail.com>
2022-12-07 12:52:09 +07:00
tretrauit 680cf60720
fix: ImportError fix 2022-09-28 12:24:24 +07:00
tretrauit 6a76c49323
perf: rework parse_pkg_version & verify_game_files
parse_pkg_version will now return a dict which we can use key to access the value (md5 sum in this case) and therefore will provide faster speed than using previous method
2022-09-20 22:47:25 +07:00
tretrauit 66163f8f45
docs: fix docs for verify_game_file 2022-09-20 17:45:09 +07:00
tretrauit 208b34c9df
feat: verify_game_file, rename verify_game to verify_game_files
You can now use verify_game_file to verify a game file from a pkg_version (either parsed pkg_version or a path to pkg_version is fine), and verify_game_files to verify all game files from a pkg_version
2022-09-20 17:43:11 +07:00
tretrauit f61e4a39cf
fix: change PathLike to Union[str, bytes, PathLike]
This allows user to use str & bytes as path too.
2022-09-20 17:23:20 +07:00
tretrauit e133508d39
poetry: add cli script 2022-09-20 17:16:16 +07:00
tretrauit 96d1c7e8d3
refactor: move gamemanager.py to game & rename to Game
Also add back some staticmethods, because it'll be useful if you only import the Game class
2022-09-20 16:35:56 +07:00
tretrauit 811403bdfa
docs: write docs for most functions
The documentation follows Google python codestyle
2022-09-20 00:37:25 +07:00
tretrauit 44f0d10cb8
refactor: move static methods to module-level function 2022-09-18 21:38:44 +07:00
tretrauit 2abbdf066b
refactor: more cleanup
feat: initial support for callbacks
2022-09-18 18:41:58 +07:00
tretrauit f909297ae0
refactor: many refactoring changes
helper: create helper.py with download_file
refactor: move hdiffpatch to its own hdiffpatch.py
fmt: use black for formatting
2022-09-18 11:09:44 +07:00
tretrauit 42d7179ea5
poetry: add lock file 2022-09-18 11:00:10 +07:00
tretrauit c8823ea3b1
refactor: remove AsyncPath
feat: experimental threaded downloading
Also add very little support to Bilibili version.
Currently this isn't usable, so it'll need some time to be refactored
2022-09-18 01:58:51 +07:00
tretrauit 7d4f4e7931
refactor: migrate to pyproject.toml 2022-09-18 00:24:00 +07:00
35 changed files with 2309 additions and 1226 deletions

786
poetry.lock generated Normal file
View File

@ -0,0 +1,786 @@
[[package]]
name = "aiohttp"
version = "3.8.1"
description = "Async http client/server framework (asyncio)"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
aiosignal = ">=1.1.2"
async-timeout = ">=4.0.0a3,<5.0"
attrs = ">=17.3.0"
charset-normalizer = ">=2.0,<3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
yarl = ">=1.0,<2.0"
[package.extras]
speedups = ["Brotli", "aiodns", "cchardet"]
[[package]]
name = "aiosignal"
version = "1.2.0"
description = "aiosignal: a list of registered asynchronous callbacks"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]]
name = "appdirs"
version = "1.4.4"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "async-timeout"
version = "4.0.2"
description = "Timeout context manager for asyncio programs"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "attrs"
version = "22.1.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=3.5"
[package.extras]
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
[[package]]
name = "bitstruct"
version = "8.15.1"
description = "This module performs conversions between Python values and C bit field structs represented as Python byte strings."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "cffi"
version = "1.15.1"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
pycparser = "*"
[[package]]
name = "charset-normalizer"
version = "2.1.1"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=3.6.0"
[package.extras]
unicode_backport = ["unicodedata2"]
[[package]]
name = "colorama"
version = "0.4.5"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "detools"
version = "0.51.0"
description = "Binary delta encoding tools."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
bitstruct = "*"
heatshrink2 = "*"
humanfriendly = "*"
lz4 = "*"
pyelftools = "*"
zstandard = "*"
[[package]]
name = "frozenlist"
version = "1.3.1"
description = "A list-like structure which implements collections.abc.MutableSequence"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "heatshrink2"
version = "0.11.0"
description = "Python bindings to the heatshrink library"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "humanfriendly"
version = "10.0"
description = "Human friendly output for text interfaces using Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""}
[[package]]
name = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "lz4"
version = "4.0.2"
description = "LZ4 Bindings for Python"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["sphinx (>=1.6.0)", "sphinx-bootstrap-theme"]
flake8 = ["flake8"]
tests = ["psutil", "pytest (!=3.3.0)", "pytest-cov"]
[[package]]
name = "multidict"
version = "6.0.2"
description = "multidict implementation"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pycparser"
version = "2.21"
description = "C parser in Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyelftools"
version = "0.29"
description = "Library for analyzing ELF files and DWARF debugging information"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
optional = false
python-versions = ">=3.6.8"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pyreadline3"
version = "3.4.1"
description = "A python implementation of GNU readline."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pytest"
version = "7.1.3"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
tomli = ">=1.0.0"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]]
name = "yarl"
version = "1.8.1"
description = "Yet another URL library"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
[[package]]
name = "zstandard"
version = "0.18.0"
description = "Zstandard bindings for Python"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\""}
[package.extras]
cffi = ["cffi (>=1.11)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "6be8320905e4c9f9da4ae9d4d3bd2066bd2d5a475851030bd7f2c362aaefeed2"
[metadata.files]
aiohttp = [
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"},
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"},
{file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"},
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"},
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"},
{file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"},
{file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"},
{file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"},
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"},
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"},
{file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"},
{file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"},
{file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"},
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"},
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"},
{file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"},
{file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"},
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"},
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"},
{file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"},
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"},
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"},
{file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"},
{file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"},
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"},
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"},
{file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"},
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"},
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"},
{file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"},
{file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"},
{file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"},
]
aiosignal = [
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
async-timeout = [
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
]
attrs = [
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
]
bitstruct = [
{file = "bitstruct-8.15.1.tar.gz", hash = "sha256:6fa6adbfb8f3b8cb68c21b13aa65d23eb2c3ac32419ab926f3fd1fff717a9125"},
]
cffi = [
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
{file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
{file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
{file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
{file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
{file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
{file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
{file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
{file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
{file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
{file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
{file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
{file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
{file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
{file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
{file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
{file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
{file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
{file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
{file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
{file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
{file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
{file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
{file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
{file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
{file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
{file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
{file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
{file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
{file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
{file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
{file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
{file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
{file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
{file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
{file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
{file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
{file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
{file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
{file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
]
charset-normalizer = [
{file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
{file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
]
colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
detools = [
{file = "detools-0.51.0.tar.gz", hash = "sha256:f53d72a9a036c52429910ca0aa7f84ef22fa758231821b2240d2021c50e822b5"},
]
frozenlist = [
{file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989"},
{file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204"},
{file = "frozenlist-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3"},
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9"},
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a"},
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8"},
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d"},
{file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca"},
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131"},
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221"},
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9"},
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2"},
{file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5"},
{file = "frozenlist-1.3.1-cp310-cp310-win32.whl", hash = "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be"},
{file = "frozenlist-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db"},
{file = "frozenlist-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944"},
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04"},
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb"},
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff"},
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b"},
{file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2"},
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b"},
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a"},
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03"},
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10"},
{file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c"},
{file = "frozenlist-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792"},
{file = "frozenlist-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc"},
{file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e"},
{file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519"},
{file = "frozenlist-1.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f"},
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8"},
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e"},
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170"},
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f"},
{file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b"},
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f"},
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83"},
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b"},
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988"},
{file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2"},
{file = "frozenlist-1.3.1-cp38-cp38-win32.whl", hash = "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845"},
{file = "frozenlist-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d"},
{file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1"},
{file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab"},
{file = "frozenlist-1.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3"},
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96"},
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b"},
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c"},
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141"},
{file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd"},
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f"},
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef"},
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6"},
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa"},
{file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc"},
{file = "frozenlist-1.3.1-cp39-cp39-win32.whl", hash = "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b"},
{file = "frozenlist-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189"},
{file = "frozenlist-1.3.1.tar.gz", hash = "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8"},
]
heatshrink2 = [
{file = "heatshrink2-0.11.0-cp310-cp310-win32.whl", hash = "sha256:afd697de31deab5742496864c286a005de099b10a574f013d353392814c3d954"},
{file = "heatshrink2-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:04e14f5f51a70249d10463d2f6190c2443ddb06b0db46eb9eba50c3c349fbfb8"},
{file = "heatshrink2-0.11.0-cp37-cp37m-win32.whl", hash = "sha256:bc744b7481be60e23eaa970adcc0a0a9ade9afa5f38cf0eb4c7756abd54e6049"},
{file = "heatshrink2-0.11.0-cp37-cp37m-win_amd64.whl", hash = "sha256:42acd10f57fdab11d193d4e59fa2852d494621b8a09d1019e889592e209b86bb"},
{file = "heatshrink2-0.11.0-cp38-cp38-win32.whl", hash = "sha256:d11c2ab1cafd591c64deda5587d311c9bfd6a43bf7e87cb7fd0a7582e8a31e14"},
{file = "heatshrink2-0.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:c71cd0427a6c472be4fe425bf83f5677cc9e8341199ff4ea4f45b5968b66b8ee"},
{file = "heatshrink2-0.11.0-cp39-cp39-win32.whl", hash = "sha256:f5d54c79a45982925fdc06b8906b2472441e4435564aa9db0f5b1d05a548efba"},
{file = "heatshrink2-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:a9052fb9f6efadbb95630f4488f6abcf5f2c6169faa01df24a1277a6f2c50165"},
{file = "heatshrink2-0.11.0.tar.gz", hash = "sha256:c6286d1d4b92fbaeb83b0fd8b5f831c9350dcd1046e776e5aaa83d1be43835f7"},
]
humanfriendly = [
{file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"},
{file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"},
]
idna = [
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
lz4 = [
{file = "lz4-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3881573c3db902db370e072eb64b40c7c8289b94b2a731e051858cc198f890e8"},
{file = "lz4-4.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:154e6e9f58a7bafc4d2a1395160305b78fc82fa708bfa58cf0ad977c443d1f8f"},
{file = "lz4-4.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cfa82f26b4f1835c797bd70e5ce20d5f1ee897b9a0c53e62d607f9029f521ce"},
{file = "lz4-4.0.2-cp310-cp310-win32.whl", hash = "sha256:fba1730cd2327a9d013192a9878714cc82f4877d2ada556222d03ea6428a80ed"},
{file = "lz4-4.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:61dbcca64e8e1655e06b588356c4b2515bccc1d7e84065f858a685abd96f0cf2"},
{file = "lz4-4.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56ea660097fec87f0c6746146b316775037f8dd886a4c5915360e5b32b7112d0"},
{file = "lz4-4.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed86ab22bfe1f4cd4fc983704134a8fdf746c1121a398f8f14cbd014c1a5b0ae"},
{file = "lz4-4.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:345608de23b4d68fbdef373f1e53d6c5abd99a062d4ff922e3350f47775ab123"},
{file = "lz4-4.0.2-cp37-cp37m-win32.whl", hash = "sha256:5fe9db7627674875e4279c2ed50b1e38fb91ec3093347f871ed996e58edbb488"},
{file = "lz4-4.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:3fa0f000d8ce39e643e9e5c49fc4d1985156ffb177e3123a0f22551f5864841b"},
{file = "lz4-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6f3b3670f52f0871885258bcbc746f483760434336f0bc5581f161cc5d4b0c9a"},
{file = "lz4-4.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea2c2182a5b0ad03f33ac09db0925a1738a1d65751a3e058110bd900c643d359"},
{file = "lz4-4.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:439898dd4176a724243002003c3f733eb6ce48a5988175f54c8560e0b100b7a6"},
{file = "lz4-4.0.2-cp38-cp38-win32.whl", hash = "sha256:35e6caced0229b90151d31d9cf1eaa541e597f8021bf5b70ff9e6374e3e43b23"},
{file = "lz4-4.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1bd56282f6993e013ccf7f6edf1530c2a13d1662741e2be072349c7f70bc0682"},
{file = "lz4-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1ed9a1875dc2a489f3b665d0211984689d0e76585e55650b044a64dbd2d22992"},
{file = "lz4-4.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b18a6d6d9071c03dbf9e30bbe22e4476f24f1a4d73b1e975605ad3ce725e6c"},
{file = "lz4-4.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d141719d3cbb7933809642a61b68b8f595ddf85657016521756ddcf826b85cd"},
{file = "lz4-4.0.2-cp39-cp39-win32.whl", hash = "sha256:a8e02c2477bd704f43113ac8dd966c361187383591388818d74e1b73e4674759"},
{file = "lz4-4.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:ee73357412c5505f6ba0ea61ff71455e2e4c1e04d8e60f17f3cd937261d773fa"},
{file = "lz4-4.0.2.tar.gz", hash = "sha256:083b7172c2938412ae37c3a090250bfdd9e4a6e855442594f86c3608ed12729b"},
]
multidict = [
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
{file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"},
{file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"},
{file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"},
{file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"},
{file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"},
{file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"},
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"},
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"},
{file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"},
{file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"},
{file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"},
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"},
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"},
{file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"},
{file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"},
{file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
{file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pycparser = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
]
pyelftools = [
{file = "pyelftools-0.29-py2.py3-none-any.whl", hash = "sha256:519f38cf412f073b2d7393aa4682b0190fa901f7c3fa0bff2b82d537690c7fc1"},
{file = "pyelftools-0.29.tar.gz", hash = "sha256:ec761596aafa16e282a31de188737e5485552469ac63b60cfcccf22263fd24ff"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pyreadline3 = [
{file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"},
{file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"},
]
pytest = [
{file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
{file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
]
tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
yarl = [
{file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"},
{file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"},
{file = "yarl-1.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880"},
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40"},
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b"},
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497"},
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a"},
{file = "yarl-1.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f"},
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd"},
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957"},
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28"},
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843"},
{file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453"},
{file = "yarl-1.8.1-cp310-cp310-win32.whl", hash = "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272"},
{file = "yarl-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0"},
{file = "yarl-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035"},
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc"},
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b"},
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231"},
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda"},
{file = "yarl-1.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507"},
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee"},
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0"},
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d"},
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd"},
{file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780"},
{file = "yarl-1.8.1-cp37-cp37m-win32.whl", hash = "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07"},
{file = "yarl-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802"},
{file = "yarl-1.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a"},
{file = "yarl-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1"},
{file = "yarl-1.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548"},
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461"},
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe"},
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae"},
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc"},
{file = "yarl-1.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae"},
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546"},
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead"},
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4"},
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e"},
{file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae"},
{file = "yarl-1.8.1-cp38-cp38-win32.whl", hash = "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0"},
{file = "yarl-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4"},
{file = "yarl-1.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936"},
{file = "yarl-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350"},
{file = "yarl-1.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357"},
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b"},
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54"},
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb"},
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c"},
{file = "yarl-1.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6"},
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64"},
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae"},
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3"},
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0"},
{file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e"},
{file = "yarl-1.8.1-cp39-cp39-win32.whl", hash = "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6"},
{file = "yarl-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be"},
{file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"},
]
zstandard = [
{file = "zstandard-0.18.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef7e8a200e4c8ac9102ed3c90ed2aa379f6b880f63032200909c1be21951f556"},
{file = "zstandard-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2dc466207016564805e56d28375f4f533b525ff50d6776946980dff5465566ac"},
{file = "zstandard-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a2ee1d4f98447f3e5183ecfce5626f983504a4a0c005fbe92e60fa8e5d547ec"},
{file = "zstandard-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d956e2f03c7200d7e61345e0880c292783ec26618d0d921dcad470cb195bbce2"},
{file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ce6f59cba9854fd14da5bfe34217a1501143057313966637b7291d1b0267bd1e"},
{file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fa67cba473623848b6e88acf8d799b1906178fd883fb3a1da24561c779593b"},
{file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdb44d7284c8c5dd1b66dfb86dda7f4560fa94bfbbc1d2da749ba44831335e32"},
{file = "zstandard-0.18.0-cp310-cp310-win32.whl", hash = "sha256:63694a376cde0aa8b1971d06ca28e8f8b5f492779cb6ee1cc46bbc3f019a42a5"},
{file = "zstandard-0.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:702a8324cd90c74d9c8780d02bf55e79da3193c870c9665ad3a11647e3ad1435"},
{file = "zstandard-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46f679bc5dfd938db4fb058218d9dc4db1336ffaf1ea774ff152ecadabd40805"},
{file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc2a4de9f363b3247d472362a65041fe4c0f59e01a2846b15d13046be866a885"},
{file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd3220d7627fd4d26397211cb3b560ec7cc4a94b75cfce89e847e8ce7fabe32d"},
{file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:39e98cf4773234bd9cebf9f9db730e451dfcfe435e220f8921242afda8321887"},
{file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5228e596eb1554598c872a337bbe4e5afe41cd1f8b1b15f2e35b50d061e35244"},
{file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d4a8fd45746a6c31e729f35196e80b8f1e9987c59f5ccb8859d7c6a6fbeb9c63"},
{file = "zstandard-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:4cbb85f29a990c2fdbf7bc63246567061a362ddca886d7fae6f780267c0a9e67"},
{file = "zstandard-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bfa6c8549fa18e6497a738b7033c49f94a8e2e30c5fbe2d14d0b5aa8bbc1695d"},
{file = "zstandard-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e02043297c1832f2666cd2204f381bef43b10d56929e13c42c10c732c6e3b4ed"},
{file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7231543d38d2b7e02ef7cc78ef7ffd86419437e1114ff08709fe25a160e24bd6"},
{file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c86befac87445927488f5c8f205d11566f64c11519db223e9d282b945fa60dab"},
{file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999a4e1768f219826ba3fa2064fab1c86dd72fdd47a42536235478c3bb3ca3e2"},
{file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df59cd1cf3c62075ee2a4da767089d19d874ac3ad42b04a71a167e91b384722"},
{file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1be31e9e3f7607ee0cdd60915410a5968b205d3e7aa83b7fcf3dd76dbbdb39e0"},
{file = "zstandard-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:490d11b705b8ae9dc845431bacc8dd1cef2408aede176620a5cd0cd411027936"},
{file = "zstandard-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:266aba27fa9cc5e9091d3d325ebab1fa260f64e83e42516d5e73947c70216a5b"},
{file = "zstandard-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b2260c4e07dd0723eadb586de7718b61acca4083a490dda69c5719d79bc715c"},
{file = "zstandard-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3af8c2383d02feb6650e9255491ec7d0824f6e6dd2bbe3e521c469c985f31fb1"},
{file = "zstandard-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28723a1d2e4df778573b76b321ebe9f3469ac98988104c2af116dd344802c3f8"},
{file = "zstandard-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19cac7108ff2c342317fad6dc97604b47a41f403c8f19d0bfc396dfadc3638b8"},
{file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:76725d1ee83a8915100a310bbad5d9c1fc6397410259c94033b8318d548d9990"},
{file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d716a7694ce1fa60b20bc10f35c4a22be446ef7f514c8dbc8f858b61976de2fb"},
{file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:49685bf9a55d1ab34bd8423ea22db836ba43a181ac6b045ac4272093d5cb874e"},
{file = "zstandard-0.18.0-cp38-cp38-win32.whl", hash = "sha256:1af1268a7dc870eb27515fb8db1f3e6c5a555d2b7bcc476fc3bab8886c7265ab"},
{file = "zstandard-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:1dc2d3809e763055a1a6c1a73f2b677320cc9a5aa1a7c6cfb35aee59bddc42d9"},
{file = "zstandard-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eea18c1e7442f2aa9aff1bb84550dbb6a1f711faf6e48e7319de8f2b2e923c2a"},
{file = "zstandard-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8677ffc6a6096cccbd892e558471c901fd821aba12b7fbc63833c7346f549224"},
{file = "zstandard-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083dc08abf03807af9beeb2b6a91c23ad78add2499f828176a3c7b742c44df02"},
{file = "zstandard-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c990063664c08169c84474acecc9251ee035871589025cac47c060ff4ec4bc1a"},
{file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:533db8a6fac6248b2cb2c935e7b92f994efbdeb72e1ffa0b354432e087bb5a3e"},
{file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb3cb8a082d62b8a73af42291569d266b05605e017a3d8a06a0e5c30b5f10f0"},
{file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d6c85ca5162049ede475b7ec98e87f9390501d44a3d6776ddd504e872464ec25"},
{file = "zstandard-0.18.0-cp39-cp39-win32.whl", hash = "sha256:75479e7c2b3eebf402c59fbe57d21bc400cefa145ca356ee053b0a08908c5784"},
{file = "zstandard-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:d85bfabad444812133a92fc6fbe463e1d07581dba72f041f07a360e63808b23c"},
{file = "zstandard-0.18.0.tar.gz", hash = "sha256:0ac0357a0d985b4ff31a854744040d7b5754385d1f98f7145c30e02c6865cb6f"},
]

23
pyproject.toml Normal file
View File

@ -0,0 +1,23 @@
[tool.poetry]
name = "worthless"
version = "3.0.0"
description = "A worthless CLI launcher written in Python"
authors = ["tretrauit <tretrauit@gmail.com>"]
license = "MIT"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
aiohttp = "^3.8.1"
appdirs = "^1.4.4"
detools = "^0.51.0"
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.3"
[tool.poetry.scripts]
worthless = 'worthless.cli:main'
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@ -1,3 +0,0 @@
aiohttp==3.8.1
appdirs~=1.4.4
aiopath~=0.6.10

View File

@ -1,27 +0,0 @@
import pathlib
from setuptools import setup
# The directory containing this file
HERE = pathlib.Path(__file__).parent
# The text of the README file
README = (HERE / "README.md").read_text()
setup(
name='worthless',
version='2.2.1',
packages=['worthless', 'worthless.classes', 'worthless.classes.launcher', 'worthless.classes.installer'],
url='https://git.froggi.es/tretrauit/worthless-launcher',
license='MIT License',
author='tretrauit',
author_email='tretrauit@cachyos.org',
description='A worthless CLI launcher written in Python.',
long_description=README,
long_description_content_type="text/markdown",
classifiers=[
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3"
],
include_package_data=True,
install_requires=["aiohttp", "appdirs", "aiopath"]
)

View File

@ -1,5 +1,4 @@
from worthless import launcher, installer
from worthless import launcher, game
Launcher = launcher.Launcher
Installer = installer.Installer
Game = game.Game

View File

@ -3,4 +3,4 @@ import asyncio
from worthless import cli
if __name__ == "__main__":
asyncio.run(cli.main())
cli.main()

View File

@ -1,4 +1,5 @@
from worthless.classes.installer import resource, game, latest, diff, voicepack
Resource = resource.Resource
Game = game.Game
Latest = latest.Latest

View File

@ -2,7 +2,9 @@ from worthless.classes.installer.voicepack import Voicepack
class Diff:
def __init__(self, name, version, path, size, md5, is_recommended_update, voice_packs, raw):
def __init__(
self, name, version, path, size, md5, is_recommended_update, voice_packs, raw
):
self.name = name
self.version = version
self.path = path
@ -18,7 +20,15 @@ class Diff:
@staticmethod
def from_dict(data):
voice_packs = []
for v in data['voice_packs']:
for v in data["voice_packs"]:
voice_packs.append(Voicepack.from_dict(v))
return Diff(data["name"], data["version"], data["path"], data["size"], data["md5"],
data["is_recommended_update"], voice_packs, data)
return Diff(
data["name"],
data["version"],
data["path"],
data["size"],
data["md5"],
data["is_recommended_update"],
voice_packs,
data,
)

View File

@ -12,8 +12,8 @@ class Game:
def from_dict(data):
try:
diffs = []
for diff in data['diffs']:
for diff in data["diffs"]:
diffs.append(Diff.from_dict(diff))
return Game(Latest.from_dict(data['latest']), diffs, data)
return Game(Latest.from_dict(data["latest"]), diffs, data)
except (KeyError, ValueError, TypeError):
return data

View File

@ -2,7 +2,19 @@ from worthless.classes.installer.voicepack import Voicepack
class Latest:
def __init__(self, name, version, path, size, md5, entry, voice_packs, decompressed_path, segments, raw):
def __init__(
self,
name,
version,
path,
size,
md5,
entry,
voice_packs,
decompressed_path,
segments,
raw,
):
self.name = name
self.version = version
self.path = path
@ -20,7 +32,17 @@ class Latest:
@staticmethod
def from_dict(data):
voice_packs = []
for v in data['voice_packs']:
for v in data["voice_packs"]:
voice_packs.append(Voicepack.from_dict(v))
return Latest(data["name"], data["version"], data["path"], data["size"], data["md5"], data["entry"],
voice_packs, data["decompressed_path"], data["segments"], data)
return Latest(
data["name"],
data["version"],
data["path"],
data["size"],
data["md5"],
data["entry"],
voice_packs,
data["decompressed_path"],
data["segments"],
data,
)

View File

@ -14,7 +14,18 @@ class Resource:
- :class:`worthless.classes.launcher.qq.QQ` post: The launcher QQ posts information.
- :class:`dict` raw: The launcher raw information.
"""
def __init__(self, game, plugin, web_url, force_update, pre_download_game, deprecated_packages, sdk, raw):
def __init__(
self,
game,
plugin,
web_url,
force_update,
pre_download_game,
deprecated_packages,
sdk,
raw,
):
self.game = game
self.plugin = plugin
self.web_url = web_url
@ -26,5 +37,13 @@ class Resource:
@staticmethod
def from_dict(data):
return Resource(Game.from_dict(data['game']), data['plugin'], data['web_url'], data['force_update'],
Game.from_dict(data['pre_download_game']), data['deprecated_packages'], data['sdk'], data)
return Resource(
Game.from_dict(data["game"]),
data["plugin"],
data["web_url"],
data["force_update"],
Game.from_dict(data["pre_download_game"]),
data["deprecated_packages"],
data["sdk"],
data,
)

View File

@ -12,4 +12,11 @@ class Voicepack:
@staticmethod
def from_dict(data):
return Voicepack(data["language"], data["name"], data["path"], data["size"], data["md5"], data)
return Voicepack(
data["language"],
data["name"],
data["path"],
data["size"],
data["md5"],
data,
)

View File

@ -1,8 +1,17 @@
from worthless.classes.launcher import background, banner, iconbutton, iconotherlink, info, post, qq
from worthless.classes.launcher import (
background,
banner,
iconbutton,
iconotherlink,
info,
post,
qq,
)
Background = background.Background
Banner = banner.Banner
IconButton = iconbutton.IconButton
IconOtherLink = iconotherlink.IconOtherLink
Info = info.Info
Post = post.Post
QQ = qq.QQ
QQ = qq.QQ

View File

@ -29,7 +29,13 @@ class Background:
self.raw = raw
@staticmethod
def from_dict(data) -> 'Background':
def from_dict(data) -> "Background":
"""Creates a launcher background from a dictionary."""
return Background(data["background"], data["icon"], data["url"],
data["version"], data["bg_checksum"], data)
return Background(
data["background"],
data["icon"],
data["url"],
data["version"],
data["bg_checksum"],
data,
)

View File

@ -15,6 +15,7 @@ class Banner:
- :class:`str` name: The banner name.
- :class:`dict` raw: The banner raw information.
"""
def __init__(self, banner_id, name, img, url, order, raw):
self.banner_id = banner_id
self.name = name
@ -24,6 +25,13 @@ class Banner:
self.raw = raw
@staticmethod
def from_dict(data) -> 'Banner':
def from_dict(data) -> "Banner":
"""Creates a launcher banner from a dictionary."""
return Banner(data["banner_id"], data["name"], data["img"], data["url"], data["order"], data)
return Banner(
data["banner_id"],
data["name"],
data["img"],
data["url"],
data["order"],
data,
)

View File

@ -20,7 +20,9 @@ class IconButton:
- :class:`dict` raw: The launcher background raw information.
"""
def __init__(self, icon_id, img, tittle, url, qr_img, qr_desc, img_hover, other_links, raw):
def __init__(
self, icon_id, img, tittle, url, qr_img, qr_desc, img_hover, other_links, raw
):
"""Inits the launcher icon class"""
self.icon_id = icon_id
self.img = img
@ -33,10 +35,19 @@ class IconButton:
self.raw = raw
@staticmethod
def from_dict(data) -> 'IconButton':
def from_dict(data) -> "IconButton":
"""Creates a launcher background from a dictionary."""
other_links = []
for link in data['other_links']:
for link in data["other_links"]:
other_links.append(IconOtherLink.from_dict(link))
return IconButton(data["icon_id"], data["img"], data["tittle"], data["url"], data["qr_img"],
data["qr_desc"], data["img_hover"], other_links, data)
return IconButton(
data["icon_id"],
data["img"],
data["tittle"],
data["url"],
data["qr_img"],
data["qr_desc"],
data["img_hover"],
other_links,
data,
)

View File

@ -4,6 +4,6 @@ class IconOtherLink:
self.url = url
@staticmethod
def from_dict(data) -> 'IconOtherLink':
def from_dict(data) -> "IconOtherLink":
"""Creates a launcher icon other link from a dictionary."""
return IconOtherLink(data["title"], data["url"])

View File

@ -1,4 +1,5 @@
from worthless.classes.launcher import background, banner, iconbutton, post, qq
Background = background.Background
Banner = banner.Banner
IconButton = iconbutton.IconButton
@ -20,8 +21,16 @@ class Info:
- :class:`worthless.classes.launcher.qq.QQ` post: The launcher QQ posts information.
- :class:`dict` raw: The launcher raw information.
"""
def __init__(self, lc_background: Background, lc_banner: list[Banner], icon: list[IconButton], lc_post: list[Post],
lc_qq: list[QQ], raw: dict):
def __init__(
self,
lc_background: Background,
lc_banner: list[Banner],
icon: list[IconButton],
lc_post: list[Post],
lc_qq: list[QQ],
raw: dict,
):
self.background = lc_background
self.banner = lc_banner
self.icon = icon
@ -45,4 +54,3 @@ class Info:
for q in data["qq"]:
lc_qq.append(QQ.from_dict(q))
return Info(bg, lc_banner, lc_icon, lc_post, lc_qq, data)

View File

@ -21,6 +21,7 @@ class Post:
- :class:`str` title: The post title.
- :class:`dict` raw: The post raw information.
"""
def __init__(self, post_id, post_type, tittle, url, show_time, order, title, raw):
self.post_id = post_id
self.type = post_type # Shadow built-in name `type`
@ -32,7 +33,15 @@ class Post:
self.raw = raw
@staticmethod
def from_dict(data) -> 'Post':
def from_dict(data) -> "Post":
"""Creates a launcher post from a dictionary."""
return Post(data["post_id"], data["type"], data["tittle"], data["url"],
data["show_time"], data["order"], data["title"], data)
return Post(
data["post_id"],
data["type"],
data["tittle"],
data["url"],
data["show_time"],
data["order"],
data["title"],
data,
)

View File

@ -11,6 +11,7 @@ class QQ:
- :class:`str` code: The QQ post url.
- :class:`dict` raw: The launcher raw information.
"""
def __init__(self, qq_id, name, number, code, raw):
self.qq_id = qq_id
self.name = name
@ -19,7 +20,7 @@ class QQ:
self.raw = raw
@staticmethod
def from_dict(raw: dict) -> 'QQ':
def from_dict(raw: dict) -> "QQ":
"""Creates a QQ object from a dictionary
Args:
@ -28,8 +29,8 @@ class QQ:
Returns:
QQ: The QQ object
"""
qq_id = raw.get('qq_id')
name = raw.get('name')
number = int(raw.get('number'))
code = raw.get('code')
qq_id = raw.get("qq_id")
name = raw.get("name")
number = int(raw.get("number"))
code = raw.get("code")
return QQ(qq_id, name, number, code, raw)

View File

@ -1,8 +0,0 @@
class mhyResponse:
"""Simple class for wrapping miHoYo web response
Currently not used for anything.
"""
def __init__(self, retcode, message, data):
self.retcode = retcode
self.message = message
self.data = data

View File

@ -1,402 +1,2 @@
#!/usr/bin/python3
import argparse
import asyncio
import appdirs
from pathlib import Path
from worthless.launcher import Launcher
from worthless.installer import Installer
from worthless.patcher import Patcher
import worthless.constants as constants
class UI:
def __init__(self, gamedir: str, noconfirm: bool, tempdir: str | Path = None, pre_download=False) -> None:
self._vo_version = None
self._noconfirm = noconfirm
self._gamedir = gamedir
self._launcher = Launcher(gamedir)
self._installer = Installer(gamedir, data_dir=tempdir)
self._patcher = Patcher(gamedir, data_dir=tempdir)
self._pre_download = pre_download
if self._pre_download:
print("Pre-download is enabled, use at your own risk!")
def _ask(self, question):
if self._noconfirm:
# Fake dialog
print(question + " [Y/n]:")
return True
# random text
answer = "kurumi3"
while answer.lower() not in ['y', 'n', '']:
answer = input(question + " [Y/n]: ")
return answer.lower() == 'y' or answer == ''
def override_game_version(self, version: str):
self._installer._version = version
def override_voiceover_version(self, version: str):
self._vo_version = version
async def get_game_version(self):
print(await self._installer.get_game_version())
async def block_telemetry(self):
print("Checking for available telemetry to block...")
try:
await self._patcher.block_telemetry()
except ValueError:
print("No telemetry to block.")
else:
print("Telemetry blocked.")
async def check_telemetry(self):
block_status = await self._patcher.is_telemetry_blocked()
if not block_status:
print("Telemetry is blocked.")
else:
print("Telemetry is not blocked, you need to block these hosts below.")
for hosts in block_status:
print(hosts)
async def _update_from_archive(self, filepath):
print("Reverting patches if patched...")
await self._patcher.revert_patch(True)
print("Updating game from archive (This may takes some time)...")
await self._installer.update_game(filepath)
self._installer.set_version_config()
async def _install_from_archive(self, filepath, force_reinstall):
print("Installing game from archive (This may takes some time)...")
print(filepath)
await self._installer.install_game(filepath, force_reinstall)
self._installer.set_version_config()
async def _apply_voiceover_from_archive(self, filepath):
print("Applying voiceover from archive (This may takes some time)...")
print("Voiceover archive:", filepath)
await self._installer.apply_voiceover(filepath)
async def install_voiceover_from_file(self, filepath):
print("Archive voiceover language: {} ({})".format(
await self._installer.get_voiceover_archive_language(filepath),
"Full archive" if await self._installer.get_voiceover_archive_type(filepath) else "Update archive"))
if not self._ask("Do you want to apply this voiceover pack? ({})".format(filepath)):
print("Aborting apply process.")
return
await self._apply_voiceover_from_archive(filepath)
print("Voiceover applied successfully.")
async def revert_patch(self):
print("Reverting patches...")
await self._patcher.revert_patch(True)
print("Patches reverted.")
async def patch_game(self, login_fix: bool = False):
print("NOTE: Hereby you are violating the game's Terms of Service!")
print("Do not patch the game if you don't know what you are doing!")
if not self._ask("Do you want to patch the game? (This will overwrite your game files!)"):
print("Aborting patch process.")
return
await self.block_telemetry()
print("Updating patches...")
await self._patcher.download_patch()
print("Patching game...")
await self._patcher.apply_patch(login_fix)
print("Game patched.")
print("Please refrain from sharing this project to public, thank you.")
async def install_from_file(self, filepath):
gamever = await self._installer.get_game_version()
print("Archive game version:", await self._installer.get_game_archive_version(filepath))
if gamever:
print("Current game installation detected. ({})".format(await self._installer.get_game_version()))
if not self._ask("Do you want to update the game? ({})".format(filepath)):
print("Aborting update process.")
return
await self._update_from_archive(filepath)
print("Game updated successfully.")
else:
print("No game installation detected.")
if not self._ask("Do you want to install the game? ({})".format(filepath)):
print("Aborting installation process.")
return
await self._install_from_archive(filepath, False)
print("Game installed successfully.")
async def download_patch(self):
print("Downloading patches...")
await self._patcher.download_patch()
async def download_game(self):
print("Downloading full game (This will take a long time)...")
await self._installer.download_full_game(self._pre_download)
async def download_game_update(self):
print("Downloading game update (This will take a long time)...")
await self._installer.download_game_update(pre_download=self._pre_download)
async def download_voiceover(self, languages: str):
res_info = await self._launcher.get_resource_info()
for lng in languages.split(" "):
for vo in res_info.game.latest.voice_packs:
if not self._installer.voiceover_lang_translate(lng) == vo.language:
continue
print("Downloading voiceover pack for {} (This will take a long time)...".format(lng))
await self._installer.download_full_voiceover(lng, pre_download=self._pre_download)
async def download_voiceover_update(self, languages: str):
res_info = await self._launcher.get_resource_info()
for lng in languages.split(" "):
for vo in res_info.game.latest.voice_packs:
if not self._installer.voiceover_lang_translate(lng) == vo.language:
continue
print("Downloading voiceover update pack for {} (This will take a long time)...".format(lng))
await self._installer.download_voiceover_update(lng, pre_download=self._pre_download)
async def install_game(self, forced: bool = False):
res_info = await self._launcher.get_resource_info()
game = res_info.game
if self._pre_download:
game = res_info.pre_download_game
print("Latest game version: {}".format(game.latest.version))
if not self._ask("Do you want to install the game?"):
print("Aborting game installation process.")
return
await self.download_game()
print("Game archive:", game.latest.get_name())
await self._install_from_archive(self._installer.temp_path.joinpath(game.latest.get_name()), forced)
async def install_voiceover(self, languages: str):
res_info = await self._launcher.get_resource_info()
for lng in languages.split(" "):
for vo in res_info.game.latest.voice_packs:
if not self._installer.voiceover_lang_translate(lng) == vo.language:
continue
if not self._ask("Do you want to install this voiceover pack? ({})".format(lng)):
print("Aborting voiceover installation process.")
break
print("Downloading voiceover pack (This will take a long time)...")
await self._installer.download_full_voiceover(lng, pre_download=self._pre_download)
await self._apply_voiceover_from_archive(
self._installer.temp_path.joinpath(vo.get_name())
)
async def update_game(self):
game_ver = await self._installer.get_game_version()
if not game_ver:
await self.install_game()
return
print("Current game installation detected: {}".format(game_ver))
diff_archive = await self._installer.get_game_diff_archive()
res_info = await self._launcher.get_resource_info()
game = res_info.game
if self._pre_download:
game = res_info.pre_download_game
if not diff_archive:
print("No game updates available.")
return
print("Latest game version: {}".format(game.latest.version))
if not self._ask("Do you want to update the game?"):
print("Aborting game update process.")
return
print("Downloading game update (This will take a long time)...")
await self._installer.download_game_update(pre_download=self._pre_download)
print("Installing game update...")
await self.install_from_file(self._installer.temp_path.joinpath(diff_archive.get_name()))
async def update_voiceover(self, languages: str | list):
if isinstance(languages, str):
languages = languages.split(" ")
game_ver = self._vo_version or await self._installer.get_game_version()
if not game_ver:
print("Couldn't detect current game installation, is game installed?")
return
installed_voiceovers = await self._installer.get_installed_voiceovers()
print(f"Installed voiceovers: {None if installed_voiceovers == [] else ', '.join(installed_voiceovers)}")
for lng in languages:
if self._installer.voiceover_lang_translate(lng, "locale") not in installed_voiceovers:
await self.install_voiceover(lng)
continue
diff_archive = await self._installer.get_voiceover_diff_archive(lng, pre_download=self._pre_download)
if not diff_archive:
print("No voiceover updates available for {}.".format(lng))
continue
if not self._ask("Do you want to update this voiceover? ({})".format(lng)):
print("Aborting this voiceover language update process.")
continue
print("Downloading voiceover update (This may takes some time)...")
await self._installer.download_voiceover_update(lng, pre_download=self._pre_download)
print("Installing voiceover update for {}...".format(lng))
await self._apply_voiceover_from_archive(self._installer.temp_path.joinpath(diff_archive.get_name()))
async def update_game_voiceover(self, languages: str):
await self.update_game()
await self.update_voiceover(languages)
async def update_voiceover_all(self):
await self.update_voiceover(await self._installer.get_installed_voiceovers())
async def update_all(self):
await self.update_game()
await self.update_voiceover(await self._installer.get_installed_voiceovers())
async def verify_game(self):
game_ver = await self._installer.get_game_version()
if not game_ver:
print("Couldn't detect current game installation, is game installed?")
return
print("Verifying game contents... (This may takes a long time)")
failed_files = await self._installer.verify_game(ignore_mismatch=True)
if not failed_files:
print("All good.")
return
print("Some game files got corrupted (mismatch md5), uh oh.")
for file in failed_files:
print("{}: expected {}, actual {}".format(file[0], file[1], file[2]))
async def clear_cache(self):
if self._ask("Do you want to clear Installer cache (contains downloaded game files, etc)?"):
await self._installer.clear_cache()
if self._ask("Do you want to clear Patcher cache (contains files used to patch)?"):
await self._patcher.clear_cache()
async def main():
default_dirs = appdirs.AppDirs(constants.APP_NAME, constants.APP_AUTHOR)
parser = argparse.ArgumentParser(prog="worthless", description="A worthless launcher written in Python.")
parser.add_argument("-D", "--dir", action="store", type=Path, default=Path.cwd(),
help="Specify the game directory (default current working directory)")
parser.add_argument("-W", "--temporary-dir", action="store", type=Path, default=None,
help="Specify the temporary directory (default {} and {})".format(default_dirs.user_data_dir,
default_dirs.user_cache_dir))
parser.add_argument("-S", "--install", action="store_true",
help="Install/update the game (if not already installed, else do nothing)")
parser.add_argument("-U", "--install-from-file", action="store", type=Path, default=None,
help="Install the game from an archive (if not already installed, \
else update from archive)")
parser.add_argument("-Uv", "--install-voiceover-from-file", action="store", type=Path, default=None,
help="Install the voiceover from an archive (if not already installed, \
else update from archive)")
parser.add_argument("-Sp", "--patch", action="store_true",
help="Patch the game (if not already patched, else do nothing)")
parser.add_argument("--login-fix", action="store_true",
help="Patch the game to fix login issues (if not already patched, else do nothing)")
parser.add_argument("-Sy", "--update", action="store", type=str, default="",
help="Update the game and specified voiceover pack only (or install if not found)")
parser.add_argument("-Sw", "--download-game", action="store_true",
help="Download the full game to the temporary directory")
parser.add_argument("-Swp", "--download-patch", action="store_true",
help="Download/Update the game patch to the temporary directory")
parser.add_argument("-Swv", "--download-voiceover", action="store", type=str,
help="Download the full voiceover to the temporary directory")
parser.add_argument("-Syw", "--download-game-update", action="store", type=str, default="",
help="Download the game and the voiceover update to the temporary directory")
parser.add_argument("-Sywv", "--download-voiceover-update", action="store", type=str,
help="Download the voiceover update to the temporary directory")
parser.add_argument("-Sv", "--update-voiceover", action="store", type=str,
help="Update the voiceover pack only (or install if not found)")
parser.add_argument("-Syu", "--update-all", action="store_true",
help="Update the game and all installed voiceover packs (or install if not found)")
parser.add_argument("-Scc", "--clear-cache", action="store_true", help="Clear cache used by worthless")
parser.add_argument("-Rs", "--remove", action="store_true", help="Remove the game (if installed)")
parser.add_argument("-Rp", "--remove-patch", action="store_true", help="Revert the game patch (if patched)")
parser.add_argument("-Rv", "--remove-voiceover", action="store_true", help="Remove a Voiceover pack (if installed)")
parser.add_argument("-V", "--verify", action="store_true", help="Verify the game installation")
parser.add_argument("--predownload", action="store_true", help="Download the game for the next update", default=False)
parser.add_argument("--get-game-version", action="store_true", help="Get the current game version")
parser.add_argument("--no-overseas", action="store_true", help="Don't use overseas server")
parser.add_argument("--check-telemetry", action="store_true", help="Check for the telemetry information")
parser.add_argument("--from-ver", action="store", help="Override the detected game version", type=str, default=None)
parser.add_argument("--from-vo-ver", action="store", help="Override the detected game version for voiceover "
"detection", type=str, default=None)
parser.add_argument("--noconfirm", action="store_true",
help="Do not ask any for confirmation. (Ignored in interactive mode)")
args = parser.parse_args()
if args.temporary_dir:
args.temporary_dir.mkdir(parents=True, exist_ok=True)
ui = UI(args.dir, args.noconfirm, args.temporary_dir, args.predownload)
if args.install and args.update:
raise ValueError("Cannot specify both --install and --update arguments.")
if args.install_from_file and args.update:
raise ValueError("Cannot specify both --install-from-file and --update arguments.")
if args.install_voiceover_from_file and args.update:
raise ValueError("Cannot specify both --install-voiceover-from-file and --update arguments.")
if args.install_from_file and args.install:
raise ValueError("Cannot specify both --install-from-file and --install arguments.")
if args.from_ver:
ui.override_game_version(args.from_ver)
if args.get_game_version:
await ui.get_game_version()
if args.check_telemetry:
await ui.check_telemetry()
if args.clear_cache:
await ui.clear_cache()
# Download
if args.download_game:
await ui.download_game()
if args.download_patch:
await ui.download_patch()
if args.download_voiceover:
await ui.download_voiceover(args.download_voiceover)
if args.download_game_update:
await ui.download_game_update()
if args.download_voiceover_update:
await ui.download_voiceover_update(args.download_voiceover_update)
# Install
if args.install:
await ui.install_game()
if args.install_from_file:
await ui.install_from_file(args.install_from_file)
if args.install_voiceover_from_file:
await ui.install_voiceover_from_file(args.install_voiceover_from_file)
# Update
if args.update_all:
await ui.update_all()
if args.update:
await ui.update_game_voiceover(args.update)
if args.update_voiceover:
await ui.update_voiceover(args.update_voiceover)
# Patch
if args.patch:
await ui.patch_game(args.login_fix)
if args.remove_patch:
await ui.revert_patch()
# Verify
if args.verify:
await ui.verify_game()
if __name__ == "__main__":
asyncio.run(main())
def main():
pass

View File

@ -1,30 +1,27 @@
from appdirs import AppDirs
APP_NAME="worthless"
APP_AUTHOR="tretrauit"
APPDIRS=AppDirs(APP_NAME, APP_AUTHOR)
APP_NAME = "worthless"
APP_AUTHOR = "tretrauit"
APPDIRS = AppDirs(APP_NAME, APP_AUTHOR)
LAUNCHER_API_URL_OS = "https://sdk-os-static.hoyoverse.com/hk4e_global/mdk/launcher/api"
LAUNCHER_API_URL_CN = "https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api"
HDIFFPATCH_GIT_URL="https://github.com/sisong/HDiffPatch"
HDIFFPATCH_GIT_URL = "https://github.com/sisong/HDiffPatch"
PATCH_LIST = {
"Krock": "https://notabug.org/Krock/dawn",
"y0soro": "https://notabug.org/y0soro/dawn"
"y0soro": "https://notabug.org/y0soro/dawn",
}
TELEMETRY_URL_LIST = [
"log-upload-os.mihoyo.com",
"log-upload-eur.mihoyo.com",
"log-upload-os.hoyoverse.com",
"overseauspider.yuanshen.com"
]
TELEMETRY_URL_CN_LIST = [
"log-upload.mihoyo.com",
"uspider.yuanshen.com"
"overseauspider.yuanshen.com",
]
TELEMETRY_URL_CN_LIST = ["log-upload.mihoyo.com", "uspider.yuanshen.com"]
TELEMETRY_OPTIONAL_URL_LIST = [
"prd-lender.cdp.internal.unity3d.com",
"thind-prd-knob.data.ie.unity3d.com",
"thind-gke-usc.prd.data.corp.unity3d.com",
"cdp.cloud.unity3d.com",
"remote-config-proxy-prd.uca.cloud.unity3d.com"
]
"remote-config-proxy-prd.uca.cloud.unity3d.com",
]

View File

@ -0,0 +1,3 @@
from . import gameenums as enums
from ._game import *
from .gamehelper import Helper

694
worthless/game/_game.py Normal file
View File

@ -0,0 +1,694 @@
import asyncio
import hashlib
import json
import logging
import re
import shutil
import zipfile
from os import PathLike
from pathlib import Path
from worthless import constants, launcher
from worthless.classes import installer
from worthless.game.gameenums import (
Variant,
VoicepackArchiveType,
UpdateStatus,
InstallStatus,
VoicepackArchiveLanguage,
)
from worthless.game.hdiffpatch import HDiffPatch
from worthless.game.launcherconfig import LauncherConfig
_logger = logging.getLogger("worthless.Game")
_game_version_regex = re.compile(r"([1-9]+\.[0-9]+\.[0-9]+)_\d+_\d+")
def _calculate_md5(file: str | bytes | PathLike):
file = Path(file)
if not file.exists():
return ""
with file.open("rb") as f:
file_hash = hashlib.md5()
while chunk := f.read(8196):
file_hash.update(chunk)
return file_hash.hexdigest()
def _read_version_from_game_file(globalgamemanagers: Path | bytes) -> str | None:
"""
Reads the version from the globalgamemanagers file. (Data/globalgamemanagers)
This function uses `An Anime Game Launcher` method to read the version:
https://github.com/an-anime-team/an-anime-game-launcher/blob/main/src/ts/Game.ts#L26
Returns:
Version as string "x.x.x" or None if not found.
"""
if isinstance(globalgamemanagers, Path):
data = globalgamemanagers.read_text(encoding="ascii", errors="ignore")
else:
data = globalgamemanagers.decode(encoding="ascii", errors="ignore")
result = _game_version_regex.search(data)
if not result:
return
return result.group(1)
def parse_pkg_version(pkg_version: str | bytes | PathLike) -> dict:
"""
Parse a pkg_version file
Args:
pkg_version: The pkg_version file to be parsed
Returns:
A parsed pkg_version as a dict
{"<file 1>": "<md5">, "<file 2>": "<md5>", ...}
"""
contents = Path(pkg_version).read_text()
pkg_version = {}
for content in contents.split("\r\n"):
if not content.strip():
continue
info = json.loads(content)
pkg_version.update({info["remoteName"]: info["md5"]})
return pkg_version
def voicepack_lang_translate(lang: str, base_language="game") -> str | None:
"""
Translates the voicepack language to the language code used by the game (and reverse)
Args:
lang: Language to translate
base_language: Base language type (game/locale/both)
Returns:
Translated language code such as "en-us" or "English(US)"
"""
if base_language == "game" or base_language == "both":
match lang.lower():
case "english(us)":
return "en-us"
case "japanese":
return "ja-jp"
case "chinese":
return "zh-cn"
case "korean":
return "ko-kr"
if base_language == "locale" or base_language == "both":
match lang.lower().replace("_", "-"):
case "en-us":
return "English(US)"
case "ja-jp":
return "Japanese"
case "zh-cn":
return "Chinese"
case "ko-kr":
return "Korean"
def get_voicepack_archive_language(
archive: str | bytes | PathLike,
) -> VoicepackArchiveLanguage:
"""
Gets voicepack archive language.
Args:
archive: The voicepack archive to get type
Returns:
VoicepackArchiveLanguage
"""
if not isinstance(archive, Path):
archive = Path(archive).resolve()
if not archive.exists():
raise FileNotFoundError(f"Voiceover archive {archive} not found")
with zipfile.ZipFile(archive, "r") as f:
for file in zipfile.Path(f).iterdir():
if file.name.startswith("Audio_") and file.name.endswith("_pkg_version"):
# Audio_<language>_pkg_version
return VoicepackArchiveLanguage(file.name.split("_")[1])
def get_voicepack_archive_type(archive: str | bytes | PathLike) -> VoicepackArchiveType:
"""
Gets voicepack archive type.
Args:
archive: The voicepack archive to get type
Returns:
VoicepackArchiveType.UPDATE for update archive, and VoicepackArchiveType.FULL for a full voicepack archive.
"""
vo_lang = get_voicepack_archive_language(archive)
with zipfile.ZipFile(archive, "r") as f:
archive_path = zipfile.Path(f)
files = f.read("Audio_{}_pkg_version".format(vo_lang)).decode().split("\n")
for file in files:
if (
file.strip()
and not archive_path.joinpath(json.loads(file)["remoteName"]).exists()
):
return VoicepackArchiveType.UPDATE
return VoicepackArchiveType.FULL
def verify_game_file(
file: str | bytes | PathLike,
game_dir: str | bytes | PathLike,
pkg_version: dict[str, str] | str | bytes | PathLike,
ignore_mismatch=True,
) -> (str, str, str) | None:
"""
Verifies a game file
It is strongly recommended to parse pkg_version with parse_pkg_version and use
that as pkg_version argument if you're planning to verify multiple game files
to save resource and verify faster.
Args:
file: File to verify (e.g. GenshinImpact_Data/sharedassets0.assets.resS)
game_dir: Game directory
pkg_version: pkg_version file to read MD5 from, if not specified it'll use
the file from the current game installation
ignore_mismatch: Do not raise exception if a file has mismatch MD5, instead
return as a (file, expected md5, actual md5) tuple
Returns:
None if the file is good, (file, expected md5, actual md5) if the file has mismatch md5
Raises:
FileNotFoundError if the file to verify doesn't exist
"""
game_dir = Path(game_dir)
file_full = game_dir.joinpath(file)
if not isinstance(pkg_version, dict):
pkg_version = parse_pkg_version(pkg_version=pkg_version)
if not pkg_version.get(file):
raise FileNotFoundError(file)
file_md5 = _calculate_md5(file_full)
if file_md5 == pkg_version[file]:
return None
if ignore_mismatch:
return file, pkg_version[file], file_md5
raise ValueError(
f"MD5 does not match for {file}, expected md5: {pkg_version[file]}, actual md5: {file_md5}"
)
async def verify_game_files(
game_dir: str | bytes | PathLike,
pkg_version: dict[str, str] | str | bytes | PathLike,
ignore_mismatch=True,
):
if not isinstance(game_dir, Path):
game_dir = Path(game_dir)
if not isinstance(pkg_version, dict):
pkg_version = parse_pkg_version(pkg_version=pkg_version)
# Wrapper for catching FileNotFoundError
def verify(f):
try:
result = verify_game_file(
file=f,
game_dir=game_dir,
pkg_version=pkg_version,
ignore_mismatch=ignore_mismatch,
)
except FileNotFoundError:
return
return result
verify_jobs = []
for file in pkg_version:
verify_jobs.append(asyncio.to_thread(verify, file))
verify_result = await asyncio.gather(*verify_jobs)
failed_files = []
for file in verify_result:
if file is not None:
failed_files.append(file)
return failed_files
class Game:
"""
Manages game & voicepacks installation.
This class handles installing and updating game & voicepacks installation.
Args:
game_dir: Game directory
variant: Game variant
cache_dir: Cache directory, if not specified it'll automatically detect your
system cache directory and use that.
"""
def __init__(
self,
game_dir: str | bytes | PathLike = None,
variant: Variant = None,
cache_dir: str | bytes | PathLike = None,
):
if not game_dir:
game_dir = Path.cwd()
if not isinstance(game_dir, Path):
game_dir = Path(game_dir)
self._game_dir = game_dir
if cache_dir:
if not isinstance(cache_dir, Path):
cache_dir = Path(cache_dir)
self._cache_path = cache_dir.joinpath("Installer/")
else:
self._cache_path = Path(constants.APPDIRS.user_cache_dir).joinpath(
"Installer"
)
Path(self._cache_path).mkdir(parents=True, exist_ok=True)
self._variant = variant
self._version = None
self._launcher = launcher.Launcher(variant=variant)
self._hdiffpatch = HDiffPatch(data_dir=cache_dir)
self._game_config_file = self._game_dir.joinpath("config.ini")
self._game_config = LauncherConfig(
self._game_config_file, game_version=self.version
)
def _read_version_from_config(self) -> str | None:
"""
Reads the version from config.ini
Returns:
Version as string "x.x.x" or None if not found.
"""
if not self._game_config_file.exists():
raise FileNotFoundError(f"Config file {self._game_config_file} not found")
return self._game_config.get("General", "game_version", fallback=None)
async def get_resource_info(self) -> installer.Resource:
"""
Alias for `launcher.get_resource_info()`
"""
return await self._launcher.get_resource_info()
async def get_server_game_info(self, pre_download: bool = False) -> installer.Game:
game_resource = await self.get_resource_info()
game = game_resource.game
if not pre_download:
return game
game = game_resource.pre_download_game
if game is None:
raise RuntimeError("Pre-download version is not available.")
@property
def version(self) -> str | None:
"""
Gets the current game version
Returns:
Version as string "x.x.x" or None if not found.
"""
if not self._version:
self._version = self.get_current_game_version()
return self._version
@property
def variant(self) -> Variant:
"""
Gets the current game variant
This property is a shorthand for get_current_game_variant()
Returns:
GameVariant
"""
if not self._variant:
self._version = self.get_current_game_variant()
return self._variant
@property
def cache_path(self) -> Path:
return self._cache_path
@staticmethod
def voicepack_lang_translate(lang: str, base_language="game") -> str | None:
"""
This function is an alias to worthless.game.voicepack_lang_translate
"""
return voicepack_lang_translate(lang=lang, base_language=base_language)
@staticmethod
def get_voicepack_archive_language(
archive: str | bytes | PathLike,
) -> VoicepackArchiveLanguage:
"""
This function is an alias to worthless.game.get_voicepack_archive_language
"""
return get_voicepack_archive_language(archive=archive)
@staticmethod
def get_voicepack_archive_type(
archive: str | bytes | PathLike,
) -> VoicepackArchiveType:
"""
This function is an alias to worthless.game.get_voicepack_archive_type
"""
return get_voicepack_archive_type(archive=archive)
def get_game_data_name(self) -> str:
"""
Gets the game data path name
Returns:
A string containing game data path (e.g. GenshinImpact_Data)
"""
match self.variant:
case Variant.INTERNATIONAL:
return "GenshinImpact_Data"
case Variant.CHINESE:
return "YuanShen_Data"
case Variant.BILIBILI:
return "YuanShen_Data"
def get_game_data_path(self) -> Path:
"""
Gets the game data path
Returns:
A Path object with the game data path.
"""
return self._game_dir.joinpath(self.get_game_data_name())
def get_archive_game_version(
self, game_archive: str | bytes | PathLike
) -> str | None:
"""
Gets the game version in the archive
Returns:
Version as string "x.x.x" or None if not found.
"""
game_archive = Path(game_archive)
if not game_archive.is_file():
raise FileNotFoundError(f"Game archive {game_archive} not found")
with zipfile.ZipFile(game_archive, "r") as f:
return _read_version_from_game_file(
f.read(self.get_game_data_name() + "globalgamemanagers")
)
def get_current_game_variant(self) -> Variant:
"""
Gets the current game variant
Returns:
GameVariant
"""
if self._game_dir.joinpath("GenshinImpact.exe").is_file():
return Variant.INTERNATIONAL
if not self._game_dir.joinpath("YuanShen.exe").is_file():
raise FileNotFoundError("Game installation not found.")
# We can't depend on get_game_data_name() because it depends on self._variant
# which depends on this function.
if self._game_dir.joinpath("YuanShen_Data/Plugins/PCGameSDK.dll").is_file():
return Variant.BILIBILI
return Variant.CHINESE
def get_current_game_version(self) -> str | None:
"""
Gets the current game version
Returns:
Version as string "x.x.x" or None if not found.
"""
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
if not globalgamemanagers.exists():
try:
return self._read_version_from_config()
except FileNotFoundError:
return
return _read_version_from_game_file(globalgamemanagers)
def get_installed_voicepacks(self) -> list[str]:
"""
Returns a list of installed voicepacks.
:return: List of installed voicepacks
"""
voicepacks = []
for file in (
self.get_game_data_path()
.joinpath("StreamingAssets/Audio/GeneratedSoundBanks/Windows")
.iterdir()
):
if file.is_dir():
voicepacks.append(file.name)
return voicepacks
async def update_game(self, game_archive: str | bytes | PathLike, callback=None):
if not self.get_game_data_path().exists():
raise FileNotFoundError(f"Game not found in {self._game_dir}")
if not isinstance(game_archive, Path):
game_archive = Path(game_archive).resolve()
if not game_archive.exists():
raise FileNotFoundError(f"Update archive {game_archive} not found")
def call(status, f=None):
if not callback:
return
callback(status=status, file=f)
if not self._hdiffpatch.get_hpatchz_executable():
call(UpdateStatus.DOWNLOADING_PATCHER)
await self._hdiffpatch.download_latest_release()
archive = zipfile.ZipFile(game_archive, "r")
files = archive.namelist()
# Don't extract these files (they're useless and if the game isn't patched then it'll
# raise 31-4xxx error in-game)
call(UpdateStatus.PREPARING_PATCH)
for file in ["deletefiles.txt", "hdifffiles.txt"]:
try:
files.remove(file)
except ValueError:
pass
# hdiffpatch implementation
hdifffiles = []
for x in (
(await asyncio.to_thread(archive.read, "hdifffiles.txt"))
.decode()
.split("\n")
):
if x:
hdifffiles.append(json.loads(x)["remoteName"])
patch_jobs = []
for file in hdifffiles:
call(UpdateStatus.PREPARING_PATCH, f=file)
current_game_file = self._game_dir.joinpath(file)
if not current_game_file.exists():
# Not patching since we don't have the file
continue
patch_file = str(file) + ".hdiff"
async def extract_and_patch(old_file, diff_file):
call(UpdateStatus.PATCHING, f=old_file)
diff_path = self._cache_path.joinpath(diff_file)
if diff_path.is_file():
diff_path.unlink(missing_ok=True)
await asyncio.to_thread(archive.extract, diff_file, self._cache_path)
patch_path = self._cache_path.joinpath(diff_file)
old_suffix = old_file.suffix
old_file = await old_file.rename(old_file.with_suffix(".bak"))
proc = await self._hdiffpatch.patch_file(
old_file, old_file.with_suffix(old_suffix), patch_path, wait=True
)
patch_path.unlink()
if proc.returncode != 0:
# Let the game download the file.
await old_file.rename(old_file.with_suffix(old_suffix))
return
await old_file.unlink()
files.remove(patch_file)
patch_jobs.append(extract_and_patch(current_game_file, patch_file))
await asyncio.gather(*patch_jobs)
call(UpdateStatus.PREPARING_REMOVE_UNUSED)
deletefiles = archive.read("deletefiles.txt").decode().split("\n")
for file in deletefiles:
current_game_file = Path(self._game_dir.joinpath(file))
if not current_game_file.exists():
continue
if current_game_file.is_file():
call(UpdateStatus.REMOVING_UNUSED, f=file)
current_game_file.unlink(missing_ok=True)
call(UpdateStatus.EXTRACTING)
await asyncio.to_thread(archive.extractall, self._game_dir, members=files)
archive.close()
# Update game version on local variable.
self._version = self.get_current_game_version()
self.set_version_config()
call(UpdateStatus.COMPLETED)
def set_version_config(self, version: str = None):
"""
Sets the version in config.ini
Args:
version: Version to be set, if not specified then it'll use the current game version
"""
if not version:
version = self.version
self._game_config.set_game_version(version)
self._game_config.save()
def _extract_game_file(self, archive: str | bytes | PathLike):
if not isinstance(archive, Path):
archive = Path(archive).resolve()
if not archive.exists():
raise FileNotFoundError(f"'{archive}' not found")
with zipfile.ZipFile(archive, "r") as f:
f.extractall(path=self._game_dir)
def apply_voicepack(self, archive: str | bytes | PathLike):
"""
Install or update a voicepack to the game
Args:
archive: The voicepack to install or update
"""
# Since voicepack packages are unclear about diff package or full package
# we will try to extract the voicepack package and apply it to the game
# making this function universal for both cases
if not self.get_game_data_path().exists():
raise FileNotFoundError(f"Game not found in {self._game_dir}")
self._extract_game_file(archive)
async def install_game(
self,
game_archive: str | bytes | PathLike,
force_reinstall: bool = False,
callback=None,
):
"""Installs the game to the current directory
If `force_reinstall` is True, the game will be uninstalled then reinstalled.
"""
def call(status):
if not callback:
return
callback(status=status)
if self.get_game_data_path().exists():
if not force_reinstall:
raise ValueError(f"Game is already installed in {self._game_dir}")
call(InstallStatus.REMOVING_OLD_GAME)
self.delete_game()
self._game_dir.mkdir(parents=True, exist_ok=True)
call(InstallStatus.EXTRACTING)
self._extract_game_file(game_archive)
self._version = self.get_current_game_version()
self.set_version_config()
call(InstallStatus.COMPLETED)
async def get_voicepack_diff_archive(
self, lang: str, from_version: str = None, pre_download=False
) -> installer.Diff:
"""Gets a diff archive from `from_version` to the latest one
If from_version is not specified, it will be taken from the current game version.
"""
from_version = from_version if from_version else self.version
game = await self.get_server_game_info(pre_download=pre_download)
translated_lang = voicepack_lang_translate(lang)
for v in game.diffs:
if v.version != from_version:
continue
for vo in v.voice_packs:
if vo.language == translated_lang:
return vo
async def get_game_diff_archive(
self, from_version: str = None, pre_download=False
) -> installer.Diff:
"""Gets a diff archive from `from_version` to the latest one
If from_version is not specified, it will be taken from the current game version.
"""
from_version = from_version if from_version else self.version
game = await self.get_server_game_info(pre_download=pre_download)
for v in game.diffs:
if v.version == from_version:
return v
def verify_game_file(
self,
file: str | bytes | PathLike,
pkg_version: dict[str, str] | str | bytes | PathLike = None,
ignore_mismatch=True,
):
"""
Verifies a game file
It is strongly recommended to parse pkg_version with parse_pkg_version and use
that as pkg_version argument if you're planning to verify multiple game files
to save resource and verify faster.
Args:
file: The file to verify (e.g. GenshinImpact_Data/sharedassets0.assets.resS)
pkg_version: The pkg_version file to read MD5 from, if not specified it'll use
the file from the current game installation
ignore_mismatch: Do not raise exception if a file has mismatch MD5, instead
return as a (file, expected md5, actual md5) tuple
Returns:
None if the file is good, (file, expected md5, actual md5) if the file has mismatch md5
Raises:
FileNotFoundError if the file to verify doesn't exist
"""
if pkg_version is None:
pkg_version = self._game_dir.joinpath("pkg_version")
return verify_game_file(
file=file,
game_dir=self._game_dir,
pkg_version=pkg_version,
ignore_mismatch=ignore_mismatch,
)
async def verify_game_files(
self,
pkg_version: dict[str, str] | str | bytes | PathLike = None,
ignore_mismatch=True,
):
"""
Verifies the current game installation
Args:
pkg_version: The pkg_version file to read MD5 from, if not specified it'll use
the file from the current game installation
ignore_mismatch: Do not raise exception if a file has mismatch MD5, instead
return all of them as a list with (file, expected md5, actual md5)
Returns:
None if all files are good, a list with (file, expected md5, actual md5) for mismatch files
"""
if pkg_version is None:
pkg_version = self._game_dir.joinpath("pkg_version")
return await verify_game_files(
game_dir=self._game_dir,
pkg_version=pkg_version,
ignore_mismatch=ignore_mismatch,
)
def delete_game(self):
"""
Deletes the current game installation
"""
shutil.rmtree(self._game_dir, ignore_errors=True)
self._version = None
self._variant = None
def clear_cache(self):
"""
Clears the cache (e.g. downloaded game files)
"""
shutil.rmtree(self._cache_path, ignore_errors=True)

View File

@ -0,0 +1,40 @@
from enum import Enum
class Variant(Enum):
INTERNATIONAL = "international"
CHINESE = "chinese"
BILIBILI = "bilibili"
class VersionLocation(Enum):
GAME_FILE = "globalgamemanagers"
LAUNCHER_CONFIG = "config.ini"
class VoicepackArchiveType(Enum):
FULL = 0
UPDATE = 1
class VoicepackArchiveLanguage(Enum):
English = "English(US)"
Japanese = "Japanese"
Korean = "Korean"
Chinese = "Chinese"
class InstallStatus(Enum):
REMOVING_OLD_GAME = 0
EXTRACTING = 1
COMPLETED = 2
class UpdateStatus(Enum):
DOWNLOADING_PATCHER = 0
PREPARING_PATCH = 1
PATCHING = 2
PREPARING_REMOVE_UNUSED = 3
REMOVING_UNUSED = 4
EXTRACTING = 5
COMPLETED = 6

View File

@ -0,0 +1,103 @@
from pathlib import Path
from worthless import helper
from worthless.game import Game
class Helper:
"""
Quick and dirty extra functions for Game
Since this is quick and dirty, you are recommended to write your own method instead.
Args:
game: A worthless.game.Game instance
"""
def __init__(self, game: Game):
self._download_chunk = 8192
self._game = game
async def _download_file(
self, file_url: str, file_name: str, file_len: int = None, overwrite=False
) -> Path:
"""
Download file name to temporary directory.
This function is a wrapper for helper.download_file
Args:
file_url: File url to download
file_name: File name to download into
Returns:
A Path object containing downloaded file
"""
return await helper.download_file(
file_url,
file_name,
self._game.cache_path,
file_len=file_len,
overwrite=overwrite,
chunks=self._download_chunk,
)
@property
def download_chunk(self):
return self._download_chunk
@download_chunk.setter
def download_chunk(self, chunk: int) -> None:
"""
Sets the download chunk for the internal download function
Args:
chunk: Chunk to set into
"""
self._download_chunk = chunk
async def download_full_game(self, pre_download: bool = False) -> Path:
game = await self._game.get_server_game_info(pre_download)
archive_name = game.latest.path.split("/")[-1]
return await self._download_file(
game.latest.path, archive_name, game.latest.size
)
async def download_full_voicepack(
self, language: str, pre_download: bool = False
) -> Path:
game = await self._game.get_server_game_info(pre_download)
translated_lang = self._game.voicepack_lang_translate(language)
for vo in game.latest.voice_packs:
if vo.language == translated_lang:
return await self._download_file(vo.path, vo.get_name(), vo.size)
async def download_game_update(
self, from_version: str = None, pre_download: bool = False
) -> Path:
from_version = from_version if from_version else self._game.version
game = await self._game.get_server_game_info(pre_download=pre_download)
if self._game.version == game.latest.version:
raise ValueError("Game is already up to date.")
diff_archive = await self._game.get_game_diff_archive(
from_version, pre_download
)
if diff_archive is None:
raise ValueError(
"Game diff archive is not available for this version, please reinstall."
)
return await self._download_file(
diff_archive.path, diff_archive.name, diff_archive.size
)
async def download_voicepack_update(
self, language: str, from_version: str = None, pre_download: bool = False
) -> Path:
from_version = from_version if from_version else self._game.version
diff_archive = await self._game.get_voicepack_diff_archive(
language, from_version, pre_download
)
if diff_archive is None:
raise ValueError("Voiceover diff archive is not available for this version")
return await self._download_file(
diff_archive.path, diff_archive.name, diff_archive.size
)

View File

@ -0,0 +1,132 @@
import platform
import shutil
import asyncio
import zipfile
import aiohttp
from pathlib import Path
from worthless import constants, helper
class HDiffPatch:
"""
Contains legacy HDiffPatch support for worthless
You should not use this class directly, since it's automatically used by Game when needed
"""
def __init__(self, git_url=None, data_dir=None):
if not git_url:
git_url = constants.HDIFFPATCH_GIT_URL
self._git_url = git_url
if not data_dir:
self._appdirs = constants.APPDIRS
self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("HDiffPatch")
self.data_path = Path(self._appdirs.user_data_dir).joinpath(
"Tools/HDiffPatch"
)
else:
if not isinstance(data_dir, Path):
data_dir = Path(data_dir)
self.data_path = Path(data_dir).joinpath("Tools/HDiffPatch")
self.temp_path = data_dir.joinpath("Temp/HDiffPatch")
self.temp_path.mkdir(parents=True, exist_ok=True)
@staticmethod
def _get_platform_arch():
match platform.system():
case "Windows":
match platform.architecture()[0]:
case "32bit":
return "windows32"
case "64bit":
return "windows64"
case "Linux":
match platform.architecture()[0]:
case "32bit":
return "linux32"
case "64bit":
return "linux64"
case "Darwin":
return "macos"
raise RuntimeError("Unsupported platform")
def _get_hdiffpatch_exec(self, exec_name):
if shutil.which(exec_name):
return exec_name
if not self.data_path.exists():
return None
if not any(self.data_path.iterdir()):
return None
platform_arch_path = self.data_path.joinpath(self._get_platform_arch())
file = platform_arch_path.joinpath(exec_name)
if file.exists():
file.chmod(0o755)
return str(file)
return None
def get_hpatchz_executable(self):
hpatchz_name = "hpatchz" + (".exe" if platform.system() == "Windows" else "")
return self._get_hdiffpatch_exec(hpatchz_name)
async def patch_file(self, in_file, out_file, patch_file, error=False, wait=False):
hpatchz = self.get_hpatchz_executable()
if not hpatchz:
raise RuntimeError("hpatchz executable not found")
proc = await asyncio.create_subprocess_exec(
hpatchz, "-f", in_file, patch_file, out_file
)
if not wait:
return proc
await proc.wait()
if error and proc.returncode != 0:
raise RuntimeError(f"Patching failed, return code is {proc.returncode}")
return proc
def get_hdiffz_executable(self):
hdiffz_name = "hdiffz" + (".exe" if platform.system() == "Windows" else "")
return self._get_hdiffpatch_exec(hdiffz_name)
async def _get_latest_release_info(self):
async with aiohttp.ClientSession() as session:
split = self._git_url.split("/")
repo = split[-1]
owner = split[-2]
rsp = await session.get(
"https://api.github.com/repos/{}/{}/releases/latest".format(
owner, repo
),
params={"Headers": "Accept: application/vnd.github.v3+json"},
)
rsp.raise_for_status()
for asset in (await rsp.json())["assets"]:
if (
asset["name"].endswith(".zip")
and "linux" not in asset["name"]
and "windows" not in asset["name"]
and "macos" not in asset["name"]
and "android" not in asset["name"]
):
return asset
async def get_latest_release_url(self):
asset = await self._get_latest_release_info()
return asset["browser_download_url"]
async def get_latest_release_name(self):
asset = await self._get_latest_release_info()
return asset["name"]
async def download_latest_release(self, extract=True):
url = await self.get_latest_release_url()
name = await self.get_latest_release_name()
if not url:
raise RuntimeError("Unable to find latest release")
await helper.download_file(
url, name, self.temp_path, overwrite=True, threads_num=1
)
if not extract:
return
with zipfile.ZipFile(self.temp_path.joinpath(name), "r") as f:
await asyncio.to_thread(f.extractall, path=self.data_path)

View File

@ -0,0 +1,83 @@
from configparser import ConfigParser
from os import PathLike
from pathlib import Path
from worthless.game.gameenums import Variant
class LauncherConfig:
"""
Provides config.ini for official launcher compatibility
You should not use this class directly, since it's automatically used by Game when needed
"""
@staticmethod
def create_config(game_version, variant: Variant):
"""
Creates `config.ini`
https://notabug.org/Krock/dawn/src/master/updater/update_gi.sh#L212
"""
sub_channel = 0
channel = 1
cps = "mihoyo"
match variant:
case Variant.INTERNATIONAL:
channel = 1
sub_channel = 0
cps = "mihoyo"
case Variant.CHINESE:
channel = 1
sub_channel = 1
cps = "mihoyo"
case Variant.BILIBILI:
channel = 14
sub_channel = 0
cps = "bilibili"
config = ConfigParser()
config.add_section("General")
config.set("General", "channel", str(channel))
config.set("General", "cps", cps)
config.set("General", "game_version", game_version)
config.set("General", "sdk_version", "")
config.set("General", "sub_channel", str(sub_channel))
return config
def __init__(
self, config_path: PathLike, game_version=None, variant: Variant = None
):
if not variant:
variant = Variant.INTERNATIONAL
if not isinstance(config_path, Path):
config_path = Path(config_path)
if not game_version:
game_version = "0.0.0"
self._config_path = config_path
self._config = ConfigParser()
if self._config_path.exists():
self._config.read(self._config_path)
else:
self._config = self.create_config(game_version, variant)
def set_game_version(self, game_version):
self._config.set("General", "game_version", game_version)
def set_variant(self, variant: Variant):
sub_channel = 0
match variant:
case Variant.INTERNATIONAL:
sub_channel = 0
case Variant.CHINESE:
sub_channel = 1
case Variant.BILIBILI:
sub_channel = 0
self._config.set("General", "sub_channel", str(sub_channel))
def save(self):
"""
Saves config.ini
"""
with self._config_path.open("w") as config_file:
self._config.write(config_file)
def get(self, section: str, option: str, **kwargs) -> str:
return self._config.get(section=section, option=option, **kwargs)

104
worthless/helper.py Normal file
View File

@ -0,0 +1,104 @@
import asyncio
import logging
import math
import secrets
from pathlib import Path
import aiohttp
_logger = logging.getLogger("worthless.helper")
async def download_file(
file_url: str,
file_name: str,
file_path: Path | str,
file_len: int = None,
overwrite: bool = False,
chunks: int = None,
threads_num: int = None,
) -> Path:
"""
Download file name to file_path.
You should implement your own download method instead of using this.
Args:
file_url: Url to download the file from
file_name: File name to download into
file_path: Path to download file into
file_len: File length to support threaded downloading
overwrite: Whether overwrite existing file or not
chunks: Chunks to write file into memory before writing to disk
threads_num: Number of download threads
Return:
Downloaded file as a Path object
"""
logger = _logger.getChild("download_file")
if not chunks:
chunks = 8192
if not threads_num:
threads_num = 8
logger.debug("Downloading chunks {} with {} thread".format(chunks, threads_num))
file_path = Path(file_path).joinpath(file_name)
async def _download(
session: aiohttp.ClientSession,
from_bytes: int,
to_bytes: int,
threaded: bool = None,
) -> Path:
headers = {"Range": f"bytes={from_bytes}-{to_bytes if to_bytes else ''}"}
if threaded:
p = file_path.parent.joinpath(secrets.token_urlsafe(16))
else:
p = file_path
p.touch(exist_ok=True)
rsp = await session.get(file_url, headers=headers, timeout=None)
if rsp.status == 416:
# Not an error, so yeah.
return p
rsp.raise_for_status()
with p.open("ab") as file:
async for chunk in rsp.content.iter_chunked(chunks):
await asyncio.to_thread(file.write, chunk)
return p
if overwrite:
file_path.unlink(missing_ok=True)
if file_path.exists():
cur_len = file_path.stat().st_size
else:
file_path.touch()
cur_len = 0
if not file_len or threads_num == 1:
async with aiohttp.ClientSession() as s:
return await _download(session=s, from_bytes=cur_len, to_bytes=file_len)
download_bytes = file_len - cur_len
# if bytes * threads is smaller than file_len then we will not get the full file.
download_bytes_t = math.ceil(download_bytes / threads_num)
download_jobs = []
current_bytes = cur_len
async with aiohttp.ClientSession() as s:
for thread in range(threads_num):
next_bytes = current_bytes + download_bytes_t
if next_bytes > file_len:
next_bytes = file_len
download_jobs.append(
_download(
session=s,
from_bytes=current_bytes,
to_bytes=next_bytes,
threaded=True,
)
)
# Move to next bytes
current_bytes = next_bytes
all_bytes = await asyncio.gather(*download_jobs)
# Merge bytes into the file
with file_path.open("ab") as f:
for bytes_path in all_bytes:
f.write(bytes_path.read_bytes())
bytes_path.unlink()

View File

@ -1,547 +0,0 @@
import asyncio
import re
import shutil
import platform
import aiohttp
import zipfile
import json
import hashlib
from pathlib import Path
from configparser import ConfigParser
from aiopath import AsyncPath
from worthless import constants
from worthless.launcher import Launcher
from worthless.launcherconfig import LauncherConfig
async def _download_file(file_url: str, file_name: str, file_path: Path | str, file_len: int = None, overwrite=False,
chunks=8192):
"""
Download file name to temporary directory,
:param file_url:
:param file_name:
:return:
"""
headers = {}
file_path = AsyncPath(file_path).joinpath(file_name)
if overwrite:
await file_path.unlink(missing_ok=True)
if await file_path.exists():
cur_len = (await file_path.stat()).st_size
headers |= {
"Range": f"bytes={cur_len}-{file_len if file_len else ''}"
}
else:
await file_path.touch()
async with aiohttp.ClientSession() as session:
rsp = await session.get(file_url, headers=headers, timeout=None)
if rsp.status == 416:
return
rsp.raise_for_status()
while True:
chunk = await rsp.content.read(chunks)
if not chunk:
break
async with file_path.open("ab") as f:
await f.write(chunk)
class HDiffPatch:
def __init__(self, git_url=None, data_dir=None):
if not git_url:
git_url = constants.HDIFFPATCH_GIT_URL
self._git_url = git_url
if not data_dir:
self._appdirs = constants.APPDIRS
self.temp_path = Path(self._appdirs.user_cache_dir).joinpath("HDiffPatch")
self.data_path = Path(self._appdirs.user_data_dir).joinpath("Tools/HDiffPatch")
else:
if not isinstance(data_dir, Path):
data_dir = Path(data_dir)
self.data_path = Path(data_dir).joinpath("Tools/HDiffPatch")
self.temp_path = data_dir.joinpath("Temp/HDiffPatch")
self.temp_path.mkdir(parents=True, exist_ok=True)
@staticmethod
def _get_platform_arch():
match platform.system():
case "Windows":
match platform.architecture()[0]:
case "32bit":
return "windows32"
case "64bit":
return "windows64"
case "Linux":
match platform.architecture()[0]:
case "32bit":
return "linux32"
case "64bit":
return "linux64"
case "Darwin":
return "macos"
# Rip BSD they need to use Linux compatibility layer to run this (or use Wine if they prefer that)
raise RuntimeError("Unsupported platform")
def _get_hdiffpatch_exec(self, exec_name):
if shutil.which(exec_name):
return exec_name
if not self.data_path.exists():
return None
if not any(self.data_path.iterdir()):
return None
platform_arch_path = self.data_path.joinpath(self._get_platform_arch())
file = platform_arch_path.joinpath(exec_name)
if file.exists():
file.chmod(0o755)
return str(file)
return None
def get_hpatchz_executable(self):
hpatchz_name = "hpatchz" + (".exe" if platform.system() == "Windows" else "")
return self._get_hdiffpatch_exec(hpatchz_name)
async def patch_file(self, in_file, out_file, patch_file, error=False, wait=False):
hpatchz = self.get_hpatchz_executable()
if not hpatchz:
raise RuntimeError("hpatchz executable not found")
proc = await asyncio.create_subprocess_exec(hpatchz, "-f", in_file, patch_file, out_file)
if not wait:
return proc
await proc.wait()
if error and proc.returncode != 0:
raise RuntimeError(f"Patching failed, return code is {proc.returncode}")
return proc
def get_hdiffz_executable(self):
hdiffz_name = "hdiffz" + (".exe" if platform.system() == "Windows" else "")
return self._get_hdiffpatch_exec(hdiffz_name)
async def _get_latest_release_info(self):
async with aiohttp.ClientSession() as session:
split = self._git_url.split("/")
repo = split[-1]
owner = split[-2]
rsp = await session.get("https://api.github.com/repos/{}/{}/releases/latest".format(owner, repo),
params={"Headers": "Accept: application/vnd.github.v3+json"})
rsp.raise_for_status()
for asset in (await rsp.json())["assets"]:
if asset["name"].endswith(".zip") and "linux" not in asset["name"] and "windows" not in asset["name"] \
and "macos" not in asset["name"] and "android" not in asset["name"]:
return asset
async def get_latest_release_url(self):
asset = await self._get_latest_release_info()
return asset["browser_download_url"]
async def get_latest_release_name(self):
asset = await self._get_latest_release_info()
return asset["name"]
async def download_latest_release(self, extract=True):
url = await self.get_latest_release_url()
name = await self.get_latest_release_name()
if not url:
raise RuntimeError("Unable to find latest release")
await _download_file(url, name, self.temp_path, overwrite=True)
if not extract:
return
with zipfile.ZipFile(self.temp_path.joinpath(name), 'r') as f:
await asyncio.to_thread(f.extractall, path=self.data_path)
class Installer:
def __init__(self, gamedir: str | Path | AsyncPath = AsyncPath.cwd(),
overseas: bool = True, data_dir: str | Path | AsyncPath = None):
if isinstance(gamedir, str | Path):
gamedir = AsyncPath(gamedir)
self._gamedir = gamedir
if not data_dir:
self._appdirs = constants.APPDIRS
self.temp_path = AsyncPath(self._appdirs.user_cache_dir).joinpath("Installer")
else:
if isinstance(data_dir, str | AsyncPath):
data_dir = AsyncPath(data_dir)
self.temp_path = data_dir.joinpath("Temp/Installer/")
Path(self.temp_path).mkdir(parents=True, exist_ok=True)
config_file = self._gamedir.joinpath("config.ini")
self._config_file = config_file
self._download_chunk = 8192
self._overseas = overseas
self._version = None
self._launcher = Launcher(self._gamedir, overseas=self._overseas)
self._hdiffpatch = HDiffPatch(data_dir=data_dir)
self._config = LauncherConfig(self._config_file, self._version)
self._game_version_re = re.compile(r"([1-9]+\.[0-9]+\.[0-9]+)_\d+_\d+")
async def _download_file(self, file_url: str, file_name: str, file_len: int = None, overwrite=False):
"""
Download file name to temporary directory,
:param file_url:
:param file_name:
:return:
"""
await _download_file(file_url, file_name, self.temp_path, file_len=file_len, overwrite=overwrite,
chunks=self._download_chunk)
async def read_version_from_config(self):
if not await self._config_file.exists():
raise FileNotFoundError(f"Config file {self._config_file} not found")
cfg = ConfigParser()
await asyncio.to_thread(cfg.read, str(self._config_file))
return cfg.get("General", "game_version")
async def read_version_from_game_file(self, globalgamemanagers: AsyncPath | Path | bytes) -> str:
"""
Reads the version from the globalgamemanagers file. (Data/globalgamemanagers)
Uses `An Anime Game Launcher` method to read the version:
https://gitlab.com/KRypt0n_/an-anime-game-launcher/-/blob/main/src/ts/Game.ts#L26
:return: Game version (ex 1.0.0)
"""
if isinstance(globalgamemanagers, Path | AsyncPath):
globalgamemanagers = AsyncPath(globalgamemanagers)
data = await globalgamemanagers.read_text("ascii", errors="ignore")
else:
data = globalgamemanagers.decode("ascii", errors="ignore")
result = self._game_version_re.search(data)
if not result:
raise ValueError("Could not find version in game file")
return result.group(1)
@staticmethod
def voiceover_lang_translate(lang: str, base_language="game") -> str:
"""
Translates the voiceover language to the language code used by the game.
:param lang: Language to translate
:param base_language: Base language type (game/locale/both)
:return: Language code
"""
if base_language == "game" or base_language == "both":
match lang.lower():
case "english(us)":
return "en-us"
case "japanese":
return "ja-jp"
case "chinese":
return "zh-cn"
case "korean":
return "ko-kr"
if base_language == "locale" or base_language == "both":
match lang.lower().replace("_", "-"):
case "en-us":
return "English(US)"
case "ja-jp":
return "Japanese"
case "zh-cn":
return "Chinese"
case "ko-kr":
return "Korean"
# If nothing else matches
return lang
@staticmethod
async def get_voiceover_archive_language(voiceover_archive: str | Path | AsyncPath) -> str:
if isinstance(voiceover_archive, str | Path):
voiceover_archive = Path(voiceover_archive).resolve()
if not voiceover_archive.exists():
raise FileNotFoundError(f"Voiceover archive {voiceover_archive} not found")
with zipfile.ZipFile(voiceover_archive, 'r') as f:
for file in zipfile.Path(f).iterdir():
if file.name.endswith("_pkg_version"):
return file.name.split("_")[1]
@staticmethod
async def get_voiceover_archive_type(voiceover_archive: str | Path) -> bool:
"""
Gets voiceover archive type.
:param voiceover_archive:
:return: True if this is a full archive, else False.
"""
vo_lang = Installer.get_voiceover_archive_language(voiceover_archive)
with zipfile.ZipFile(voiceover_archive, 'r') as f:
archive_path = zipfile.Path(f)
files = (await asyncio.to_thread(f.read, "Audio_{}_pkg_version".format(vo_lang))).decode().split("\n")
for file in files:
if file.strip() and not archive_path.joinpath(json.loads(file)["remoteName"]).exists():
return False
return True
def set_download_chunk(self, chunk: int):
self._download_chunk = chunk
def get_game_data_name(self):
if self._overseas:
return "GenshinImpact_Data/"
else:
return "YuanShen_Data/"
def get_game_data_path(self) -> AsyncPath:
return self._gamedir.joinpath(self.get_game_data_name())
async def get_game_archive_version(self, game_archive: str | Path):
game_archive = Path(game_archive)
if not game_archive.is_file():
raise FileNotFoundError(f"Game archive {game_archive} not found")
with zipfile.ZipFile(game_archive, 'r') as f:
return await self.read_version_from_game_file(
await asyncio.to_thread(f.read, self.get_game_data_name() + "globalgamemanagers")
)
async def get_game_version(self) -> str | None:
globalgamemanagers = self.get_game_data_path().joinpath("./globalgamemanagers")
if not await globalgamemanagers.exists():
try:
return await self.read_version_from_config()
except FileNotFoundError:
return
return await self.read_version_from_game_file(globalgamemanagers)
async def get_installed_voiceovers(self) -> list[str]:
"""
Returns a list of installed voiceovers.
:return: List of installed voiceovers
"""
voiceovers = []
async for file in self.get_game_data_path()\
.joinpath("StreamingAssets/Audio/GeneratedSoundBanks/Windows").iterdir():
if await file.is_dir():
voiceovers.append(file.name)
return voiceovers
async def update_game(self, game_archive: str | Path | AsyncPath):
if not await self.get_game_data_path().exists():
raise FileNotFoundError(f"Game not found in {self._gamedir}")
if isinstance(game_archive, str | Path):
game_archive = Path(game_archive).resolve()
if not game_archive.exists():
raise FileNotFoundError(f"Update archive {game_archive} not found")
archive = zipfile.ZipFile(game_archive, 'r')
if not self._hdiffpatch.get_hpatchz_executable():
await self._hdiffpatch.download_latest_release()
files = archive.namelist()
# Don't extract these files (they're useless and if the game isn't patched then it'll
# raise 31-4xxx error ingame)
for file in ["deletefiles.txt", "hdifffiles.txt"]:
try:
files.remove(file)
except ValueError:
pass
# hdiffpatch implementation
hdifffiles = []
for x in (await asyncio.to_thread(archive.read, "hdifffiles.txt")).decode().split("\n"):
if x:
hdifffiles.append(json.loads(x)["remoteName"])
patch_jobs = []
for file in hdifffiles:
current_game_file = self._gamedir.joinpath(file)
if not await current_game_file.exists():
# Not patching since we don't have the file
continue
patch_file = str(file) + ".hdiff"
async def extract_and_patch(old_file, diff_file):
diff_path = self.temp_path.joinpath(diff_file)
if await diff_path.is_file():
await diff_path.unlink(missing_ok=True)
await asyncio.to_thread(archive.extract, diff_file, self.temp_path)
patch_path = self.temp_path.joinpath(diff_file)
old_suffix = old_file.suffix
old_file = await old_file.rename(old_file.with_suffix(".bak"))
proc = await self._hdiffpatch.patch_file(old_file, old_file.with_suffix(old_suffix),
patch_path, wait=True)
await patch_path.unlink()
if proc.returncode != 0:
# Let the game download the file.
await old_file.rename(old_file.with_suffix(old_suffix))
return
await old_file.unlink()
files.remove(patch_file)
patch_jobs.append(extract_and_patch(current_game_file, patch_file))
await asyncio.gather(*patch_jobs)
deletefiles = archive.read("deletefiles.txt").decode().split("\n")
for file in deletefiles:
current_game_file = Path(self._gamedir.joinpath(file))
if not current_game_file.exists():
continue
if current_game_file.is_file():
current_game_file.unlink(missing_ok=True)
await asyncio.to_thread(archive.extractall, self._gamedir, members=files)
archive.close()
# Update game version on local variable.
self._version = await self.get_game_version()
self.set_version_config()
def set_version_config(self, version: str = None):
if not version:
version = self._version
self._config.set_game_version(version)
self._config.save()
async def download_full_game(self, pre_download=False):
game = await self._get_game(pre_download)
archive_name = game.latest.path.split("/")[-1]
await self._download_file(game.latest.path, archive_name, game.latest.size)
async def download_full_voiceover(self, language: str, pre_download=False):
game = await self._get_game(pre_download)
translated_lang = self.voiceover_lang_translate(language)
for vo in game.latest.voice_packs:
if vo.language == translated_lang:
await self._download_file(vo.path, vo.get_name(), vo.size)
async def uninstall_game(self):
await asyncio.to_thread(shutil.rmtree, self._gamedir, ignore_errors=True)
async def _extract_game_file(self, archive: str | Path | AsyncPath):
if isinstance(archive, str | AsyncPath):
archive = Path(archive).resolve()
if not archive.exists():
raise FileNotFoundError(f"'{archive}' not found")
with zipfile.ZipFile(archive, 'r') as f:
await asyncio.to_thread(f.extractall, path=self._gamedir)
async def apply_voiceover(self, voiceover_archive: str | Path):
# Since Voiceover packages are unclear about diff package or full package
# we will try to extract the voiceover package and apply it to the game
# making this function universal for both cases
if not await self.get_game_data_path().exists():
raise FileNotFoundError(f"Game not found in {self._gamedir}")
await self._extract_game_file(voiceover_archive)
async def install_game(self, game_archive: str | Path | AsyncPath, force_reinstall: bool = False):
"""Installs the game to the current directory
If `force_reinstall` is True, the game will be uninstalled then reinstalled.
"""
if await self.get_game_data_path().exists():
if not force_reinstall:
raise ValueError(f"Game is already installed in {self._gamedir}")
await self.uninstall_game()
await self._gamedir.mkdir(parents=True, exist_ok=True)
await self._extract_game_file(game_archive)
self._version = await self.get_game_version()
self.set_version_config()
async def _get_game_version(self):
if self._version:
from_version = self._version
else:
from_version = self._version = await self.get_game_version()
return from_version
async def _get_game_resource(self):
game_resource = await self._launcher.get_resource_info()
if not game_resource:
raise ValueError("Could not fetch game resource")
return game_resource
async def _get_game(self, pre_download=False):
game_resource = await self._get_game_resource()
game = game_resource.game
if pre_download:
game = game_resource.pre_download_game
return game
async def download_game_update(self, from_version: str = None, pre_download=False):
if not from_version:
from_version = await self._get_game_version()
game = await self._get_game(pre_download=pre_download)
if self._version == game.latest.version:
raise ValueError("Game is already up to date.")
diff_archive = await self.get_game_diff_archive(from_version, pre_download)
if diff_archive is None:
raise ValueError("Game diff archive is not available for this version, please reinstall.")
await self._download_file(diff_archive.path, diff_archive.name, diff_archive.size)
async def download_voiceover_update(self, language: str, from_version: str = None, pre_download=False):
if not from_version:
from_version = await self._get_game_version()
diff_archive = await self.get_voiceover_diff_archive(language, from_version, pre_download)
if diff_archive is None:
raise ValueError("Voiceover diff archive is not available for this version, please reinstall.")
await self._download_file(diff_archive.path, diff_archive.name, diff_archive.size)
async def get_voiceover_diff_archive(self, lang: str, from_version: str = None, pre_download=False):
"""Gets a diff archive from `from_version` to the latest one
If from_version is not specified, it will be taken from the game version.
"""
if not from_version:
from_version = await self._get_game_version()
game = await self._get_game(pre_download=pre_download)
translated_lang = self.voiceover_lang_translate(lang)
for v in game.diffs:
if v.version != from_version:
continue
for vo in v.voice_packs:
if vo.language == translated_lang:
return vo
async def get_game_diff_archive(self, from_version: str = None, pre_download=False):
"""Gets a diff archive from `from_version` to the latest one
If from_version is not specified, it will be taken from the game version.
"""
if not from_version:
from_version = await self._get_game_version()
game = await self._get_game(pre_download=pre_download)
for v in game.diffs:
if v.version == from_version:
return v
async def verify_from_pkg_version(self, pkg_version: AsyncPath, ignore_mismatch=False):
contents = await pkg_version.read_text()
async def calculate_md5(file_to_calculate):
file_to_calculate = AsyncPath(file_to_calculate)
if not await file_to_calculate.exists():
return ""
async with file_to_calculate.open("rb") as f:
file_hash = hashlib.md5()
while chunk := await f.read(self._download_chunk):
file_hash.update(chunk)
return file_hash.hexdigest()
async def verify_file(file_to_verify, md5):
file_md5 = await calculate_md5(file_to_verify)
if file_md5 == md5:
return None
if ignore_mismatch:
return file_to_verify, md5, file_md5
raise ValueError(f"MD5 does not match for {file_to_verify}, expected md5: {md5}, actual md5: {file_md5}")
verify_jobs = []
for content in contents.split("\r\n"):
if not content.strip():
continue
info = json.loads(content)
verify_jobs.append(verify_file(self._gamedir.joinpath(info["remoteName"]), info["md5"]))
verify_result = await asyncio.gather(*verify_jobs)
failed_files = []
for file in verify_result:
if file is not None:
failed_files.append(file)
return None if not failed_files else failed_files
async def verify_game(self, pkg_version: str | Path | AsyncPath = None, ignore_mismatch=False):
if pkg_version is None:
pkg_version = self._gamedir.joinpath("pkg_version")
return await self.verify_from_pkg_version(pkg_version, ignore_mismatch)
async def clear_cache(self):
await asyncio.to_thread(shutil.rmtree, self.temp_path, ignore_errors=True)

View File

@ -1,32 +1,17 @@
import aiohttp
import locale
from aiopath import AsyncPath
import aiohttp
from worthless import constants
from pathlib import Path
from worthless.classes import launcher, installer
async def _get(url, **kwargs) -> dict:
# Workaround because miHoYo uses retcode for their API instead of HTTP status code
async with aiohttp.ClientSession() as session:
rsp = await session.get(url, **kwargs)
rsp_json = await rsp.json()
if rsp_json["retcode"] != 0:
# TODO: Add more information to the error message
raise aiohttp.ClientResponseError(code=rsp_json["retcode"],
message=rsp_json["message"],
history=rsp.history,
request_info=rsp.request_info)
return rsp_json
from worthless.game.gameenums import Variant
def _get_system_language() -> str:
"""Gets system language compatible with server parameters.
Return:
System language with format xx-xx.
System language with format xx-xx (e.g. en-us).
"""
try:
@ -42,54 +27,81 @@ class Launcher:
Contains functions to get information from server and client like the official launcher.
"""
def __init__(self, gamedir: str | Path = Path.cwd(), language: str = None, overseas=True):
"""Initialize the launcher API
def __init__(
self,
language: str = None,
variant: Variant = None,
):
"""Initialize the launcher API"""
if not variant:
variant = Variant.INTERNATIONAL
if not language:
language = _get_system_language()
self._variant = variant
match variant:
case Variant.INTERNATIONAL:
self._api = constants.LAUNCHER_API_URL_OS
self._params = {
"key": "gcStgarh",
"launcher_id": "10",
}
self._lang = language.lower().replace("_", "-")
case Variant.CHINESE:
self._api = constants.LAUNCHER_API_URL_CN
self._params = {
"key": "eYd89JmJ",
"launcher_id": "18",
"channel_id": "1",
}
self._lang = (
"zh-cn" # Uses Chinese language because this is Chinese version
)
case Variant.BILIBILI:
self._api = constants.LAUNCHER_API_URL_CN
self._params = {
"key": "KAtdSsoQ",
"launcher_id": "17",
"channel_id": "14",
}
self._lang = (
"zh-cn" # Use Chinese language because this is Chinese version
)
# There is one for beta version too, but I don't have access to that version
self._session = aiohttp.ClientSession()
Args:
gamedir (Path): Path to the game directory.
"""
self._overseas = overseas
if overseas:
self._api = constants.LAUNCHER_API_URL_OS
self._params = {
"key": "gcStgarh",
"launcher_id": "10",
}
self._lang = language.lower().replace("_", "-") if language else _get_system_language()
else:
self._api = constants.LAUNCHER_API_URL_CN
self._params = {
"key": "eYd89JmJ",
"launcher_id": "18",
"channel_id": "1"
}
self._lang = "zh-cn" # Use chinese language because this is chinese version
if isinstance(gamedir, str | AsyncPath):
gamedir = Path(gamedir)
self._gamedir = gamedir.resolve()
async def _get(self, url, **kwargs) -> dict:
# Workaround because miHoYo uses retcode for their API instead of HTTP status code (always 200)
rsp = await self._session.get(url, **kwargs)
rsp_json = await rsp.json()
if rsp_json["retcode"] != 0:
# TODO: Add more information to the error message
raise aiohttp.ClientResponseError(
message=rsp_json["message"],
history=rsp.history,
request_info=rsp.request_info,
headers=rsp.headers,
status=rsp_json["retcode"],
)
return rsp_json
@property
def lang(self):
return self._lang
async def _get_launcher_info(self, adv=True) -> launcher.Info:
params = self._params | {"filter_adv": str(adv).lower(),
"language": self._lang}
rsp = await _get(self._api + "/content", params=params)
params = self._params | {"filter_adv": str(adv).lower(), "language": self._lang}
rsp = await self._get(self._api + "/content", params=params)
if rsp["data"]["adv"] is None:
params["language"] = "en-us"
rsp = await _get(self._api + "/content", params=params)
rsp = await self._get(self._api + "/content", params=params)
lc_info = launcher.Info.from_dict(rsp["data"])
return lc_info
async def override_gamedir(self, gamedir: str | Path) -> None:
"""Overrides game directory with another directory.
@lang.setter
def lang(self, language: str) -> None:
"""Sets current language to another language in xx-xx format.
Args:
gamedir (str): New directory to override with.
"""
if isinstance(gamedir, str):
gamedir = Path(gamedir).resolve()
self._gamedir = gamedir
async def override_language(self, language: str) -> None:
"""Overrides system detected language with another language.
This method will automatically convert language with xx_XX to xx-xx (e.g. en_US to en-us)
Args:
language (str): Language to override with.
@ -108,7 +120,7 @@ class Launcher:
aiohttp.ClientResponseError: An error occurred while fetching the information.
"""
rsp = await _get(self._api + "/resource", params=self._params)
rsp = await self._get(self._api + "/resource", params=self._params)
return installer.Resource.from_dict(rsp["data"])
async def get_launcher_info(self) -> launcher.Info:
@ -134,15 +146,3 @@ class Launcher:
"""
return await self._get_launcher_info(adv=False)
async def get_launcher_background_url(self) -> str:
"""Gets launcher background image url from the server.
Returns:
Background image url.
Raises:
aiohttp.ClientResponseError: An error occurred while fetching the background image.
"""
rsp = await self.get_launcher_info()
return rsp.background.background

View File

@ -1,51 +0,0 @@
from configparser import ConfigParser
from pathlib import Path
from aiopath import AsyncPath
class LauncherConfig:
"""
Provides config.ini for official launcher compatibility
"""
@staticmethod
def create_config(game_version, overseas=True):
"""
Creates config.ini
"""
sub_channel = "0" if overseas else "1"
config = ConfigParser()
config.add_section("General")
config.set("General", "channel", "1")
config.set("General", "cps", "mihoyo")
config.set("General", "game_version", game_version)
config.set("General", "sdk_version", "")
config.set("General", "sub_channel", sub_channel)
return config
def __init__(self, config_path, game_version=None, overseas=True):
if isinstance(config_path, str | AsyncPath):
config_path = Path(config_path)
if not game_version:
game_version = "0.0.0"
self.config_path = config_path
self.config = ConfigParser()
if self.config_path.exists():
self.config.read(self.config_path)
else:
self.config = self.create_config(game_version, overseas)
def set_game_version(self, game_version):
self.config.set("General", "game_version", game_version)
def set_overseas(self, overseas=True):
sub_channel = "0" if overseas else "1"
self.config.set("General", "sub_channel", sub_channel)
def save(self):
"""
Saves config.ini
"""
with self.config_path.open("w") as config_file:
self.config.write(config_file)

View File

@ -1,19 +1,18 @@
import asyncio
from os import PathLike
from pathlib import Path
from aiopath import AsyncPath
class LinuxUtils:
"""Utilities for Linux-specific tasks.
"""
"""Utilities for Linux-specific tasks."""
def __init__(self):
pass
@staticmethod
async def _exec_command(args):
"""Execute a command using pkexec (friendly gui)
"""
if not await AsyncPath("/usr/bin/pkexec").exists():
"""Execute a command using pkexec (friendly gui)"""
if not Path("/usr/bin/pkexec").is_file():
raise FileNotFoundError("pkexec not found.")
rsp = await asyncio.create_subprocess_shell(args)
await rsp.wait()
@ -25,16 +24,16 @@ class LinuxUtils:
return rsp
async def write_text_to_file(self, text, file_path: str | Path | AsyncPath):
"""Write text to a file using pkexec (friendly gui)
"""
if isinstance(file_path, Path | AsyncPath):
async def write_text_to_file(self, text, file_path: PathLike):
"""Write text to a file using pkexec (friendly gui)"""
if isinstance(file_path, Path):
file_path = str(file_path)
await self._exec_command('echo -e "{}" | pkexec tee {}'.format(text, file_path))
async def append_text_to_file(self, text, file_path: str | Path | AsyncPath):
"""Append text to a file using pkexec (friendly gui)
"""
async def append_text_to_file(self, text, file_path: PathLike):
"""Append text to a file using pkexec (friendly gui)"""
if isinstance(file_path, Path):
file_path = str(file_path)
await self._exec_command('echo -e "{}" | pkexec tee -a {}'.format(text, file_path))
await self._exec_command(
'echo -e "{}" | pkexec tee -a {}'.format(text, file_path)
)

View File

@ -1,16 +1,15 @@
import asyncio
import os
import platform
import shutil
import tarfile
from pathlib import Path
import shutil
import aiohttp
import asyncio
from aiopath import AsyncPath
import aiohttp
from worthless import constants
from worthless.game import Game as Installer
from worthless.launcher import Launcher
from worthless.installer import Installer
match platform.system():
case "Linux":
@ -28,21 +27,29 @@ except ImportError:
class Patcher:
def __init__(self, gamedir: Path | AsyncPath | str = AsyncPath.cwd(), data_dir: str | Path | AsyncPath = None,
patch_url: str = None, overseas=True, patch_provider="y0soro"):
def __init__(
self,
gamedir: Path | str = None,
data_dir: str | Path = None,
patch_url: str = None,
overseas=True,
patch_provider="Krock",
):
if isinstance(gamedir, str | Path):
gamedir = AsyncPath(gamedir)
gamedir = Path(gamedir)
self._gamedir = gamedir
if not patch_url:
patch_url = constants.PATCH_LIST[patch_provider].replace("http://", "https://")
self._patch_url = (patch_url if patch_url else constants.PATCH_GIT_URL).replace('http://', 'https://')
patch_url = constants.PATCH_LIST[patch_provider].replace(
"http://", "https://"
)
self._patch_url = patch_url
if not data_dir:
self._appdirs = constants.APPDIRS
self._patch_path = AsyncPath(self._appdirs.user_data_dir).joinpath("Patch")
self._temp_path = AsyncPath(self._appdirs.user_cache_dir).joinpath("Patcher")
self._patch_path = Path(self._appdirs.user_data_dir).joinpath("Patch")
self._temp_path = Path(self._appdirs.user_cache_dir).joinpath("Patcher")
else:
if isinstance(data_dir, str | Path):
data_dir = AsyncPath(data_dir)
data_dir = Path(data_dir)
self._patch_path = data_dir.joinpath("Patch")
self._temp_path = data_dir.joinpath("Temp/Patcher")
self._overseas = overseas
@ -67,14 +74,14 @@ class Patcher:
:return: Archive file in bytes
"""
# Replace http with https
if self._patch_url.startswith('https://notabug.org'):
archive_url = self._patch_url + '/archive/master.{}'.format(archive_format)
if self._patch_url.startswith("https://notabug.org"):
archive_url = self._patch_url + "/archive/master.{}".format(archive_format)
return await (await self._get(archive_url)).read()
try:
url_split = self._patch_url.split('//')
url_split = self._patch_url.split("//")
git_server = url_split[0]
git_owner, git_repo = url_split[1].split('/')
archive_url = git_server + '/api/v1/repos/{}/{}/archive/{}.{}'.format(
git_owner, git_repo = url_split[1].split("/")
archive_url = git_server + "/api/v1/repos/{}/{}/archive/{}.{}".format(
git_owner, git_repo, branch, archive_format
)
archive = await self._get(archive_url)
@ -85,10 +92,17 @@ class Patcher:
async def _download_repo(self, fallback=False):
if shutil.which("git") and not fallback:
if not await self._patch_path.is_dir() or not await self._patch_path.joinpath(".git").exists():
proc = await asyncio.create_subprocess_exec("git", "clone", self._patch_url, str(self._patch_path))
if (
not await self._patch_path.is_dir()
or not await self._patch_path.joinpath(".git").exists()
):
proc = await asyncio.create_subprocess_exec(
"git", "clone", self._patch_url, str(self._patch_path)
)
else:
proc = await asyncio.create_subprocess_exec("git", "pull", cwd=str(self._patch_path))
proc = await asyncio.create_subprocess_exec(
"git", "pull", cwd=str(self._patch_path)
)
await proc.wait()
if proc.returncode != 0:
raise RuntimeError("Cannot download patch repository through git.")
@ -134,7 +148,11 @@ class Patcher:
for url in telemetry_url:
try:
await session.get("https://" + url, timeout=15)
except (aiohttp.ClientResponseError, aiohttp.ClientConnectorError, asyncio.exceptions.TimeoutError):
except (
aiohttp.ClientResponseError,
aiohttp.ClientConnectorError,
asyncio.exceptions.TimeoutError,
):
continue
else:
unblocked_list.append(url)
@ -158,32 +176,41 @@ class Patcher:
gamever = "".join((await self._installer.get_game_version()).split("."))
unity_path = self._gamedir.joinpath("UnityPlayer.dll")
await unity_path.rename(self._gamedir.joinpath("UnityPlayer.dll.bak"))
proc = await asyncio.create_subprocess_exec("xdelta3", "-d", "-s",
str(self._gamedir.joinpath("UnityPlayer.dll.bak")),
str(self._patch_path.joinpath(
"{}/patch_files/{}".format(gamever, patch))),
str(self._gamedir.joinpath("UnityPlayer.dll")), cwd=self._gamedir)
proc = await asyncio.create_subprocess_exec(
"xdelta3",
"-d",
"-s",
str(self._gamedir.joinpath("UnityPlayer.dll.bak")),
str(self._patch_path.joinpath("{}/patch_files/{}".format(gamever, patch))),
str(self._gamedir.joinpath("UnityPlayer.dll")),
cwd=self._gamedir,
)
await proc.wait()
async def _patch_xlua_fallback(self, patch):
gamever = "".join((await self._installer.get_game_version()).split("."))
data_name = self._installer.get_game_data_name()
xlua_path = self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))
await xlua_path.rename(self._gamedir.joinpath("{}/Plugins/xlua.dll.bak".format(data_name)))
proc = await asyncio.create_subprocess_exec("xdelta3", "-d", "-s",
str(self._gamedir.joinpath(
"{}/Plugins/xlua.dll.bak".format(data_name))),
str(self._patch_path.joinpath(
"{}/patch_files/{}".format(gamever, patch))),
str(self._gamedir.joinpath(
"{}/Plugins/xlua.dll".format(data_name))),
cwd=self._gamedir)
await xlua_path.rename(
self._gamedir.joinpath("{}/Plugins/xlua.dll.bak".format(data_name))
)
proc = await asyncio.create_subprocess_exec(
"xdelta3",
"-d",
"-s",
str(self._gamedir.joinpath("{}/Plugins/xlua.dll.bak".format(data_name))),
str(self._patch_path.joinpath("{}/patch_files/{}".format(gamever, patch))),
str(self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))),
cwd=self._gamedir,
)
await proc.wait()
async def _patch_unityplayer(self, patch):
gamever = "".join((await self._installer.get_game_version()).split("."))
unity_path = self._gamedir.joinpath("UnityPlayer.dll")
patch_bytes = self._patch_path.joinpath("{}/patch_files/{}".format(gamever, patch)).read_bytes()
patch_bytes = self._patch_path.joinpath(
"{}/patch_files/{}".format(gamever, patch)
).read_bytes()
patched_unity_bytes = xdelta3.decode(unity_path.read_bytes(), patch_bytes)
unity_path.rename(self._gamedir.joinpath("UnityPlayer.dll.bak"))
with Path(self._gamedir.joinpath("UnityPlayer.dll")).open("wb") as f:
@ -193,10 +220,16 @@ class Patcher:
gamever = "".join((await self._installer.get_game_version()).split("."))
data_name = self._installer.get_game_data_name()
xlua_path = self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))
patch_bytes = self._patch_path.joinpath("{}/patch_files/{}".format(gamever, patch)).read_bytes()
patch_bytes = self._patch_path.joinpath(
"{}/patch_files/{}".format(gamever, patch)
).read_bytes()
patched_xlua_bytes = xdelta3.decode(xlua_path.read_bytes(), patch_bytes)
xlua_path.rename(self._gamedir.joinpath("{}/Plugins/xlua.dll.bak".format(data_name)))
with Path(self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))).open("wb") as f:
xlua_path.rename(
self._gamedir.joinpath("{}/Plugins/xlua.dll.bak".format(data_name))
)
with Path(self._gamedir.joinpath("{}/Plugins/xlua.dll".format(data_name))).open(
"wb"
) as f:
f.write(patched_xlua_bytes)
async def apply_xlua_patch(self, fallback=True):
@ -241,21 +274,21 @@ class Patcher:
for file in disable_files:
file_path = Path(self._gamedir.joinpath(file)).resolve()
if file_path.exists():
await AsyncPath(file_path).rename(str(file_path) + ".bak")
await Path(file_path).rename(str(file_path) + ".bak")
patch_jobs.append(disable_crashreporters())
await asyncio.gather(*patch_jobs)
@staticmethod
async def _creation_date(file_path: str | Path | AsyncPath):
async def _creation_date(file_path: str | Path | Path):
"""
Try to get the date that a file was created, falling back to when it was
last modified if that isn't possible.
See http://stackoverflow.com/a/39501288/1709587 for explanation.
"""
if isinstance(file_path, str | Path):
file_path = AsyncPath(file_path)
if platform.system() == 'Windows':
file_path = Path(file_path)
if platform.system() == "Windows":
return os.path.getctime(file_path)
else:
stat = await file_path.stat()
@ -266,13 +299,23 @@ class Patcher:
# so we'll settle for when its content was last modified.
return stat.st_mtime
async def _revert_file(self, original_file: str, base_file: AsyncPath, ignore_error=False):
async def _revert_file(
self, original_file: str, base_file: Path, ignore_error=False
):
original_path = Path(self._gamedir.joinpath(original_file + ".bak")).resolve()
target_file = Path(self._gamedir.joinpath(original_file)).resolve()
if original_path.exists():
if abs(await self._creation_date(base_file) - await self._creation_date(original_path)) > 86400: # 24 hours
if (
abs(
await self._creation_date(base_file)
- await self._creation_date(original_path)
)
> 86400
): # 24 hours
if not ignore_error:
raise RuntimeError("{} is not for this game version.".format(original_path.name))
raise RuntimeError(
"{} is not for this game version.".format(original_path.name)
)
original_path.unlink(missing_ok=True)
else:
target_file.unlink(missing_ok=True)
@ -284,7 +327,9 @@ class Patcher:
:return: None
"""
game_exec = self._gamedir.joinpath((await self._launcher.get_resource_info()).game.latest.entry)
game_exec = self._gamedir.joinpath(
(await self._launcher.get_resource_info()).game.latest.entry
)
revert_files = [
"UnityPlayer.dll",
self._installer.get_game_data_name() + "upload_crash.exe",
@ -303,9 +348,9 @@ class Patcher:
all_files.extend(Path(self._gamedir).glob(ext))
return all_files
files = get_files(('*.dxvk-cache', '*_d3d9.log', '*_d3d11.log', '*_dxgi.log'))
files = get_files(("*.dxvk-cache", "*_d3d9.log", "*_d3d11.log", "*_dxgi.log"))
for file in files:
revert_job.append(AsyncPath(file).unlink(missing_ok=True))
revert_job.append(Path(file).unlink(missing_ok=True))
await asyncio.gather(*revert_job)