├── .github ├── dependabot.yml └── workflows │ └── continuous.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── appimagecraft.yml ├── poetry.lock ├── pyproject.toml ├── pyuploadtool ├── __init__.py ├── __main__.py ├── build_systems │ ├── __init__.py │ ├── base.py │ ├── exceptions.py │ ├── factory.py │ └── github_actions.py ├── changelog │ ├── __init__.py │ ├── author.py │ ├── changelog.py │ ├── changelog_spec.py │ ├── commit.py │ ├── factory │ │ ├── __init__.py │ │ ├── base.py │ │ └── github.py │ ├── parsers │ │ ├── __init__.py │ │ ├── markdown.py │ │ └── parser.py │ └── types.py ├── exceptions.py ├── logging.py ├── metadata.py ├── releases_hosting_provider │ ├── __init__.py │ ├── base.py │ ├── exceptions.py │ ├── factory.py │ ├── github_releases.py │ └── webdav.py └── types.py └── tox.ini /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/continuous.yml: -------------------------------------------------------------------------------- 1 | name: Continuous release 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | qa: 7 | name: Quality Assurance 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Install dependencies 12 | run: | 13 | python3 -m pip install poetry 14 | python3 -m poetry install 15 | - name: Check code formatting with black 16 | run: python3 -m poetry run black --check pyuploadtool/ 17 | 18 | build-appimage: 19 | name: Build AppImage 20 | needs: 21 | - qa 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Build AppImage 26 | run: | 27 | export APPIMAGE_EXTRACT_AND_RUN=1 28 | wget https://github.com/TheAssassin/appimagecraft/releases/download/continuous/appimagecraft-x86_64.AppImage 29 | chmod +x appimagecraft-x86_64.AppImage 30 | ./appimagecraft-x86_64.AppImage 31 | - name: Archive artifacts 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: AppImage 35 | path: pyuploadtool*.AppImage* 36 | 37 | upload: 38 | name: Create release and upload artifacts 39 | needs: 40 | - build-appimage 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Download artifacts 44 | uses: actions/download-artifact@v4 45 | - name: Inspect directory after downloading artifacts 46 | run: ls -alFR 47 | - name: Create release and upload artifacts 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | run: | 51 | cd AppImage 52 | chmod +x pyuploadtool-x86_64.AppImage 53 | export APPIMAGE_EXTRACT_AND_RUN=1 54 | ./pyuploadtool-x86_64.AppImage pyuploadtool-x86_64.AppImage 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.py[c|o] 3 | __pycache__/ 4 | .idea/ 5 | *.AppImage* 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 TheAssassin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyuploadtool 2 | 3 | A build-system-agnostic tool for creating releases and uploading artifacts on various hosting providers. 4 | 5 | *Inspired by [uploadtool](https://github.com/probonopd/uploadtool), but much better in so many ways...* 6 | 7 | 8 | ## Projects using pyuploadtool 9 | 10 | - [appimagecraft](https://github.com/TheAssassin/appimagecraft/) 11 | - [linuxdeploy](https://github.com/linuxdeploy/linuxdeploy) 12 | - [linuxdeploy-plugin-appimage](https://github.com/linuxdeploy/linuxdeploy-plugin-appimage) 13 | - [linuxdeploy-plugin-qt](https://github.com/linuxdeploy/linuxdeploy-plugin-qt) 14 | - [Blue Nebula](https://blue-nebula.org/) 15 | - [Pext](https://github.com/Pext/Pext) 16 | - [zsync2](https://github.com/AppImage/zsync2/) 17 | - [AppImageKit](https://github.com/AppImage/AppImageKit/) 18 | - [AppImageUpdate](https://github.com/AppImage/AppImageUpdate/) 19 | - [SpinED](https://github.com/twesterhout/spin-ed) 20 | 21 | ... and a lot more! Some projects can be found [on GitHub](https://github.com/search?q=pyuploadtool&type=code). 22 | 23 | 24 | ## Usage 25 | 26 | Using this tool is fairly straightforward. Ideally, in one of the supported build environments, all you have to do is to run it! The tool figures out its configuration from the environment variables (which either are provided by the build system, or set by the user). 27 | 28 | Please see the following sections for more information on how to use the tool with the supported build systems and release hosting providers. 29 | 30 | 31 | ## Supported build systems 32 | 33 | This tool supports various build systems. Build system implementations are used to read metadata about a release from the environment, e.g., by processing available environment variables. 34 | 35 | ### GitHub actions 36 | 37 | To use the tool with [GitHub actions](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/about-releases), all you have to do is to provide `GITHUB_TOKEN` in the environment of your upload step. 38 | 39 | The tool can easily upload files to *GitHub releases* if you make the `GITHUB_TOKEN` secret available as environment variable. 40 | 41 | An example pipeline step look like this: 42 | 43 | ```yaml 44 | - name: Create release and upload artifacts 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | run: | 48 | wget -q https://github.com/TheAssassin/pyuploadtool/releases/download/continuous/pyuploadtool-x86_64.AppImage 49 | chmod +x pyuploadtool-x86_64.AppImage 50 | ./pyuploadtool-x86_64.AppImage myfile myotherfile 51 | ``` 52 | 53 | If you want to upload to WebDAV, too, you could use the following step: 54 | 55 | An example pipeline step look like this: 56 | 57 | ```yaml 58 | - name: Create release and upload artifacts 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | WEBDAV_URL: ${{ secrets.WEBDAV_URL }} 62 | WEBDAV_USER: ${{ secrets.WEBDAV_USER }} 63 | WEBDAV_PASSWORD: ${{ secrets.WEBDAV_PASSWORD }} 64 | run: | 65 | wget -q https://github.com/TheAssassin/pyuploadtool/releases/download/continuous/pyuploadtool-x86_64.AppImage 66 | chmod +x pyuploadtool-x86_64.AppImage 67 | ./pyuploadtool-x86_64.AppImage myfile myotherfile 68 | ``` 69 | 70 | 71 | ## Supported releases hosting providers 72 | 73 | ### GitHub releases 74 | 75 | Uploading data on GitHub releases is currently supported out of the box with *GitHub actions*. 76 | 77 | By default, the release is marked as Stable for tags, and Prerelease for continuous builds, which are released with the tag name `continuous`. It is possible to override 78 | the tag name and release type, with the following environment variables: 79 | 80 | - `GITHUB_CONTINUOUS_RELEASE_TYPE`: type of release to be published. (supported values: `stable`, `prerelease`, default: `stable` for tags, `prerelease` otherwise) 81 | - `GITHUB_CONTINUOUS_RELEASE_NAME`: The title of the release (default: "Continuous Build") 82 | - `GITHUB_CONTINUOUS_RELEASE_TAG`: The tag used for release (default: `continuous`) 83 | 84 | 85 | ### WebDAV 86 | 87 | You can upload to any WebDAV server which supports `PUT` operations. The following environment variables need to be set: 88 | 89 | - `$WEBDAV_URL`: URL to the directory on the WebDAV server where you want to put your files 90 | - `$WEBDAV_USER`: name of user authorized to upload 91 | - `$WEBDAV_PASSWORD`: user's password 92 | - `$WEBDAV_RELEASE_NAME`: name of the release directory (optional on *GitHub actions*) 93 | 94 | **Note:** Secrets must not be stored inside the repository, nor be visible to end users. You need to store them securely, ideally using the credentials storage your build system provides (on GitHub actions, there's *Secrets*, for instance). 95 | 96 | 97 | ## Changelog Generation 98 | `pyuploadtool` support Changelog generation, which is optional, and can be enabled with the `CHANGELOG_TYPE` environment variable. 99 | ```bash 100 | CHANGELOG_TYPE=standard ./pyuploadtool*.AppImage 101 | ``` 102 | 103 | ### Changelog Types 104 | `CHANGELOG_TYPE` can have any of the following values: 105 | * `CHANGELOG_TYPE=none`, to disable generating Changelog (default) 106 | * `CHANGELOG_TYPE=standard`, Standard Changelog 107 | * `CHANGELOG_TYPE=conventional`, Conventional changelog, follows the [Conventional Commit Spec](https://www.conventionalcommits.org/) which classifies your commits as Features, Bug Fixes, etc, provided your commits follow the spec. 108 | 109 | By default, `CHANGELOG_TYPE` is `none` unless explicitly specified. 110 | -------------------------------------------------------------------------------- /appimagecraft.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | project: 4 | name: com.github.theassassin.pyuploadtool 5 | version_command: cd "$PROJECT_ROOT" && git rev-parse --short HEAD 6 | 7 | build: 8 | null: 9 | 10 | scripts: 11 | post_build: 12 | - |2 13 | cat > "$BUILD_DIR"/pyuploadtool.desktop <<\EOF 14 | [Desktop Entry] 15 | Name=pyuploadtool 16 | Type=Application 17 | Icon=pyuploadtool 18 | Exec=pyuploadtool 19 | NoDisplay=true 20 | Terminal=true 21 | Categories=Utility; 22 | EOF 23 | - |2 24 | cat > "$BUILD_DIR"/AppRun.sh <<\EOF 25 | #! /bin/bash 26 | 27 | this_dir=$(dirname "$0") 28 | 29 | # add own bin dir as fallback 30 | # might come in handy if readelf binary is missing on the system (not sure if that's even possible, though) 31 | # also makes using bundled shellcheck easier 32 | export PATH="$PATH":"$this_dir"/usr/bin 33 | 34 | # disable legacy cryptography 35 | export CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1 36 | 37 | "$this_dir"/usr/bin/python -m pyuploadtool "$@" 38 | EOF 39 | - chmod +x AppRun.sh 40 | - touch "$BUILD_DIR"/pyuploadtool.svg 41 | 42 | 43 | appimage: 44 | linuxdeploy: 45 | plugins: 46 | - conda 47 | extra_args: -i "$BUILD_DIR"/pyuploadtool.svg -d "$BUILD_DIR"/pyuploadtool.desktop --custom-apprun "$BUILD_DIR"/AppRun.sh -e $(which shellcheck) 48 | environment: 49 | PIP_REQUIREMENTS: . 50 | raw_environment: 51 | - PIP_WORKDIR="$PROJECT_ROOT" 52 | - OUTPUT=pyuploadtool-"$ARCH".AppImage 53 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "25.1.0" 6 | description = "The uncompromising code formatter." 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, 12 | {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, 13 | {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, 14 | {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, 15 | {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, 16 | {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, 17 | {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, 18 | {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, 19 | {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, 20 | {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, 21 | {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, 22 | {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, 23 | {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, 24 | {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, 25 | {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, 26 | {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, 27 | {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, 28 | {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, 29 | {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, 30 | {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, 31 | {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, 32 | {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, 33 | ] 34 | 35 | [package.dependencies] 36 | click = ">=8.0.0" 37 | mypy-extensions = ">=0.4.3" 38 | packaging = ">=22.0" 39 | pathspec = ">=0.9.0" 40 | platformdirs = ">=2" 41 | 42 | [package.extras] 43 | colorama = ["colorama (>=0.4.3)"] 44 | d = ["aiohttp (>=3.10)"] 45 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 46 | uvloop = ["uvloop (>=0.15.2)"] 47 | 48 | [[package]] 49 | name = "certifi" 50 | version = "2025.1.31" 51 | description = "Python package for providing Mozilla's CA Bundle." 52 | optional = false 53 | python-versions = ">=3.6" 54 | groups = ["main"] 55 | files = [ 56 | {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, 57 | {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, 58 | ] 59 | 60 | [[package]] 61 | name = "cffi" 62 | version = "1.17.1" 63 | description = "Foreign Function Interface for Python calling C code." 64 | optional = false 65 | python-versions = ">=3.8" 66 | groups = ["main"] 67 | files = [ 68 | {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, 69 | {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, 70 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, 71 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, 72 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, 73 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, 74 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, 75 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, 76 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, 77 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, 78 | {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, 79 | {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, 80 | {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, 81 | {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, 82 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, 83 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, 84 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, 85 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, 86 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, 87 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, 88 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, 89 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, 90 | {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, 91 | {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, 92 | {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, 93 | {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, 94 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, 95 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, 96 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, 97 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, 98 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, 99 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, 100 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, 101 | {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, 102 | {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, 103 | {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, 104 | {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, 105 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, 106 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, 107 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, 108 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, 109 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, 110 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, 111 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, 112 | {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, 113 | {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, 114 | {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, 115 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, 116 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, 117 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, 118 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, 119 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, 120 | {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, 121 | {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, 122 | {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, 123 | {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, 124 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, 125 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, 126 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, 127 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, 128 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, 129 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, 130 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, 131 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, 132 | {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, 133 | {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, 134 | {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, 135 | ] 136 | 137 | [package.dependencies] 138 | pycparser = "*" 139 | 140 | [[package]] 141 | name = "charset-normalizer" 142 | version = "3.4.1" 143 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 144 | optional = false 145 | python-versions = ">=3.7" 146 | groups = ["main"] 147 | files = [ 148 | {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, 149 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, 150 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, 151 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, 152 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, 153 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, 154 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, 155 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, 156 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, 157 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, 158 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, 159 | {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, 160 | {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, 161 | {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, 162 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, 163 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, 164 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, 165 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, 166 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, 167 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, 168 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, 169 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, 170 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, 171 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, 172 | {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, 173 | {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, 174 | {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, 175 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, 176 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, 177 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, 178 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, 179 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, 180 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, 181 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, 182 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, 183 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, 184 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, 185 | {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, 186 | {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, 187 | {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, 188 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, 189 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, 190 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, 191 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, 192 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, 193 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, 194 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, 195 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, 196 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, 197 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, 198 | {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, 199 | {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, 200 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, 201 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, 202 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, 203 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, 204 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, 205 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, 206 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, 207 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, 208 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, 209 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, 210 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, 211 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, 212 | {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, 213 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, 214 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, 215 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, 216 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, 217 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, 218 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, 219 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, 220 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, 221 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, 222 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, 223 | {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, 224 | {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, 225 | {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, 226 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, 227 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, 228 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, 229 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, 230 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, 231 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, 232 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, 233 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, 234 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, 235 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, 236 | {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, 237 | {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, 238 | {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, 239 | {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, 240 | ] 241 | 242 | [[package]] 243 | name = "click" 244 | version = "8.1.8" 245 | description = "Composable command line interface toolkit" 246 | optional = false 247 | python-versions = ">=3.7" 248 | groups = ["dev"] 249 | files = [ 250 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 251 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 252 | ] 253 | 254 | [package.dependencies] 255 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 256 | 257 | [[package]] 258 | name = "colorama" 259 | version = "0.4.6" 260 | description = "Cross-platform colored terminal text." 261 | optional = false 262 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 263 | groups = ["dev"] 264 | markers = "platform_system == \"Windows\"" 265 | files = [ 266 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 267 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 268 | ] 269 | 270 | [[package]] 271 | name = "coloredlogs" 272 | version = "15.0.1" 273 | description = "Colored terminal output for Python's logging module" 274 | optional = false 275 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 276 | groups = ["main"] 277 | files = [ 278 | {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, 279 | {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, 280 | ] 281 | 282 | [package.dependencies] 283 | humanfriendly = ">=9.1" 284 | 285 | [package.extras] 286 | cron = ["capturer (>=2.4)"] 287 | 288 | [[package]] 289 | name = "cryptography" 290 | version = "44.0.1" 291 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 292 | optional = false 293 | python-versions = "!=3.9.0,!=3.9.1,>=3.7" 294 | groups = ["main"] 295 | files = [ 296 | {file = "cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009"}, 297 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f"}, 298 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2"}, 299 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911"}, 300 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69"}, 301 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026"}, 302 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd"}, 303 | {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0"}, 304 | {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf"}, 305 | {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864"}, 306 | {file = "cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a"}, 307 | {file = "cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00"}, 308 | {file = "cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008"}, 309 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862"}, 310 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3"}, 311 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7"}, 312 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a"}, 313 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c"}, 314 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62"}, 315 | {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41"}, 316 | {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b"}, 317 | {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7"}, 318 | {file = "cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9"}, 319 | {file = "cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f"}, 320 | {file = "cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183"}, 321 | {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12"}, 322 | {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83"}, 323 | {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420"}, 324 | {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4"}, 325 | {file = "cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7"}, 326 | {file = "cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14"}, 327 | ] 328 | 329 | [package.dependencies] 330 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 331 | 332 | [package.extras] 333 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""] 334 | docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] 335 | nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] 336 | pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] 337 | sdist = ["build (>=1.0.0)"] 338 | ssh = ["bcrypt (>=3.1.5)"] 339 | test = ["certifi (>=2024)", "cryptography-vectors (==44.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] 340 | test-randomorder = ["pytest-randomly"] 341 | 342 | [[package]] 343 | name = "deprecated" 344 | version = "1.2.18" 345 | description = "Python @deprecated decorator to deprecate old python classes, functions or methods." 346 | optional = false 347 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 348 | groups = ["main"] 349 | files = [ 350 | {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, 351 | {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, 352 | ] 353 | 354 | [package.dependencies] 355 | wrapt = ">=1.10,<2" 356 | 357 | [package.extras] 358 | dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] 359 | 360 | [[package]] 361 | name = "flake8" 362 | version = "7.2.0" 363 | description = "the modular source code checker: pep8 pyflakes and co" 364 | optional = false 365 | python-versions = ">=3.9" 366 | groups = ["dev"] 367 | files = [ 368 | {file = "flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343"}, 369 | {file = "flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426"}, 370 | ] 371 | 372 | [package.dependencies] 373 | mccabe = ">=0.7.0,<0.8.0" 374 | pycodestyle = ">=2.13.0,<2.14.0" 375 | pyflakes = ">=3.3.0,<3.4.0" 376 | 377 | [[package]] 378 | name = "humanfriendly" 379 | version = "10.0" 380 | description = "Human friendly output for text interfaces using Python" 381 | optional = false 382 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 383 | groups = ["main"] 384 | files = [ 385 | {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, 386 | {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, 387 | ] 388 | 389 | [package.dependencies] 390 | pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} 391 | 392 | [[package]] 393 | name = "idna" 394 | version = "3.10" 395 | description = "Internationalized Domain Names in Applications (IDNA)" 396 | optional = false 397 | python-versions = ">=3.6" 398 | groups = ["main"] 399 | files = [ 400 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 401 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 402 | ] 403 | 404 | [package.extras] 405 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 406 | 407 | [[package]] 408 | name = "mccabe" 409 | version = "0.7.0" 410 | description = "McCabe checker, plugin for flake8" 411 | optional = false 412 | python-versions = ">=3.6" 413 | groups = ["dev"] 414 | files = [ 415 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 416 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 417 | ] 418 | 419 | [[package]] 420 | name = "mypy-extensions" 421 | version = "1.0.0" 422 | description = "Type system extensions for programs checked with the mypy type checker." 423 | optional = false 424 | python-versions = ">=3.5" 425 | groups = ["dev"] 426 | files = [ 427 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 428 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 429 | ] 430 | 431 | [[package]] 432 | name = "packaging" 433 | version = "24.2" 434 | description = "Core utilities for Python packages" 435 | optional = false 436 | python-versions = ">=3.8" 437 | groups = ["dev"] 438 | files = [ 439 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 440 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 441 | ] 442 | 443 | [[package]] 444 | name = "pathspec" 445 | version = "0.12.1" 446 | description = "Utility library for gitignore style pattern matching of file paths." 447 | optional = false 448 | python-versions = ">=3.8" 449 | groups = ["dev"] 450 | files = [ 451 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 452 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 453 | ] 454 | 455 | [[package]] 456 | name = "platformdirs" 457 | version = "4.3.6" 458 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 459 | optional = false 460 | python-versions = ">=3.8" 461 | groups = ["dev"] 462 | files = [ 463 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 464 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 465 | ] 466 | 467 | [package.extras] 468 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 469 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 470 | type = ["mypy (>=1.11.2)"] 471 | 472 | [[package]] 473 | name = "pycodestyle" 474 | version = "2.13.0" 475 | description = "Python style guide checker" 476 | optional = false 477 | python-versions = ">=3.9" 478 | groups = ["dev"] 479 | files = [ 480 | {file = "pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9"}, 481 | {file = "pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"}, 482 | ] 483 | 484 | [[package]] 485 | name = "pycparser" 486 | version = "2.22" 487 | description = "C parser in Python" 488 | optional = false 489 | python-versions = ">=3.8" 490 | groups = ["main"] 491 | files = [ 492 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 493 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 494 | ] 495 | 496 | [[package]] 497 | name = "pyflakes" 498 | version = "3.3.1" 499 | description = "passive checker of Python programs" 500 | optional = false 501 | python-versions = ">=3.9" 502 | groups = ["dev"] 503 | files = [ 504 | {file = "pyflakes-3.3.1-py2.py3-none-any.whl", hash = "sha256:af4d63344d478524956e9950a9ae11da51414622479b8c148647fe9722e96837"}, 505 | {file = "pyflakes-3.3.1.tar.gz", hash = "sha256:8752eee11d4ef3a4be642d774863047864b47406cba906fabf8dd892cf98d5b3"}, 506 | ] 507 | 508 | [[package]] 509 | name = "pygithub" 510 | version = "2.6.1" 511 | description = "Use the full Github API v3" 512 | optional = false 513 | python-versions = ">=3.8" 514 | groups = ["main"] 515 | files = [ 516 | {file = "PyGithub-2.6.1-py3-none-any.whl", hash = "sha256:6f2fa6d076ccae475f9fc392cc6cdbd54db985d4f69b8833a28397de75ed6ca3"}, 517 | {file = "pygithub-2.6.1.tar.gz", hash = "sha256:b5c035392991cca63959e9453286b41b54d83bf2de2daa7d7ff7e4312cebf3bf"}, 518 | ] 519 | 520 | [package.dependencies] 521 | Deprecated = "*" 522 | pyjwt = {version = ">=2.4.0", extras = ["crypto"]} 523 | pynacl = ">=1.4.0" 524 | requests = ">=2.14.0" 525 | typing-extensions = ">=4.0.0" 526 | urllib3 = ">=1.26.0" 527 | 528 | [[package]] 529 | name = "pyjwt" 530 | version = "2.10.1" 531 | description = "JSON Web Token implementation in Python" 532 | optional = false 533 | python-versions = ">=3.9" 534 | groups = ["main"] 535 | files = [ 536 | {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, 537 | {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, 538 | ] 539 | 540 | [package.dependencies] 541 | cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} 542 | 543 | [package.extras] 544 | crypto = ["cryptography (>=3.4.0)"] 545 | dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] 546 | docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] 547 | tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] 548 | 549 | [[package]] 550 | name = "pynacl" 551 | version = "1.5.0" 552 | description = "Python binding to the Networking and Cryptography (NaCl) library" 553 | optional = false 554 | python-versions = ">=3.6" 555 | groups = ["main"] 556 | files = [ 557 | {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, 558 | {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, 559 | {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, 560 | {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, 561 | {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, 562 | {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, 563 | {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, 564 | {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, 565 | {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, 566 | {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, 567 | ] 568 | 569 | [package.dependencies] 570 | cffi = ">=1.4.1" 571 | 572 | [package.extras] 573 | docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] 574 | tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] 575 | 576 | [[package]] 577 | name = "pyreadline3" 578 | version = "3.5.4" 579 | description = "A python implementation of GNU readline." 580 | optional = false 581 | python-versions = ">=3.8" 582 | groups = ["main"] 583 | markers = "sys_platform == \"win32\"" 584 | files = [ 585 | {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, 586 | {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, 587 | ] 588 | 589 | [package.extras] 590 | dev = ["build", "flake8", "mypy", "pytest", "twine"] 591 | 592 | [[package]] 593 | name = "requests" 594 | version = "2.32.3" 595 | description = "Python HTTP for Humans." 596 | optional = false 597 | python-versions = ">=3.8" 598 | groups = ["main"] 599 | files = [ 600 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 601 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 602 | ] 603 | 604 | [package.dependencies] 605 | certifi = ">=2017.4.17" 606 | charset-normalizer = ">=2,<4" 607 | idna = ">=2.5,<4" 608 | urllib3 = ">=1.21.1,<3" 609 | 610 | [package.extras] 611 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 612 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 613 | 614 | [[package]] 615 | name = "typing-extensions" 616 | version = "4.12.2" 617 | description = "Backported and Experimental Type Hints for Python 3.8+" 618 | optional = false 619 | python-versions = ">=3.8" 620 | groups = ["main"] 621 | files = [ 622 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 623 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 624 | ] 625 | 626 | [[package]] 627 | name = "urllib3" 628 | version = "2.3.0" 629 | description = "HTTP library with thread-safe connection pooling, file post, and more." 630 | optional = false 631 | python-versions = ">=3.9" 632 | groups = ["main"] 633 | files = [ 634 | {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, 635 | {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, 636 | ] 637 | 638 | [package.extras] 639 | brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] 640 | h2 = ["h2 (>=4,<5)"] 641 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 642 | zstd = ["zstandard (>=0.18.0)"] 643 | 644 | [[package]] 645 | name = "wrapt" 646 | version = "1.17.2" 647 | description = "Module for decorators, wrappers and monkey patching." 648 | optional = false 649 | python-versions = ">=3.8" 650 | groups = ["main"] 651 | files = [ 652 | {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, 653 | {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, 654 | {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, 655 | {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, 656 | {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, 657 | {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, 658 | {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, 659 | {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, 660 | {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, 661 | {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, 662 | {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, 663 | {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, 664 | {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, 665 | {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, 666 | {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, 667 | {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, 668 | {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, 669 | {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, 670 | {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, 671 | {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, 672 | {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, 673 | {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, 674 | {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, 675 | {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, 676 | {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, 677 | {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, 678 | {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, 679 | {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, 680 | {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, 681 | {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, 682 | {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, 683 | {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, 684 | {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, 685 | {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, 686 | {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, 687 | {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, 688 | {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, 689 | {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, 690 | {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, 691 | {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, 692 | {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, 693 | {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, 694 | {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, 695 | {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, 696 | {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, 697 | {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, 698 | {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, 699 | {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, 700 | {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, 701 | {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, 702 | {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, 703 | {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, 704 | {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, 705 | {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, 706 | {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, 707 | {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, 708 | {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, 709 | {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, 710 | {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, 711 | {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, 712 | {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, 713 | {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, 714 | {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, 715 | {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, 716 | {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, 717 | {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, 718 | {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, 719 | {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, 720 | {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, 721 | {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, 722 | {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, 723 | {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, 724 | {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, 725 | {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, 726 | {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, 727 | {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, 728 | {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, 729 | {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, 730 | {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, 731 | ] 732 | 733 | [metadata] 734 | lock-version = "2.1" 735 | python-versions = "^3.12.0" 736 | content-hash = "0b635f47147c4f5f8c201250e7bfa915eb402805dc08fe30375c93b7c986960b" 737 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pyuploadtool" 3 | version = "20250213-2" 4 | description = "A simple tool to upload artifacts to GitHub releases" 5 | authors = [ 6 | "TheAssassin " 7 | ] 8 | license = "MIT" 9 | readme = "README.md" 10 | homepage = "https://github.com/TheAssassin/pyuploadtool" 11 | repository = "https://github.com/TheAssassin/pyuploadtool" 12 | 13 | [tool.black] 14 | line-length = 120 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.12.0" 18 | pygithub = "^2.6.1" 19 | coloredlogs = "^15" 20 | requests = "^2.0" 21 | 22 | [tool.poetry.dev-dependencies] 23 | black = "*" 24 | flake8 = "*" 25 | 26 | [tool.poetry.scripts] 27 | pyuploadtool = 'pyuploadtool:__main__' 28 | 29 | [build-system] 30 | requires = ["poetry-core>=1.0"] 31 | build-backend = "poetry.core.masonry.api" 32 | -------------------------------------------------------------------------------- /pyuploadtool/__init__.py: -------------------------------------------------------------------------------- 1 | from .types import BuildType 2 | from .metadata import ReleaseMetadata 3 | from .metadata import update_metadata_with_user_specified_data # noqa (fixes import issue) 4 | from .build_systems import BuildSystemFactory 5 | from .releases_hosting_provider import ReleasesHostingProviderFactory 6 | 7 | __all__ = (ReleaseMetadata, BuildSystemFactory, ReleasesHostingProviderFactory, BuildType) 8 | -------------------------------------------------------------------------------- /pyuploadtool/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Commandline interface. 3 | """ 4 | 5 | import os 6 | import sys 7 | 8 | 9 | from pyuploadtool import ( 10 | ReleaseMetadata, 11 | ReleasesHostingProviderFactory, 12 | update_metadata_with_user_specified_data, 13 | BuildSystemFactory, 14 | ) 15 | from pyuploadtool.logging import make_logger, setup_logging 16 | 17 | setup_logging() 18 | 19 | logger = make_logger("cli") 20 | 21 | # TODO: use some real CLI library 22 | artifacts = sys.argv[1:] 23 | 24 | if not artifacts: 25 | logger.error(f"Usage: {sys.argv[0]} [...]") 26 | sys.exit(1) 27 | 28 | 29 | for artifact in artifacts: 30 | if not os.path.exists(artifact): 31 | raise FileNotFoundError(artifact) 32 | 33 | 34 | def get_metadata(): 35 | # create some metadata, which will be updated with the data from the build system 36 | logger.debug("creating empty metadata object") 37 | metadata = ReleaseMetadata() 38 | 39 | # the tool is supposed to run as part of some CI/CD workflow 40 | # therefore, we try to guess the build system from the environment 41 | # TODO: support specifying a build system explicitly 42 | logger.debug("detecting release metadata from build environment") 43 | build_system = BuildSystemFactory.from_environment() 44 | build_system.update_release_metadata(metadata) 45 | 46 | # TODO: support overwriting release metadata with environment variables 47 | # this should be done in some function or class which updates 48 | 49 | return metadata 50 | 51 | 52 | def get_release_hosting_providers(): 53 | # try to guess the releases hosting provider from the environment, too 54 | logger.debug("detecting available release hosting providers from build environment") 55 | releases_hosting_provider = ReleasesHostingProviderFactory.from_environment() 56 | 57 | return releases_hosting_provider 58 | 59 | 60 | logger.info("collecting release metadata") 61 | metadata = get_metadata() 62 | 63 | logger.info("updating metadata with user-specified values (if any)") 64 | update_metadata_with_user_specified_data(metadata) 65 | 66 | logger.info("build metadata: %s", metadata) 67 | 68 | providers = get_release_hosting_providers() 69 | 70 | if not providers: 71 | # there's no point in considering "no providers found" a success 72 | logger.error("could not detect any release hosting providers") 73 | sys.exit(1) 74 | 75 | logger.info("available release hosting providers: %s", ", ".join((p.name for p in providers))) 76 | 77 | for provider in providers: 78 | logger.info("creating release on hosting provider %s", provider.name) 79 | provider.create_release(metadata, artifacts) 80 | 81 | logger.info("done!") 82 | -------------------------------------------------------------------------------- /pyuploadtool/build_systems/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import BuildSystemBase 2 | from .exceptions import BuildSystemError 3 | from .github_actions import GitHubActions 4 | from .factory import BuildSystemFactory 5 | 6 | __all__ = (BuildSystemBase, BuildSystemError, GitHubActions, BuildSystemFactory) 7 | -------------------------------------------------------------------------------- /pyuploadtool/build_systems/base.py: -------------------------------------------------------------------------------- 1 | from ..metadata import ReleaseMetadata 2 | 3 | 4 | class BuildSystemBase: 5 | @staticmethod 6 | def from_environment(): 7 | """ 8 | Instantiate build system from provided environment variables. 9 | """ 10 | 11 | raise NotImplementedError 12 | 13 | def update_release_metadata(self, metadata: ReleaseMetadata): 14 | """ 15 | Update provided metadata with all data available in the build environment. 16 | :arg metadata: metadata object to be updated 17 | """ 18 | 19 | raise NotImplementedError 20 | -------------------------------------------------------------------------------- /pyuploadtool/build_systems/exceptions.py: -------------------------------------------------------------------------------- 1 | from pyuploadtool.exceptions import PyUploadtoolError 2 | 3 | 4 | class BuildSystemError(PyUploadtoolError): 5 | pass 6 | -------------------------------------------------------------------------------- /pyuploadtool/build_systems/factory.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from . import GitHubActions, BuildSystemError, BuildSystemBase 4 | from ..logging import make_logger 5 | 6 | 7 | class BuildSystemFactory: 8 | logger = make_logger("build-system-factory") 9 | 10 | @classmethod 11 | def from_environment(cls) -> BuildSystemBase: 12 | cls.logger.info("guessing build system from environment variables") 13 | 14 | if "GITHUB_ACTIONS" in os.environ: 15 | cls.logger.info("detected GitHub actions environment") 16 | return GitHubActions.from_environment() 17 | 18 | raise BuildSystemError("failed to guess build system from environment") 19 | -------------------------------------------------------------------------------- /pyuploadtool/build_systems/github_actions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from . import BuildSystemBase, BuildSystemError 5 | from .. import BuildType 6 | from ..logging import make_logger 7 | from ..metadata import ReleaseMetadata 8 | 9 | 10 | class GitHubActions(BuildSystemBase): 11 | logger = make_logger("github_actions") 12 | 13 | def __init__(self, repository, run_id, event_name, ref, sha, workflow, run_number): 14 | # dependency injection 15 | self.repository = repository 16 | self.run_id = run_id 17 | self.event_name = event_name 18 | self.ref = ref 19 | self.commit = sha 20 | self.workflow = workflow 21 | self.run_number = run_number 22 | 23 | @classmethod 24 | def from_environment(cls): 25 | try: 26 | repository = os.environ["GITHUB_REPOSITORY"] 27 | run_id = int(os.environ["GITHUB_RUN_ID"]) 28 | event_name = os.environ["GITHUB_EVENT_NAME"] 29 | ref = os.environ["GITHUB_REF"] 30 | sha = os.environ["GITHUB_SHA"] 31 | workflow = os.environ["GITHUB_WORKFLOW"] 32 | run_number = int(os.environ["GITHUB_RUN_NUMBER"]) 33 | 34 | except KeyError as e: 35 | raise BuildSystemError(f"Could not find environment variable ${e.args[0]}") 36 | 37 | return GitHubActions(repository, run_id, event_name, ref, sha, workflow, run_number) 38 | 39 | def update_release_metadata(self, metadata: ReleaseMetadata): 40 | # extract tag name, if possible (for release builds) 41 | branch_match = re.match(r"(?:refs/)?tags/(.+)", self.ref) 42 | if branch_match: 43 | metadata.tag = branch_match.group(1) 44 | 45 | branch_match = re.match(r"(?:refs/)?heads/(.+)", self.ref) 46 | if branch_match: 47 | metadata.branch = branch_match.group(1) 48 | 49 | metadata.build_log_url = f"https://github.com/{self.repository}/actions/runs/{self.run_id}" 50 | metadata.unique_build_id = str(self.run_id) 51 | metadata.repository_slug = self.repository 52 | metadata.commit = self.commit 53 | metadata.pipeline_name = self.workflow 54 | metadata.pipeline_run_number = self.run_number 55 | 56 | event_name = self.event_name.lower() 57 | 58 | # the create event can occur whenever a tag or branch is created 59 | if event_name == "pull_request": 60 | metadata.build_type = BuildType.PULL_REQUEST 61 | elif event_name == "push": 62 | if metadata.tag: 63 | metadata.build_type = BuildType.TAG 64 | else: 65 | metadata.build_type = BuildType.PUSH 66 | 67 | elif event_name == "schedule": 68 | metadata.build_type = BuildType.SCHEDULED 69 | 70 | elif event_name in ["workflow_dispatch", "repository_dispatch"]: 71 | metadata.build_type = BuildType.MANUAL 72 | 73 | else: 74 | raise BuildSystemError("Could not detect build type or build type is unsupported") 75 | -------------------------------------------------------------------------------- /pyuploadtool/changelog/__init__.py: -------------------------------------------------------------------------------- 1 | from .changelog import Changelog 2 | from .types import ChangelogType 3 | from .changelog_spec import ConventionalCommitChangelog 4 | 5 | 6 | __all__ = (Changelog, ConventionalCommitChangelog, ChangelogType) 7 | -------------------------------------------------------------------------------- /pyuploadtool/changelog/author.py: -------------------------------------------------------------------------------- 1 | class Author: 2 | def __init__( 3 | self, 4 | name: str = None, 5 | email: str = None, 6 | ): 7 | self._name = name 8 | self._email = email 9 | 10 | @property 11 | def name(self): 12 | return self._name 13 | 14 | @property 15 | def email(self): 16 | return self._email 17 | -------------------------------------------------------------------------------- /pyuploadtool/changelog/changelog.py: -------------------------------------------------------------------------------- 1 | from .commit import ChangelogEntry 2 | 3 | 4 | class Changelog: 5 | def __init__(self): 6 | self._data = dict() 7 | for spec in self.structure(): 8 | self._data[spec] = list() 9 | 10 | def __repr__(self): 11 | print(f"{self.__name__}({self._data})") 12 | 13 | def __iter__(self): 14 | return iter(self._data) 15 | 16 | def __getitem__(self, item): 17 | return self._data[item] 18 | 19 | @staticmethod 20 | def structure() -> dict: 21 | """ 22 | Returns a dictionary with a minimal structure of a changelog. 23 | All commits would be classified as others by default. 24 | :return: A dictionary with keys and their descriptive 25 | names which would be used for creating headings 26 | """ 27 | return {"others": "Commits"} 28 | 29 | def push(self, commit: ChangelogEntry) -> str: 30 | """ 31 | Adds a commit to the changelog 32 | :return: The classification of the commit = other 33 | """ 34 | self._data["others"].append(commit) 35 | return "others" 36 | 37 | @property 38 | def changelog(self) -> dict: 39 | return self._data 40 | -------------------------------------------------------------------------------- /pyuploadtool/changelog/changelog_spec.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .changelog import Changelog 4 | from .commit import ChangelogEntry 5 | 6 | 7 | class ConventionalCommitChangelog(Changelog): 8 | @staticmethod 9 | def structure() -> dict: 10 | """ 11 | Returns a structure of the Conventional Commit Spec 12 | according to https://cheatography.com/albelop/cheat-sheets/conventional-commits/ 13 | 14 | The order of the commits in the dictionary is according to the 15 | priority 16 | :return: 17 | :rtype: 18 | """ 19 | return { 20 | "feat": "Features", 21 | "fix": "Bug Fixes", 22 | "perf": "Performance Improvements", 23 | "docs": "Documentation", 24 | "ci": "Continuous Integration", 25 | "refactor": "Refactoring", 26 | "test": "Tests", 27 | "build": "Builds", 28 | "revert": "Reverts", 29 | "chore": "Chores", 30 | "others": "Commits", 31 | } 32 | 33 | def push(self, commit: ChangelogEntry) -> str: 34 | """ 35 | Adds a commit to the changelog and aligns each commit 36 | based on their category. See self.structure 37 | :param commit 38 | :type commit: ChangelogEntry 39 | :return: The classification of the commit == self.structure.keys() 40 | :rtype: str 41 | """ 42 | 43 | for spec in self.structure(): 44 | if commit.message.startswith(f"{spec}:"): 45 | commit.message = commit.message[len(f"{spec}:") + 1 :].strip() 46 | self._data[spec].append(commit) 47 | return spec 48 | elif re.search(f"{spec}.*(.*):.*", commit.message): 49 | commit.message = commit.message[commit.message.find(":") + 1 :].strip() 50 | self._data[spec].append(commit) 51 | return spec 52 | 53 | # it did not fit into any proper category, lets push to others 54 | self._data["others"].append(commit) 55 | return "others" 56 | -------------------------------------------------------------------------------- /pyuploadtool/changelog/commit.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple 2 | 3 | from github.Commit import Commit 4 | 5 | from .author import Author 6 | 7 | 8 | class ChangelogEntry: 9 | def __init__(self, author: Author, message: str, sha: str): 10 | self.author = author 11 | self.message = message 12 | self.sha = sha 13 | 14 | @classmethod 15 | def from_github_commit(cls, commit: Commit): 16 | """ 17 | Converts a github commit to a pyuploadtool compatible 18 | ChangelogEntry instance 19 | """ 20 | author = Author(name=commit.author.name, email=commit.author.email) 21 | # only get the first line of the commit 22 | message = commit.commit.message.split("\n")[0] 23 | sha = commit.sha 24 | return ChangelogEntry(author=author, message=message, sha=sha) 25 | -------------------------------------------------------------------------------- /pyuploadtool/changelog/factory/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import ChangelogFactory 2 | from .github import GitHubChangelogFactory 3 | 4 | __all__ = (ChangelogFactory, GitHubChangelogFactory) 5 | -------------------------------------------------------------------------------- /pyuploadtool/changelog/factory/base.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | 3 | from .. import ChangelogType, Changelog, ConventionalCommitChangelog 4 | 5 | 6 | SUPPORTED_CHANGELOG_TYPES = {ChangelogType.STANDARD: Changelog, ChangelogType.CONVENTIONAL: ConventionalCommitChangelog} 7 | 8 | 9 | class ChangelogTypeNotImplemented(NotImplementedError): 10 | pass 11 | 12 | 13 | class ChangelogFactory: 14 | def __init__(self, changelog_type: ChangelogType = None): 15 | self.changelog_type = changelog_type 16 | self.changelog_generator = self.get_changelog_generator() 17 | 18 | def get_changelog_generator(self) -> Type[Changelog]: 19 | """ 20 | Get the corresponding changelog generator from the environment 21 | if it is not supplied. 22 | :return: 23 | :rtype: ChangelogType 24 | """ 25 | if self.changelog_type is None: 26 | self.changelog_type = ChangelogType.from_environment() 27 | 28 | generator = SUPPORTED_CHANGELOG_TYPES.get(self.changelog_type) 29 | if generator is None: 30 | raise ChangelogTypeNotImplemented(f"{self.changelog_type} is not a supported ChangeLogType") 31 | 32 | return generator 33 | -------------------------------------------------------------------------------- /pyuploadtool/changelog/factory/github.py: -------------------------------------------------------------------------------- 1 | import github 2 | 3 | from typing import Optional 4 | from github import Github 5 | from github.GitRelease import GitRelease 6 | 7 | from .. import Changelog 8 | from .base import ChangelogFactory 9 | from ..commit import ChangelogEntry 10 | from ...metadata import ReleaseMetadata 11 | from ...logging import make_logger 12 | 13 | 14 | class GitHubChangelogFactory(ChangelogFactory): 15 | logger = make_logger("github-changelog-generator") 16 | 17 | def __init__(self, github_client: Github, metadata: ReleaseMetadata): 18 | """ 19 | Prepares the changelog using GitHub REST API by 20 | comparing the current commit against the latest release (pre-release / stable) 21 | """ 22 | super().__init__() 23 | self.metadata = metadata 24 | self.github_client = github_client 25 | self.repository = github_client.get_repo(metadata.repository_slug) 26 | 27 | def get_latest_release(self): 28 | """ 29 | Gets the latest release by semver, like v8.0.1, v4.5.9, if not 30 | Fallback to continuous releases, like 'continuous', 'stable', 'nightly' 31 | 32 | :return: the tag name of the latest release, and the date on which it was created 33 | :rtype: GitRelease 34 | """ 35 | 36 | releases = self.repository.get_releases() 37 | latest_release = None 38 | rolling_release = None 39 | for release in releases: 40 | if not release.tag_name.startswith("v") or not release.tag_name[0].isdigit(): 41 | # the release does not follow semver specs 42 | 43 | if rolling_release is None or (rolling_release and release.created_at > rolling_release.created_at): 44 | # probably, we are looking at a rolling release 45 | # like 'continuous', 'beta', etc.. 46 | rolling_release = release 47 | 48 | elif latest_release is None: 49 | # we still dont have a latest release, 50 | # so we need to set whatever release we currently are at 51 | # as the latest release 52 | latest_release = release 53 | 54 | elif release.created_at > latest_release.created_at: 55 | # we found a release for which, the current release is newer 56 | # than the stored one 57 | latest_release = release 58 | 59 | # we found a release which does not follow 60 | # semver specs, and it is a probably a rolling release 61 | # just provide that as the latest release 62 | # so we need to return that, if we didnt find a suitable latest_release 63 | return latest_release or rolling_release 64 | 65 | def get_commits_since(self, tag) -> Optional[github.Comparison.Comparison]: 66 | """ 67 | Gets all the commits since a tag to self.commit_sha 68 | :return 69 | """ 70 | try: 71 | commits = self.repository.compare(tag, self.metadata.commit).commits 72 | except Exception as e: 73 | self.logger.warn( 74 | f"Failed to compared across {tag} and " f"{self.metadata.commit}: {e}. " f"Not generating changelog." 75 | ) 76 | return list() 77 | return commits 78 | 79 | def get_changelog(self): 80 | """ 81 | Wrapper command to generate the changelog 82 | :return: markdown data as changelog 83 | :rtype: Changelog 84 | """ 85 | 86 | latest_release = self.get_latest_release() 87 | 88 | if latest_release is None: 89 | # We couldn't find out the latest release. Lets stick with 90 | # the commit above the commit we are working against. 91 | 92 | # FIXME: Looks like it works fine... Need some tests here 93 | latest_release = f"{self.metadata.commit}^1" 94 | else: 95 | latest_release = latest_release.tag_name 96 | 97 | commits = self.get_commits_since(latest_release) 98 | self.logger.debug(f"Found {len(commits)} commits") 99 | 100 | changelog = self.changelog_generator() 101 | 102 | for commit in commits: 103 | changelog.push(ChangelogEntry.from_github_commit(commit)) 104 | 105 | return changelog 106 | -------------------------------------------------------------------------------- /pyuploadtool/changelog/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | from .parser import ChangelogParser 2 | from .markdown import MarkdownChangelogParser 3 | 4 | 5 | __all__ = (ChangelogParser, MarkdownChangelogParser) 6 | -------------------------------------------------------------------------------- /pyuploadtool/changelog/parsers/markdown.py: -------------------------------------------------------------------------------- 1 | from .parser import ChangelogParser 2 | 3 | 4 | class MarkdownChangelogParser(ChangelogParser): 5 | def render_to_markdown(self) -> str: 6 | """ 7 | Parses the changelog to Markdown format 8 | :return: a string containing parsed markdown information 9 | """ 10 | markdown_changelog = list() 11 | # add the title if it is provided 12 | if self.title is not None: 13 | markdown_changelog.append(f"# {self.title}") 14 | 15 | for spec in self.changelog.structure(): 16 | 17 | if len(self.changelog[spec]) > 0: 18 | # append a new line before then next section 19 | markdown_changelog.append("\n") 20 | markdown_changelog.append(f"## {self.changelog.structure().get(spec)}") 21 | 22 | for commit in self.changelog[spec]: 23 | commit_link_text = commit.author.name if commit.author.name is not None else commit.sha[:7] 24 | if self.commit_link_prefix: 25 | author = f"([{commit_link_text}]({self.commit_link_prefix}/{commit.sha}))" 26 | else: 27 | author = f"({commit_link_text})" 28 | 29 | markdown_changelog.append(f"* {commit.message} {author}") 30 | 31 | return "\n".join(markdown_changelog) 32 | -------------------------------------------------------------------------------- /pyuploadtool/changelog/parsers/parser.py: -------------------------------------------------------------------------------- 1 | from .. import Changelog 2 | 3 | 4 | class ChangelogParser: 5 | def __init__( 6 | self, 7 | changelog: Changelog, 8 | title: str = None, 9 | commit_link_prefix: str = None, 10 | ): 11 | """ 12 | Generates a changelog by arranging the commits according 13 | to the Conventional Commit Spec 14 | 15 | :param title: the title of the release, generally, the tag name 16 | :type title: str 17 | 18 | :param commit_link_prefix: a link prefix, which can be used to show a commit 19 | for example 20 | commit_link_prefix = https://github.com/$GITHUB_REPOSITORY/commit 21 | here, we will add the commit hash to the end. 22 | :type commit_link_prefix: str 23 | """ 24 | self.changelog = changelog 25 | self.commit_link_prefix = commit_link_prefix.rstrip("/") 26 | self.title = title 27 | -------------------------------------------------------------------------------- /pyuploadtool/changelog/types.py: -------------------------------------------------------------------------------- 1 | import os 2 | from enum import Enum 3 | 4 | 5 | class ChangelogType(Enum): 6 | # none 7 | NONE = -1 8 | 9 | # default 10 | STANDARD = 0 11 | 12 | # follows the Conventional Commit Spec 13 | CONVENTIONAL = 1 14 | 15 | @staticmethod 16 | def from_environment(): 17 | # not set or set to empty value should default to NONE 18 | 19 | type = os.getenv("CHANGELOG_TYPE") 20 | 21 | if type: 22 | for i in ChangelogType: 23 | if type.isdigit() and int(type) == i.value or type.lower() == i.name.lower(): 24 | return i 25 | 26 | return ChangelogType.NONE 27 | -------------------------------------------------------------------------------- /pyuploadtool/exceptions.py: -------------------------------------------------------------------------------- 1 | class PyUploadtoolError(Exception): 2 | """ 3 | Base exception for all errors created by the package itself. 4 | """ 5 | 6 | pass 7 | -------------------------------------------------------------------------------- /pyuploadtool/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import coloredlogs 5 | 6 | 7 | def setup_logging(): 8 | loglevel = logging.INFO 9 | force_colors = False 10 | 11 | if "DEBUG" in os.environ: 12 | loglevel = logging.DEBUG 13 | 14 | if "FORCE_COLORS" in os.environ: 15 | force_colors = True 16 | 17 | fmt = "%(asctime)s,%(msecs)03d %(name)s [%(levelname)s] %(message)s" 18 | 19 | # basic logging setup 20 | styles = coloredlogs.DEFAULT_FIELD_STYLES 21 | styles["pathname"] = { 22 | "color": "magenta", 23 | } 24 | styles["levelname"] = { 25 | "color": "cyan", 26 | } 27 | 28 | # configure our own loggers only 29 | base_logger = make_logger() 30 | base_logger.setLevel(loglevel) 31 | 32 | kwargs = dict(fmt=fmt, styles=styles, logger=base_logger) 33 | 34 | if force_colors: 35 | kwargs["isatty"] = True 36 | 37 | coloredlogs.install(loglevel, **kwargs) 38 | 39 | # hide all other loggers by default 40 | logging.getLogger().setLevel(logging.INFO) 41 | base_logger.setLevel(loglevel) 42 | 43 | # allow user to re-enable some loggers for debugging 44 | if "DEBUG_GITHUB" in os.environ: 45 | logging.getLogger("github").setLevel(logging.INFO) 46 | logging.getLogger("urllib3").setLevel(logging.INFO) 47 | 48 | 49 | def make_logger(name: str = None) -> logging.Logger: 50 | base_logger = logging.getLogger("pyuploadtool") 51 | 52 | if name is None: 53 | return base_logger 54 | 55 | return base_logger.getChild(name) 56 | -------------------------------------------------------------------------------- /pyuploadtool/metadata.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from . import BuildType 4 | from .changelog import Changelog 5 | 6 | 7 | class ReleaseMetadata: 8 | """ 9 | Data object holding all metadata about a release that can be gathered from, e.g., a build system's environment. 10 | 11 | All the data is optional. Release hosting providers have to either handle missing values, or raise an exception if 12 | mandatory data is missing. 13 | 14 | Release metadata instances are created by the user and then handed to, e.g., build systems, to fill in data. 15 | """ 16 | 17 | def __init__( 18 | self, 19 | tag: str = None, 20 | branch: str = None, 21 | release_name: str = None, 22 | release_description: str = None, 23 | build_log_url: str = None, 24 | unique_build_id: str = None, 25 | repository_slug: str = None, 26 | commit: str = None, 27 | pipeline_name: str = None, 28 | pipeline_run_number: str = None, 29 | build_type: BuildType = None, 30 | changelog: Changelog = None, 31 | ): 32 | # name of the current tag 33 | self.tag = tag 34 | 35 | # name of the current branch (might not be available, e.g., when building on a tag) 36 | self.branch = branch 37 | 38 | # name of the release to be created (might be the same as tag_name, usually user-specified or auto-generated) 39 | self.release_name = release_name 40 | 41 | # optional release description 42 | # will be prepended to the auto-generated description for release hosting platforms that support descriptions 43 | self.release_description = release_description 44 | 45 | # URL to build log 46 | self.build_log_url = build_log_url 47 | 48 | # an ID unique to the running build (e.g., build/run number) 49 | # example use case: allow release hosting provider implementations to handle duplicate build runs properly 50 | self.unique_build_id = unique_build_id 51 | 52 | # repository slug 53 | # this is required by releases hosting platforms such as GitHub releases 54 | self.repository_slug = repository_slug 55 | 56 | # Git commit hash 57 | self.commit = commit 58 | 59 | # build pipeline metadata 60 | # used to determine the release directory name in hosting providers such as WebDAV 61 | self.pipeline_name = pipeline_name 62 | # the run number shall be monotonically increasing, and start at 0 or 1 63 | self.pipeline_run_number = pipeline_run_number 64 | 65 | # current build's type 66 | if build_type is None: 67 | build_type = BuildType.UNKNOWN 68 | self.build_type = build_type 69 | 70 | # changelog 71 | self.changelog = changelog 72 | 73 | def __repr__(self): 74 | args = ", ".join( 75 | ( 76 | f"{i}={repr(getattr(self, i))}" 77 | for i in ( 78 | "tag", 79 | "branch", 80 | "release_name", 81 | "release_description", 82 | "build_log_url", 83 | "unique_build_id", 84 | "repository_slug", 85 | "commit", 86 | "pipeline_name", 87 | "pipeline_run_number", 88 | "build_type", 89 | ) 90 | ) 91 | ) 92 | 93 | return f"<{self.__class__.__name__}({args})>" 94 | 95 | 96 | def update_metadata_with_user_specified_data(metadata: ReleaseMetadata): 97 | """ 98 | Update metadata with values from environment variables users may specify. This can be used to overwrite 99 | auto-detected values or provide additional data the auto detection can't determine itself. 100 | 101 | :param metadata: metadata to update 102 | """ 103 | 104 | # probonopd/uploadtool compatibility 105 | try: 106 | metadata.description = os.environ["UPLOADTOOL_BODY"] 107 | except KeyError: 108 | pass 109 | -------------------------------------------------------------------------------- /pyuploadtool/releases_hosting_provider/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import ReleasesHostingProviderBase 2 | from .exceptions import ReleaseHostingProviderError 3 | from .github_releases import GitHubReleases 4 | from .webdav import WebDAV 5 | from .factory import ReleasesHostingProviderFactory 6 | 7 | __all__ = ( 8 | ReleasesHostingProviderBase, 9 | GitHubReleases, 10 | ReleaseHostingProviderError, 11 | ReleasesHostingProviderFactory, 12 | WebDAV, 13 | ) 14 | -------------------------------------------------------------------------------- /pyuploadtool/releases_hosting_provider/base.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | from .. import ReleaseMetadata 4 | 5 | 6 | class ReleasesHostingProviderBase: 7 | @property 8 | def name(self): 9 | raise NotImplementedError 10 | 11 | @staticmethod 12 | def from_environment(): 13 | """ 14 | Create new instance from environment variables. 15 | Variables to be set vary from implementation to implementation. 16 | :return: new instance 17 | """ 18 | raise NotImplementedError 19 | 20 | def create_release(self, metadata: ReleaseMetadata, artifacts: Iterable[str]): 21 | """ 22 | Create new release on hosting provider from the provided metadata and upload provided artifacts there. 23 | :param metadata: metadata to create release from 24 | :param artifacts: files to be uploaded into the release 25 | """ 26 | 27 | raise NotImplementedError 28 | -------------------------------------------------------------------------------- /pyuploadtool/releases_hosting_provider/exceptions.py: -------------------------------------------------------------------------------- 1 | from pyuploadtool.exceptions import PyUploadtoolError 2 | 3 | 4 | class ReleaseHostingProviderError(PyUploadtoolError): 5 | pass 6 | -------------------------------------------------------------------------------- /pyuploadtool/releases_hosting_provider/factory.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | from . import ReleasesHostingProviderBase, GitHubReleases, WebDAV 5 | from ..logging import make_logger 6 | 7 | 8 | class ReleasesHostingProviderFactory: 9 | logger = make_logger("releases-hosting-provider-factory") 10 | 11 | @classmethod 12 | def from_environment(cls) -> List[ReleasesHostingProviderBase]: 13 | providers = [] 14 | 15 | # TODO: support more than one provider at a time 16 | cls.logger.info("guessing releases hosting provider from environment variables") 17 | 18 | if "WEBDAV_URL" in os.environ: 19 | cls.logger.info("detected WebDAV") 20 | providers.append(WebDAV.from_environment()) 21 | 22 | if "GITHUB_TOKEN" in os.environ: 23 | cls.logger.info("detected GitHub releases") 24 | providers.append(GitHubReleases.from_environment()) 25 | 26 | return providers 27 | -------------------------------------------------------------------------------- /pyuploadtool/releases_hosting_provider/github_releases.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from enum import Enum 4 | from github import Github, UnknownObjectException 5 | 6 | from . import ReleaseHostingProviderError 7 | from .base import ReleasesHostingProviderBase 8 | from .. import ReleaseMetadata, BuildType 9 | from ..changelog import ChangelogType 10 | from ..changelog.parsers import MarkdownChangelogParser 11 | from ..changelog.factory.github import GitHubChangelogFactory 12 | from ..logging import make_logger 13 | 14 | 15 | class InvalidGitHubReleaseTypeError(ValueError): 16 | pass 17 | 18 | 19 | class GitHubReleaseTypes(Enum): 20 | STABLE = "stable" 21 | PRERELEASE = "prerelease" 22 | 23 | @staticmethod 24 | def validate(release_type: str) -> None: 25 | if release_type not in (release.value for release in GitHubReleaseTypes): 26 | raise InvalidGitHubReleaseTypeError() 27 | 28 | @staticmethod 29 | def is_prerelease(release_type: str) -> bool: 30 | GitHubReleaseTypes.validate(release_type) 31 | return release_type == GitHubReleaseTypes.PRERELEASE.value 32 | 33 | 34 | class GitHubReleases(ReleasesHostingProviderBase): 35 | logger = make_logger("github-releases") 36 | 37 | def __init__(self, github_client: Github): 38 | # using dependency injection to allow for easier testing 39 | self.github_client = github_client 40 | 41 | @staticmethod 42 | def from_environment(): 43 | try: 44 | github_client = Github(os.environ["GITHUB_TOKEN"]) 45 | except KeyError as e: 46 | raise ReleaseHostingProviderError(f"could not find required environment variable: {e.args[0]}") 47 | 48 | return GitHubReleases(github_client) 49 | 50 | def create_release(self, metadata: ReleaseMetadata, artifacts): 51 | repo = self.github_client.get_repo(metadata.repository_slug) 52 | 53 | prerelease = False 54 | 55 | # fallback values (for continuous release setup) 56 | if metadata.build_type in [BuildType.SCHEDULED, BuildType.MANUAL, BuildType.PUSH]: 57 | if metadata.build_type == BuildType.PUSH: 58 | if metadata.branch != repo.default_branch: 59 | self.logger.warning( 60 | f'not creating release for branch "{metadata.branch}" as it is not the default branch ' 61 | f'"{repo.default_branch}"' 62 | ) 63 | return 64 | 65 | else: 66 | if not metadata.branch: 67 | metadata.branch = repo.default_branch 68 | 69 | elif metadata.branch == repo.default_branch: 70 | pass 71 | 72 | else: 73 | raise ReleaseHostingProviderError( 74 | f'refusing to create continuous release for non-default branch "{metadata.branch}" ' 75 | f'(build type {metadata.build_type}, default branch "{repo.default_branch}")' 76 | ) 77 | 78 | self.logger.warning("push to default branch, creating continuous release") 79 | 80 | # not using "latest", as this value is reserved by GitHub 81 | metadata.tag = os.getenv("GITHUB_CONTINUOUS_RELEASE_TAG", "continuous") 82 | metadata.release_name = os.getenv("GITHUB_CONTINUOUS_RELEASE_NAME", "Continuous build") 83 | prerelease = GitHubReleaseTypes.is_prerelease( 84 | os.getenv("GITHUB_CONTINUOUS_RELEASE_TYPE", GitHubReleaseTypes.PRERELEASE.value) 85 | ) 86 | 87 | elif metadata.build_type == BuildType.PULL_REQUEST: 88 | self.logger.warning("not creating release as this is a pull request build") 89 | return 90 | 91 | elif metadata.build_type == BuildType.TAG: 92 | self.logger.info("build of tag, creating regular release") 93 | 94 | if not metadata.release_name: 95 | metadata.release_name = f"Release {metadata.tag}" 96 | self.logger.info(f"automatically using tag name as release name: {metadata.release_name}") 97 | 98 | else: 99 | raise ReleaseHostingProviderError(f"unsupported build type: {metadata.build_type}") 100 | 101 | # recreate existing tag if it has the same name but is based on a different commit 102 | # this usually happens with continuous releases 103 | # deleting the tag is safe in this case 104 | for existing_tag in repo.get_tags(): 105 | if existing_tag.name == metadata.tag and existing_tag.commit.sha != metadata.commit: 106 | self.logger.warning(f"recreating tag {metadata.tag} for commit {metadata.commit}") 107 | 108 | self.logger.debug(f"deleting tag {existing_tag.name}") 109 | existing_tag_ref = repo.get_git_ref(f"tags/{existing_tag.name}") 110 | existing_tag_ref.delete() 111 | 112 | try: 113 | release = repo.get_release(metadata.tag) 114 | except UnknownObjectException: 115 | release = None 116 | 117 | message = f"Build log: {metadata.build_log_url}" 118 | 119 | should_generate_changelog = ChangelogType.from_environment() != ChangelogType.NONE 120 | 121 | if should_generate_changelog and metadata.release_description is None: 122 | github_changelog = GitHubChangelogFactory(github_client=self.github_client, metadata=metadata) 123 | changelog = github_changelog.get_changelog() 124 | markdown_changelog = MarkdownChangelogParser( 125 | changelog, commit_link_prefix=f"https://github.com/{metadata.repository_slug}/commit/" 126 | ).render_to_markdown() 127 | message = f"{markdown_changelog}\n\n{message}" 128 | 129 | elif metadata.release_description is not None: 130 | message = f"{metadata.release_description}\n\n{message}" 131 | 132 | # for some annoying reason, you have to specify all the metadata both when drafting _and_ creating the release 133 | release_data = dict( 134 | name=metadata.release_name, 135 | message=message, 136 | prerelease=prerelease, 137 | target_commitish=metadata.commit, 138 | ) 139 | 140 | # in case we have multiple jobs build and upload in parallel, the release may already have been created by 141 | # one job 142 | # therefore, we want to make sure we only (re)create the release if it is not based on the commit we're 143 | # processing right now 144 | if release is not None: 145 | if release.target_commitish == metadata.commit: 146 | self.logger.info( 147 | f'found an existing release called "{metadata.release_name}" for commit "{metadata.commit}"' 148 | ) 149 | else: 150 | self.logger.warning(f"deleting existing release for tag {metadata.tag}") 151 | release.delete_release() 152 | release = None 153 | 154 | if release is None: 155 | self.logger.info(f'drafting new release "{metadata.release_name}" for tag "{metadata.tag}"') 156 | release = repo.create_git_release(tag=metadata.tag, draft=True, **release_data) 157 | 158 | for path in artifacts: 159 | self.logger.info(f'uploading artifact "{path}"') 160 | 161 | # in case there's an existing artifact with the same filename, we need to delete the old file first 162 | for asset in release.get_assets(): 163 | if asset.name == os.path.basename(path): 164 | self.logger.info(f'deleting existing asset {asset.id} with name "{asset.name}"') 165 | asset.delete_asset() 166 | 167 | release.upload_asset(path) 168 | 169 | self.logger.info("publishing release") 170 | # for some annoying reason, you have to re-provide all options 171 | release.update_release(draft=False, **release_data) 172 | 173 | @property 174 | def name(self): 175 | return "GitHub Releases" 176 | -------------------------------------------------------------------------------- /pyuploadtool/releases_hosting_provider/webdav.py: -------------------------------------------------------------------------------- 1 | import os 2 | import string 3 | from urllib.parse import quote 4 | 5 | import requests 6 | 7 | from . import ReleaseHostingProviderError 8 | from .base import ReleasesHostingProviderBase 9 | from .. import ReleaseMetadata, BuildType 10 | from ..logging import make_logger 11 | 12 | 13 | class WebDAV(ReleasesHostingProviderBase): 14 | logger = make_logger("webdav") 15 | 16 | def __init__(self, url: str, requests_session: requests.Session, release_name: str = None): 17 | # using dependency injection to allow for easier testing 18 | self.url = url 19 | self.requests_session = requests_session 20 | self.release_name = release_name 21 | 22 | @staticmethod 23 | def from_environment(): 24 | try: 25 | url = os.environ["WEBDAV_URL"] 26 | user = os.environ["WEBDAV_USER"] 27 | password = os.environ["WEBDAV_PASSWORD"] 28 | 29 | # optional features 30 | try: 31 | release_name = os.environ["WEBDAV_RELEASE_NAME"] 32 | except KeyError: 33 | release_name = None 34 | 35 | except KeyError as e: 36 | raise ReleaseHostingProviderError(f"could not find required environment variable: {e.args[0]}") 37 | 38 | session = requests.Session() 39 | session.auth = (user, password) 40 | 41 | return WebDAV(url, session, release_name) 42 | 43 | def create_release(self, metadata: ReleaseMetadata, artifacts): 44 | def sanitize(s): 45 | out = [] 46 | 47 | for i in str(s): 48 | if i in string.ascii_letters + string.digits + "_- ": 49 | out.append(i) 50 | else: 51 | out.append("_") 52 | 53 | return quote("".join(out)) 54 | 55 | # credentials would be missing anyway in a pull request build 56 | if metadata.build_type == BuildType.PULL_REQUEST: 57 | self.logger.warning("not uploading to server as this is a pull request build") 58 | return 59 | 60 | # if the user specifies a release name via env vars, we prefer that one 61 | # note: we permit an empty string to allow for uploading to the specified URL's root directory 62 | if self.release_name is not None: 63 | self.logger.info(f'using user-specified release name "{self.release_name}"') 64 | target_directory = sanitize(self.release_name) 65 | 66 | elif metadata.pipeline_name and metadata.pipeline_run_number: 67 | target_directory = f"{sanitize(metadata.pipeline_name)}/{sanitize(metadata.pipeline_run_number)}" 68 | 69 | else: 70 | raise ReleaseHostingProviderError("cannot determine base URL") 71 | 72 | self.logger.info(f'target directory: "{target_directory}"') 73 | 74 | # avoid consecutive / in URLs, as some webservers don't like these 75 | while "//" in target_directory: 76 | target_directory = target_directory.replace("//", "/") 77 | 78 | base_url = f"{self.url}/{target_directory}" 79 | base_url = base_url.rstrip("/") 80 | 81 | for path in artifacts: 82 | self.logger.info(f'uploading artifact "{path}"') 83 | 84 | artifact_url = f"{base_url}/{quote(os.path.basename(path))}" 85 | with open(path, "rb") as f: 86 | response = self.requests_session.put(artifact_url, data=f) 87 | response.raise_for_status() 88 | 89 | if metadata.build_log_url: 90 | build_info_filename = "build-info.txt" 91 | self.logger.info(f"uploading build log URL to {build_info_filename}") 92 | build_info = f"Build log: {metadata.build_log_url}\n" 93 | build_info_url = f"{base_url}/{quote(build_info_filename)}" 94 | put_response = self.requests_session.put(build_info_url, data=build_info) 95 | put_response.raise_for_status() 96 | 97 | @property 98 | def name(self): 99 | return "WebDAV" 100 | -------------------------------------------------------------------------------- /pyuploadtool/types.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class BuildType(Enum): 5 | # default 6 | UNKNOWN = 0 7 | 8 | # a normal push to a Git branch 9 | PUSH = 1 10 | 11 | # a tag was created 12 | TAG = 2 13 | 14 | # a pull request (merge request, ...) is being built 15 | PULL_REQUEST = 3 16 | 17 | # a scheduled event, e.g., triggered periodically 18 | SCHEDULED = 4 19 | 20 | # manual build, triggered by the user 21 | MANUAL = 5 22 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | --------------------------------------------------------------------------------