diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2c3c26c..ae697bd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,7 +18,7 @@ runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Poetry run: pipx install poetry - id: python @@ -42,7 +42,7 @@ runs-on: ${{ matrix.os }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install GNU Make if: runner.os == 'macOS' # https://formulae.brew.sh/formula/make @@ -114,7 +114,7 @@ runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Poetry run: pipx install poetry - name: Setup Python diff --git a/.github/workflows/license.yaml b/.github/workflows/license.yaml index 8e96037..5c86e4b 100644 --- a/.github/workflows/license.yaml +++ b/.github/workflows/license.yaml @@ -2,7 +2,7 @@ on: schedule: - - cron: 0 3 1 1 * # 3:00 AM on January 1st + - cron: 0 3 1 1 * permissions: contents: write diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b050110..e8153b0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ hooks: - id: prettier - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-added-large-files - id: check-ast @@ -69,7 +69,7 @@ args: - --profile=black - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.26.3 + rev: 0.27.0 hooks: - id: check-azure-pipelines - id: check-bamboo-spec diff --git a/Makefile b/Makefile index fab7508..c951ea4 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,17 @@ NAME := tld -BUILD := $(CURDIR)/build -DIST := $(CURDIR)/dist +BUILD := build +DIST := dist SYSTEM != python -c 'import platform; print(platform.system().lower())' MACHINE != python -c 'import platform; print(platform.machine().lower())' + ifeq ($(SYSTEM), windows) EXE := .exe else EXE := endif + DIST_TARGET := $(DIST)/$(NAME)-$(SYSTEM)-$(MACHINE)$(EXE) all: @@ -17,18 +19,13 @@ include make/*.mk clean: demo-clean - @ find $(CURDIR) -type d -name '__pycache__' -exec $(RM) --recursive --verbose '{}' + - @ find $(CURDIR) -type f -name '*.spec' -exec $(RM) --verbose '{}' + - $(RM) --recursive $(BUILD) - $(RM) --recursive $(DIST) - -docs: $(CURDIR)/main.py - typer $< utils docs --name=$(NAME) + @ $(RM) --recursive --verbose $(BUILD) + @ $(RM) --recursive --verbose $(DIST) + @ find . -type d -name '__pycache__' -exec $(RM) --recursive --verbose '{}' + + @ find . -type f -name '*.spec' -exec $(RM) --verbose '{}' + dist: $(DIST_TARGET) -pretty: black prettier - setup: poetry install conda install --yes libpython-static @@ -37,16 +34,10 @@ # Auxiliary Targets # ##################### -$(DIST_TARGET): $(CURDIR)/main.py +$(DIST_TARGET): main.py + @ mkdir -p -v $(@D) ifneq ($(SYSTEM), windows) python -m nuitka --standalone --onefile --output-filename=$(@F) --output-dir=$(@D) --remove-output $< else pyinstaller --distpath=$(DIST) --workpath=$(BUILD) --onefile --name=$(NAME)-$(SYSTEM)-$(MACHINE) $< endif - -black: - isort --profile black $(CURDIR) - black $(CURDIR) - -prettier: $(CURDIR)/.gitignore - prettier --write --ignore-path=$< $(CURDIR) diff --git a/README.md b/README.md index 2c109f6..089c209 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,18 @@ **Options**: -- `-u, --username TEXT`: [default: liqin20] -- `-p, --password TEXT`: [required] -- `-s, --semester TEXT`: [default: 2022-2023-2] +- `-u, --username TEXT` +- `-p, --password TEXT` +- `--prefix DIRECTORY`: [default: $HOME/thu-learn] +- `-s, --semester TEXT`: [default: 2023-2024-1] - `-c, --course TEXT` -- `--prefix PATH`: [default: /home/liblaf/Desktop/thu-learn] -- `-s, --size-limit INTEGER`: [default: 9223372036854775807] +- `--document / --no-document`: [default: document] +- `--homework / --no-homework`: [default: homework] +- `-j, --jobs INTEGER`: [default: 8] +- `-l, --language [en|zh]`: [default: en] +- `--log-level [NOTSET|DEBUG|INFO|WARNING|ERROR|CRITICAL]`: [env var: LOG_LEVEL] [default: INFO] +- `--install-completion`: Install completion for the current shell. +- `--show-completion`: Show completion for the current shell, to copy it or customize the installation. - `--help`: Show this message and exit. ## Features diff --git a/demo.tape b/demo.tape index 54d36fa..a93f51b 100644 --- a/demo.tape +++ b/demo.tape @@ -1,11 +1,9 @@ -Set Width 1920 -Set Height 1440 -Set FontSize 16 +Set Width 3840 +Set Height 2440 +Set FontSize 32 -Require bw Require tld -Type 'tld --password "$(bw get password id.tsinghua.edu.cn)"' -Sleep 500ms +Type "OPENSSL_CONF=openssl.cnf tld" Enter Sleep 5s diff --git a/main.py b/main.py index 6739686..4ebb786 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,4 @@ -from thu_learn_downloader.__main__ import app +from thu_learn_downloader.main import app if __name__ == "__main__": app() diff --git a/make/demo.mk b/make/demo.mk index e472434..c6d2e4d 100644 --- a/make/demo.mk +++ b/make/demo.mk @@ -1,26 +1,24 @@ -ASSETS := $(CURDIR)/assets -FRAMES := $(CURDIR)/frames -TEMPLATE := $(CURDIR)/template +ASSETS := assets +FRAMES := frames +TEMPLATE := template demo: $(ASSETS)/demo.png $(TEMPLATE)/scripts/deploy-gh-pages.sh - bash $(TEMPLATE)/scripts/deploy-gh-pages.sh $(dir $<) assets + bash $(TEMPLATE)/scripts/deploy-gh-pages.sh $(=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "certifi" version = "2023.7.22" @@ -84,166 +137,184 @@ {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "cffi" -version = "1.15.1" +version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {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"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, ] [package.dependencies] pycparser = "*" +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "charset-normalizer" -version = "3.2.0" +version = "3.3.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, + {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"}, + {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "click" version = "8.1.7" @@ -258,6 +329,11 @@ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "colorama" version = "0.4.6" @@ -269,6 +345,52 @@ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + +[[package]] +name = "executing" +version = "2.0.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = "*" +files = [ + {file = "executing-2.0.0-py2.py3-none-any.whl", hash = "sha256:06df6183df67389625f4e763921c6cf978944721abf3e714000200aab95b0657"}, + {file = "executing-2.0.0.tar.gz", hash = "sha256:0ff053696fdeef426cda5bd18eacd94f82c91f49823a2e9090124212ceea9b08"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + +[[package]] +name = "icecream" +version = "2.1.3" +description = "Never use print() to debug again; inspect variables, expressions, and program execution with a single, simple function call." +optional = false +python-versions = "*" +files = [ + {file = "icecream-2.1.3-py2.py3-none-any.whl", hash = "sha256:757aec31ad4488b949bc4f499d18e6e5973c40cc4d4fc607229e78cfaec94c34"}, + {file = "icecream-2.1.3.tar.gz", hash = "sha256:0aa4a7c3374ec36153a1d08f81e3080e83d8ac1eefd97d2f4fe9544e8f9b49de"}, +] + +[package.dependencies] +asttokens = ">=2.0.1" +colorama = ">=0.3.9" +executing = ">=0.3.1" +pygments = ">=2.2.0" + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "idna" version = "3.4" @@ -280,6 +402,11 @@ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "isort" version = "5.12.0" @@ -297,6 +424,11 @@ plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "macholib" version = "1.16.3" @@ -311,6 +443,11 @@ [package.dependencies] altgraph = ">=0.17" +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -335,6 +472,11 @@ rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "mdurl" version = "0.1.2" @@ -346,6 +488,11 @@ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -357,20 +504,30 @@ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "nuitka" -version = "1.8.2" +version = "1.8.4" description = "Python compiler with full language support and CPython compatibility" optional = false python-versions = "*" files = [ - {file = "Nuitka-1.8.2.tar.gz", hash = "sha256:720db0cae384d44bf713c200cf062b8e0e927b948783f92e5c667d662879c92c"}, + {file = "Nuitka-1.8.4.tar.gz", hash = "sha256:a0a3896a5260226918081f017c2c71da34d9959cd2c9a2631f3c54e821adb99b"}, ] [package.dependencies] ordered-set = ">=4.1.0" zstandard = ">=0.15" +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "ordered-set" version = "4.1.0" @@ -385,17 +542,27 @@ [package.extras] dev = ["black", "mypy", "pytest"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "packaging" -version = "23.1" +version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "pathspec" version = "0.11.2" @@ -407,6 +574,11 @@ {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "pefile" version = "2023.2.7" @@ -418,21 +590,31 @@ {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "platformdirs" -version = "3.10.0" +version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, ] [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "pycparser" version = "2.21" @@ -444,6 +626,158 @@ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + +[[package]] +name = "pydantic" +version = "2.4.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"}, + {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.10.1" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + +[[package]] +name = "pydantic-core" +version = "2.10.1" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"}, + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"}, + {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"}, + {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"}, + {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"}, + {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"}, + {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"}, + {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"}, + {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"}, + {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f"}, + {file = "pydantic_core-2.10.1-cp37-none-win32.whl", hash = "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c"}, + {file = "pydantic_core-2.10.1-cp37-none-win_amd64.whl", hash = "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"}, + {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"}, + {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"}, + {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"}, + {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"}, + {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "pygments" version = "2.16.1" @@ -458,50 +792,84 @@ [package.extras] plugins = ["importlib-metadata"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "pyinstaller" -version = "5.13.2" +version = "6.0.0" description = "PyInstaller bundles a Python application and all its dependencies into a single package." optional = false -python-versions = "<3.13,>=3.7" +python-versions = "<3.13,>=3.8" files = [ - {file = "pyinstaller-5.13.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:16cbd66b59a37f4ee59373a003608d15df180a0d9eb1a29ff3bfbfae64b23d0f"}, - {file = "pyinstaller-5.13.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8f6dd0e797ae7efdd79226f78f35eb6a4981db16c13325e962a83395c0ec7420"}, - {file = "pyinstaller-5.13.2-py3-none-manylinux2014_i686.whl", hash = "sha256:65133ed89467edb2862036b35d7c5ebd381670412e1e4361215e289c786dd4e6"}, - {file = "pyinstaller-5.13.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:7d51734423685ab2a4324ab2981d9781b203dcae42839161a9ee98bfeaabdade"}, - {file = "pyinstaller-5.13.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:2c2fe9c52cb4577a3ac39626b84cf16cf30c2792f785502661286184f162ae0d"}, - {file = "pyinstaller-5.13.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c63ef6133eefe36c4b2f4daf4cfea3d6412ece2ca218f77aaf967e52a95ac9b8"}, - {file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:aadafb6f213549a5906829bb252e586e2cf72a7fbdb5731810695e6516f0ab30"}, - {file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b2e1c7f5cceb5e9800927ddd51acf9cc78fbaa9e79e822c48b0ee52d9ce3c892"}, - {file = "pyinstaller-5.13.2-py3-none-win32.whl", hash = "sha256:421cd24f26144f19b66d3868b49ed673176765f92fa9f7914cd2158d25b6d17e"}, - {file = "pyinstaller-5.13.2-py3-none-win_amd64.whl", hash = "sha256:ddcc2b36052a70052479a9e5da1af067b4496f43686ca3cdda99f8367d0627e4"}, - {file = "pyinstaller-5.13.2-py3-none-win_arm64.whl", hash = "sha256:27cd64e7cc6b74c5b1066cbf47d75f940b71356166031deb9778a2579bb874c6"}, - {file = "pyinstaller-5.13.2.tar.gz", hash = "sha256:c8e5d3489c3a7cc5f8401c2d1f48a70e588f9967e391c3b06ddac1f685f8d5d2"}, + {file = "pyinstaller-6.0.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d84b06fb9002109bfc542e76860b81459a8585af0bbdabcfc5dcf272ef230de7"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:aa922d1d73881d0820a341d2c406a571cc94630bdcdc275427c844a12e6e376e"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:52e5b3a2371d7231de17515c7c78d8d4a39d70c8c095e71d55b3b83434a193a8"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4a75bde5cda259bb31f2294960d75b9d5c148001b2b0bd20a91f9c2116675a6c"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:5314f6f08d2bcbc031778618ba97d9098d106119c2e616b3b081171fe42f5415"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0ad7cc3776ca17d0bededcc352cba2b1c89eb4817bfabaf05972b9da8c424935"}, + {file = "pyinstaller-6.0.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cccdad6cfe7a5db7d7eb8df2e5678f8375268739d5933214e180da300aa54e37"}, + {file = "pyinstaller-6.0.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fb6af82989dac7c58bd25ed9ba3323bc443f8c1f03804f69c9f5e363bf4a021c"}, + {file = "pyinstaller-6.0.0-py3-none-win32.whl", hash = "sha256:68769f5e6722474bb1038e35560444659db8b951388bfe0c669bb52a640cd0eb"}, + {file = "pyinstaller-6.0.0-py3-none-win_amd64.whl", hash = "sha256:438a9e0d72a57d5bba4f112d256e39ea4033c76c65414c0693d8311faa14b090"}, + {file = "pyinstaller-6.0.0-py3-none-win_arm64.whl", hash = "sha256:16a473065291dd7879bf596fa20e65bd9d1e8aafc2cef1bffa3e42e707e2e68e"}, + {file = "pyinstaller-6.0.0.tar.gz", hash = "sha256:d702cff041f30e7a53500b630e07b081e5328d4655023319253d73935e75ade2"}, ] [package.dependencies] altgraph = "*" macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +packaging = ">=20.0" pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} pyinstaller-hooks-contrib = ">=2021.4" pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} setuptools = ">=42.0.0" [package.extras] -encryption = ["tinyaes (>=1.0.0)"] hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "pyinstaller-hooks-contrib" -version = "2023.8" +version = "2023.9" description = "Community maintained hooks for PyInstaller" optional = false python-versions = ">=3.7" files = [ - {file = "pyinstaller-hooks-contrib-2023.8.tar.gz", hash = "sha256:318ccc316fb2b8c0bbdff2456b444bf1ce0e94cb3948a0f4dd48f6fc33d41c01"}, - {file = "pyinstaller_hooks_contrib-2023.8-py2.py3-none-any.whl", hash = "sha256:d091a52fbeed71cde0359aa9ad66288521a8441cfba163d9446606c5136c72a8"}, + {file = "pyinstaller-hooks-contrib-2023.9.tar.gz", hash = "sha256:76084b5988e3957a9df169d2a935d65500136967e710ddebf57263f1a909cd80"}, + {file = "pyinstaller_hooks_contrib-2023.9-py2.py3-none-any.whl", hash = "sha256:f34f4c6807210025c8073ebe665f422a3aa2ac5f4c7ebf4c2a26cc77bebf63b5"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "pywin32-ctypes" version = "0.2.2" @@ -513,6 +881,11 @@ {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "requests" version = "2.31.0" @@ -534,15 +907,20 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "rich" -version = "13.5.3" +version = "13.6.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.5.3-py3-none-any.whl", hash = "sha256:9257b468badc3d347e146a4faa268ff229039d4c2d176ab0cffb4c4fbc73d5d9"}, - {file = "rich-13.5.3.tar.gz", hash = "sha256:87b43e0543149efa1253f485cd845bb7ee54df16c9617b8a893650ab84b4acb6"}, + {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, + {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, ] [package.dependencies] @@ -552,6 +930,11 @@ [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "setuptools" version = "68.2.2" @@ -568,6 +951,27 @@ testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "soupsieve" version = "2.5" @@ -579,6 +983,30 @@ {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + +[[package]] +name = "tenacity" +version = "8.2.3" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, + {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, +] + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "typer" version = "0.9.0" @@ -600,6 +1028,11 @@ doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "typing-extensions" version = "4.8.0" @@ -611,15 +1044,20 @@ {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "urllib3" -version = "2.0.5" +version = "2.0.6" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ - {file = "urllib3-2.0.5-py3-none-any.whl", hash = "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e"}, - {file = "urllib3-2.0.5.tar.gz", hash = "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594"}, + {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"}, + {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"}, ] [package.extras] @@ -628,6 +1066,11 @@ socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "zstandard" version = "0.21.0" @@ -686,7 +1129,12 @@ [package.extras] cffi = ["cffi (>=1.11)"] +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [metadata] lock-version = "2.0" -python-versions = ">=3.11,<3.12" -content-hash = "bc914f02f73968c7732b470fa09f6c97a1ab0984a0564263e249b0b92924e5de" +python-versions = ">=3.11,<3.13" +content-hash = "d73538c06a761d8256b7fb16a735dc799b83357b6c395b8ecdcd71680af55289" diff --git a/pyproject.toml b/pyproject.toml index 7dda976..afdbdf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,24 +10,32 @@ description = "Download everything from Web Learning of Tsinghua University" license = "MIT" name = "thu-learn-downloader" -packages = [{ include = "thu_learn_downloader" }] readme = "README.md" repository = "https://github.com/liblaf/thu-learn-downloader" version = "0.1.19" [tool.poetry.dependencies] beautifulsoup4 = "^4.12.2" -python = ">=3.11,<3.12" -requests = "^2.29.0" -rich = "^13.3.5" +pydantic = "^2.4.2" +python = ">=3.11,<3.13" +python-dateutil = "^2.8.2" +requests = "^2.31.0" +rich = "^13.6.0" +tenacity = "^8.2.3" typer = "^0.9.0" [tool.poetry.group.dev.dependencies] -black = "^23.3.0" +black = "^23.9.1" +icecream = "^2.1.3" isort = "^5.12.0" -nuitka = "^1.6.1" -pyinstaller = "^5.11.0" +nuitka = "^1.8.4" +pyinstaller = "^6.0.0" [tool.poetry.scripts] -thu-learn-downloader = "thu_learn_downloader.__main__:app" -tld = "thu_learn_downloader.__main__:app" +thu-learn-downloader = "thu_learn_downloader.main:app" +tld = "thu_learn_downloader.main:app" + +[[tool.poetry.source]] +name = "mirrors" +priority = "default" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" diff --git a/requirements.txt b/requirements.txt index a0431fa..7afed1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,21 @@ -beautifulsoup4==4.12.2 ; python_version >= "3.11" and python_version < "3.12" -certifi==2023.7.22 ; python_version >= "3.11" and python_version < "3.12" -charset-normalizer==3.2.0 ; python_version >= "3.11" and python_version < "3.12" -click==8.1.7 ; python_version >= "3.11" and python_version < "3.12" -colorama==0.4.6 ; python_version >= "3.11" and python_version < "3.12" and platform_system == "Windows" -idna==3.4 ; python_version >= "3.11" and python_version < "3.12" -markdown-it-py==3.0.0 ; python_version >= "3.11" and python_version < "3.12" -mdurl==0.1.2 ; python_version >= "3.11" and python_version < "3.12" -pygments==2.16.1 ; python_version >= "3.11" and python_version < "3.12" -requests==2.31.0 ; python_version >= "3.11" and python_version < "3.12" -rich==13.5.3 ; python_version >= "3.11" and python_version < "3.12" -soupsieve==2.5 ; python_version >= "3.11" and python_version < "3.12" -typer==0.9.0 ; python_version >= "3.11" and python_version < "3.12" -typing-extensions==4.8.0 ; python_version >= "3.11" and python_version < "3.12" -urllib3==2.0.5 ; python_version >= "3.11" and python_version < "3.12" +annotated-types==0.6.0 ; python_version >= "3.11" and python_version < "3.13" +beautifulsoup4==4.12.2 ; python_version >= "3.11" and python_version < "3.13" +certifi==2023.7.22 ; python_version >= "3.11" and python_version < "3.13" +charset-normalizer==3.3.0 ; python_version >= "3.11" and python_version < "3.13" +click==8.1.7 ; python_version >= "3.11" and python_version < "3.13" +colorama==0.4.6 ; python_version >= "3.11" and python_version < "3.13" and platform_system == "Windows" +idna==3.4 ; python_version >= "3.11" and python_version < "3.13" +markdown-it-py==3.0.0 ; python_version >= "3.11" and python_version < "3.13" +mdurl==0.1.2 ; python_version >= "3.11" and python_version < "3.13" +pydantic-core==2.10.1 ; python_version >= "3.11" and python_version < "3.13" +pydantic==2.4.2 ; python_version >= "3.11" and python_version < "3.13" +pygments==2.16.1 ; python_version >= "3.11" and python_version < "3.13" +python-dateutil==2.8.2 ; python_version >= "3.11" and python_version < "3.13" +requests==2.31.0 ; python_version >= "3.11" and python_version < "3.13" +rich==13.6.0 ; python_version >= "3.11" and python_version < "3.13" +six==1.16.0 ; python_version >= "3.11" and python_version < "3.13" +soupsieve==2.5 ; python_version >= "3.11" and python_version < "3.13" +tenacity==8.2.3 ; python_version >= "3.11" and python_version < "3.13" +typer==0.9.0 ; python_version >= "3.11" and python_version < "3.13" +typing-extensions==4.8.0 ; python_version >= "3.11" and python_version < "3.13" +urllib3==2.0.6 ; python_version >= "3.11" and python_version < "3.13" diff --git a/template b/template index ff2f337..d47bc60 160000 --- a/template +++ b/template @@ -1 +1 @@ -Subproject commit ff2f337063accbb2431bdb627844af100f804839 +Subproject commit d47bc6079899495c74e41fc8d0a0ad83956261bd diff --git a/thu_learn_downloader/__main__.py b/thu_learn_downloader/__main__.py deleted file mode 100644 index a9c32c8..0000000 --- a/thu_learn_downloader/__main__.py +++ /dev/null @@ -1,83 +0,0 @@ -import sys -from pathlib import Path -from typing import Annotated - -import typer -from rich.console import Group -from rich.live import Live -from rich.panel import Panel -from rich.progress import ( - BarColumn, - MofNCompleteColumn, - Progress, - TextColumn, - TimeElapsedColumn, -) - -from . import sync -from .config import Config -from .constants import MAX_ACTIVE_TASKS, SUCCESS_PREFIX -from .downloader import Downloader -from .helper import Helper - -app = typer.Typer(name="tld") - - -@app.command(name="tld") -def main( - *, - username: Annotated[str, typer.Option("-u", "--username")] = "liqin20", - password: Annotated[ - str, typer.Option("-p", "--password", prompt=True, hide_input=True) - ], - semester: Annotated[list[str], typer.Option("-s", "--semester")] = ["2023-2024-1"], - course: Annotated[list[str], typer.Option("-c", "--course")] = [], - prefix: Annotated[Path, typer.Option("--prefix")] = Path.home() - / "Desktop" - / "thu-learn", - size_limit: Annotated[int, typer.Option("--size-limit")] = sys.maxsize, -) -> None: - config = Config( - username=username, - password=password, - semesters=semester, - courses=course, - prefix=prefix, - size_limit=size_limit, - ) - helper = Helper() - downloader = Downloader() - overall_progress = Progress( - TextColumn("{task.description}", style="bold bright_blue"), - BarColumn(), - MofNCompleteColumn(), - TimeElapsedColumn(), - ) - semesters_task_id = overall_progress.add_task(description="Semesters") - courses_task_id = overall_progress.add_task(description="Courses") - progress_group = Group( - Panel(downloader.progress, height=MAX_ACTIVE_TASKS + 2), - Panel(overall_progress), - ) - - with Live(progress_group) as live: - with downloader.pool: - helper.login(username=username, password=password) - live.console.log( - SUCCESS_PREFIX, - f"Login as {username} SUCCESS", - style="bold bright_green", - ) - sync.sync_all( - helper=helper, - downloader=downloader, - config=config, - console=live.console, - overall_progress=overall_progress, - semesters_task_id=semesters_task_id, - courses_task_id=courses_task_id, - ) - - -if __name__ == "__main__": - app() diff --git a/thu_learn_downloader/client/__init__.py b/thu_learn_downloader/client/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/thu_learn_downloader/client/__init__.py diff --git a/thu_learn_downloader/client/client.py b/thu_learn_downloader/client/client.py new file mode 100644 index 0000000..630f460 --- /dev/null +++ b/thu_learn_downloader/client/client.py @@ -0,0 +1,26 @@ +from enum import StrEnum +from typing import Any + +from requests import Response, Session + +MAX_SIZE: int = 200 + + +class Language(StrEnum): + ENGLISH = "en" + CHINESE = "zh" + + +class Client(Session): + language: Language + + def __init__(self, language: Language = Language.ENGLISH, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.language = language + + def get_with_token(self, url: str, params: dict[str, Any] = {}) -> Response: + return self.get(url=url, params={**params, "_csrf": self.token}) + + @property + def token(self) -> None: + return self.cookies["XSRF-TOKEN"] diff --git a/thu_learn_downloader/client/course.py b/thu_learn_downloader/client/course.py new file mode 100644 index 0000000..d9c2d78 --- /dev/null +++ b/thu_learn_downloader/client/course.py @@ -0,0 +1,77 @@ +from collections.abc import Sequence +from typing import Any + +from pydantic import Field +from requests import Response + +from . import url +from .client import MAX_SIZE, Client +from .document import Document, DocumentClass +from .homework import Homework +from .model import BaseModel + + +class Course(BaseModel): + client: Client = Field(exclude=True) + id: str = Field(alias="wlkcid") + chinese_name: str = Field(alias="kcm") + course_number: str = Field(alias="kch") + english_name: str = Field(alias="ywkcm") + name: str = Field(alias="zywkcm") + teacher_name: str = Field(alias="jsm") + teacher_number: str = Field(alias="jsh") + + @property + def document_classes(self) -> Sequence[DocumentClass]: + return [ + DocumentClass(client=self.client, **result) + for result in self.client.get_with_token( + url=url.make_url(path="/b/wlxt/kj/wlkc_kjflb/student/pageList"), + params={"wlkcid": self.id}, + ).json()["object"]["rows"] + ] + + @property + def documents(self) -> Sequence[Document]: + documents: Sequence[Document] = [ + Document(client=self.client, **result) + for result in self.client.get_with_token( + url=url.make_url( + path="/b/wlxt/kj/wlkc_kjxxb/student/kjxxbByWlkcidAndSizeForStudent" + ), + params={"wlkcid": self.id, "size": MAX_SIZE}, + ).json()["object"] + ] + documents.sort(key=lambda document: document.upload_time) + return documents + + @property + def homeworks(self) -> Sequence[Homework]: + return [ + *self._homeworks_at_url( + url=url.make_url( + path="/b/wlxt/kczy/zy/student/index/zyListWj", + query={"wlkcid": self.id, "size": MAX_SIZE}, + ), + ), + *self._homeworks_at_url( + url=url.make_url( + path="/b/wlxt/kczy/zy/student/index/zyListYjwg", + query={"wlkcid": self.id, "size": MAX_SIZE}, + ), + ), + *self._homeworks_at_url( + url=url.make_url( + path="/b/wlxt/kczy/zy/student/index/zyListYpg", + query={"wlkcid": self.id, "size": MAX_SIZE}, + ), + ), + ] + + def _homeworks_at_url(self, url: str) -> Sequence[Homework]: + resp: Response = self.client.get_with_token(url=url) + json: dict[str, Any] = resp.json() + results: Sequence[dict[str, Any]] = json["object"]["aaData"] or [] + return [ + Homework.from_json(client=self.client, json=result) for result in results + ] diff --git a/thu_learn_downloader/client/document.py b/thu_learn_downloader/client/document.py new file mode 100644 index 0000000..c811ca6 --- /dev/null +++ b/thu_learn_downloader/client/document.py @@ -0,0 +1,30 @@ +from datetime import datetime + +from pydantic import Field + +from . import url +from .client import Client +from .model import BaseModel + + +class DocumentClass(BaseModel): + client: Client = Field(exclude=True) + id: str = Field(alias="kjflid") + title: str = Field(alias="bt") + + +class Document(BaseModel): + client: Client = Field(exclude=True) + id: str = Field(alias="wjid") + class_id: str = Field(alias="kjflid") + file_type: str = Field(alias="wjlx") + size: int = Field(alias="wjdx") + title: str = Field(alias="bt") + upload_time: datetime = Field(alias="scsj") + + @property + def download_url(self) -> str: + return url.make_url( + path="/b/wlxt/kj/wlkc_kjxxb/student/downloadFile", + query={"sfgk": 0, "wjid": self.id}, + ) diff --git a/thu_learn_downloader/client/homework.py b/thu_learn_downloader/client/homework.py new file mode 100644 index 0000000..955a3aa --- /dev/null +++ b/thu_learn_downloader/client/homework.py @@ -0,0 +1,189 @@ +import html +import shutil +import subprocess +import urllib.parse +from collections.abc import Sequence +from datetime import datetime +from enum import StrEnum +from subprocess import CompletedProcess +from typing import Any, Optional, Self +from urllib.parse import SplitResult + +from bs4 import BeautifulSoup, Tag +from pydantic import Field +from requests import Response + +from thu_learn_downloader.common.typing import cast + +from . import url +from .client import Client +from .model import BaseModel + +MARKDOWN_TEMPLATE: str = """## {title} + +### Contents and Requirements + +- Starts: {start_time} +- Deadline: {deadline} + +#### Description + +{description} + +#### ANS + +{answer_content} + +### My coursework submitted + +- Date: {submit_time} + +#### Content + +{submitted_content} + +### Instructors' comments + +- By: {grader_name} +- Date: {grade_time} +- Grade: {grade} + +#### Comment + +{grade_content} +""" + + +class AttachmentType(StrEnum): + ANSWER = "answer" + ATTACHMENT = "attach" + COMMENT = "comment" + SUBMITTED = "submit" + + +class Attachment(BaseModel): + id: str + download_url: str + name: str + type_: AttachmentType + + +class Homework(BaseModel): + client: Client = Field(exclude=True) + id: str = Field(alias="zyid") + answer_attachment: Optional[Attachment] + answer_content: str + attachment: Optional[Attachment] + deadline: datetime = Field(alias="jzsj") + description: str + grade_attachment: Optional[Attachment] + grade_content: str = Field("", alias="pynr") + grade_time: Optional[datetime] = Field(alias="pysj") + grade: Optional[int | str] = Field(alias="cj") + grader_name: str = Field("", alias="jsm") + number: int = Field(alias="wz") + start_time: datetime = Field(alias="kssj") + submit_time: Optional[datetime] = Field(alias="scsj") + submitted_attachment: Optional[Attachment] + submitted_content: str + title: str = Field(alias="bt") + + @staticmethod + def parse_homework_file( + file_div: Tag, type_: AttachmentType + ) -> Optional[Attachment]: + fl: Optional[Tag] = file_div.select_one(".txt.fl") + if fl is None: + return None + file_node: Optional[Tag] = fl.select_one("a") + if file_node is None: + return None + + href: str = cast(str, file_node["href"]) + assert isinstance(href, str) + split_result: SplitResult = urllib.parse.urlsplit(href) + query: dict[str, list[str]] = urllib.parse.parse_qs(split_result.query) + return Attachment( + id=query["fileId"][0], + download_url=url.make_url(path=query["downloadUrl"][0]), + name=file_node.get_text(), + type_=type_, + ) + + @classmethod + def from_json(cls, client: Client, json: dict[str, Any]) -> Self: + response: Response = client.get_with_token( + url=url.make_url( + path="/f/wlxt/kczy/zy/student/viewCj", + query={ + "wlkcid": json["wlkcid"], + "xszyid": json["xszyid"], + "zyid": json["zyid"], + }, + ) + ) + soup: BeautifulSoup = BeautifulSoup(response.text, features="html.parser") + c55s: list[Tag] = soup.select( + "div.list.calendar.clearfix > div.fl.right > div.c55" + ) + file_divs: list[Tag] = soup.select("div.list.fujian.clearfix") + boxbox: Tag = soup.select("div.boxbox")[1] + right: Tag = boxbox.select("div.right")[2] + description: str = html.unescape(c55s[0].get_text().strip()) + answer_content: str = html.unescape(c55s[1].get_text().strip()) + submitted_content: str = html.unescape(right.get_text().strip()) + json["bt"] = html.unescape(json["bt"]).strip() + return cls( + **json, + client=client, + description=description, + attachment=cls.parse_homework_file( + file_div=file_divs[0], type_=AttachmentType.ATTACHMENT + ), + answer_content=answer_content, + answer_attachment=cls.parse_homework_file( + file_div=file_divs[1], type_=AttachmentType.ANSWER + ), + submitted_content=submitted_content, + submitted_attachment=cls.parse_homework_file( + file_div=file_divs[2], type_=AttachmentType.SUBMITTED + ), + grade_attachment=cls.parse_homework_file( + file_div=file_divs[3], type_=AttachmentType.COMMENT + ), + ) + + @property + def attachments(self) -> Sequence[Attachment]: + attachments: Sequence[Attachment] = [] + if self.attachment: + attachments.append(self.attachment) + if self.submitted_attachment: + attachments.append(self.submitted_attachment) + if self.grade_attachment: + attachments.append(self.grade_attachment) + if self.answer_attachment: + attachments.append(self.answer_attachment) + return attachments + + @property + def markdown(self) -> str: + def format_value(value: Any) -> str: + if not value: + return "" + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %H:%M:%S") + return str(value) + + result: str = MARKDOWN_TEMPLATE.format_map( + {key: format_value(value) for key, value in self.model_dump().items()} + ) + if shutil.which("prettier"): + process: CompletedProcess = subprocess.run( + args=["prettier", "--parser=markdown"], + capture_output=True, + input=result, + text=True, + ) + result = process.stdout + return result diff --git a/thu_learn_downloader/client/learn.py b/thu_learn_downloader/client/learn.py new file mode 100644 index 0000000..a49e731 --- /dev/null +++ b/thu_learn_downloader/client/learn.py @@ -0,0 +1,55 @@ +import functools +import urllib.parse +from collections.abc import Sequence +from urllib.parse import ParseResult + +from bs4 import BeautifulSoup, Tag +from requests import Response + +from thu_learn_downloader.common.typing import cast + +from . import url +from .client import Client, Language +from .semester import Semester + + +class Learn: + client: Client + + def __init__(self, language: Language = Language.ENGLISH, *args, **kwargs) -> None: + self.client = Client(language=language, *args, **kwargs) + + def login(self, username: str, password: str) -> None: + response: Response = self.client.get(url=url.make_url()) + soup: BeautifulSoup = BeautifulSoup( + markup=response.text, features="html.parser" + ) + login_form: Tag = cast(Tag, soup.select_one(selector="#loginForm")) + action: str = cast(str, login_form["action"]) + response: Response = self.client.post( + url=action, data={"i_user": username, "i_pass": password, "atOnce": True} + ) + soup: BeautifulSoup = BeautifulSoup( + markup=response.text, features="html.parser" + ) + a: Tag = cast(Tag, soup.select_one(selector="a")) + href: str = cast(str, a["href"]) + parse_result: ParseResult = urllib.parse.urlparse(url=href) + query: dict[str, list[str]] = urllib.parse.parse_qs(qs=parse_result.query) + status, ticket = query["status"][0], query["ticket"][0] + self.client.get(url=href) + self.client.get( + url=url.make_url(path="/b/j_spring_security_thauth_roaming_entry"), + params={"ticket": ticket}, + ) + self.client.get(url=url.make_url(path="/f/wlxt/index/course/student/")) + assert status == "SUCCESS" + + @functools.cached_property + def semesters(self) -> Sequence[Semester]: + return [ + Semester(client=self.client, id=result) + for result in self.client.get_with_token( + url=url.make_url(path="/b/wlxt/kc/v_wlkc_xs_xktjb_coassb/queryxnxq") + ).json() + ] diff --git a/thu_learn_downloader/client/model.py b/thu_learn_downloader/client/model.py new file mode 100644 index 0000000..644ec74 --- /dev/null +++ b/thu_learn_downloader/client/model.py @@ -0,0 +1,6 @@ +import pydantic +from pydantic import ConfigDict + + +class BaseModel(pydantic.BaseModel): + model_config: ConfigDict = ConfigDict(arbitrary_types_allowed=True) diff --git a/thu_learn_downloader/client/semester.py b/thu_learn_downloader/client/semester.py new file mode 100644 index 0000000..cc8ee96 --- /dev/null +++ b/thu_learn_downloader/client/semester.py @@ -0,0 +1,24 @@ +from collections.abc import Sequence + +from pydantic import Field + +from . import url +from .client import Client +from .course import Course +from .model import BaseModel + + +class Semester(BaseModel): + client: Client = Field(exclude=True) + id: str + + @property + def courses(self) -> Sequence[Course]: + return [ + Course(client=self.client, **result) + for result in self.client.get_with_token( + url=url.make_url( + path=f"/b/wlxt/kc/v_wlkc_xs_xkb_kcb_extend/student/loadCourseBySemesterId/{self.id}/{self.client.language}" + ) + ).json()["resultList"] + ] diff --git a/thu_learn_downloader/client/url.py b/thu_learn_downloader/client/url.py new file mode 100644 index 0000000..38c831d --- /dev/null +++ b/thu_learn_downloader/client/url.py @@ -0,0 +1,26 @@ +import urllib.parse +from urllib.parse import SplitResult + +SCHEME: str = "https" +NETLOC: str = "learn.tsinghua.edu.cn" + + +def make_url( + scheme: str = SCHEME, + netloc: str = NETLOC, + path: str = "", + query: dict = {}, + fragment="", +) -> str: + return urllib.parse.urlunsplit( + SplitResult( + scheme=scheme, + netloc=netloc, + path=path, + query=urllib.parse.urlencode(query), + fragment=fragment, + ) + ) + + +LEARN_PREFIX: str = make_url() diff --git a/thu_learn_downloader/common/__init__.py b/thu_learn_downloader/common/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/thu_learn_downloader/common/__init__.py diff --git a/thu_learn_downloader/common/logging.py b/thu_learn_downloader/common/logging.py new file mode 100644 index 0000000..94edb57 --- /dev/null +++ b/thu_learn_downloader/common/logging.py @@ -0,0 +1,17 @@ +import logging +from enum import StrEnum + +from rich.logging import RichHandler + +logging.basicConfig( + format="%(message)s", datefmt="[%X]", level=logging.NOTSET, handlers=[RichHandler()] +) + + +class LogLevel(StrEnum): + NOTSET = "NOTSET" + DEBUG = "DEBUG" + INFO = "INFO" + WARNING = "WARNING" + ERROR = "ERROR" + CRITICAL = "CRITICAL" diff --git a/thu_learn_downloader/common/typing.py b/thu_learn_downloader/common/typing.py new file mode 100644 index 0000000..0dcca6a --- /dev/null +++ b/thu_learn_downloader/common/typing.py @@ -0,0 +1,3 @@ +def cast(typ, val): + assert isinstance(val, typ) + return val diff --git a/thu_learn_downloader/config.py b/thu_learn_downloader/config.py deleted file mode 100644 index f72e7ca..0000000 --- a/thu_learn_downloader/config.py +++ /dev/null @@ -1,15 +0,0 @@ -import dataclasses -import sys -from pathlib import Path - - -@dataclasses.dataclass(kw_only=True) -class Config: - username: str = "liqin20" - password: str - - semesters: list[str] = dataclasses.field(default_factory=lambda: ["2022-2023-2"]) - courses: list[str] = dataclasses.field(default_factory=list) - - prefix: Path = Path.home() / "Desktop" / "thu-learn" - size_limit: int = sys.maxsize diff --git a/thu_learn_downloader/constants.py b/thu_learn_downloader/constants.py deleted file mode 100644 index fd34c99..0000000 --- a/thu_learn_downloader/constants.py +++ /dev/null @@ -1,57 +0,0 @@ -from rich.style import Style - -from . import typing as t - -BS_FEATURES = "html.parser" -CHUNK_SIZE = 1024 * 1024 -MAX_ACTIVE_TASKS = 16 - - -FAILURE_PREFIX = "[reverse] FAILURE [/]" -RETRY_PREFIX = "[reverse] RETRY [/]" -SKIPPED_PREFIX = "[reverse] SKIPPED [/]" -SUCCESS_PREFIX = "[reverse] SUCCESS [/]" -HOMEWORK_README = """## {title} - -### Contents and Requirements - -- Starts : {starts} -- Deadline : {deadline} - -#### Description - -{description} - -#### ANS - -{ans} - -### My coursework submitted - -- Date : {submit_time} - -#### Content - -{submit_content} - -### Instructors' comments - -- By : {grader_name} -- Date : {grade_time} -- Grade : {grade} - -#### Comment - -{comment} -""" - - -SEASONS: dict[int, t.SemesterSeason] = { - 1: t.SemesterSeason.FALL, - 2: t.SemesterSeason.SPRING, - 3: t.SemesterSeason.SUMMER, -} - - -DOCUMENT_STYLE = Style(color="bright_magenta") -HOMEWORK_STYLE = Style(color="bright_cyan") diff --git a/thu_learn_downloader/download/__init__.py b/thu_learn_downloader/download/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/thu_learn_downloader/download/__init__.py diff --git a/thu_learn_downloader/download/description.py b/thu_learn_downloader/download/description.py new file mode 100644 index 0000000..7652dbd --- /dev/null +++ b/thu_learn_downloader/download/description.py @@ -0,0 +1,27 @@ +from pathlib import Path + +from thu_learn_downloader.client.course import Course +from thu_learn_downloader.client.document import Document, DocumentClass +from thu_learn_downloader.client.homework import Attachment, Homework +from thu_learn_downloader.client.semester import Semester + + +def document( + semester: Semester, + course: Course, + document_class: DocumentClass, + document: Document, + index: int, +) -> str: + filename: str = f"{index:02d}-{document.title}" + if document.file_type: + filename += "." + document.file_type + return f"{course.name} > {filename}" + + +def attachment( + semester: Semester, course: Course, homework: Homework, attachment: Attachment +) -> str: + return ( + f"{course.name} > {homework.number:02d}-{homework.title} > {attachment.type_}" + ) diff --git a/thu_learn_downloader/download/downloader.py b/thu_learn_downloader/download/downloader.py new file mode 100644 index 0000000..a51d7a9 --- /dev/null +++ b/thu_learn_downloader/download/downloader.py @@ -0,0 +1,320 @@ +import logging +import os +from collections.abc import Sequence +from concurrent.futures import Executor, ThreadPoolExecutor +from datetime import datetime +from pathlib import Path +from typing import Optional, Self + +import dateutil.parser +import tenacity +from requests import Response +from rich.console import Group +from rich.live import Live +from rich.panel import Panel +from rich.progress import ( + BarColumn, + DownloadColumn, + MofNCompleteColumn, + Progress, + TaskID, + TaskProgressColumn, + TextColumn, + TimeElapsedColumn, + TimeRemainingColumn, + TransferSpeedColumn, +) +from rich.style import Style, StyleType + +from thu_learn_downloader.client.client import Client +from thu_learn_downloader.client.course import Course +from thu_learn_downloader.client.document import Document, DocumentClass +from thu_learn_downloader.client.homework import Attachment, Homework +from thu_learn_downloader.client.semester import Semester + +from . import description, filename, style +from .selector import Selector + + +class Downloader: + prefix: Path + selector: Selector + + executor: Executor + + live: Live + semesters_task_id: TaskID + courses_task_id: TaskID + documents_task_id: TaskID + + def __init__( + self, + prefix: Path = Path.home() / "thu-learn", + selector: Selector = Selector(), + jobs: int = 8, + ) -> None: + self.prefix = prefix + self.selector = selector + self.executor = ThreadPoolExecutor(max_workers=jobs) + + self.progress_prepare = Progress( + TextColumn("{task.description}", style="bold bright_blue"), + BarColumn(), + MofNCompleteColumn(), + TimeElapsedColumn(), + ) + self.progress_download = Progress( + TextColumn("{task.description}", style="bold"), + BarColumn(), + DownloadColumn(), + TaskProgressColumn(), + TimeElapsedColumn(), + TimeRemainingColumn(), + TransferSpeedColumn(), + ) + self.semesters_task_id = self.progress_prepare.add_task(description="Semesters") + self.courses_task_id = self.progress_prepare.add_task(description="Courses") + self.documents_task_id = self.progress_prepare.add_task(description="Documents") + self.live = Live( + Group( + Panel(self.progress_download, height=jobs + 2), + Panel(self.progress_prepare), + ) + ) + + def __enter__(self) -> Self: + self.live.__enter__() + self.executor.__enter__() + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.executor.__exit__(exc_type, exc_val, exc_tb) + self.live.__exit__(exc_type, exc_val, exc_tb) + + @tenacity.retry( + stop=tenacity.stop_after_attempt(max_attempt_number=4), + before_sleep=tenacity.before_sleep_log(logging.getLogger(), logging.DEBUG), + ) + def download( + self, + client: Client, + url: str, + output: Path, + *, + task_id: TaskID, + ) -> None: + self.progress_download.update(task_id=task_id, visible=True) + response: Response = client.get(url=url, stream=True) + os.makedirs(name=output.parent, exist_ok=True) + with output.open(mode="wb") as file: + for chunk in response.iter_content(chunk_size=8192): + bytes_written: int = file.write(chunk) + self.progress_download.advance(task_id=task_id, advance=bytes_written) + self.progress_download.update(task_id=task_id, visible=False) + + def do_sync_file( + self, + client: Client, + url: str, + output: Path, + description: str, + *, + remote_size: Optional[int] = None, + remote_time: Optional[datetime] = None, + style: StyleType = Style(), + ) -> None: + if remote_size is None or remote_time is None: + response: Response = client.get(url=url, stream=True) + if remote_size is None: + try: + remote_size = int(response.headers["Content-Length"]) + except: + remote_size = None + if remote_time is None: + try: + remote_time = dateutil.parser.parse(response.headers["Date"]) + except: + remote_time = None + if remote_size is not None: + if output.exists() and output.stat().st_size == remote_size: + if not isinstance(style, Style): + style = Style.parse(style) + self.live.console.log( + "[reverse] SKIPPED [/]", description, style=style + Style(dim=True) + ) + return + task_id: TaskID = self.progress_download.add_task( + description=description, total=remote_size + ) + self.download(client=client, url=url, output=output, task_id=task_id) + if remote_time: + os.utime( + path=output, times=(remote_time.timestamp(), remote_time.timestamp()) + ) + self.live.console.log("[reverse] SUCCESS [/]", description, style=style) + + def sync_file( + self, + client: Client, + url: str, + output: Path, + description: str, + *, + remote_size: Optional[int] = None, + remote_time: Optional[datetime] = None, + style: StyleType = Style(), + ) -> None: + self.executor.submit( + self.do_sync_file, + client=client, + url=url, + output=output, + description=description, + remote_size=remote_size, + remote_time=remote_time, + style=style, + ) + + def sync_semesters(self, semesters: Sequence[Semester]) -> None: + if self.selector.semesters: + semesters = [ + semester + for semester in semesters + if semester.id in self.selector.semesters + ] + self.progress_prepare.reset(task_id=self.semesters_task_id) + for semester in self.progress_prepare.track( + sequence=semesters, + total=len(semesters), + task_id=self.semesters_task_id, + description="Semesters", + ): + self.sync_semester(semester=semester) + + def sync_semester(self, semester: Semester) -> None: + self.sync_courses(semester=semester, courses=semester.courses) + + def sync_courses(self, semester: Semester, courses: Sequence[Course]) -> None: + if self.selector.courses: + courses = [ + course for course in courses if course.id in self.selector.courses + ] + self.progress_prepare.reset(task_id=self.courses_task_id) + for course in self.progress_prepare.track( + sequence=courses, + total=len(courses), + task_id=self.courses_task_id, + description="Courses", + ): + self.sync_course(semester=semester, course=course) + + def sync_course(self, semester: Semester, course: Course) -> None: + if self.selector.document: + self.sync_documents( + semester=semester, + course=course, + document_classes=course.document_classes, + documents=course.documents, + ) + if self.selector.homework: + self.sync_homeworks( + semester=semester, course=course, homeworks=course.homeworks + ) + + def sync_documents( + self, + semester: Semester, + course: Course, + document_classes: Sequence[DocumentClass], + documents: Sequence[Document], + ) -> None: + document_class_map: dict[str, DocumentClass] = { + document_class.id: document_class for document_class in document_classes + } + self.progress_prepare.reset(task_id=self.documents_task_id) + for index, document in enumerate( + self.progress_prepare.track( + sequence=documents, + total=len(documents), + task_id=self.documents_task_id, + description="Documents", + ) + ): + self.sync_document( + semester=semester, + course=course, + document_class=document_class_map[document.class_id], + document=document, + index=index, + ) + + def sync_document( + self, + semester: Semester, + course: Course, + document_class: DocumentClass, + document: Document, + index: int, + ) -> None: + self.sync_file( + client=document.client, + url=document.download_url, + output=filename.document( + prefix=self.prefix, + semester=semester, + course=course, + document_class=document_class, + document=document, + index=index, + ), + description=description.document( + semester=semester, + course=course, + document_class=document_class, + document=document, + index=index, + ), + remote_size=document.size, + remote_time=document.upload_time, + style=style.DOCUMENT, + ) + + def sync_homeworks( + self, semester: Semester, course: Course, homeworks: Sequence[Homework] + ) -> None: + self.progress_prepare.reset(task_id=self.documents_task_id) + for homework in self.progress_prepare.track( + sequence=homeworks, + total=len(homeworks), + task_id=self.documents_task_id, + description="Homeworks", + ): + self.sync_homework(semester=semester, course=course, homework=homework) + + def sync_homework( + self, semester: Semester, course: Course, homework: Homework + ) -> None: + readme_path: Path = filename.homework( + prefix=self.prefix, semester=semester, course=course, homework=homework + ) + os.makedirs(readme_path.parent, exist_ok=True) + readme_path.write_text(homework.markdown) + for attachment in homework.attachments: + self.sync_file( + client=homework.client, + url=attachment.download_url, + output=filename.attachment( + prefix=self.prefix, + semester=semester, + course=course, + homework=homework, + attachment=attachment, + ), + description=description.attachment( + semester=semester, + course=course, + homework=homework, + attachment=attachment, + ), + style=style.HOMEWORK, + ) diff --git a/thu_learn_downloader/download/filename.py b/thu_learn_downloader/download/filename.py new file mode 100644 index 0000000..806a18f --- /dev/null +++ b/thu_learn_downloader/download/filename.py @@ -0,0 +1,59 @@ +from pathlib import Path + +from thu_learn_downloader.client.course import Course +from thu_learn_downloader.client.document import Document, DocumentClass +from thu_learn_downloader.client.homework import Attachment, Homework +from thu_learn_downloader.client.semester import Semester + + +def document( + prefix: Path, + semester: Semester, + course: Course, + document_class: DocumentClass, + document: Document, + index: int, +) -> Path: + filename: Path = ( + prefix + / semester.id + / course.name + / "docs" + / document_class.title + / f"{index:02d}-{document.title}" + ) + if document.file_type: + filename = filename.with_suffix("." + document.file_type) + return filename + + +def homework( + prefix: Path, semester: Semester, course: Course, homework: Homework +) -> Path: + return ( + prefix + / semester.id + / course.name + / "work" + / f"{homework.number:02d}-{homework.title}" + / "README.md" + ) + + +def attachment( + prefix: Path, + semester: Semester, + course: Course, + homework: Homework, + attachment: Attachment, +) -> Path: + filename: Path = Path(attachment.name) + filename = filename.with_stem(f"{attachment.type_}-{homework.title}") + return ( + prefix + / semester.id + / course.name + / "work" + / f"{homework.number:02d}-{homework.title}" + / filename + ) diff --git a/thu_learn_downloader/download/selector.py b/thu_learn_downloader/download/selector.py new file mode 100644 index 0000000..01df79f --- /dev/null +++ b/thu_learn_downloader/download/selector.py @@ -0,0 +1,10 @@ +from collections.abc import Sequence + +from pydantic import BaseModel + + +class Selector(BaseModel): + semesters: Sequence[str] = [] + courses: Sequence[str] = [] + document: bool = True + homework: bool = True diff --git a/thu_learn_downloader/download/style.py b/thu_learn_downloader/download/style.py new file mode 100644 index 0000000..e700f90 --- /dev/null +++ b/thu_learn_downloader/download/style.py @@ -0,0 +1,4 @@ +from rich.style import Style + +DOCUMENT: Style = Style(color="cyan") +HOMEWORK: Style = Style(color="magenta") diff --git a/thu_learn_downloader/downloader.py b/thu_learn_downloader/downloader.py deleted file mode 100644 index 27fe9d3..0000000 --- a/thu_learn_downloader/downloader.py +++ /dev/null @@ -1,181 +0,0 @@ -import os -import os.path -import time -from concurrent.futures import Executor, ThreadPoolExecutor -from datetime import datetime -from pathlib import Path -from typing import Optional - -import requests -from rich.console import Console -from rich.progress import ( - BarColumn, - DownloadColumn, - Progress, - TaskID, - TaskProgressColumn, - TextColumn, - TimeElapsedColumn, - TimeRemainingColumn, - TransferSpeedColumn, -) -from rich.style import Style, StyleType - -from .constants import ( - CHUNK_SIZE, - FAILURE_PREFIX, - MAX_ACTIVE_TASKS, - RETRY_PREFIX, - SKIPPED_PREFIX, - SUCCESS_PREFIX, -) - - -def download_once( - url: str, - output: str | Path, - session: Optional[requests.Session], - raw_size: Optional[int] = None, - upload_time: Optional[datetime] = None, - progress: Progress = Progress(), - task_id: TaskID = TaskID(0), -) -> bool: - output = Path(output) - - resp: requests.Response = (session or requests).get(url=url, stream=True) - raw_size = int(resp.headers.get("Content-Length", 0)) or raw_size - - if upload_time and os.path.exists(output) and os.path.getsize(output) == raw_size: - mtime: float = os.path.getmtime(output) - if mtime >= upload_time.timestamp(): - return False - - os.makedirs(output.parent, exist_ok=True) - progress.reset(task_id=task_id, total=raw_size) - with open(file=output, mode="wb") as fp: - progress.start_task(task_id=task_id) - for chunk in resp.iter_content(chunk_size=CHUNK_SIZE): - bytes_written = fp.write(chunk) - progress.advance(task_id=task_id, advance=bytes_written) - if not raw_size: - progress.update(task_id=task_id, total=fp.tell()) - - if upload_time: - mtime: float = upload_time.timestamp() - os.utime(path=output, times=(mtime, mtime)) - - return True - - -def download( - url: str, - output: str | Path, - session: Optional[requests.Session], - raw_size: Optional[int] = None, - upload_time: Optional[datetime] = None, - progress: Progress = Progress(), - task_id: TaskID = TaskID(0), - *, - description: str = "", - max_retries: int = 4, - console: Console = Console(), - style: StyleType = Style(color="bright_cyan", bold=True), -) -> None: - if not description: - description = progress.tasks[task_id].description - style = style if isinstance(style, Style) else Style.parse(style) - for i in range(max_retries + 1): - try: - if i: - console.log( - RETRY_PREFIX, - i, - description, - style=style + Style(color="bright_yellow"), - ) - rtn: bool = download_once( - url=url, - output=output, - session=session, - raw_size=raw_size, - upload_time=upload_time, - progress=progress, - task_id=task_id, - ) - except: - console.log( - FAILURE_PREFIX, - description, - style=style + Style(color="bright_red"), - ) - else: - if rtn: - console.log(SUCCESS_PREFIX, description, style=style) - else: - console.log( - SKIPPED_PREFIX, - description, - style=style + Style(dim=True), - ) - break - progress.update(task_id=task_id, visible=False) - - -class Downloader: - pool: Executor - progress: Progress - - def __init__(self) -> None: - self.pool = ThreadPoolExecutor() - self.progress = Progress( - TextColumn("{task.description}"), - BarColumn(), - DownloadColumn(), - TaskProgressColumn(), - TimeElapsedColumn(), - TimeRemainingColumn(), - TransferSpeedColumn(), - ) - - def schedule_download( - self, - url: str, - output: str | Path, - session: Optional[requests.Session], - raw_size: Optional[int] = None, - upload_time: Optional[datetime] = None, - # progress: Progress = Progress(), - # task_id: TaskID = TaskID(0), - max_retries: int = 4, - console: Console = Console(), - style: StyleType = Style(color="bright_cyan", bold=True), - *, - description: str, - ) -> None: - while True: - num_visible_tasks: int = len( - list(filter(lambda task: task.visible, self.progress.tasks)) - ) - if num_visible_tasks < MAX_ACTIVE_TASKS: - break - else: - time.sleep(1) - if not isinstance(style, Style): - style = Style.parse(style) - task_id: TaskID = self.progress.add_task( - description=style.render(description), start=False, total=raw_size - ) - self.pool.submit( - download, - url=url, - output=output, - session=session, - raw_size=raw_size, - upload_time=upload_time, - progress=self.progress, - task_id=task_id, - description=description, - max_retries=max_retries, - console=console, - style=style, - ) diff --git a/thu_learn_downloader/helper.py b/thu_learn_downloader/helper.py deleted file mode 100644 index 166c87a..0000000 --- a/thu_learn_downloader/helper.py +++ /dev/null @@ -1,139 +0,0 @@ -import urllib.parse - -from bs4 import BeautifulSoup, Tag -from requests import Request, Response, Session - -from . import parser -from . import typing as t -from . import urls -from .constants import BS_FEATURES - - -class Helper(Session): - def fetch(self, request: Request) -> Response: - match request.method: - case "GET": - return self.get(url=request.url, params=request.params) - case "POST": - return self.post(url=request.url, data=request.data) - case _: - raise NotImplemented() - - def login(self, username: str, password: str) -> bool: - resp: Response = self.get(url=urls.make_url()) - soup: BeautifulSoup = BeautifulSoup(markup=resp.text, features=BS_FEATURES) - login_form = soup.select_one(selector="#loginForm") - assert isinstance(login_form, Tag) - action = login_form["action"] - assert isinstance(action, str) - - resp: Response = self.fetch( - request=urls.id_login(action=action, username=username, password=password) - ) - soup: BeautifulSoup = BeautifulSoup(markup=resp.text, features=BS_FEATURES) - a = soup.select_one(selector="a") - assert isinstance(a, Tag) - href = a["href"] - assert isinstance(href, str) - parse_result: urllib.parse.ParseResult = urllib.parse.urlparse(url=href) - query = urllib.parse.parse_qs(qs=parse_result.query) - status, ticket = query["status"][0], query["ticket"][0] - - _ = self.get(url=href) - - _ = self.fetch(urls.learn_auth_roam(ticket=ticket)) - - _ = self.fetch(urls.learn_student_course_list_page()) - - return status == "SUCCESS" - - @property - def token(self) -> str: - return self.cookies.get(name="XSRF-TOKEN") - - def fetch_with_token(self, request: Request, *args, **kwargs) -> Response: - assert isinstance(request.params, dict) - request.params["_csrf"] = self.token - return self.fetch(request=request, *args, **kwargs) - - def get_semester_id_list(self) -> list[str]: - resp: Response = self.fetch_with_token(request=urls.learn_semester_list()) - return list(filter(None, resp.json())) - - def get_course_list( - self, semester_id: str, course_type: t.CourseType = t.CourseType.STUDENT - ) -> list[t.CourseInfo]: - resp: Response = self.fetch_with_token( - urls.learn_course_list(semester=semester_id, course_type=course_type) - ) - results = resp.json()["resultList"] or [] - return list(map(parser.parse_course_info, results)) - - def get_file_list( - self, course_id: str, course_type: t.CourseType = t.CourseType.STUDENT - ) -> list[t.File]: - resp: Response = self.fetch_with_token( - urls.learn_file_clazz(course_id=course_id) - ) - file_clazz: dict[str, str] = {} - rows = resp.json()["object"]["rows"] - for row in rows: - file_clazz[row["kjflid"]] = row["bt"] # 课件分类 ID, 标题 - - resp: Response = self.fetch_with_token( - urls.learn_file_list(course_id=course_id, course_type=course_type) - ) - json = resp.json() - - if "resultsList" in json["object"]: - results = json["object"]["resultsList"] - else: - results = json["object"] - - files: list[t.File] = [ - parser.parse_file( - raw=result, - file_clazz=file_clazz, - course_id=course_id, - course_type=course_type, - ) - for result in results - ] - files.sort(key=lambda x: x.upload_time) - - return files - - def get_homework_list( - self, course_id: str, course_type: t.CourseType = t.CourseType.STUDENT - ) -> list[t.Homework]: - assert course_type == t.CourseType.STUDENT - works: list[t.Homework] = [ - *self.get_homework_list_at_url( - req=urls.learn_homework_list_new(course_id=course_id), - status=t.HomeworkStatus(submitted=False, graded=False), - ), - *self.get_homework_list_at_url( - req=urls.learn_homework_list_submitted(course_id=course_id), - status=t.HomeworkStatus(submitted=True, graded=False), - ), - *self.get_homework_list_at_url( - req=urls.learn_homework_list_graded(course_id=course_id), - status=t.HomeworkStatus(submitted=True, graded=False), - ), - ] - return works - - def get_homework_list_at_url( - self, req: Request, status: t.HomeworkStatus - ) -> list[t.Homework]: - resp: Response = self.fetch_with_token(request=req) - json = resp.json() - res = json["object"]["aaData"] or [] - return list( - map( - parser.parse_homework, - res, - [status] * len(res), - [self] * len(res), - ) - ) diff --git a/thu_learn_downloader/login/__init__.py b/thu_learn_downloader/login/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/thu_learn_downloader/login/__init__.py diff --git a/thu_learn_downloader/login/auto.py b/thu_learn_downloader/login/auto.py new file mode 100644 index 0000000..32564ab --- /dev/null +++ b/thu_learn_downloader/login/auto.py @@ -0,0 +1,21 @@ +from . import bitwarden + + +def username() -> str: + try: + username: str = bitwarden.username() + if username: + return username + except: + pass + return "" + + +def password() -> str: + try: + password: str = bitwarden.password() + if password: + return password + except: + pass + return "" diff --git a/thu_learn_downloader/login/bitwarden.py b/thu_learn_downloader/login/bitwarden.py new file mode 100644 index 0000000..f685bfb --- /dev/null +++ b/thu_learn_downloader/login/bitwarden.py @@ -0,0 +1,20 @@ +import subprocess +from subprocess import CompletedProcess + + +def username() -> str: + process: CompletedProcess = subprocess.run( + args=["bw", "--nointeraction", "get", "username", "id.tsinghua.edu.cn"], + capture_output=True, + text=True, + ) + return process.stdout + + +def password() -> str: + process: CompletedProcess = subprocess.run( + args=["bw", "--nointeraction", "get", "password", "id.tsinghua.edu.cn"], + capture_output=True, + text=True, + ) + return process.stdout diff --git a/thu_learn_downloader/main.py b/thu_learn_downloader/main.py new file mode 100644 index 0000000..77178e9 --- /dev/null +++ b/thu_learn_downloader/main.py @@ -0,0 +1,54 @@ +import logging +from pathlib import Path +from typing import Annotated + +import typer +from typer import Option, Typer + +from .client.client import Language +from .client.learn import Learn +from .common.logging import LogLevel +from .download.downloader import Downloader +from .download.selector import Selector +from .login import auto as login + +app: Typer = Typer(name="tld") + + +@app.command() +def main( + username: Annotated[str, Option("-u", "--username")] = "", + password: Annotated[str, Option("-p", "--password")] = "", + *, + prefix: Annotated[Path, Option(file_okay=False, writable=True)] = Path.home() + / "thu-learn", + semesters: Annotated[list[str], Option("-s", "--semester")] = ["2023-2024-1"], + courses: Annotated[list[str], Option("-c", "--course")] = [], + document: Annotated[bool, Option()] = True, + homework: Annotated[bool, Option()] = True, + jobs: Annotated[int, Option("-j", "--jobs")] = 8, + language: Annotated[Language, Option("-l", "--language")] = Language.ENGLISH, + log_level: Annotated[LogLevel, Option(envvar="LOG_LEVEL")] = LogLevel.INFO, +) -> None: + logging.getLogger().setLevel(log_level) + username = username or login.username() or typer.prompt(text="Username") + password = ( + password or login.password() or typer.prompt(text="Password", hide_input=True) + ) + learn: Learn = Learn(language=language) + learn.login(username=username, password=password) + with Downloader( + prefix=prefix, + selector=Selector( + semesters=semesters, + courses=courses, + document=document, + homework=homework, + ), + jobs=jobs, + ) as downloader: + downloader.sync_semesters(semesters=learn.semesters) + + +if __name__ == "__main__": + app() diff --git a/thu_learn_downloader/parser.py b/thu_learn_downloader/parser.py deleted file mode 100644 index 38ef93f..0000000 --- a/thu_learn_downloader/parser.py +++ /dev/null @@ -1,123 +0,0 @@ -import html -import urllib.parse -from datetime import datetime -from typing import TYPE_CHECKING, Optional - -from bs4 import BeautifulSoup, Tag -from requests import Response - -from . import typing as t -from . import urls, utils -from .constants import BS_FEATURES - -if TYPE_CHECKING: - from .helper import Helper - - -def parse_course_info(raw: dict) -> t.CourseInfo: - return t.CourseInfo( - id=raw["wlkcid"], # 网络课程 ID - name=raw["kcm"], # 课程名 - english_name=raw["ywkcm"], # 英文课程名 - course_number=raw["kch"], # 课程号 - course_index=int(raw["kxh"]), # 课序号 - ) - - -def parse_file( - raw: dict, - file_clazz: dict[str, str], - course_id: str, - course_type: t.CourseType = t.CourseType.STUDENT, -) -> t.File: - return t.File( - id=raw["wjid"], # 文件 ID - raw_size=raw["wjdx"], # 文件大小 - title=html.unescape(raw["bt"]), # 标题 - upload_time=datetime.strptime(raw["scsj"], "%Y-%m-%d %H:%M"), # 上传时间 - download_url=urls.to_url( - urls.learn_file_download( - file_id=raw["wjid"], # 文件 ID - course_id=course_id, - course_type=course_type, - ) - ), - file_type=raw["wjlx"], # 文件类型 - file_clazz=file_clazz[raw["kjflid"]], # 课件分类 ID - ) - - -def parse_homework(raw: dict, status: t.HomeworkStatus, helper: "Helper") -> t.Homework: - detail: t.HomeworkDetail = parse_homework_detail( - course_id=raw["wlkcid"], # 网络课程 ID - homework_id=raw["zyid"], # 作业 ID - student_homework_id=raw["xszyid"], # 学生作业 ID - helper=helper, - ) - - return t.Homework( - id=raw["zyid"], # 作业 ID - student_homework_id=raw["xszyid"], # 学生作业 ID - number=int(raw["wz"]), # - title=str(html.unescape(raw["bt"])).strip(), # 标题 - starts_time=utils.parse_time(raw.get("kssj")), # 开始时间 - deadline=utils.parse_time(raw.get("jzsj")), # 截止时间 - submit_time=utils.parse_time(raw.get("scsj")), # 上传时间 - grade=raw.get("cj", ""), # 成绩 - grader_name=raw.get("jsm", ""), # 教师名 - grade_time=utils.parse_time(raw.get("pysj")), # 批阅时间 - grade_content=raw.get("pynr", ""), # 批阅内容 - **utils.dataclass_as_dict_shallow(status), - **utils.dataclass_as_dict_shallow(detail), - ) - - -def parse_homework_detail( - course_id: str, homework_id: str, student_homework_id: str, helper: "Helper" -) -> t.HomeworkDetail: - resp: Response = helper.fetch_with_token( - request=urls.learn_homework_detail( - course_id=course_id, - homework_id=homework_id, - student_homework_id=student_homework_id, - ) - ) - soup: BeautifulSoup = BeautifulSoup(resp.text, features=BS_FEATURES) - - c55s: list[Tag] = soup.select("div.list.calendar.clearfix > div.fl.right > div.c55") - file_divs: list[Tag] = soup.select("div.list.fujian.clearfix") - boxbox: Tag = soup.select("div.boxbox")[1] - right: Tag = boxbox.select("div.right")[2] - - description: str = html.unescape(c55s[0].get_text().strip()) - answer_content: str = html.unescape(c55s[1].get_text().strip()) - submitted_content: str = html.unescape(right.get_text().strip()) - - return t.HomeworkDetail( - description=description, - attachment=parse_homework_file(file_div=file_divs[0]), - answer_content=answer_content, - answer_attachment=parse_homework_file(file_div=file_divs[1]), - submitted_content=submitted_content, - submitted_attachment=parse_homework_file(file_div=file_divs[2]), - grade_attachment=parse_homework_file(file_div=file_divs[3]), - ) - - -def parse_homework_file(file_div: Tag) -> Optional[t.RemoteFile]: - fl = file_div.select_one(".txt.fl") - if not fl: - return None - file_node = fl.select_one("a") - if not file_node: - return None - - href = file_node["href"] - assert isinstance(href, str) - parse_result = urllib.parse.urlparse(href) - query = urllib.parse.parse_qs(parse_result.query) - return t.RemoteFile( - id=query["fileId"][0], - name=file_node.get_text(), - download_url=urls.make_url(path=query["downloadUrl"][0]), - ) diff --git a/thu_learn_downloader/sync.py b/thu_learn_downloader/sync.py deleted file mode 100644 index 9e084a5..0000000 --- a/thu_learn_downloader/sync.py +++ /dev/null @@ -1,290 +0,0 @@ -import os -import shutil -import subprocess -from datetime import datetime -from pathlib import Path -from typing import Optional - -from rich.console import Console -from rich.progress import Progress, TaskID - -from . import typing as t -from . import utils -from .config import Config -from .constants import DOCUMENT_STYLE, HOMEWORK_STYLE, SKIPPED_PREFIX, SUCCESS_PREFIX -from .downloader import Downloader -from .helper import Helper - - -def sync_all( - helper: Helper, - downloader: Downloader, - config: Config, - *, - console: Console = Console(), - overall_progress: Progress = Progress(), - semesters_task_id: TaskID = TaskID(0), - courses_task_id: TaskID = TaskID(1), -) -> None: - semesters = config.semesters or helper.get_semester_id_list() - - for semester in overall_progress.track( - semesters, task_id=semesters_task_id, description="Semesters" - ): - sync_semester( - helper=helper, - downloader=downloader, - semester_id=semester, - config=config, - console=console, - overall_progress=overall_progress, - courses_task_id=courses_task_id, - ) - - -def sync_semester( - helper: Helper, - downloader: Downloader, - config: Config, - semester_id: str, - *, - console: Console = Console(), - overall_progress: Progress = Progress(), - courses_task_id: TaskID = TaskID(1), -) -> None: - courses: list[t.CourseInfo] = helper.get_course_list(semester_id=semester_id) - - if config.courses: - courses = [ - course - for course in courses - if (course.name in config.courses) - or (course.english_name in config.courses) - ] - - overall_progress.update( - task_id=courses_task_id, description=utils.format_semester_id(semester_id) - ) - for course in overall_progress.track(courses, task_id=courses_task_id): - sync_course( - helper=helper, - downloader=downloader, - config=config, - course=course, - console=console, - ) - - -def sync_course( - helper: Helper, - downloader: Downloader, - config: Config, - course: t.CourseInfo, - *, - console: Console = Console(), -) -> None: - os.makedirs(config.prefix / course.english_name, exist_ok=True) - sync_files( - helper=helper, - downloader=downloader, - config=config, - course=course, - console=console, - ) - sync_homeworks( - helper=helper, - downloader=downloader, - config=config, - course=course, - console=console, - ) - - -def sync_files( - helper: Helper, - downloader: Downloader, - config: Config, - course: t.CourseInfo, - *, - console: Console = Console(), -) -> None: - prefix: Path = config.prefix - size_limit: int = config.size_limit - - files: list[t.File] = helper.get_file_list(course_id=course.id) - files: list[t.File] = sorted(files, key=lambda f: f.id) - - for i, f in enumerate(files): - filename: str = utils.format_doc_filename(title=f.title, file_type=f.file_type) - filename: str = f"{i:02d}-{filename}" - if f.raw_size > size_limit: - console.log( - SKIPPED_PREFIX, - utils.describe_doc_file( - course_name=course.english_name, filename=filename - ), - "size limit exceeded", - style=DOCUMENT_STYLE, - ) - continue - - downloader.schedule_download( - url=f.download_url, - output=prefix / course.english_name / "docs" / f.file_clazz / filename, - session=helper, - raw_size=f.raw_size, - upload_time=f.upload_time, - console=console, - style=DOCUMENT_STYLE, - description=utils.describe_doc_file( - course_name=course.english_name, filename=filename - ), - ) - - -def sync_homeworks( - helper: Helper, - downloader: Downloader, - config: Config, - course: t.CourseInfo, - *, - console: Console = Console(), -) -> None: - homeworks = helper.get_homework_list(course_id=course.id) - - for hw in homeworks: - sync_homework_detail( - helper=helper, - downloader=downloader, - config=config, - course=course, - hw=hw, - console=console, - ) - sync_homework_attachments( - helper=helper, - downloader=downloader, - config=config, - course=course, - hw=hw, - console=console, - ) - - -def sync_homework_detail( - helper: Helper, - downloader: Downloader, - config: Config, - course: t.CourseInfo, - hw: t.Homework, - *, - console: Console = Console(), -) -> None: - prefix: Path = config.prefix - title: str = f"{hw.number:02d}-{hw.title}" - filepath: Path = prefix / course.english_name / "work" / title / "README.md" - os.makedirs(filepath.parent, exist_ok=True) - with open(file=filepath, mode="w") as fp: - fp.write(utils.format_homework_readme(hw=hw)) - fp.flush() - if shutil.which("prettier"): - subprocess.run(args=["prettier", "--write", filepath], capture_output=True) - console.log( - SUCCESS_PREFIX, - course.english_name, - ">", - title, - style=HOMEWORK_STYLE, - ) - - -def sync_homework_attachments( - helper: Helper, - downloader: Downloader, - config: Config, - course: t.CourseInfo, - hw: t.Homework, - *, - console: Console = Console(), -) -> None: - sync_homework_attachment( - helper=helper, - downloader=downloader, - config=config, - course=course, - hw=hw, - attach=hw.attachment, - attach_type="attach", - console=console, - ) - sync_homework_attachment( - helper=helper, - downloader=downloader, - config=config, - course=course, - hw=hw, - attach=hw.answer_attachment, - attach_type="ans", - console=console, - ) - sync_homework_attachment( - helper=helper, - downloader=downloader, - config=config, - course=course, - hw=hw, - attach=hw.submitted_attachment, - attach_type="submit", - console=console, - ) - sync_homework_attachment( - helper=helper, - downloader=downloader, - config=config, - course=course, - hw=hw, - attach=hw.grade_attachment, - attach_type="comment", - console=console, - ) - - -def sync_homework_attachment( - helper: Helper, - downloader: Downloader, - config: Config, - course: t.CourseInfo, - hw: t.Homework, - attach: Optional[t.RemoteFile], - attach_type: str, - *, - console: Console = Console(), -) -> None: - if not attach: - return - prefix: Path = config.prefix - title: str = f"{hw.number:02d}-{hw.title}" - filename: str = utils.remove_attachment_prefix(attach.name) - filename: str = f"{attach_type}-{filename}" - filepath: Path = prefix / course.english_name / "work" / title / filename - upload_time: Optional[datetime] = None - match attach_type: - case "attach": - upload_time = hw.starts_time - case "ans": - pass - case "submit": - upload_time = hw.submit_time - case "comment": - upload_time = hw.grade_time - downloader.schedule_download( - url=attach.download_url, - output=filepath, - session=helper, - upload_time=upload_time, - console=console, - style=HOMEWORK_STYLE, - description=utils.describe_work_file( - course_name=course.english_name, hw_title=title, filename=filename - ), - ) diff --git a/thu_learn_downloader/typing.py b/thu_learn_downloader/typing.py deleted file mode 100644 index 812c405..0000000 --- a/thu_learn_downloader/typing.py +++ /dev/null @@ -1,75 +0,0 @@ -import dataclasses -from datetime import datetime -from enum import Enum -from typing import Optional - - -class CourseType(Enum): - STUDENT = "student" - TEACHER = "teacher" - - -class SemesterSeason(Enum): - FALL = "Autumn Term" - SPRING = "Spring Term" - SUMMER = "Summer Term" - - -@dataclasses.dataclass(kw_only=True) -class CourseInfo: - id: str # 网络课程 ID - wlkcid - name: str # 课程名 - kcm - english_name: str # 英文课程名 - ywkcm - course_number: str # 课程号 - kcm - course_index: int # 课序号 - kxh - - -@dataclasses.dataclass(kw_only=True) -class RemoteFile: - id: str - name: str - download_url: str - - -@dataclasses.dataclass(kw_only=True) -class File: - id: str # 文件 ID - wjid - raw_size: int # 文件大小 - wjdx - title: str # 标题 - bt - upload_time: datetime # 上传时间 - scsj - download_url: str - file_type: str # 文件类型 - wjlx - file_clazz: str # 课件分类 ID - kjflid - - -@dataclasses.dataclass(kw_only=True) -class HomeworkStatus: - submitted: bool - graded: bool - - -@dataclasses.dataclass(kw_only=True) -class HomeworkDetail: - description: str = "" - attachment: Optional[RemoteFile] = None - answer_content: str = "" - answer_attachment: Optional[RemoteFile] = None - submitted_content: str = "" - submitted_attachment: Optional[RemoteFile] = None - # grade_content: str = "" # 批阅内容 - pynr - grade_attachment: Optional[RemoteFile] = None - - -@dataclasses.dataclass(kw_only=True) -class Homework(HomeworkStatus, HomeworkDetail): - id: str # 作业 ID - zyid - student_homework_id: str # 学生作业 ID - xszyid - number: int # - wz - title: str # 标题 - bt - starts_time: Optional[datetime] = None # 开始时间 - kssj - deadline: Optional[datetime] = None # 截止时间 - jzsj - submit_time: Optional[datetime] = None # 上传时间 - scsj - grade: str = "" # 成绩 - cj - grade_time: Optional[datetime] = None # 批阅时间 - pysj - grader_name: str = "" # 教师名 - jsm - grade_content: str = "" # 批阅内容 - pynr diff --git a/thu_learn_downloader/urls.py b/thu_learn_downloader/urls.py deleted file mode 100644 index 370edae..0000000 --- a/thu_learn_downloader/urls.py +++ /dev/null @@ -1,145 +0,0 @@ -import urllib.parse -from urllib.parse import ParseResult - -from requests import Request - -from . import typing as t - -MAX_SIZE = 200 -LEARN_PREFIX = "learn.tsinghua.edu.cn" - - -def make_url(scheme: str = "https", netloc: str = LEARN_PREFIX, path: str = "") -> str: - return ParseResult( - scheme=scheme, netloc=netloc, path=path, params="", query="", fragment="" - ).geturl() - - -def make_req( - method: str = "GET", - url: str = make_url(), - data: dict = {}, - params: dict = {}, -) -> Request: - return Request(method=method, url=url, data=data, params=params) - - -def to_url(request: Request) -> str: - parse_result: urllib.parse.ParseResult = urllib.parse.urlparse(url=request.url) - parse_result = urllib.parse.ParseResult( - scheme=parse_result.scheme, - netloc=parse_result.netloc, - path=parse_result.path, - params=parse_result.params, - query=urllib.parse.urlencode(query=request.params), - fragment=parse_result.fragment, - ) - return urllib.parse.urlunparse(parse_result) - - -def id_login(action: str, username: str, password: str) -> Request: - return make_req( - method="POST", - url=action, - data={"i_user": username, "i_pass": password, "atOnce": True}, - ) - - -def learn_auth_roam(ticket: str) -> Request: - return make_req( - url=make_url(path="/b/j_spring_security_thauth_roaming_entry"), - params={"ticket": ticket}, - ) - - -def learn_student_course_list_page() -> Request: - return make_req(url=make_url(path="/f/wlxt/index/course/student/")) - - -def learn_semester_list() -> Request: - return make_req(url=make_url(path="/b/wlxt/kc/v_wlkc_xs_xktjb_coassb/queryxnxq")) - - -def learn_course_list( - semester: str, course_type: t.CourseType = t.CourseType.STUDENT -) -> Request: - match course_type: - case t.CourseType.STUDENT: - return make_req( - url=make_url( - path=f"/b/wlxt/kc/v_wlkc_xs_xkb_kcb_extend/student/loadCourseBySemesterId/{semester}/en" - ) - ) - case t.CourseType.TEACHER: - raise NotImplementedError() - - -def learn_file_list( - course_id: str, course_type: t.CourseType = t.CourseType.STUDENT -) -> Request: - match course_type: - case t.CourseType.STUDENT: - return make_req( - url=make_url( - path="/b/wlxt/kj/wlkc_kjxxb/student/kjxxbByWlkcidAndSizeForStudent" - ), - params={"wlkcid": course_id, "size": MAX_SIZE}, - ) - case t.CourseType.TEACHER: - raise NotImplementedError() - - -def learn_file_clazz(course_id: str) -> Request: - return make_req( - url=make_url(path="/b/wlxt/kj/wlkc_kjflb/student/pageList"), - params={"wlkcid": course_id}, - ) - - -def learn_file_download( - file_id: str, - course_id: str, - course_type: t.CourseType = t.CourseType.STUDENT, -) -> Request: - match course_type: - case t.CourseType.STUDENT: - return make_req( - url=make_url(path="/b/wlxt/kj/wlkc_kjxxb/student/downloadFile"), - params={"sfgk": 0, "wjid": file_id}, - ) - case t.CourseType.TEACHER: - raise NotImplementedError() - - -def learn_homework_list_new(course_id: str) -> Request: - return make_req( - url=make_url(path="/b/wlxt/kczy/zy/student/index/zyListWj"), - params={"wlkcid": course_id, "size": MAX_SIZE}, - ) - - -def learn_homework_list_submitted(course_id: str) -> Request: - return make_req( - url=make_url(path="/b/wlxt/kczy/zy/student/index/zyListYjwg"), - params={"wlkcid": course_id, "size": MAX_SIZE}, - ) - - -def learn_homework_list_graded(course_id: str) -> Request: - return make_req( - url=make_url(path="/b/wlxt/kczy/zy/student/index/zyListYpg"), - params={"wlkcid": course_id, "size": MAX_SIZE}, - ) - - -def learn_homework_detail( - course_id: str, homework_id: str, student_homework_id: str -) -> Request: - return make_req( - url=make_url(path="/f/wlxt/kczy/zy/student/viewCj"), - params={ - "wlkcid": course_id, - "zyid": homework_id, - "xszyid": student_homework_id, - }, - ) diff --git a/thu_learn_downloader/utils.py b/thu_learn_downloader/utils.py deleted file mode 100644 index 093d46a..0000000 --- a/thu_learn_downloader/utils.py +++ /dev/null @@ -1,67 +0,0 @@ -import dataclasses -from datetime import datetime -from typing import Any, Optional - -from . import typing as t -from .constants import HOMEWORK_README, SEASONS - - -def format_semester_id(semester_id: str) -> str: - years: str = semester_id[:-2] - season: t.SemesterSeason = SEASONS[int(semester_id[-1:])] - return f"{years} {season.value}" - - -def format_doc_filename(title: str, file_type: str) -> str: - if file_type: - return f"{title}.{file_type}" - else: - return title - - -def describe_doc_file(course_name: str, filename: str) -> str: - return f"{course_name} > {filename}" - - -def describe_work_file(course_name: str, hw_title: str, filename: str) -> str: - return f"{course_name} > {hw_title} > {filename}" - - -def parse_time(t: Optional[float | str]) -> Optional[datetime]: - if not t: - return None - match t: - case float(): - return datetime.fromtimestamp(t / 1000.0) - case str(): - return datetime.strptime(t, "%Y-%m-%d %H:%M") - - -def dataclass_as_dict_shallow(obj: Any) -> dict[str, Any]: - return dict( - (field.name, getattr(obj, field.name)) for field in dataclasses.fields(obj) - ) - - -def remove_attachment_prefix(name: str) -> str: - prefixes: list[str] = ["attach-", "ans-", "submit-", "comment-", "-"] - while name.startswith(tuple(prefixes)): - for p in prefixes: - name = name.removeprefix(p) - return name - - -def format_homework_readme(hw: t.Homework) -> str: - return HOMEWORK_README.format( - title=hw.title, - starts=str(hw.starts_time or ""), - deadline=str(hw.deadline or ""), - description=hw.description or "", - ans=hw.answer_content or "", - submit_time=str(hw.submit_time or ""), - submit_content=hw.submitted_content or "", - grader_name=hw.grader_name or "", - grade_time=str(hw.grade_time or ""), - grade=hw.grade, - comment=hw.grade_content, - )