commit 25f6c864142868373466217242b838745c9bef70 Author: Fusselkater Date: Sun Nov 27 19:19:11 2022 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97a2b06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,159 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..d52a860 --- /dev/null +++ b/Pipfile @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +requests = "*" +tabulate = "*" +prettytable = "*" +appdirs = "*" +pyaml = "*" +mergedeep = "*" +tqdm = "*" + +[dev-packages] +pylint = "*" + +[requires] +python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..dc660e4 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,314 @@ +{ + "_meta": { + "hash": { + "sha256": "dc202b159c57e6810d3eaf45002e5b45c0cac8beed961086cfb75cd00e61dd5c" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "index": "pypi", + "version": "==1.4.4" + }, + "certifi": { + "hashes": [ + "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", + "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" + ], + "markers": "python_version >= '3.6'", + "version": "==2022.9.24" + }, + "charset-normalizer": { + "hashes": [ + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + ], + "markers": "python_version >= '3.6'", + "version": "==2.1.1" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "mergedeep": { + "hashes": [ + "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", + "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307" + ], + "index": "pypi", + "version": "==1.3.4" + }, + "prettytable": { + "hashes": [ + "sha256:52f682ba4efe29dccb38ff0fe5bac8a23007d0780ff92a8b85af64bc4fc74d72", + "sha256:fe391c3b545800028edf5dbb6a5360893feb398367fcc1cf8d7a5b29ce5c59a1" + ], + "index": "pypi", + "version": "==3.5.0" + }, + "pyaml": { + "hashes": [ + "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0", + "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383" + ], + "index": "pypi", + "version": "==21.10.1" + }, + "pyyaml": { + "hashes": [ + "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", + "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", + "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", + "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", + "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", + "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", + "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", + "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", + "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", + "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", + "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", + "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", + "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", + "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", + "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", + "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", + "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", + "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", + "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", + "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", + "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", + "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", + "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", + "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", + "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", + "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", + "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", + "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", + "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", + "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", + "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", + "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", + "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", + "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + ], + "markers": "python_version >= '3.6'", + "version": "==6.0" + }, + "requests": { + "hashes": [ + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + ], + "index": "pypi", + "version": "==2.28.1" + }, + "tabulate": { + "hashes": [ + "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", + "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f" + ], + "index": "pypi", + "version": "==0.9.0" + }, + "tqdm": { + "hashes": [ + "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4", + "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1" + ], + "index": "pypi", + "version": "==4.64.1" + }, + "urllib3": { + "hashes": [ + "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", + "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.13" + }, + "wcwidth": { + "hashes": [ + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + ], + "version": "==0.2.5" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907", + "sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7" + ], + "markers": "python_full_version >= '3.7.2'", + "version": "==2.12.13" + }, + "dill": { + "hashes": [ + "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0", + "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373" + ], + "markers": "python_version >= '3.7'", + "version": "==0.3.6" + }, + "isort": { + "hashes": [ + "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", + "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" + ], + "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", + "version": "==5.10.1" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada", + "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d", + "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7", + "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe", + "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd", + "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c", + "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858", + "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288", + "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec", + "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f", + "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891", + "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c", + "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25", + "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156", + "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8", + "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f", + "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e", + "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0", + "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b" + ], + "markers": "python_version >= '3.7'", + "version": "==1.8.0" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "platformdirs": { + "hashes": [ + "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7", + "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10" + ], + "markers": "python_version >= '3.7'", + "version": "==2.5.4" + }, + "pylint": { + "hashes": [ + "sha256:15060cc22ed6830a4049cf40bc24977744df2e554d38da1b2657591de5bcd052", + "sha256:25b13ddcf5af7d112cf96935e21806c1da60e676f952efb650130f2a4483421c" + ], + "index": "pypi", + "version": "==2.15.6" + }, + "tomlkit": { + "hashes": [ + "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b", + "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73" + ], + "markers": "python_version >= '3.6'", + "version": "==0.11.6" + }, + "wrapt": { + "hashes": [ + "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3", + "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b", + "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4", + "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2", + "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656", + "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3", + "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff", + "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310", + "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a", + "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57", + "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069", + "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383", + "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe", + "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87", + "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d", + "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b", + "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907", + "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f", + "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0", + "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28", + "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1", + "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853", + "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc", + "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3", + "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3", + "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164", + "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1", + "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c", + "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1", + "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7", + "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1", + "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320", + "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed", + "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1", + "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248", + "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c", + "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456", + "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77", + "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef", + "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1", + "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7", + "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86", + "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4", + "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d", + "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d", + "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8", + "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5", + "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471", + "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00", + "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68", + "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3", + "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d", + "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735", + "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d", + "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569", + "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7", + "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59", + "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5", + "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb", + "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b", + "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f", + "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462", + "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015", + "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af" + ], + "markers": "python_version >= '3.11'", + "version": "==1.14.1" + } + } +} diff --git a/lib/handlers.py b/lib/handlers.py new file mode 100644 index 0000000..816fbdf --- /dev/null +++ b/lib/handlers.py @@ -0,0 +1,111 @@ +import os.path +import hashlib +from typing import Literal +import requests +from lib import itch +from prettytable import PrettyTable +from tqdm import tqdm + + +class HandlerException(Exception): + pass + +class GameHandler: + + OUTPUT_FORMATS = Literal['table', 'json'] + OS_TYPES = Literal['linux', 'osx', 'windows'] + + def __init__(self, api_key, output_format: OUTPUT_FORMATS ='table'): + self.__itch_client = itch.Client(api_key) + self.__output_format = output_format + + def __print_table(self, table: PrettyTable): + if self.__output_format == 'table': + return print(table) + if self.__output_format == 'json': + return print(table.get_json_string()) + + def find(self, query: str): + games = self.__itch_client.find_game(query) + + table = PrettyTable(header=True, align='l', autowrap=False) + table.field_names = ['ID', 'Title', 'Description', 'URL', 'Purchaseable', 'Min price', 'Linux', 'Windows', 'OSX'] + + for game in games: + table.add_row([ + game['id'], + game['title'] if 'title' in game else '-', + game['short_text'] if 'short_text' in game else '-', + game['url'] if 'url' in game else '-', + 'yes' if game['can_be_bought'] == True else 'no', + float(game['min_price']) / 100, + 'yes' if game['p_linux'] == True else 'no', + 'yes' if game['p_windows'] == True else 'no', + 'yes' if game['p_osx'] == True else 'no' + ]) + self.__print_table(table) + + def uploads(self, game_id: int): + uploads = self.__itch_client.game_uploads(game_id) + + table = PrettyTable(header=True, align='l', autowrap=False) + table.field_names = ['ID', 'File Name', 'Last Update', 'Size', 'MD5', 'Linux', 'Windows', 'OSX'] + + for upload in uploads: + table.add_row([ + upload['id'], + upload['filename'] if 'filename' in upload else '-', + upload['updated_at'] if 'updated_at' in upload else '-', + upload['size'] if 'size' in upload else '-', + upload['md5_hash'] if 'md5_hash' in upload else '-', + 'yes' if upload['p_linux'] == True else 'no', + 'yes' if upload['p_windows'] == True else 'no', + 'yes' if upload['p_osx'] == True else 'no' + ]) + self.__print_table(table) + + def download(self, game_id: int, dst_dir: str, os_type: OS_TYPES = 'linux'): + meta = self.__itch_client.game_meta(game_id) + uploads = self.__itch_client.game_uploads(game_id) + for upload in uploads: + if upload[f'p_{os_type}'] == True: + url = self.__itch_client.game_url(upload['id']) + dst = os.path.join(dst_dir, upload['filename']) + + # Download game + md5 = hashlib.md5() + + with requests.get(url, stream=True) as r: + r.raise_for_status() + total_size = int(r.headers.get('content-length', 0)) + with tqdm.wrapattr(open(dst, 'wb'), 'write', miniters=1, desc='Downloading game', total=total_size) as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + md5.update(chunk) + + if md5.hexdigest() != upload['md5_hash']: + raise HandlerException('Invalid checksum') + + return (dst, upload, meta) + raise HandlerException('No valid upload found') + + def list_local(self, games): + table = PrettyTable(header=True, align='l', autowrap=False) + table.field_names = ['ID', 'Title', 'Description', 'URL', 'Last update', 'Instllation path'] + + for id, game in games.items(): + table.add_row([ + id, + game['title'], + game['description'], + game['url'], + game['last_update'], + game['path'] + ]) + self.__print_table(table) + + def check_update(self, game_id, last_update, os_type: OS_TYPES = 'linux'): + uploads = self.__itch_client.game_uploads(game_id) + for upload in uploads: + if upload[f'p_{os_type}'] == True: + return upload['updated_at'] != last_update \ No newline at end of file diff --git a/lib/itch.py b/lib/itch.py new file mode 100644 index 0000000..f6646bc --- /dev/null +++ b/lib/itch.py @@ -0,0 +1,57 @@ +import requests +from urllib.parse import urljoin + +class API_ERROR(Exception): + pass + +class Client: + + ITCH_API_BASE = 'https://itch.io/api/1/' + + def __init__(self, api_key): + self.__api_key = api_key + self.__session = requests.Session() + + self.__session.headers = { + 'Content-Type': 'application/json' + } + + def find_game(self, query: str, page: int = 1): + url = urljoin(self.ITCH_API_BASE, f'{self.__api_key}/search/games?query={query}&page={page}') + response = self.__session.get(url) + try: + return response.json()['games'] + except requests.exceptions.JSONDecodeError as err: + raise API_ERROR(str(err)) + except KeyError as err: + raise API_ERROR(str(err)) + + def game_uploads(self, game_id: int): + url = urljoin(self.ITCH_API_BASE, f'{self.__api_key}/game/{game_id}/uploads') + response = self.__session.get(url) + try: + return response.json()['uploads'] + except requests.exceptions.JSONDecodeError as err: + raise API_ERROR(str(err)) + except KeyError as err: + raise API_ERROR(str(err)) + + def game_url(self, upload_id: int): + url = urljoin(self.ITCH_API_BASE, f'{self.__api_key}/upload/{upload_id}/download') + response = self.__session.get(url) + try: + return response.json()['url'] + except requests.exceptions.JSONDecodeError as err: + raise API_ERROR(str(err)) + except KeyError as err: + raise API_ERROR(str(err)) + + def game_meta(self, game_id: int): + url = urljoin(self.ITCH_API_BASE, f'{self.__api_key}/game/{game_id}') + response = self.__session.get(url) + try: + return response.json()['game'] + except requests.exceptions.JSONDecodeError as err: + raise API_ERROR(str(err)) + except KeyError as err: + raise API_ERROR(str(err)) \ No newline at end of file diff --git a/minitch.py b/minitch.py new file mode 100644 index 0000000..c6b7243 --- /dev/null +++ b/minitch.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +import os +import subprocess +import argparse +from shutil import unpack_archive +import yaml +import mergedeep +from lib import handlers +from appdirs import user_data_dir, user_config_dir + +config = { + 'global': { + 'game_dir': os.path.join(user_data_dir(), 'minitch'), + 'os': 'linux' + }, + 'games': {} +} + +def read_config(): + global config + path = os.path.join(user_config_dir(), 'minitchrc') + try: + with open(path, 'r') as f: + c = {**config, **yaml.safe_load(f)} + mergedeep.merge(config, c) + + except FileNotFoundError: + pass + except yaml.YAMLError: + raise SystemExit('Invalid config') + +def write_config(): + global config + path = os.path.join(user_config_dir(), 'minitchrc') + with open(path, 'w') as f: + yaml.dump(config, f, default_flow_style=False) + +def parse_config(args): + if args.api_key: + config['global']['api_key'] = args.api_key + +def parse_search(args): + global config + if not 'api_key' in config['global']: + raise SystemExit('You have to set your api key first.') + game_handler = handlers.GameHandler(config['global']['api_key'], output_format=args.output) + game_handler.find(args.query) + +def parse_uploads(args): + global config + if not 'api_key' in config['global']: + raise SystemExit('You have to set your api key first.') + game_handler = handlers.GameHandler(config['global']['api_key'], output_format=args.output) + game_handler.uploads(args.game_id) + +def install_game(game_id, dst_path, output_format): + global config + + # Download game + game_handler = handlers.GameHandler(config['global']['api_key'], output_format=output_format) + dst, upload, meta = game_handler.download(game_id, dst_path, os_type=config['global']['os']) + + # Unpack game + print('Extracting...') + unpack_archive(dst, dst_path) + + return dst, upload, meta + +def parse_install(args): + global config + + # Create target directory + game_path = os.path.join(config['global']['game_dir'], args.game_id) + os.makedirs(game_path, exist_ok=True) + + dst, upload, meta = install_game(args.game_id, game_path, args.output) + + # Write game to config + config['games'][args.game_id] = { + 'path': game_path, + 'last_update': upload['updated_at'], + 'url': meta['url'], + 'title': meta['title'], + 'description': meta['short_text'] + } + + print('FYI: As itch.io does not have any information on how to execute the game, you have to set the executable manually on first run with the --executable option') + +def parser_run(args): + global config + if not args.game_id in config['games']: + raise SystemExit('Game is not installed.') + if args.executable: + config['games'][args.game_id]['executable'] = args.executable + if not 'executable' in config['games'][args.game_id]: + raise SystemExit('You have to define the executable before you can run the game.') + + if args.update: + game_handler = handlers.GameHandler(config['global']['api_key'], output_format=args.output) + print('Checking for updates...') + if game_handler.check_update(args.game_id, config['games'][args.game_id]['last_update'], config['global']['os']): + dst, upload, meta = install_game(args.game_id, config['games'][args.game_id]['path'], args.output) + config['games'][args.game_id]['last_update'] = upload['updated_at'] + + + # Run executable + os.chdir(config['games'][args.game_id]['path']) + os.chmod(config['games'][args.game_id]['executable'], 755) + subprocess.run(config['games'][args.game_id]['executable']) + +def parser_list(args): + global config + game_handler = handlers.GameHandler(config['global']['api_key'], output_format=args.output) + game_handler.list_local(config['games']) + +def main(): + read_config() + + parser = argparse.ArgumentParser(description='Tiny little itch.io client') + subparsers = parser.add_subparsers(title='subcommands', description='valid subcommands', help='additional help') + + # Global arguments + parser.add_argument('--output', '-o', choices=('table', 'json'), default='table', help='set the output format') + + # Parser for "config" command + parser_config = subparsers.add_parser('config', aliases=['c'], help='config help') + parser_config.add_argument('--api_key', type=str, help='set api key') + parser_config.set_defaults(func=parse_config) + + # Parser for "search" command + parser_search = subparsers.add_parser('search', aliases=['s'], help='search help') + parser_search.add_argument('query', type=str, help='the search query') + parser_search.set_defaults(func=parse_search) + + # Parser for "uploads" command + parser_search = subparsers.add_parser('uploads', help='uploads help') + parser_search.add_argument('game_id', type=str, help='game_id to query') + parser_search.set_defaults(func=parse_uploads) + + # Parser for "install" command + parser_search = subparsers.add_parser('install', aliases=['i'], help='install help') + parser_search.add_argument('game_id', type=str, help='game_id to query') + parser_search.set_defaults(func=parse_install) + + # Parser for "run" command + parser_search = subparsers.add_parser('run', aliases=['r'], help='run help') + parser_search.add_argument('game_id', type=str, help='game_id to run') + parser_search.add_argument('--executable', '-e', type=str, help='set executable relative to game path (e.g. -e "chess_2_linux_v2/chess 2.x86_64")') + parser_search.add_argument('--update', '-u', help='check for update before running', action='store_true') + parser_search.set_defaults(func=parser_run) + + # Parser for "list" command + parser_search = subparsers.add_parser('list', aliases=['l'], help='list help') + parser_search.set_defaults(func=parser_list) + + args = parser.parse_args() + args.func(args) + + write_config() + +if __name__ == "__main__": + main() \ No newline at end of file