diff --git a/.github/actions/.gitignore b/.github/actions/.gitignore deleted file mode 100644 index 71860a7..0000000 --- a/.github/actions/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!build diff --git a/.github/actions/build/action.yaml b/.github/actions/build/action.yaml deleted file mode 100644 index 048152c..0000000 --- a/.github/actions/build/action.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build Artifacts -description: Build Artifacts - -inputs: - python-version: - description: "Python Version" - required: false - default: "3.10" - -runs: - using: "composite" - steps: - - name: Setup Python - id: setup-python - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - - name: Load Cached venv - id: cached-poetry-dependencies - uses: actions/cache@v3 - with: - path: .venv - key: venv-build-${{ runner.os }}-${{ runner.arch }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} - - name: Install Dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - shell: bash - run: poetry install --no-interaction --no-root - - name: Install Project - shell: bash - run: poetry install --no-interaction - - if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }} - name: Build Artifacts - shell: bash - run: | - source .venv/bin/activate - poetry run build - mv dist/* dist/$(poetry version | tr ' ' '-')-${{ runner.os }}-${{ runner.arch }}-py${{ steps.setup-python.outputs.python-version }} - - if: ${{ runner.os == 'Windows' }} - name: Build Artifacts - shell: bash - run: | - source .venv/Scripts/activate - poetry run build - mv dist/* dist/$(poetry version | tr ' ' '-')-${{ runner.os }}-${{ runner.arch }}-py${{ steps.setup-python.outputs.python-version }}.exe diff --git a/.github/actions/docs/action.yaml b/.github/actions/docs/action.yaml deleted file mode 100644 index 2ddce9f..0000000 --- a/.github/actions/docs/action.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: Deploy MkDocs -description: Build and Deploy MkDocs to GitHub Pages - -inputs: - python-version: - description: "Python Version" - required: false - default: "3.10" - -runs: - using: "composite" - steps: - - name: Setup Python - id: setup-python - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python-version }} - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - - name: Load Cached venv - id: cached-poetry-dependencies - uses: actions/cache@v3 - with: - path: .venv - key: venv-docs-${{ runner.os }}-${{ runner.arch }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} - - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - name: Install dependencies - shell: bash - run: poetry install --with docs --no-root --no-interaction - - name: Deploy GitHub Pages - shell: bash - run: | - source .venv/bin/activate - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - mkdocs gh-deploy --force diff --git a/.github/actions/publish/action.yaml b/.github/actions/publish/action.yaml deleted file mode 100644 index de0e0f1..0000000 --- a/.github/actions/publish/action.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: Publish to PyPI -description: Publish Python Package to PyPI - -inputs: - pypi_token: - description: "PyPI Token" - required: true - -runs: - using: "composite" - steps: - - if: ${{ inputs.pypi_token != '' }} - name: Build and Publish to PyPI - uses: JRubics/poetry-publish@v1.13 - with: - pypi_token: ${{ inputs.pypi_token }} - continue-on-error: true diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 4ab058f..17fa230 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,18 +1,11 @@ version: 2 + updates: - - package-ecosystem: "gitsubmodule" - directory: "/" + - package-ecosystem: github-actions + directory: / schedule: - interval: "weekly" - - package-ecosystem: "github-actions" - directory: "/" + interval: weekly + - package-ecosystem: pip + directory: / schedule: - interval: "weekly" - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "weekly" + interval: weekly diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml new file mode 100644 index 0000000..048267a --- /dev/null +++ b/.github/sync-repo-settings.yaml @@ -0,0 +1,2 @@ +# Automatically delete head branches after merging PRs. Defaults to `true`. +deleteBranchOnMerge: true diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 25465ce..1c6b12c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,116 +3,172 @@ on: push: branches: - - "main" + - main + +# - [x] Settings > General > Pull Requests > Automatically delete head branches +# - [x] Settings > Actions > General > Workflow permissions > Allow GitHub Actions to create and approve pull requests +permissions: + contents: write + pull-requests: write + +env: + PYTHON_VERSION: "3.11" jobs: - cz: - if: ${{ github.repository != 'liblaf/template' }} + detect-build: + name: Detect Build Script + runs-on: ubuntu-latest outputs: - bumped: ${{ steps.bumped.outputs.bumped }} - version: ${{ steps.cz.outputs.version }} - runs-on: ubuntu-latest + build: ${{ steps.detect.outputs.build }} steps: - name: Checkout - uses: actions/checkout@v3.1.0 - with: - fetch-depth: 0 - - id: cz - name: Create Bump and Changelog - uses: commitizen-tools/commitizen-action@0.15.1 - with: - github_token: ${{ github.token }} - changelog_increment_filename: body.md - - id: bumped + uses: actions/checkout@v3 + - id: detect + name: Detect Build Script run: | - if [[ -n "$(cat body.md)" ]]; then - echo "bumped = true" - echo "bumped=true" >> $GITHUB_OUTPUT + if [[ -f "scripts/build.sh" ]]; then + echo "build=true" >> "${GITHUB_OUTPUT}" + echo ":heavy_check_mark: Build Script Detected" >> "${GITHUB_STEP_SUMMARY}" else - echo "bumped = false" - echo "bumped=false" >> $GITHUB_OUTPUT + echo "build=false" >> "${GITHUB_OUTPUT}" + echo ":x: Build Script Not Found" >> "${GITHUB_STEP_SUMMARY}" fi - - if: ${{ steps.bumped.outputs.bumped == 'true' }} - name: Upload Changelog - uses: actions/upload-artifact@v3.1.1 - with: - name: changelog - path: body.md - publish: - needs: cz - if: ${{ needs.cz.outputs.bumped == 'true' }} - continue-on-error: true + detect-publish: + name: Detect PyPI Credential + runs-on: ubuntu-latest + outputs: + publish: ${{ steps.detect.outputs.publish }} + steps: + - id: detect + name: Detect PyPI Credential + run: | + if [[ -n "${PYPI_USERNAME}" && -n "${PYPI_PASSWORD}" ]]; then + echo "publish=true" >> "${GITHUB_OUTPUT}" + echo ":heavy_check_mark: PyPI Credential Detected" >> "${GITHUB_STEP_SUMMARY}" + else + echo "publish=false" >> "${GITHUB_OUTPUT}" + echo ":x: PyPI Credential Not Found" >> "${GITHUB_STEP_SUMMARY}" + fi + env: + PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} + PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + + build-pkg: + name: Build Package runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3.1.0 - - run: git pull - - name: Publish Package - uses: ./.github/actions/publish + uses: actions/checkout@v3 + - name: Install Poetry + run: pipx install poetry + - id: python + name: Setup Python + uses: actions/setup-python@v4 with: - pypi_token: ${{ secrets.PYPI_TOKEN }} + python-version: ${{ env.PYTHON_VERSION }} + cache: poetry + - name: Install Dependencies + run: poetry install --no-interaction + - name: Build Package + run: poetry build --no-interaction + - name: Upload Build Artifact + uses: actions/upload-artifact@v3 + with: + name: package + path: dist/* - build: - needs: cz - if: ${{ needs.cz.outputs.bumped == 'true' }} - continue-on-error: true + build-exe: + name: Build Executable + needs: + - detect-build + if: needs.detect-build.outputs.build == 'true' + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install Poetry + run: pipx install poetry + - id: python + name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: poetry + - name: Install Dependencies + run: poetry install --no-interaction + - name: Build Executable + run: poetry run bash "scripts/build.sh" + - if: runner.os != 'Windows' + name: Rename Build Artifact + run: mv dist/* "dist/$(poetry version | tr ' ' -)-${{ runner.os }}-${{ runner.arch }}" + - if: runner.os == 'Windows' + name: Rename Build Artifact + run: mv dist/* "dist/$(poetry version | tr ' ' -)-${{ runner.os }}-${{ runner.arch }}.exe" + - name: Upload Build Artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ runner.os }}-${{ runner.arch }} + path: dist/* strategy: matrix: os: - macos-latest - ubuntu-latest - windows-latest - runs-on: ${{ matrix.os }} - steps: - - name: Checkout - uses: actions/checkout@v3.1.0 - - run: git pull - - id: detect - name: Detect Build Script - shell: bash - run: | - set +o errexit - set +o pipefail - if grep 'build = ".*.build:run"' pyproject.toml; then - echo "build=true" >> $GITHUB_OUTPUT - else - echo "build=false" >> $GITHUB_OUTPUT - fi - - if: ${{ steps.detect.outputs.build == 'true' }} - name: Build Artifacts - uses: ./.github/actions/build - - if: ${{ steps.detect.outputs.build == 'true' }} - name: Upload Artifacts - uses: actions/upload-artifact@v3.1.1 - with: - name: ${{ matrix.os }} - path: dist/**/* release: + name: Create GitHub Release + runs-on: ubuntu-latest + outputs: + releases-created: ${{ steps.release.outputs.releases_created }} + tag-name: ${{ steps.release.outputs.tag_name }} + steps: + - id: release + name: Create GitHub Release + uses: google-github-actions/release-please-action@v3 + with: + release-type: python + + upload: + name: Upload Release Assets needs: - - cz - - build - if: ${{ needs.cz.outputs.bumped == 'true' }} + - build-exe + - build-pkg + - release + if: always() && needs.release.outputs.releases-created == 'true' + runs-on: ubuntu-latest + steps: + - name: Download Artifacts + uses: actions/download-artifact@v3 + with: + path: artifacts + - name: Upload Release Assets + uses: svenstaro/upload-release-action@master + with: + file: artifacts/**/* + tag: ${{ needs.release.outputs.tag-name }} + file_glob: true + overwrite: true + + publish: + name: Publish to PyPI + needs: + - detect-publish + - release + if: needs.detect-publish.outputs.publish == 'true' && needs.release.outputs.releases-created == 'true' runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3.1.0 - - run: git pull - - name: Download Changelog - uses: actions/download-artifact@v3.0.1 + uses: actions/checkout@v3 + - name: Install Poetry + run: pipx install poetry + - name: Setup Python + uses: actions/setup-python@v4 with: - name: changelog - - name: Download Artifacts - uses: actions/download-artifact@v3.0.1 - with: - path: artifacts - - name: GitHub Release - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ needs.cz.outputs.version }} - body_path: body.md - files: | - artifacts/macos-latest/**/* - artifacts/ubuntu-latest/**/* - artifacts/windows-latest/**/* + python-version: ${{ env.PYTHON_VERSION }} + cache: poetry + - name: Install Dependencies + run: poetry install --no-interaction + - name: Publish to PyPI + run: poetry publish --username "${{ secrets.PYPI_USERNAME }}" --password "${{ secrets.PYPI_PASSWORD }}" --build --no-interaction diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml deleted file mode 100644 index 980b90d..0000000 --- a/.github/workflows/gh-pages.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: Deploy GitHub Pages - -on: - push: - branches: - - main - workflow_dispatch: - -jobs: - docs: - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - name: Deploy MkDocs - uses: ./.github/actions/docs diff --git a/.github/workflows/license.yaml b/.github/workflows/license.yaml new file mode 100644 index 0000000..e690d61 --- /dev/null +++ b/.github/workflows/license.yaml @@ -0,0 +1,16 @@ +name: Update Copyright Year(s) in License File + +on: + schedule: + - cron: "0 3 1 1 *" # 03:00 AM on January 1 + +jobs: + update-license-year: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: FantasticFiasco/action-update-license-year@v2 + with: + token: ${{ github.token }} diff --git a/.github/workflows/update-license-year.yaml b/.github/workflows/update-license-year.yaml deleted file mode 100644 index e690d61..0000000 --- a/.github/workflows/update-license-year.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: Update Copyright Year(s) in License File - -on: - schedule: - - cron: "0 3 1 1 *" # 03:00 AM on January 1 - -jobs: - update-license-year: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - uses: FantasticFiasco/action-update-license-year@v2 - with: - token: ${{ github.token }} diff --git a/.gitignore b/.gitignore index 20ccb39..219a425 100644 --- a/.gitignore +++ b/.gitignore @@ -163,5 +163,14 @@ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + # End of https://www.toptal.com/developers/gitignore/api/python -outputs + +*.gif +dist diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 155556e..5fe3116 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,25 +3,26 @@ - latexindent-system - poetry-lock - shfmt-system + repos: - repo: https://github.com/commitizen-tools/commitizen - rev: "v2.37.0" + rev: "v2.42.0" hooks: - id: commitizen - repo: https://github.com/liblaf/pre-commit-hooks - rev: "0.2.2" + rev: "0.2.3" hooks: - id: latexindent-system - id: shfmt-system - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks - rev: "v2.4.0" + rev: "v2.7.0" hooks: - id: pretty-format-toml args: - "--autofix" exclude: poetry.lock - repo: https://github.com/pre-commit/mirrors-clang-format - rev: "v15.0.4" + rev: "v15.0.7" hooks: - id: clang-format args: @@ -34,7 +35,7 @@ stages: - commit - repo: https://github.com/pre-commit/pre-commit-hooks - rev: "v4.3.0" + rev: "v4.4.0" hooks: - id: check-added-large-files - id: check-case-conflict @@ -48,12 +49,12 @@ - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/psf/black - rev: "22.10.0" + rev: "23.1.0" hooks: - id: black - id: black-jupyter - repo: https://github.com/PyCQA/isort - rev: "5.10.1" + rev: "5.12.0" hooks: - id: isort args: @@ -61,13 +62,13 @@ - "black" - "--filter-files" - repo: https://github.com/python-jsonschema/check-jsonschema - rev: "0.19.2" + rev: "0.21.0" hooks: - id: check-dependabot - id: check-github-actions - id: check-github-workflows - repo: https://github.com/python-poetry/poetry - rev: "1.2.1" + rev: "1.3.0" hooks: - id: poetry-check - id: poetry-lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 0866d39..6d94168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +# Changelog + ## 0.1.1 (2022-12-26) ### Fix diff --git a/README.md b/README.md index 1a25137..ea8bd84 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,25 @@ # thu-learn-downloader -Auto download files from thu-learn +Download everything from Web Learning of Tsinghua University ## Demo -See Screen Recording at [demo.webm](https://drive.liblaf.top/github/thu-learn-downloader/demo.webm). +![Demo](https://res.cloudinary.com/liblaf/image/upload/v1677213088/2023/02/24/20230224-1677213085.gif) The resulting file structure looks like: ``` thu-learn -└── engineering-mechanics-for-civil-engineering +└── Quantum Mechanics(1) ├── docs - │ ├── 作业与思考题 - │ │ └── 第三周部分作业及思考题.pdf - │ ├── 电子教案 - │ │ └── 第13讲-杆件拉伸和压缩.pdf - │ └── 课外阅读 - │ └── 基于月面原位资源的月球基地建造技术.pdf + │ └── 电子教案 + │ ├── 01-0量子力学介绍1.pdf + │ └── 04-0量子力学介绍2.pdf └── work - ├── 期中考试 - │ └── README.md - └── 第2周作业 - ├── attach-第2周作业.docx - ├── comment-2020012872-李钦-6544.pdf - ├── README.md - └── submit-第2周作业.pdf + └── 01-第一周作业 + ├── attach-第一周作业.pdf + ├── submit-第一周作业.pdf + └── README.md ``` ## Features diff --git a/config.yaml b/config.yaml index e1b2bf6..bffe50d 100644 --- a/config.yaml +++ b/config.yaml @@ -1,6 +1,4 @@ -username: "liqin20" -password: "**************" -prefix: /home/liblaf/Desktop/thu-learn +username: liqin20 +password: "****************" semesters: - - "2022-2023-1" -courses: ~ + - 2022-2023-2 diff --git a/demo.tape b/demo.tape new file mode 100644 index 0000000..be71893 --- /dev/null +++ b/demo.tape @@ -0,0 +1,12 @@ +Output demo.gif + +Set Width 1920 +Set Height 1080 +Set FontSize 12 + +Require thu-learn-downloader + +Type 'thu-learn-downloader password="$(bw get password id.tsinghua.edu.cn)"' +Sleep 500ms +Enter +Sleep 10s diff --git a/docs/index.md b/docs/index.md deleted file mode 120000 index 32d46ee..0000000 --- a/docs/index.md +++ /dev/null @@ -1 +0,0 @@ -../README.md \ No newline at end of file diff --git a/entry_point.py b/entry_point.py new file mode 100644 index 0000000..5078d73 --- /dev/null +++ b/entry_point.py @@ -0,0 +1,4 @@ +from thu_learn_downloader.__main__ import main + +if __name__ == "__main__": + main() diff --git a/mkdocs.yaml b/mkdocs.yaml deleted file mode 100644 index 2724164..0000000 --- a/mkdocs.yaml +++ /dev/null @@ -1,23 +0,0 @@ -site_name: thu-learn-downloader -theme: - name: material - palette: - - media: "(prefers-color-scheme: dark)" - scheme: slate - toggle: - icon: material/brightness-4 - - media: "(prefers-color-scheme: light)" - scheme: default - toggle: - icon: material/brightness-7 -plugins: - - search - - git-revision-date-localized: - enable_creation_date: true - - git-committers: - repository: liblaf/thu-learn-downloader - branch: main - - git-authors -repo_url: https://github.com/liblaf/thu-learn-downloader -repo_name: liblaf/thu-learn-downloader -edit_uri: edit/main/docs diff --git a/poetry.lock b/poetry.lock index 40f70eb..7ffc26c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,5 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + [[package]] name = "altgraph" version = "0.17.3" @@ -5,6 +7,10 @@ category = "dev" optional = false python-versions = "*" +files = [ + {file = "altgraph-0.17.3-py2.py3-none-any.whl", hash = "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"}, + {file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"}, +] [[package]] name = "antlr4-python3-runtime" @@ -13,25 +19,21 @@ category = "main" optional = false python-versions = "*" - -[[package]] -name = "Babel" -version = "2.11.0" -description = "Internationalization utilities" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pytz = ">=2015.7" +files = [ + {file = "antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b"}, +] [[package]] name = "beautifulsoup4" -version = "4.11.1" +version = "4.11.2" description = "Screen-scraping library" category = "main" optional = false python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.11.2-py3-none-any.whl", hash = "sha256:0e79446b10b3ecb499c1556f7e228a53e64a2bfcebd455f370d8927cb5b59e39"}, + {file = "beautifulsoup4-4.11.2.tar.gz", hash = "sha256:bc4bdda6717de5a2987436fb8d72f45dc90dd856bdfd512a1314ce90349a0106"}, +] [package.dependencies] soupsieve = ">1.2" @@ -42,108 +44,129 @@ [[package]] name = "certifi" -version = "2022.9.24" +version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "3.0.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=3.6.0" - -[package.extras] -unicode_backport = ["unicodedata2"] - -[[package]] -name = "click" -version = "8.1.3" -description = "Composable command line interface toolkit" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" - -[[package]] -name = "commonmark" -version = "0.9.1" -description = "Python parser for the CommonMark Markdown spec" -category = "main" -optional = false python-versions = "*" - -[package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] - -[[package]] -name = "future" -version = "0.18.2" -description = "Clean single-source support for Python 3 and 2" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "ghp-import" -version = "2.1.0" -description = "Copy your docs directly to the gh-pages branch." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -python-dateutil = ">=2.8.1" - -[package.extras] -dev = ["flake8", "markdown", "twine", "wheel"] - -[[package]] -name = "gitdb" -version = "4.0.10" -description = "Git Object Database" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -smmap = ">=3.0.1,<6" - -[[package]] -name = "GitPython" -version = "3.1.29" -description = "GitPython is a python library used to interact with Git repositories" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -gitdb = ">=4.0.1,<5" +files = [ + {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, + {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, +] [[package]] name = "hydra-core" -version = "1.2.0" +version = "1.3.2" description = "A framework for elegantly configuring complex applications" category = "main" optional = false python-versions = "*" +files = [ + {file = "hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824"}, + {file = "hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b"}, +] [package.dependencies] antlr4-python3-runtime = ">=4.9.0,<4.10.0" -omegaconf = ">=2.2,<3.0" +omegaconf = ">=2.2,<2.4" packaging = "*" [[package]] @@ -153,34 +176,10 @@ category = "main" optional = false python-versions = ">=3.5" - -[[package]] -name = "Jinja2" -version = "3.1.2" -description = "A very fast and expressive template engine." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "lxml" -version = "4.9.1" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=0.29.7)"] +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] [[package]] name = "macholib" @@ -189,131 +188,62 @@ category = "dev" optional = false python-versions = "*" +files = [ + {file = "macholib-1.16.2-py2.py3-none-any.whl", hash = "sha256:44c40f2cd7d6726af8fa6fe22549178d3a4dfecc35a9cd15ea916d9c83a688e0"}, + {file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"}, +] [package.dependencies] altgraph = ">=0.17" [[package]] -name = "Markdown" -version = "3.3.7" -description = "Python implementation of Markdown." -category = "dev" +name = "markdown-it-py" +version = "2.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, + {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" [package.extras] -testing = ["coverage", "pyyaml"] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] -name = "MarkupSafe" -version = "2.1.1" -description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "main" optional = false python-versions = ">=3.7" - -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "mkdocs" -version = "1.4.2" -description = "Project documentation with Markdown." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} -ghp-import = ">=1.0" -jinja2 = ">=2.11.1" -markdown = ">=3.2.1,<3.4" -mergedeep = ">=1.3.4" -packaging = ">=20.5" -pyyaml = ">=5.1" -pyyaml-env-tag = ">=0.1" -watchdog = ">=2.0" - -[package.extras] -i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] - -[[package]] -name = "mkdocs-git-authors-plugin" -version = "0.6.5" -description = "Mkdocs plugin to display git authors of a page" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -mkdocs = ">=1.0" - -[[package]] -name = "mkdocs-git-committers-plugin-2" -version = "1.1.1" -description = "An MkDocs plugin to create a list of contributors on the page" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -beautifulsoup4 = "*" -gitpython = "*" -mkdocs = ">=1.0.3" -requests = "*" - -[[package]] -name = "mkdocs-git-revision-date-localized-plugin" -version = "1.1.0" -description = "Mkdocs plugin that enables displaying the localized date of the last git modification of a markdown file." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -babel = ">=2.7.0" -GitPython = "*" -mkdocs = ">=1.0" - -[[package]] -name = "mkdocs-material" -version = "8.5.10" -description = "Documentation that simply works" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -jinja2 = ">=3.0.2" -markdown = ">=3.2" -mkdocs = ">=1.4.0" -mkdocs-material-extensions = ">=1.1" -pygments = ">=2.12" -pymdown-extensions = ">=9.4" -requests = ">=2.26" - -[[package]] -name = "mkdocs-material-extensions" -version = "1.1.1" -description = "Extension pack for Python Markdown and MkDocs Material." -category = "dev" -optional = false -python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] [[package]] name = "omegaconf" -version = "2.2.3" +version = "2.3.0" description = "A flexible configuration library" category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b"}, + {file = "omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7"}, +] [package.dependencies] antlr4-python3-runtime = ">=4.9.0,<4.10.0" @@ -321,44 +251,64 @@ [[package]] name = "packaging" -version = "21.3" +version = "23.0" description = "Core utilities for Python packages" category = "main" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.7" +files = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] [[package]] name = "pefile" -version = "2022.5.30" +version = "2023.2.7" description = "Python PE parsing module" category = "dev" optional = false python-versions = ">=3.6.0" - -[package.dependencies] -future = "*" +files = [ + {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, + {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, +] [[package]] -name = "Pygments" -version = "2.13.0" +name = "pygments" +version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, +] [package.extras] plugins = ["importlib-metadata"] [[package]] name = "pyinstaller" -version = "5.6.2" +version = "5.8.0" description = "PyInstaller bundles a Python application and all its dependencies into a single package." category = "dev" optional = false python-versions = "<3.12,>=3.7" +files = [ + {file = "pyinstaller-5.8.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:502a2166165a8e8c3d99c19272e923d2548bac2132424d78910ef9dd8bb11705"}, + {file = "pyinstaller-5.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:bf1f7b7e88b467d7aefcdb2bc9cbd2e856ca88c5ab232c0efe0848f146d3bd5f"}, + {file = "pyinstaller-5.8.0-py3-none-manylinux2014_i686.whl", hash = "sha256:a62ee598b137202ef2e99d8dbaee6bc7379a6565c3ddf0331decb41b98eff1a2"}, + {file = "pyinstaller-5.8.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e68bcadf32edc1171ccb06117699a6a4f8e924b7c2c8812cfa00fd0186ade4ee"}, + {file = "pyinstaller-5.8.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ded780f0d3642d7bfc21d97b98d4ec4b41d2fe70c3f5c5d243868612f536e011"}, + {file = "pyinstaller-5.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f9361eff44c7108c2312f39d85ed768c4ada7e0aa729046bbcef3ef3c1577d18"}, + {file = "pyinstaller-5.8.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5c9632a20faecd6d79f0124afb31e6557414d19be271e572765b474f860f8d76"}, + {file = "pyinstaller-5.8.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8d004699c5d71c704c14a5f81eec233faa4f87a3bf0ae68e222b87d63f5dd17e"}, + {file = "pyinstaller-5.8.0-py3-none-win32.whl", hash = "sha256:3b74f50a57b1413047042e47033480b7324b091f23dff790a4494af32b377d94"}, + {file = "pyinstaller-5.8.0-py3-none-win_amd64.whl", hash = "sha256:4f4d818588e2d8de4bf24ed018056c3de0c95898ad25719e12d68626161b4933"}, + {file = "pyinstaller-5.8.0-py3-none-win_arm64.whl", hash = "sha256:bacf236b5c2f8f674723a39daca399646dceb470881f842f52e393b9a67ff2f8"}, + {file = "pyinstaller-5.8.0.tar.gz", hash = "sha256:314fb883caf3cbf06adbea2b77671bb73c3481568e994af0467ea7e47eb64755"}, +] [package.dependencies] altgraph = "*" @@ -366,74 +316,23 @@ pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} pyinstaller-hooks-contrib = ">=2021.4" pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} -setuptools = "*" +setuptools = ">=42.0.0" [package.extras] encryption = ["tinyaes (>=1.0.0)"] -hook_testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] [[package]] name = "pyinstaller-hooks-contrib" -version = "2022.13" +version = "2023.0" description = "Community maintained hooks for PyInstaller" category = "dev" optional = false python-versions = ">=3.7" - -[[package]] -name = "pymdown-extensions" -version = "9.9" -description = "Extension pack for Python Markdown." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -markdown = ">=3.2" - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-slugify" -version = "6.1.2" -description = "A Python slugify application that also handles Unicode" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[package.dependencies] -text-unidecode = ">=1.3" - -[package.extras] -unidecode = ["Unidecode (>=1.1.1)"] - -[[package]] -name = "pytz" -version = "2022.6" -description = "World timezone definitions, modern and historical" -category = "dev" -optional = false -python-versions = "*" +files = [ + {file = "pyinstaller-hooks-contrib-2023.0.tar.gz", hash = "sha256:bd578781cd6a33ef713584bf3726f7cd60a3e656ec08a6cc7971e39990808cc0"}, + {file = "pyinstaller_hooks_contrib-2023.0-py2.py3-none-any.whl", hash = "sha256:29d052eb73e0ab8f137f11df8e73d464c1c6d4c3044d9dc8df2af44639d8bfbf"}, +] [[package]] name = "pywin32-ctypes" @@ -442,403 +341,19 @@ category = "dev" optional = false python-versions = "*" +files = [ + {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, + {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, +] [[package]] -name = "PyYAML" +name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" category = "main" optional = false python-versions = ">=3.6" - -[[package]] -name = "pyyaml_env_tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyyaml = "*" - -[[package]] -name = "requests" -version = "2.28.1" -description = "Python HTTP for Humans." -category = "main" -optional = false -python-versions = ">=3.7, <4" - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rich" -version = "12.6.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "main" -optional = false -python-versions = ">=3.6.3,<4.0.0" - -[package.dependencies] -commonmark = ">=0.9.0,<0.10.0" -pygments = ">=2.6.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] - -[[package]] -name = "setuptools" -version = "65.6.3" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "smmap" -version = "5.0.0" -description = "A pure Python implementation of a sliding window memory map manager" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "soupsieve" -version = "2.3.2.post1" -description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "text-unidecode" -version = "1.3" -description = "The most basic Text::Unidecode port" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "urllib3" -version = "1.26.13" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "watchdog" -version = "2.1.9" -description = "Filesystem events monitoring" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[metadata] -lock-version = "1.1" -python-versions = ">=3.10,<3.12" -content-hash = "28cad533c85919d3a1ffeb3f51f3a2adcc15e6d8fb1d883a6f4f651fca329663" - -[metadata.files] -altgraph = [ - {file = "altgraph-0.17.3-py2.py3-none-any.whl", hash = "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"}, - {file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"}, -] -antlr4-python3-runtime = [ - {file = "antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b"}, -] -Babel = [ - {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, - {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, -] -beautifulsoup4 = [ - {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, - {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, -] -certifi = [ - {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, - {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -colorama = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -commonmark = [ - {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, - {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, -] -future = [ - {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, -] -ghp-import = [ - {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, - {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, -] -gitdb = [ - {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, - {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, -] -GitPython = [ - {file = "GitPython-3.1.29-py3-none-any.whl", hash = "sha256:41eea0deec2deea139b459ac03656f0dd28fc4a3387240ec1d3c259a2c47850f"}, - {file = "GitPython-3.1.29.tar.gz", hash = "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd"}, -] -hydra-core = [ - {file = "hydra-core-1.2.0.tar.gz", hash = "sha256:4990721ce4ac69abafaffee566d6b63a54faa6501ecce65b338d3251446ff634"}, - {file = "hydra_core-1.2.0-py3-none-any.whl", hash = "sha256:b6614fd6d6a97a9499f7ddbef02c9dd38f2fec6a9bc83c10e248db1dae50a528"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -Jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] -lxml = [ - {file = "lxml-4.9.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed"}, - {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc"}, - {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc"}, - {file = "lxml-4.9.1-cp27-cp27m-win32.whl", hash = "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3"}, - {file = "lxml-4.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627"}, - {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84"}, - {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837"}, - {file = "lxml-4.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad"}, - {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5"}, - {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8"}, - {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8"}, - {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d"}, - {file = "lxml-4.9.1-cp310-cp310-win32.whl", hash = "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7"}, - {file = "lxml-4.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b"}, - {file = "lxml-4.9.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d"}, - {file = "lxml-4.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3"}, - {file = "lxml-4.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29"}, - {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d"}, - {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318"}, - {file = "lxml-4.9.1-cp35-cp35m-win32.whl", hash = "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7"}, - {file = "lxml-4.9.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4"}, - {file = "lxml-4.9.1-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb"}, - {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067"}, - {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536"}, - {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8"}, - {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b"}, - {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf"}, - {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3"}, - {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391"}, - {file = "lxml-4.9.1-cp36-cp36m-win32.whl", hash = "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e"}, - {file = "lxml-4.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7"}, - {file = "lxml-4.9.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2"}, - {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc"}, - {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c"}, - {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4"}, - {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3"}, - {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca"}, - {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785"}, - {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785"}, - {file = "lxml-4.9.1-cp37-cp37m-win32.whl", hash = "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a"}, - {file = "lxml-4.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e"}, - {file = "lxml-4.9.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b"}, - {file = "lxml-4.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97"}, - {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21"}, - {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2"}, - {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130"}, - {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715"}, - {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036"}, - {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387"}, - {file = "lxml-4.9.1-cp38-cp38-win32.whl", hash = "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94"}, - {file = "lxml-4.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345"}, - {file = "lxml-4.9.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67"}, - {file = "lxml-4.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb"}, - {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448"}, - {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7"}, - {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91"}, - {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000"}, - {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25"}, - {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd"}, - {file = "lxml-4.9.1-cp39-cp39-win32.whl", hash = "sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb"}, - {file = "lxml-4.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d"}, - {file = "lxml-4.9.1-pp37-pypy37_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c"}, - {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b"}, - {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc"}, - {file = "lxml-4.9.1-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b"}, - {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2"}, - {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73"}, - {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c"}, - {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9"}, - {file = "lxml-4.9.1.tar.gz", hash = "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"}, -] -macholib = [ - {file = "macholib-1.16.2-py2.py3-none-any.whl", hash = "sha256:44c40f2cd7d6726af8fa6fe22549178d3a4dfecc35a9cd15ea916d9c83a688e0"}, - {file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"}, -] -Markdown = [ - {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, - {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, -] -MarkupSafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -mergedeep = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] -mkdocs = [ - {file = "mkdocs-1.4.2-py3-none-any.whl", hash = "sha256:c8856a832c1e56702577023cd64cc5f84948280c1c0fcc6af4cd39006ea6aa8c"}, - {file = "mkdocs-1.4.2.tar.gz", hash = "sha256:8947af423a6d0facf41ea1195b8e1e8c85ad94ac95ae307fe11232e0424b11c5"}, -] -mkdocs-git-authors-plugin = [ - {file = "mkdocs-git-authors-plugin-0.6.5.tar.gz", hash = "sha256:5e10d1685745e128b0bc88e229c13cf62bab2b5a1b716b68301216b94e206bd4"}, - {file = "mkdocs_git_authors_plugin-0.6.5-py3-none-any.whl", hash = "sha256:b82aad1a476a673d0746e8ce5ccccbd39822fc730ef0a6a113ec8517d30016fc"}, -] -mkdocs-git-committers-plugin-2 = [ - {file = "mkdocs-git-committers-plugin-2-1.1.1.tar.gz", hash = "sha256:4f6eb6137f35967dfa444703b6ea293f05bf2fd183506bc51db8fb21b061d5a3"}, - {file = "mkdocs_git_committers_plugin_2-1.1.1-py3-none-any.whl", hash = "sha256:14d4a89bf8965ab62ca9b8b0cd90f6c9b421bb89bfedca0d91c5119f18791360"}, -] -mkdocs-git-revision-date-localized-plugin = [ - {file = "mkdocs-git-revision-date-localized-plugin-1.1.0.tar.gz", hash = "sha256:38517e2084229da1a1b9460e846c2748d238c2d79efd405d1b9174a87bd81d79"}, - {file = "mkdocs_git_revision_date_localized_plugin-1.1.0-py3-none-any.whl", hash = "sha256:4ba0e49abea3e9f6ee26e2623ff7283873da657471c61f1d0cfbb986f403316d"}, -] -mkdocs-material = [ - {file = "mkdocs_material-8.5.10-py3-none-any.whl", hash = "sha256:51760fa4c9ee3ca0b3a661ec9f9817ec312961bb84ff19e5b523fdc5256e1d6c"}, - {file = "mkdocs_material-8.5.10.tar.gz", hash = "sha256:7623608f746c6d9ff68a8ef01f13eddf32fa2cae5e15badb251f26d1196bc8f1"}, -] -mkdocs-material-extensions = [ - {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, - {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"}, -] -omegaconf = [ - {file = "omegaconf-2.2.3-py3-none-any.whl", hash = "sha256:d6f2cbf79a992899eb76c6cb1aedfcf0fe7456a8654382edd5ee0c1b199c0657"}, - {file = "omegaconf-2.2.3.tar.gz", hash = "sha256:59ff9fba864ffbb5fb710b64e8a9ba37c68fa339a2e2bb4f1b648d6901552523"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pefile = [ - {file = "pefile-2022.5.30.tar.gz", hash = "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"}, -] -Pygments = [ - {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, - {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, -] -pyinstaller = [ - {file = "pyinstaller-5.6.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:1b1e3b37a22fb36555d917f0c3dfb998159ff4af6d8fa7cc0074d630c6fe81ad"}, - {file = "pyinstaller-5.6.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:05df5d2b9ca645cc6ef61d8a85451d2aabe5501997f1f50cd94306fd6bc0485d"}, - {file = "pyinstaller-5.6.2-py3-none-manylinux2014_i686.whl", hash = "sha256:eb083c25f711769af0898852ea30dcb727ba43990bbdf9ffbaa9c77a7bd0d720"}, - {file = "pyinstaller-5.6.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:0d167d57036219914188f1400427dd297b975707e78c32a5511191e607be920a"}, - {file = "pyinstaller-5.6.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:32727232f446aa96e394f01b0c35b3de0dc3513c6ba3e26d1ef64c57edb1e9e5"}, - {file = "pyinstaller-5.6.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:181856ade585b090379ae26b7017dc2c30620e36e3a804b381417a6dc3b2a82b"}, - {file = "pyinstaller-5.6.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:77888f52b61089caa0bee70809bbce9e9b1c613c88b6cb0742ff2a45f1511cbb"}, - {file = "pyinstaller-5.6.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d888db9afedff290d362ee296d30eb339abeba707ca1565916ce1cd5947131c3"}, - {file = "pyinstaller-5.6.2-py3-none-win32.whl", hash = "sha256:e026adc92c60158741d0bfca27eefaa2414801f61328cb84d0c88241fe8c2087"}, - {file = "pyinstaller-5.6.2-py3-none-win_amd64.whl", hash = "sha256:04ecf805bde2ef25b8e3642410871e6747c22fa7254107f155b8cd179c2a13b6"}, - {file = "pyinstaller-5.6.2.tar.gz", hash = "sha256:865025b6809d777bb0f66d8f8ab50cc97dc3dbe0ff09a1ef1f2fd646432714fc"}, -] -pyinstaller-hooks-contrib = [ - {file = "pyinstaller-hooks-contrib-2022.13.tar.gz", hash = "sha256:e06d0881e599d94dc39c6ed1917f0ad9b1858a2478b9892faac18bd48bcdc2de"}, - {file = "pyinstaller_hooks_contrib-2022.13-py2.py3-none-any.whl", hash = "sha256:91ecb30db757a8db8b6661d91d5df99e0998245f05f5cfaade0550922c7030a3"}, -] -pymdown-extensions = [ - {file = "pymdown_extensions-9.9-py3-none-any.whl", hash = "sha256:ac698c15265680db5eb13cd4342abfcde2079ac01e5486028f47a1b41547b859"}, - {file = "pymdown_extensions-9.9.tar.gz", hash = "sha256:0f8fb7b74a37a61cc34e90b2c91865458b713ec774894ffad64353a5fce85cfc"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -python-dateutil = [ - {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"}, -] -python-slugify = [ - {file = "python-slugify-6.1.2.tar.gz", hash = "sha256:272d106cb31ab99b3496ba085e3fea0e9e76dcde967b5e9992500d1f785ce4e1"}, - {file = "python_slugify-6.1.2-py2.py3-none-any.whl", hash = "sha256:7b2c274c308b62f4269a9ba701aa69a797e9bca41aeee5b3a9e79e36b6656927"}, -] -pytz = [ - {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"}, - {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"}, -] -pywin32-ctypes = [ - {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, - {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, -] -PyYAML = [ +files = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -880,66 +395,95 @@ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] -pyyaml_env_tag = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, + +[[package]] +name = "requests" +version = "2.28.2" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, ] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "13.3.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.3.1-py3-none-any.whl", hash = "sha256:8aa57747f3fc3e977684f0176a88e789be314a99f99b43b75d1e9cb5dc6db9e9"}, + {file = "rich-13.3.1.tar.gz", hash = "sha256:125d96d20c92b946b983d0d392b84ff945461e5a06d3867e9f9e575f8697b67f"}, ] -rich = [ - {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, - {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, + +[package.dependencies] +markdown-it-py = ">=2.1.0,<3.0.0" +pygments = ">=2.14.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "setuptools" +version = "67.4.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"}, + {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"}, ] -setuptools = [ - {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, - {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "soupsieve" +version = "2.4" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "soupsieve-2.4-py3-none-any.whl", hash = "sha256:49e5368c2cda80ee7e84da9dbe3e110b70a4575f196efb74e51b94549d921955"}, + {file = "soupsieve-2.4.tar.gz", hash = "sha256:e28dba9ca6c7c00173e34e4ba57448f0688bb681b7c5e8bf4971daafc093d69a"}, ] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + +[[package]] +name = "urllib3" +version = "1.26.14" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, ] -smmap = [ - {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, - {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, -] -soupsieve = [ - {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, - {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, -] -text-unidecode = [ - {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, - {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, -] -urllib3 = [ - {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, - {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, -] -watchdog = [ - {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"}, - {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d"}, - {file = "watchdog-2.1.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658"}, - {file = "watchdog-2.1.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591"}, - {file = "watchdog-2.1.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33"}, - {file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846"}, - {file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3"}, - {file = "watchdog-2.1.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654"}, - {file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39"}, - {file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7"}, - {file = "watchdog-2.1.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd"}, - {file = "watchdog-2.1.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3"}, - {file = "watchdog-2.1.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d"}, - {file = "watchdog-2.1.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_armv7l.whl", hash = "sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_i686.whl", hash = "sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64.whl", hash = "sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_s390x.whl", hash = "sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6"}, - {file = "watchdog-2.1.9-py3-none-win32.whl", hash = "sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1"}, - {file = "watchdog-2.1.9-py3-none-win_amd64.whl", hash = "sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c"}, - {file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"}, - {file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"}, -] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.11,<3.12" +content-hash = "2ea0a8023516ec4064839aafc36b6fcf2fa4ca5dac5f3ccae81d60128403cf13" diff --git a/pyproject.toml b/pyproject.toml index b0302b0..3925262 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,44 +1,25 @@ [build-system] -requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" +requires = ["poetry-core"] -[tool.commitizen] -name = "cz_conventional_commits" -version = "0.1.1" -tag_format = "$version" -version_files = ["pyproject.toml:version"] - -[tool.iosrt] +[tool.isort] profile = "black" [tool.poetry] -name = "thu-learn-downloader" -version = "0.1.1" -description = "Auto download files from thu-learn" authors = ["Qin Li "] +description = "Download everything from Web Learning of Tsinghua University" +name = "thu-learn-downloader" +packages = [{include = "thu_learn_downloader"}] readme = "README.md" -license = "MIT" -homepage = "https://liblaf.github.io/thu-learn-downloader/" -documentation = "https://liblaf.github.io/thu-learn-downloader/" repository = "https://github.com/liblaf/thu-learn-downloader" +version = "0.1.1" [tool.poetry.dependencies] -python = ">=3.10,<3.12" -beautifulsoup4 = "^4.11.1" -hydra-core = "^1.2.0" -python-slugify = "^6.1.2" -requests = "^2.28.1" -rich = "^12.6.0" +beautifulsoup4 = "^4.11.2" +hydra-core = "^1.3.1" +python = ">=3.11,<3.12" +requests = "^2.28.2" +rich = "^13.3.1" -[tool.poetry.group.build.dependencies] -pyinstaller = "^5.6.2" - -[tool.poetry.group.docs.dependencies] -lxml = "^4.9.1" -mkdocs-git-authors-plugin = "^0.6.4" -mkdocs-git-committers-plugin-2 = "^1.1.1" -mkdocs-git-revision-date-localized-plugin = "^1.1.0" -mkdocs-material = "^8.5.7" - -[tool.poetry.scripts] -build = "thu_learn_downloader.build:run" +[tool.poetry.group.dev.dependencies] +pyinstaller = "^5.8.0" diff --git a/requirements.txt b/requirements.txt index 76c413a..21b7224 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,16 @@ -antlr4-python3-runtime==4.9.3 ; python_version >= "3.10" and python_version < "3.12" -beautifulsoup4==4.11.1 ; python_version >= "3.10" and python_version < "3.12" -certifi==2022.9.24 ; python_version >= "3.10" and python_version < "3.12" -charset-normalizer==2.1.1 ; python_version >= "3.10" and python_version < "3.12" -commonmark==0.9.1 ; python_version >= "3.10" and python_version < "3.12" -hydra-core==1.2.0 ; python_version >= "3.10" and python_version < "3.12" -idna==3.4 ; python_version >= "3.10" and python_version < "3.12" -omegaconf==2.2.3 ; python_version >= "3.10" and python_version < "3.12" -packaging==21.3 ; python_version >= "3.10" and python_version < "3.12" -pygments==2.13.0 ; python_version >= "3.10" and python_version < "3.12" -pyparsing==3.0.9 ; python_version >= "3.10" and python_version < "3.12" -python-slugify==6.1.2 ; python_version >= "3.10" and python_version < "3.12" -pyyaml==6.0 ; python_version >= "3.10" and python_version < "3.12" -requests==2.28.1 ; python_version >= "3.10" and python_version < "3.12" -rich==12.6.0 ; python_version >= "3.10" and python_version < "3.12" -soupsieve==2.3.2.post1 ; python_version >= "3.10" and python_version < "3.12" -text-unidecode==1.3 ; python_version >= "3.10" and python_version < "3.12" -urllib3==1.26.13 ; python_version >= "3.10" and python_version < "3.12" +antlr4-python3-runtime==4.9.3 ; python_version >= "3.11" and python_version < "3.12" +beautifulsoup4==4.11.2 ; python_version >= "3.11" and python_version < "3.12" +certifi==2022.12.7 ; python_version >= "3.11" and python_version < "3.12" +charset-normalizer==3.0.1 ; python_version >= "3.11" and python_version < "3.12" +hydra-core==1.3.2 ; python_version >= "3.11" and python_version < "3.12" +idna==3.4 ; python_version >= "3.11" and python_version < "3.12" +markdown-it-py==2.2.0 ; python_version >= "3.11" and python_version < "3.12" +mdurl==0.1.2 ; python_version >= "3.11" and python_version < "3.12" +omegaconf==2.3.0 ; python_version >= "3.11" and python_version < "3.12" +packaging==23.0 ; python_version >= "3.11" and python_version < "3.12" +pygments==2.14.0 ; python_version >= "3.11" and python_version < "3.12" +pyyaml==6.0 ; python_version >= "3.11" and python_version < "3.12" +requests==2.28.2 ; python_version >= "3.11" and python_version < "3.12" +rich==13.3.1 ; python_version >= "3.11" and python_version < "3.12" +soupsieve==2.4 ; python_version >= "3.11" and python_version < "3.12" +urllib3==1.26.14 ; python_version >= "3.11" and python_version < "3.12" diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..9157642 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +function run() { + if command -v gum > /dev/null 2>&1; then + prefix="$(gum style --background=14 --padding="0 1" RUN)" + message="$(gum style --foreground=14 "${*}")" + gum join --horizontal "${prefix}" " " "${message}" + fi + "${@}" +} + +workspace="$(git rev-parse --show-toplevel || pwd)" +cd "${workspace}" +name="$(poetry version | awk '{ print $1 }')" + +run poetry install --with dev +run poetry run pyinstaller --onefile --name "${name}" "${workspace}/entry_point.py" diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100644 index 0000000..e6a644c --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +function run() { + if command -v gum > /dev/null 2>&1; then + prefix="$(gum style --background=14 --padding="0 1" RUN)" + message="$(gum style --foreground=14 "${*}")" + gum join --horizontal "${prefix}" " " "${message}" + fi + "${@}" +} + +workspace="$(git rev-parse --show-toplevel || pwd)" + +run isort --profile black "${workspace}" +run black "${workspace}" diff --git a/scripts/install.sh b/scripts/install.sh index d8b9302..8d6621d 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,53 +1,32 @@ -#!/usr/bin/bash +#!/bin/bash set -o errexit set -o nounset set -o pipefail -function exists() { - command -v "${@}" > /dev/null 2>&1 -} +BIN="${BIN:-"${HOME}/.local/bin"}" -function info() { - if exists rich; then - rich --print --style "bold bright_blue" "${*}" - else - echo -e -n "\x1b[1;94m" - echo -n "${*}" - echo -e "\x1b[0m" +function run() { + if command -v gum > /dev/null 2>&1; then + prefix="$(gum style --background=14 --padding="0 1" RUN)" + message="$(gum style --foreground=14 "${*}")" + gum join --horizontal "${prefix}" " " "${message}" fi -} - -function success() { - if exists rich; then - rich --print --style "bold bright_green" "${*}" - else - echo -e -n "\x1b[1;92m" - echo -n "${*}" - echo -e "\x1b[0m" - fi -} - -function call() { - info "+ ${*}" "${@}" } function copy() { - mkdir --parents "$(realpath --canonicalize-missing ${2}/..)" - cp --force --recursive "${1}" "${2}" - success "Copy: ${1} -> ${2}" + mkdir --parents "$(dirname "${2}")" + cp "${1}" "${2}" + if command -v gum > /dev/null 2>&1; then + prefix="$(gum style --background=10 --padding="0 1" COPY)" + message="$(gum style --foreground=10 "${1} -> ${2}")" + gum join --horizontal "${prefix}" " " "${message}" + fi } -cd "$(git rev-parse --show-toplevel || echo .)" -call poetry run build -files=(dist/*) -for file in "${files[@]}"; do - case "${file}" in - *.tar.gz) ;; - *.whl) ;; - *) - mkdir --parents "${HOME}/.local/bin" - copy "${file}" "${HOME}/.local/bin" - ;; - esac -done +workspace="$(git rev-parse --show-toplevel || pwd)" +cd "${workspace}" +name="$(poetry version | awk '{ print $1 }')" + +run bash "${workspace}/scripts/build.sh" +copy "${workspace}/dist/${name}" "${BIN}/${name}" diff --git a/scripts/template.sh b/scripts/template.sh deleted file mode 100644 index 91e9cd4..0000000 --- a/scripts/template.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/bash -set -o errexit -set -o nounset -set -o pipefail - -function exists() { - command -v "${@}" > /dev/null 2>&1 -} - -function info() { - if exists rich; then - rich --print --style "bold bright_blue" "${*}" - else - echo -e -n "\x1b[1;94m" - echo -n "${*}" - echo -e "\x1b[0m" - fi -} - -function call() { - info "+ ${*}" - "${@}" -} - -cd "$(git rev-parse --show-toplevel || echo .)" -REPO_NAME="$(basename "$(pwd)")" - -description="${*}" -echo "# ${REPO_NAME}" > "README.md" -if [[ -n ${description} ]]; then - echo "" >> "README.md" - echo "${description}" >> "README.md" -fi - -files=( - "mkdocs.yaml" - "pyproject.toml" -) -for file in "${files[@]}"; do - call sed --in-place "s@template@${REPO_NAME}@g" "${file}" -done - -call sed --in-place "s@description =.*@description = \"${description}\"@g" pyproject.toml - -call gh repo edit --description "${description}" -call gh repo edit --homepage "https://liblaf.github.io/${REPO_NAME}/" - -call git add --all -call git commit --message "build: initialize" --gpg-sign -call git push diff --git a/thu_learn_downloader/__main__.py b/thu_learn_downloader/__main__.py index 6a8affb..9ef3ca4 100644 --- a/thu_learn_downloader/__main__.py +++ b/thu_learn_downloader/__main__.py @@ -1,50 +1,56 @@ import os -import sys import hydra -import omegaconf -import rich.console -import rich.live -import rich.panel -import rich.progress -import rich.table +from omegaconf import DictConfig +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 thu_learn_downloader import sync -from thu_learn_downloader.downloader import Downloader -from thu_learn_downloader.helper import LearnHelper +from . import sync +from .constants import MAX_ACTIVE_TASKS, SUCCESS_PREFIX +from .downloader import Downloader +from .helper import Helper -@hydra.main(config_path=os.getcwd(), config_name="config.yaml", version_base="1.2") -def main(config: omegaconf.DictConfig) -> int: - helper = LearnHelper( - username=config.get("username"), password=config.get("password") - ) +@hydra.main(config_path=os.getcwd(), config_name="config.yaml", version_base="1.3") +def main(config: DictConfig) -> None: + helper = Helper() downloader = Downloader() - overall_progress = rich.progress.Progress( - rich.progress.TextColumn("{task.description}", style="bold bright_blue"), - rich.progress.BarColumn(), - rich.progress.MofNCompleteColumn(), - rich.progress.TimeElapsedColumn(), + 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 = rich.console.Group( - rich.panel.Panel(overall_progress), - rich.panel.Panel(downloader.progress), + progress_group = Group( + Panel(downloader.progress, height=MAX_ACTIVE_TASKS + 2), + Panel(overall_progress), ) - with rich.live.Live(progress_group) as live: + username: str = config.get("username") + password: str = config.get("password") + with Live(progress_group) as live: with downloader.pool: try: - helper.login() + helper.login(username=username, password=password) except: live.console.log( - f"Login as {helper.username} {helper.status or 'FAILED'}", + f"Login as {username} FAILED", style="bold bright_red", ) else: live.console.log( - f"Login as {helper.username} {helper.status}", + SUCCESS_PREFIX, + f"Login as {username} SUCCESS", style="bold bright_green", ) sync.sync_all( @@ -57,8 +63,6 @@ courses_task_id=courses_task_id, ) - return 0 - if __name__ == "__main__": - sys.exit(main()) + pass diff --git a/thu_learn_downloader/build.py b/thu_learn_downloader/build.py deleted file mode 100644 index b1cd9f9..0000000 --- a/thu_learn_downloader/build.py +++ /dev/null @@ -1,14 +0,0 @@ -import os.path - -import PyInstaller.__main__ - - -def run(): - PyInstaller.__main__.run( - [ - os.path.join(__package__, "__main__.py"), - "--onefile", - "--name", - "thu-learn-downloader", - ] - ) diff --git a/thu_learn_downloader/constants.py b/thu_learn_downloader/constants.py new file mode 100644 index 0000000..2fd78c2 --- /dev/null +++ b/thu_learn_downloader/constants.py @@ -0,0 +1,60 @@ +from pathlib import Path + +from rich.style import Style + +from . import typing as t + +BS_FEATURES = "html.parser" +CHUNK_SIZE = 1024 * 1024 +DEFAULT_PREFIX = Path.home() / "Desktop" / "thu-learn" +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/downloader.py b/thu_learn_downloader/downloader.py index dd2d2c3..27fe9d3 100644 --- a/thu_learn_downloader/downloader.py +++ b/thu_learn_downloader/downloader.py @@ -1,146 +1,181 @@ -import concurrent.futures -import datetime import os +import os.path import time -import typing +from concurrent.futures import Executor, ThreadPoolExecutor +from datetime import datetime +from pathlib import Path +from typing import Optional import requests -import rich.console -import rich.progress +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, - file: str, - raw_size: typing.Optional[int] = 0, - upload_time: typing.Optional[datetime.datetime] = None, - session: typing.Optional[requests.Session] = None, - console: rich.console.Console = rich.console.Console(), - progress: rich.progress.Progress = rich.progress.Progress(), - task_id: rich.progress.TaskID = rich.progress.TaskID(0), + 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: - res = (session or requests).get(url=url, stream=True) - raw_size = int(res.headers.get("Content-Length", 0)) or raw_size - if os.path.exists(file): - if os.path.getsize(file) == raw_size: - if upload_time: - mtime = os.path.getmtime(filename=file) - mtime = datetime.datetime.fromtimestamp(mtime) - if mtime >= upload_time: - return False - os.makedirs(name=os.path.dirname(file), exist_ok=True) + 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=file, mode="wb") as fp: + with open(file=output, mode="wb") as fp: progress.start_task(task_id=task_id) - for chunk in res.iter_content(chunk_size=1024 * 1024): + 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 = int(upload_time.timestamp()) - os.utime(path=file, times=(mtime, mtime)) + mtime: float = upload_time.timestamp() + os.utime(path=output, times=(mtime, mtime)) + return True def download( url: str, - file: str, - raw_size: typing.Optional[int] = None, - upload_time: typing.Optional[datetime.datetime] = None, - session: typing.Optional[requests.Session] = None, - console: rich.console.Console = rich.console.Console(), - progress: rich.progress.Progress = rich.progress.Progress(), - task_id: rich.progress.TaskID = rich.progress.TaskID(0), + 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( - f"Retry {i}: {progress.tasks[task_id].description}", - style="bold bright_blue", + RETRY_PREFIX, + i, + description, + style=style + Style(color="bright_yellow"), ) - ret = download_once( + rtn: bool = download_once( url=url, - file=file, + output=output, + session=session, raw_size=raw_size, upload_time=upload_time, - session=session, - console=console, progress=progress, task_id=task_id, ) except: console.log( - f"Download Failed: {progress.tasks[task_id].description}", - style="bold bright_red", + FAILURE_PREFIX, + description, + style=style + Style(color="bright_red"), ) else: - if ret: - console.log( - f"Download Success: {progress.tasks[task_id].description}", - style="bold bright_green", - ) + if rtn: + console.log(SUCCESS_PREFIX, description, style=style) else: console.log( - f"Download Skipped: {progress.tasks[task_id].description}", - style="dim", + SKIPPED_PREFIX, + description, + style=style + Style(dim=True), ) - progress.update(task_id=task_id, visible=False) - return + break + progress.update(task_id=task_id, visible=False) class Downloader: - pool: concurrent.futures.Executor - progress: rich.progress.Progress + pool: Executor + progress: Progress def __init__(self) -> None: - self.pool = concurrent.futures.ThreadPoolExecutor() - self.progress = rich.progress.Progress( - rich.progress.TextColumn( - text_format="{task.description}", style="bold blue" - ), - rich.progress.BarColumn(), - rich.progress.DownloadColumn(), - rich.progress.TaskProgressColumn(), - rich.progress.TransferSpeedColumn(), - rich.progress.TimeRemainingColumn(), + self.pool = ThreadPoolExecutor() + self.progress = Progress( + TextColumn("{task.description}"), + BarColumn(), + DownloadColumn(), + TaskProgressColumn(), + TimeElapsedColumn(), + TimeRemainingColumn(), + TransferSpeedColumn(), ) def schedule_download( self, url: str, - file: str, - raw_size: typing.Optional[int] = None, - upload_time: typing.Optional[datetime.datetime] = None, - session: typing.Optional[requests.Session] = None, - console: rich.console.Console = rich.console.Console(), - description: typing.Optional[str] = None, + 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: - running_tasks = 0 - for task in self.progress.tasks: - if task.visible: - running_tasks += 1 - if running_tasks < 16: + 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) - - task_id = self.progress.add_task( - description=description or file, start=False, total=raw_size + 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, - file=file, + output=output, + session=session, raw_size=raw_size, upload_time=upload_time, - session=session, - console=console, 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 index ccd5c16..830a69b 100644 --- a/thu_learn_downloader/helper.py +++ b/thu_learn_downloader/helper.py @@ -1,268 +1,136 @@ -import datetime -import typing import urllib.parse -import bs4 -import requests -import requests.adapters +from bs4 import BeautifulSoup, Tag +from requests import Request, Response, Session -from . import parser, types, urls +from . import parser +from . import typing as t +from . import urls +from .constants import BS_FEATURES -class LearnHelper(requests.Session): - username: str - password: str - status: str = "" +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 __init__(self, username: str, password: str) -> None: - super().__init__() - self.username = username - self.password = password - self.mount( - prefix="https://", - adapter=requests.adapters.HTTPAdapter( - max_retries=4, - ), + 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", default="") + return self.cookies.get(name="XSRF-TOKEN") - def get_with_token(self, url: types.URL) -> requests.Response: - if url.query: - assert isinstance(url.query, dict) - url.query["_csrf"] = self.token - else: - url.query = { - "_csrf": self.token, - } - return self.get(url.str()) - - def login(self) -> bool: - response: requests.Response = self.get(urls.LEARN_PREFIX.str()) - soup = bs4.BeautifulSoup(response.text, features="html.parser") - form = typing.cast(bs4.Tag, soup.select_one("#loginForm")) - action = typing.cast(str, form["action"]) - payload = urls.id_login_form_data( - username=self.username, - password=self.password, - ).query - response = self.post(url=action, data=payload) - soup = bs4.BeautifulSoup(response.text, features="html.parser") - a = typing.cast(bs4.Tag, soup.select_one("a")) - href = typing.cast(str, a["href"]) - parse_result = urllib.parse.urlparse(url=href) - query = urllib.parse.parse_qs(qs=parse_result.query) - self.status = query["status"][0] - self.ticket = query["ticket"][0] - - response = self.get(href) - - url = urls.learn_auth_roam(ticket=self.ticket) - response = self.get(url.str()) - - url = urls.learn_student_course_list_page() - response = self.get(url.str()) - - return self.status == "SUCCESS" - - def logout(self) -> None: - url = urls.learn_logout() - response = self.post(url.str()) + 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]: - url = urls.learn_semester_list() - response = self.get_with_token(url) - json = response.json() - return [semester_id for semester_id in json if semester_id] - - def get_current_semester(self) -> types.SemesterInfo: - url = urls.learn_current_semester() - response = self.get_with_token(url) - json = response.json() - result = json["result"] - return types.SemesterInfo( - id=result["id"], - start_date=datetime.datetime.strptime(result["kssj"], r"%Y-%m-%d").date(), - end_date=datetime.datetime.strptime(result["jssj"], r"%Y-%m-%d").date(), - start_year=int(result["xnxq"][0:4]), - end_year=int(result["xnxq"][5:9]), - type=typing.cast(types.SemesterType, int(result["xnxq"][10:])), - ) + 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: types.CourseType = types.CourseType.STUDENT, - ) -> list[types.CourseInfo]: - url = urls.learn_course_list(semester_id, course_type) - response = self.get_with_token(url) - json = response.json() - result = json["resultList"] - courses = [ - parser.parse_course_info(course=course, course_type=course_type) - for course in result - ] - return courses + 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 list() + return list(map(parser.parse_course_info, results)) def get_file_list( - self, - course_id: str, - course_type: types.CourseType = types.CourseType.STUDENT, - ) -> list[types.File]: - url = urls.learn_file_classify(course_id=course_id) - response = self.get_with_token(url) - json = response.json() - records = json["object"]["rows"] - clazz = {} - for record in records: - clazz[record["kjflid"]] = record["bt"] # 课件分类 ID, 标题 - - url = urls.learn_file_list( - course_id=course_id, - course_type=course_type, + 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) ) - response = self.get_with_token(url) - json = response.json() - result = [] - if course_type == types.CourseType.STUDENT: - result = json["object"] - else: # teacher - result = json["object"]["resultList"] + file_clazz: dict[str, str] = dict() + rows = resp.json()["object"]["rows"] + for row in rows: + file_clazz[row["kjflid"]] = row["bt"] # 课件分类 ID, 标题 - files = [ - parser.parse_file( - file=file, course_id=course_id, clazz=clazz, course_type=course_type + 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"] + + return list( + map( + parser.parse_file, + results, + [file_clazz] * len(results), + [course_id] * len(results), + [course_type] * len(results), ) - for file in result - ] - return files + ) def get_homework_list( - self, course_id: str, course_type: types.CourseType = types.CourseType.STUDENT - ) -> list[types.Homework]: - result = [] - url = urls.learn_homework_list_new(course_id=course_id) - result += self.get_homework_list_at_url( - url=url, - status=types.HomeworkStatus(submitted=False, graded=False), - ) - url = urls.learn_homework_list_submitted(course_id=course_id) - result += self.get_homework_list_at_url( - url=url, - status=types.HomeworkStatus(submitted=True, graded=False), - ) - url = urls.learn_homework_list_graded(course_id=course_id) - result += self.get_homework_list_at_url( - url=url, - status=types.HomeworkStatus(submitted=True, graded=True), - ) - - return result + 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, url: types.URL, status: types.HomeworkStatus - ) -> list[types.Homework]: - response = self.get_with_token(url) - json = response.json() - - result = json["object"]["aaData"] - - def parse_homework(work: dict) -> types.Homework: - detail = self.parse_homework_detail( - course_id=work["wlkcid"], # 课程 ID - homework_id=work["zyid"], # 作业 ID - student_homework_id=work["xszyid"], # 学生作业 ID + 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 list() + return list( + map( + parser.parse_homework, + res, + [status] * len(res), + [self] * len(res), ) - - return types.Homework( - id=work["zyid"], # 作业 ID - student_homework_id=work["xszyid"], # 学生作业 ID - title=work["bt"], # 标题 - url=str( - urls.learn_homework_detail( - course_id=work["wlkcid"], # 课程 ID - homework_id=work["zyid"], # 作业 ID - student_homework_id=work["xszyid"], # 学生作业 ID - ) - ), - deadline=datetime.datetime.fromtimestamp(work["jzsj"] / 1000.0) - if work["jzsj"] - else None, # 截止时间 - submit_url=urls.learn_homework_submit( - work["wlkcid"], work["xszyid"] # 课程 ID, 学生作业 ID - ).str(), - submit_time=datetime.datetime.fromtimestamp(work["scsj"] / 1000.0) - if work["scsj"] - else None, # 上传时间 - grade=work["cj"], # 成绩 - grader_name=work["jsm"], # 教师名 - grade_content=work["pynr"], # 批阅内容 - grade_time=datetime.datetime.fromtimestamp(work["pysj"] / 1000.0) - if work["pysj"] - else None, # 批阅时间 - submitted=status.submitted, - graded=status.graded, - description=detail.description, - answer_content=detail.answer_content, - submitted_content=detail.submitted_content, - attachment=detail.attachment, - answer_attachment=detail.answer_attachment, - submitted_attachment=detail.submitted_attachment, - grade_attachment=detail.grade_attachment, - ) - - return [parse_homework(work) for work in result] - - def parse_homework_detail( - self, course_id: str, homework_id: str, student_homework_id: str - ) -> types.HomeworkDetail: - url = urls.learn_homework_detail( - course_id=course_id, - homework_id=homework_id, - student_homework_id=student_homework_id, - ) - response = self.get_with_token(url) - soup = bs4.BeautifulSoup(markup=response.text, features="html.parser") - boxboxs = soup.select(".boxbox") - ( - contents_and_requirements, - my_coursework_submitted, - instructors_comments, - ) = boxboxs - - div_c55s = contents_and_requirements.select( - "div.list.calendar.clearfix > div.fl.right > div.c55" - ) - description = div_c55s[0].get_text() - answer_content = div_c55s[1].get_text() - - submitted_content = my_coursework_submitted.select("div.list > div.right")[ - 2 - ].get_text() - - grade_content = typing.cast( - bs4.Tag, instructors_comments.select_one("div.list.description > div.right") - ).get_text() - - div_list_fujian_clearfix = soup.select("div.list.fujian.clearfix") - - return types.HomeworkDetail( - description=description.strip(), - answer_content=answer_content.strip(), - submitted_content=submitted_content.strip(), - grade_content=grade_content.strip(), - attachment=parser.parse_homework_file(div_list_fujian_clearfix[0]) - if len(div_list_fujian_clearfix) > 0 - else None, - answer_attachment=parser.parse_homework_file(div_list_fujian_clearfix[1]) - if len(div_list_fujian_clearfix) > 1 - else None, - submitted_attachment=parser.parse_homework_file(div_list_fujian_clearfix[2]) - if len(div_list_fujian_clearfix) > 2 - else None, - grade_attachment=parser.parse_homework_file(div_list_fujian_clearfix[3]) - if len(div_list_fujian_clearfix) > 3 - else None, ) diff --git a/thu_learn_downloader/parser.py b/thu_learn_downloader/parser.py index 747219a..a0d00f3 100644 --- a/thu_learn_downloader/parser.py +++ b/thu_learn_downloader/parser.py @@ -1,99 +1,122 @@ -import datetime import html -import typing import urllib.parse +from datetime import datetime +from typing import TYPE_CHECKING, Optional -import bs4 +from bs4 import BeautifulSoup, Tag +from requests import Response -from . import types, urls +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( - course: dict, - course_type: types.CourseType = types.CourseType.STUDENT, -): - # url = urls.learn_course_time_location(course["wlkcid"]) - # response = self.get_with_token(url) - return types.CourseInfo( - id=course["wlkcid"], - name=course["kcm"], - english_name=course["ywkcm"], - # time_and_location=response.json(), - url=str(urls.learn_course_url(course["wlkcid"], typing.cast(str, course_type))), - teacher_name=course["jsm"] - if course["jsm"] - else "", # teacher can not fetch this - teacher_number=course["jsh"], - course_number=course["kch"], - course_index=int( - course["kxh"] - ), # course["kxh"] could be string (teacher mode) or number (student mode) - course_type=course_type, +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( - file: dict, + raw: dict, + file_clazz: dict[str, str], course_id: str, - clazz: dict[str, str], - course_type: types.CourseType = types.CourseType.STUDENT, -) -> types.File: - title: str = html.unescape(file["bt"]) - download_url: str = str( - urls.learn_file_download( - file_id=file["wjid"] - if course_type == types.CourseType.STUDENT - else file["id"], - course_type=typing.cast(str, course_type), + 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=html.unescape(raw["bt"]), # 标题 + starts_time=utils.from_timestamp(raw.get("kssj")), # 开始时间 + deadline=utils.from_timestamp(raw.get("jzsj")), # 截止时间 + submit_time=utils.from_timestamp(raw.get("scsj")), # 上传时间 + grade=raw.get("cj", ""), # 成绩 + grader_name=raw.get("jsm", ""), # 教师名 + grade_time=utils.from_timestamp(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, ) ) - preview_url = None - return types.File( - id=file["wjid"], # 文件 ID - title=title, # 标题 - description=file["ms"], # 描述 - raw_size=file["wjdx"], # 文件大小 - size=file["fileSize"], - upload_time=datetime.datetime.strptime(file["scsj"], r"%Y-%m-%d %H:%M"), # 上传时间 - download_url=str(download_url), - preview_url=str(preview_url), - is_new=file["isNew"], - marked_important=file["sfqd"] == 1, # 是否强调 - visit_count=file["llcs"] or 0, # 浏览次数 - download_count=file["xzcs"] or 0, # 下载次数 - file_type=file["wjlx"], # 文件类型 - remote_file=types.RemoteFile( - id=file["wjid"], # 文件 ID - name=title, - download_url=str(download_url), - preview_url=str(preview_url), - size=file["fileSize"], - ), - clazz=clazz[file["kjflid"]], # 课件分类 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(div: bs4.Tag) -> typing.Optional[types.RemoteFile]: - ftitle = typing.cast( - bs4.Tag, div.select_one("span.ftitle") or div.select_one("span.ft") - ) - if not ftitle: +def parse_homework_file(file_div: Tag) -> Optional[t.RemoteFile]: + ftitle = file_div.select_one(".ftitle") or file_div.select_one(".fl") + assert ftitle + file_node = ftitle.select_one("a") + if not file_node: return None - a = typing.cast(bs4.Tag, ftitle.select_one("a")) - span = typing.cast(bs4.Tag, div.select_one("span.color_999")) - size = span.getText() - href = typing.cast(str, a["href"]) - params = dict(urllib.parse.parse_qsl(urllib.parse.urlparse(href).query)) - attachment_id = params["fileId"] - download_url = types.URL( - netloc=urls.LEARN_PREFIX.netloc, - path=params["downloadUrl"] if "downloadUrl" in params else href, - ) - return types.RemoteFile( - id=attachment_id, - name=a.getText(), - download_url=download_url.str(), - preview_url=None, - size=size.strip(), + + 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/readme.py b/thu_learn_downloader/readme.py deleted file mode 100644 index c06cbf3..0000000 --- a/thu_learn_downloader/readme.py +++ /dev/null @@ -1,61 +0,0 @@ -from . import types - -WORK_README = """## {title} - -### Contents and Requirements - -#### Description - -{description} - -#### ANS - -{ans} - -#### Deadline - -{deadline} - -### My coursework submitted - -#### Date - -{submit_time} - -#### Content - -{submit_content} - -### Instructors' comments - -#### By - -{grader_name} - -#### Date - -{grade_time} - -#### Grade - -{grade} - -#### Comment - -{comment} -""" - - -def dump_work(work: types.Homework) -> str: - return WORK_README.format( - title=work.title, - description=work.description or "", - ans=work.answer_content or "", - deadline=str(work.deadline or ""), - submit_time=str(work.submit_time or ""), - submit_content=work.submitted_content or "", - grader_name=work.grader_name or "", - grade_time=str(work.grade_time or ""), - grade=work.grade or work.grade_level or "", - comment=work.grade_content, - ) diff --git a/thu_learn_downloader/sync.py b/thu_learn_downloader/sync.py index b499b8c..f606965 100644 --- a/thu_learn_downloader/sync.py +++ b/thu_learn_downloader/sync.py @@ -1,27 +1,40 @@ import os import shutil +import subprocess import sys -import typing +from datetime import datetime +from pathlib import Path +from typing import Optional -import omegaconf -import rich.console -import rich.progress +from omegaconf import DictConfig +from rich.console import Console +from rich.progress import Progress, TaskID -from . import readme, types, utils +from . import typing as t +from . import utils +from .constants import ( + DEFAULT_PREFIX, + DOCUMENT_STYLE, + HOMEWORK_STYLE, + SKIPPED_PREFIX, + SUCCESS_PREFIX, +) from .downloader import Downloader -from .helper import LearnHelper +from .helper import Helper def sync_all( - helper: LearnHelper, + helper: Helper, downloader: Downloader, - config: omegaconf.DictConfig, - console: rich.console.Console = rich.console.Console(), - overall_progress: rich.progress.Progress = rich.progress.Progress(), - semesters_task_id: rich.progress.TaskID = rich.progress.TaskID(0), - courses_task_id: rich.progress.TaskID = rich.progress.TaskID(1), + config: DictConfig, + *, + console: Console = Console(), + overall_progress: Progress = Progress(), + semesters_task_id: TaskID = TaskID(0), + courses_task_id: TaskID = TaskID(1), ) -> None: semesters = config.get("semesters") or helper.get_semester_id_list() + for semester in overall_progress.track( semesters, task_id=semesters_task_id, description="Semesters" ): @@ -37,201 +50,253 @@ def sync_semester( - helper: LearnHelper, + helper: Helper, downloader: Downloader, + config: DictConfig, semester_id: str, - config: omegaconf.DictConfig, - console: rich.console.Console = rich.console.Console(), - overall_progress: rich.progress.Progress = rich.progress.Progress(), - courses_task_id: rich.progress.TaskID = rich.progress.TaskID(1), -): - courses = helper.get_course_list(semester_id=semester_id) + *, + 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.get("courses"): courses = [ course for course in courses - if ( - (course.name in config["courses"]) - or (course.english_name in config["courses"]) - ) + if course.name in config["courses"] + or course.english_name in config["courses"] ] + overall_progress.update( - task_id=courses_task_id, - description=f"{semester_id[:-2]} {utils.parse_semester_type(int(semester_id[-1])).value}", + task_id=courses_task_id, description=utils.format_semester_id(semester_id) ) - for course in overall_progress.track( - courses, task_id=courses_task_id, description=semester_id - ): + for course in overall_progress.track(courses, task_id=courses_task_id): sync_course( helper=helper, downloader=downloader, - course=course, config=config, + course=course, console=console, ) def sync_course( - helper: LearnHelper, + helper: Helper, downloader: Downloader, - course: types.CourseInfo, - config: omegaconf.DictConfig, - console: rich.console.Console = rich.console.Console(), -): + config: DictConfig, + course: t.CourseInfo, + *, + console: Console = Console(), +) -> None: sync_files( helper=helper, downloader=downloader, - course=course, config=config, + course=course, console=console, ) - sync_works( + sync_homeworks( helper=helper, downloader=downloader, - course=course, config=config, + course=course, console=console, ) def sync_files( - helper: LearnHelper, + helper: Helper, downloader: Downloader, - course: types.CourseInfo, - config: omegaconf.DictConfig, - console: rich.console.Console = rich.console.Console(), -): - prefix = config.get( - key="prefix", default_value=os.path.join(os.getcwd(), "thu-learn") - ) - file_size_limit = config.get(key="file_size_limit") or sys.maxsize - files = helper.get_file_list(course_id=course.id, course_type=course.course_type) - for file in files: - if file.raw_size >= file_size_limit: + config: DictConfig, + course: t.CourseInfo, + *, + console: Console = Console(), +) -> None: + prefix: Path = Path( + config.get(key="prefix", default_value=DEFAULT_PREFIX) + ).expanduser() + size_limit: int = config.get(key="size_limit", default_value=sys.maxsize) + + 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( - f"Skip {file.remote_file.name} of size {file.size}", - style="bold bright_yellow", + SKIPPED_PREFIX, + utils.describe_doc_file( + course_name=course.english_name, filename=filename + ), + "size limit exceeded", + style=DOCUMENT_STYLE, ) continue + downloader.schedule_download( - url=file.remote_file.download_url, - file=os.path.join( - prefix, - utils.slugify(course.english_name), - "docs", - utils.slugify(file.clazz), - utils.slugify(f"{file.remote_file.name}.{file.file_type}"), - ), - raw_size=file.raw_size, - upload_time=file.upload_time, + url=f.download_url, + output=prefix / course.english_name / "docs" / f.file_clazz / filename, session=helper, - description=f"[bold bright_green]{course.name} > {file.remote_file.name}", + 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_works( - helper: LearnHelper, +def sync_homeworks( + helper: Helper, downloader: Downloader, - course: types.CourseInfo, - config: omegaconf.DictConfig, - console: rich.console.Console = rich.console.Console(), -): - prefix = config.get( - key="prefix", default_value=os.path.join(os.getcwd(), "thu-learn") - ) - works = helper.get_homework_list( - course_id=course.id, course_type=course.course_type - ) - for work in works: - sync_work_detail(course=course, work=work, config=config, console=console) - - def sync_attach( - attachment: typing.Optional[types.RemoteFile], - attachment_type: typing.Literal["attach"] - | typing.Literal["ans"] - | typing.Literal["submit"] - | typing.Literal["comment"], - ) -> None: - sync_work_attachment( - helper=helper, - downloader=downloader, - course=course, - work=work, - attachment=attachment, - attachment_type=attachment_type, - config=config, - console=console, - ) - - sync_attach(work.attachment, "attach") - sync_attach(work.answer_attachment, "ans") - sync_attach(work.submitted_attachment, "submit") - sync_attach(work.grade_attachment, "comment") - - -def sync_work_detail( - course: types.CourseInfo, - work: types.Homework, - config: omegaconf.DictConfig, - console: rich.console.Console = rich.console.Console(), -): - prefix = config.get( - key="prefix", default_value=os.path.join(os.getcwd(), "thu-learn") - ) - file = os.path.join( - prefix, - utils.slugify(course.english_name), - "work", - utils.slugify(work.title), - "README.md", - ) - os.makedirs(name=os.path.dirname(file), exist_ok=True) - with open(file=file, mode="w") as fp: - fp.write(readme.dump_work(work)) - if shutil.which(cmd="prettier"): - os.system(f"prettier --write {file} > /dev/null") - - -def sync_work_attachment( - helper: LearnHelper, - downloader: Downloader, - course: types.CourseInfo, - work: types.Homework, - attachment: typing.Optional[types.RemoteFile], - attachment_type: typing.Literal["attach"] - | typing.Literal["ans"] - | typing.Literal["submit"] - | typing.Literal["comment"], - config: omegaconf.DictConfig, - console: rich.console.Console = rich.console.Console(), + config: DictConfig, + course: t.CourseInfo, + *, + console: Console = Console(), ) -> None: - prefix = config.get( - key="prefix", default_value=os.path.join(os.getcwd(), "thu-learn") - ) - if attachment: - title = attachment.name - for p in ["attach", "ans", "submit", "comment"]: - title = title.removeprefix(p + "-") - title = attachment_type + "-" + title - upload_time = None - if attachment_type == "submit": - upload_time = work.submit_time - elif attachment_type == "comment": - upload_time = work.grade_time - else: - upload_time = None - downloader.schedule_download( - url=attachment.download_url, - file=os.path.join( - prefix, - utils.slugify(course.english_name), - "work", - utils.slugify(work.title), - utils.slugify(title), - ), - upload_time=upload_time, - session=helper, + 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, - description=f"[bold bright_blue]{course.name} > {title}", ) + sync_homework_attachments( + helper=helper, + downloader=downloader, + config=config, + course=course, + hw=hw, + console=console, + ) + + +def sync_homework_detail( + helper: Helper, + downloader: Downloader, + config: DictConfig, + course: t.CourseInfo, + hw: t.Homework, + *, + console: Console = Console(), +) -> None: + prefix: Path = Path( + config.get(key="prefix", default_value=DEFAULT_PREFIX) + ).expanduser() + 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: DictConfig, + 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: DictConfig, + course: t.CourseInfo, + hw: t.Homework, + attach: Optional[t.RemoteFile], + attach_type: str, + *, + console: Console = Console(), +) -> None: + if not attach: + return + prefix: Path = Path( + config.get(key="prefix", default_value=DEFAULT_PREFIX) + ).expanduser() + 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/types.py b/thu_learn_downloader/types.py deleted file mode 100644 index 41075f0..0000000 --- a/thu_learn_downloader/types.py +++ /dev/null @@ -1,216 +0,0 @@ -import dataclasses -import datetime -import enum -import os -import typing -import urllib.parse - - -@dataclasses.dataclass(kw_only=True) -class URL: - scheme: str = "https" - netloc: str = "" - path: str = "" - params: str = "" - query: str | dict | list = "" - fragment: str = "" - - def __str__(self) -> str: - return urllib.parse.urlunparse(self.astuple()) - - def str(self) -> str: - return str(self) - - def astuple(self) -> urllib.parse.ParseResult: - return urllib.parse.ParseResult( - scheme=self.scheme, - netloc=self.netloc, - path=os.path.join(self.path) if isinstance(self.path, list) else self.path, - params=self.params, - query=self.query - if isinstance(self.query, str) - else urllib.parse.urlencode(self.query, doseq=True), - fragment=self.fragment, - ) - - -@dataclasses.dataclass(kw_only=True) -class Credential: - username: str - password: str - - -class FailReason(enum.Enum): - NO_CREDENTIAL = "no credential provided" - ERROR_FETCH_FROM_ID = "could not fetch ticket from id.tsinghua.edu.cn" - BAD_CREDENTIAL = "bad credential" - ERROR_ROAMING = "could not roam to learn.tsinghua.edu.cn" - NOT_LOGGED_IN = "not logged in or login timeout" - NOT_IMPLEMENTED = "not implemented" - INVALID_RESPONSE = "invalid response" - UNEXPECTED_STATUS = "unexpected status" - - -@dataclasses.dataclass(kw_only=True) -class APIError: - reason: FailReason - extra = None - - -class SemesterType(enum.Enum): - FALL = "Autumn Term" - SPRING = "Spring Term" - SUMMER = "Summer Term" - UNKNOWN = "" - - -class ContentType(enum.Enum): - NOTIFICATION = "notification" - FILE = "file" - HOMEWORK = "homework" - DISCUSSION = "discussion" - QUESTION = "question" - - -@dataclasses.dataclass(kw_only=True) -class SemesterInfo: - id: str - start_date: datetime.date - end_date: datetime.date - start_year: int - end_year: int - type: SemesterType - - -class CourseType(enum.Enum): - STUDENT = "student" - TEACHER = "teacher" - - -@dataclasses.dataclass(kw_only=True) -class CourseInfo: - id: str - name: str - english_name: str - time_and_location: typing.Optional[list[str]] = None - url: str - teacher_name: str - teacher_number: str - course_number: str - course_index: int - course_type: CourseType - - -@dataclasses.dataclass(kw_only=True) -class RemoteFile: - id: str - name: str - download_url: str - preview_url: typing.Optional[str] = None - size: str - - -@dataclasses.dataclass(kw_only=True) -class Notification: - id: str - title: str - content: str - has_read: bool - url: str - marked_important: bool - publish_time: datetime.datetime - publisher: str - - # notification detail - attachment: typing.Optional[RemoteFile] = None - - -@dataclasses.dataclass(kw_only=True) -class File: - id: str - raw_size: int # size in byte - size: str # inaccurate size description (like '1M') - title: str - description: str - upload_time: datetime.datetime - download_url: str # for teachers, this url will not initiate download directly - preview_url: str # preview is not supported on all types of files, check before use - is_new: bool - marked_important: bool - visit_count: int - download_count: int - file_type: str - remote_file: RemoteFile # for compatibility - - clazz: str - - -@dataclasses.dataclass(kw_only=True) -class HomeworkStatus: - submitted: bool - graded: bool - - -@dataclasses.dataclass(kw_only=True) -class HomeworkDetail: - description: typing.Optional[str] = None - attachment: typing.Optional[RemoteFile] = None # attachment from teacher - answer_content: typing.Optional[str] = None # answer from teacher - answer_attachment: typing.Optional[RemoteFile] = None - submitted_content: typing.Optional[str] = None # submitted content from student - submitted_attachment: typing.Optional[RemoteFile] = None - grade_content: typing.Optional[str] = None - grade_attachment: typing.Optional[RemoteFile] = None # grade from teacher - - -@dataclasses.dataclass(kw_only=True) -class Homework(HomeworkStatus, HomeworkDetail): - id: str - student_homework_id: str - title: str - deadline: typing.Optional[datetime.datetime] - url: str - submit_url: str - submit_time: typing.Optional[datetime.datetime] = None - grade: typing.Optional[int] = None - # some homework has levels but not grades, like A/B/.../F - grade_level: typing.Optional[str] = None - grade_time: typing.Optional[datetime.datetime] = None - grader_name: typing.Optional[str] = None - - -@dataclasses.dataclass(kw_only=True) -class DiscussionBase: - id: str - title: str - publisher_name: str - publish_date: datetime.datetime - last_replier_name: str - last_reply_time: datetime.datetime - visit_count: int - reply_count: int - - -@dataclasses.dataclass(kw_only=True) -class Discussion(DiscussionBase): - url: str - board_id: str - - -@dataclasses.dataclass(kw_only=True) -class Question(DiscussionBase): - url: str - question: str - - -Content = Notification | File | Homework | Discussion | Question - - -@dataclasses.dataclass(kw_only=True) -class CalendarEvent: - location: str - status: str - start_time: str - end_time: str - date: str - course_name: str diff --git a/thu_learn_downloader/typing.py b/thu_learn_downloader/typing.py new file mode 100644 index 0000000..cb6b517 --- /dev/null +++ b/thu_learn_downloader/typing.py @@ -0,0 +1,75 @@ +import dataclasses +from datetime import datetime +from enum import StrEnum +from typing import Optional + + +class CourseType(StrEnum): + STUDENT = "student" + TEACHER = "teacher" + + +class SemesterSeason(StrEnum): + 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 index 2589fea..0122a09 100644 --- a/thu_learn_downloader/urls.py +++ b/thu_learn_downloader/urls.py @@ -1,420 +1,145 @@ -from . import types, utils +import urllib.parse +from urllib.parse import ParseResult -LEARN_PREFIX = types.URL(netloc="learn.tsinghua.edu.cn") -REGISTRAR_PREFIX = types.URL(netloc="zhjw.cic.tsinghua.edu.cn") +from requests import Request +from . import typing as t MAX_SIZE = 200 +LEARN_PREFIX = "learn.tsinghua.edu.cn" -def id_login() -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/do/off/ui/auth/login/post/bb5df85216504820be7bba2b0ae1535b/0", - query="/login.do", +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 = dict(), + params: dict = 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 id_login_form_data(username: str, password: str) -> types.URL: - return types.URL( - query={ - "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_auth_roam(ticket: str) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/j_spring_security_thauth_roaming_entry", - query={ - "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}" + ) + ) + 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_logout() -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/f/j_spring_security_logout", - ) - - -def learn_student_course_list_page() -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/f/wlxt/index/course/student/", - ) - - -def learn_semester_list() -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/wlxt/kc/v_wlkc_xs_xktjb_coassb/queryxnxq", - ) - - -def learn_current_semester() -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/kc/zhjw_v_code_xnxq/getCurrentAndNextSemester", - ) - - -def learn_course_list(semester: str, course_type: types.CourseType) -> types.URL: - if course_type == types.CourseType.STUDENT: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path=f"/b/wlxt/kc/v_wlkc_xs_xkb_kcb_extend/student/loadCourseBySemesterId/{semester}", - ) - else: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path=f"/b/kc/v_wlkc_kcb/queryAsorCoCourseList/{semester}/0", - ) - - -def learn_course_url(course_id: str, course_type: str) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path=f"/f/wlxt/index/course/{course_type}/course", - query={ - "wlkcid": course_id, - }, - ) - - -def learn_course_time_location(course_id: str) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/kc/v_wlkc_xk_sjddb/detail", - query={ - "id": course_id, - }, - ) - - -def learn_teacher_course_url(course_id: str) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/f/wlxt/index/course/teacher/course", - query={ - "wlkcid": course_id, - }, - ) - - -def learn_file_list(course_id: str, course_type: types.CourseType) -> types.URL: - if course_type == types.CourseType.STUDENT: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/wlxt/kj/wlkc_kjxxb/student/kjxxbByWlkcidAndSizeForStudent", - query={ - "wlkcid": course_id, - "size": MAX_SIZE, - }, - ) - else: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/wlxt/kj/v_kjxxb_wjwjb/teacher/queryByWlkcid", - query={ - "wlkcid": course_id, - "size": MAX_SIZE, - }, - ) - - -def learn_file_classify(course_id: str) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/wlxt/kj/wlkc_kjflb/student/pageList", - query={ - "wlkcid": course_id, - }, - ) - - -def learn_file_download(file_id: str, course_type: str, course_id: str) -> types.URL: - if course_type == types.CourseType.STUDENT: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/wlxt/kj/wlkc_kjxxb/student/downloadFile", - query={ - "sfgk": 0, - "wjid": file_id, - }, - ) - else: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/f/wlxt/kj/wlkc_kjxxb/teacher/beforeView", - query={ - "id": file_id, - "wlkcid": course_id, - }, - ) - - -def learn_file_preview( - type: types.ContentType, +def learn_file_download( file_id: str, - course_type: types.CourseType, - first_page_only: bool = False, -) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path=f"/f/wlxt/kc/wj_wjb/{course_type}/beforePlay", - query={ - "wjid": file_id, - "mk": utils.get_mk_from_type(type), - "browser": -1, - "sfgk": 0, - "pageType": "first" if first_page_only else "all", - }, + 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_notification_list(course_id: str, course_type: types.CourseType) -> types.URL: - if course_type == types.CourseType.STUDENT: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/wlxt/kcgg/wlkc_ggb/student/kcggListXs", - query={ - "wlkcid": course_id, - "size": MAX_SIZE, - }, - ) - else: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/wlxt/kcgg/wlkc_ggb/teacher/kcggList", - query={ - "wlkcid": course_id, - "size": MAX_SIZE, - }, - ) - - -def learn_notification_detail( - course_id: str, notification_id: str, course_type: types.CourseType -) -> types.URL: - if course_type == types.CourseType.STUDENT: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/f/wlxt/kcgg/wlkc_ggb/student/beforeViewXs", - query={ - "wlkcid": course_id, - "id": notification_id, - }, - ) - else: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/f/wlxt/kcgg/wlkc_ggb/teacher/beforeViewJs", - query={ - "wlkcid": course_id, - "id": notification_id, - }, - ) - - -def learn_notification_edit(course_type: types.CourseType) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path=f"/b/wlxt/kcgg/wlkc_ggb/{course_type}/editKcgg", +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_source(course_id: str) -> list[dict]: - return [ - { - "url": learn_homework_list_new(course_id), - "status": types.HomeworkStatus(submitted=False, graded=False), - }, - { - "url": learn_homework_list_submitted(course_id), - "status": types.HomeworkStatus(submitted=True, graded=False), - }, - { - "url": learn_homework_list_graded(course_id), - "status": types.HomeworkStatus(submitted=True, graded=True), - }, - ] - - -def learn_homework_list_new(course_id: str) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/wlxt/kczy/zy/student/index/zyListWj", - query={ - "wlkcid": course_id, - "size": MAX_SIZE, - }, - ) - - -def learn_homework_list_submitted(course_id: str) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/wlxt/kczy/zy/student/index/zyListYjwg", - query={ - "wlkcid": course_id, - "size": MAX_SIZE, - }, - ) - - -def learn_homework_list_graded(course_id: str) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/wlxt/kczy/zy/student/index/zyListYpg", - query={ - "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 -) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/f/wlxt/kczy/zy/student/viewCj", - query={ +) -> 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, }, ) - - -def learn_homework_download(course_id: str, attachment_id: str) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path=f"/b/wlxt/kczy/zy/student/downloadFile/{course_id}/{attachment_id}", - ) - - -def learn_homework_submit(course_id: str, student_homework_id: str) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/f/wlxt/kczy/zy/student/tijiao", - query={ - "wlkcid": course_id, - "xszyid": student_homework_id, - }, - ) - - -def learn_discussion_list(course_id: str, course_type: types.CourseType) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path=f"/b/wlxt/bbs/bbs_tltb/{course_type}/kctlList", - query={ - "wlkcid": course_id, - "size": MAX_SIZE, - }, - ) - - -def learn_discussion_detail( - course_id: str, - board_id: str, - discussion_id: str, - course_type: types.CourseType, - tab_id=1, -) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path=f"/f/wlxt/bbs/bbs_tltb/{course_type}/viewTlById", - query={ - "wlkcid": course_id, - "id": discussion_id, - "tabbh": tab_id, - "bqid": board_id, - }, - ) - - -def learn_question_list_answered( - course_id: str, course_type: types.CourseType -) -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path=f"/b/wlxt/bbs/bbs_tltb/{course_type}/kcdyList", - query={ - "wlkcid": course_id, - "size": MAX_SIZE, - }, - ) - - -def learn_question_detail( - course_id: str, question_id: str, course_type: types.CourseType -) -> types.URL: - if course_type == types.CourseType.STUDENT: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/f/wlxt/bbs/bbs_kcdy/student/viewDyById", - query={ - "wlkcid": course_id, - "id": question_id, - }, - ) - else: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/f/wlxt/bbs/bbs_kcdy/teacher/beforeEditDy", - query={ - "wlkcid": course_id, - "id": question_id, - }, - ) - - -def registrar_ticket_form_data() -> types.URL: - return types.URL( - query={ - "appId": "ALL_ZHJW", - }, - ) - - -def registrar_ticket() -> types.URL: - return types.URL( - netloc=LEARN_PREFIX.netloc, - path="/b/wlxt/common/auth/gnt", - ) - - -def registrar_auth(ticket: str) -> types.URL: - return types.URL( - netloc=REGISTRAR_PREFIX.netloc, - path="/j_acegi_login.do", - query={ - "url": "/", - "ticket": ticket, - }, - ) - - -def registrar_calendar( - start_date: str, - end_date: str, - graduate: bool = False, - callback_name: str = "unknown", -) -> types.URL: - return types.URL( - netloc=REGISTRAR_PREFIX.netloc, - path="/jxmh_out.do", - query={ - "m": ("yjs" if graduate else "bks") + "_jxrl_all", - "p_start_date": start_date, - "p_end_date": end_date, - "jsoncallback": callback_name, - }, - ) diff --git a/thu_learn_downloader/utils.py b/thu_learn_downloader/utils.py index 3a6b6c2..b7a0a61 100644 --- a/thu_learn_downloader/utils.py +++ b/thu_learn_downloader/utils.py @@ -1,36 +1,63 @@ -import slugify as slug +import dataclasses +from datetime import datetime +from typing import Any, Optional -from . import types +from . import typing as t +from .constants import HOMEWORK_README, SEASONS -def slugify(text: str) -> str: - return ".".join( - [ - slug.slugify(text=segment, word_boundary=True, allow_unicode=True) - for segment in text.split(".") - ] +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 from_timestamp(t: Optional[float]) -> Optional[datetime]: + if not t: + return None + return datetime.fromtimestamp(t / 1000.0) + + +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 parse_semester_type(n: int) -> types.SemesterType: - if n == 1: - return types.SemesterType.FALL - elif n == 2: - return types.SemesterType.SPRING - elif n == 3: - return types.SemesterType.SUMMER - else: - return types.SemesterType.UNKNOWN +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 -CONTENT_TYPE_MK_MAP: dict[types.ContentType, str] = { - types.ContentType.NOTIFICATION: "kcgg", # 课程公告 - types.ContentType.FILE: "kcwj", # 课程文件 - types.ContentType.HOMEWORK: "kczy", # 课程作业 - types.ContentType.DISCUSSION: "", - types.ContentType.QUESTION: "", -} - - -def get_mk_from_type(type: types.ContentType) -> str: - return "mk_" + CONTENT_TYPE_MK_MAP.get(type, "UNKNOWN") +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, + )