├── .editorconfig ├── .flake8 ├── .gitattributes ├── .github └── workflows │ ├── build_and_test.sh │ └── build_and_test.yml ├── .gitignore ├── .idea ├── inspectionProfiles │ ├── grr_inspections.xml │ └── profiles_settings.xml ├── mypy.xml ├── scopes │ └── python_files.xml └── watcherTasks.xml ├── .isort.cfg ├── AUTHORS ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── check_all.sh ├── ci └── check_headers.py ├── dev_shell.sh.template ├── dictionary.dic ├── docs ├── conventions.md └── releases.md ├── fix_all.sh ├── github_release_retry ├── __init__.py ├── github_release_retry.py └── py.typed ├── github_release_retry_tests ├── __init__.py ├── fixtures │ ├── get_release_by_tag.json │ └── release_already_exists.json ├── test_github_api.py ├── test_node_id_extraction.py └── testcase.py ├── mypy.ini ├── pylintrc ├── pyproject.toml ├── setup.cfg └── setup.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | tab_width = 2 13 | # IDEA specific. 14 | continuation_indent_size = 4 15 | 16 | [*.py] 17 | indent_size = 4 18 | tab_width = 4 19 | # IDEA specific. 20 | continuation_indent_size = 4 21 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The github-release-retry Project Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This is not a TOML file; no quoting. 16 | 17 | [flake8] 18 | filename = */github_release_retry/*.py, */github_release_retry_tests/*.py 19 | exclude = *_pb2.py* 20 | max-line-length = 88 21 | ignore = 22 | E203 23 | W503 24 | E501 25 | 26 | # Missing doc strings. 27 | D100 28 | D101 29 | D103 30 | D104 31 | D105 32 | D107 33 | D102 34 | 35 | # Audit url open for permitted schemes. 36 | S310 37 | 38 | # Standard pseudo-random generators are not suitable for security/cryptographic purposes. 39 | S311 40 | 41 | # Use of subprocess call and run. 42 | S404 43 | S603 44 | 45 | # Warning: First line should be in imperative mood. 46 | # This is the opposite of the Google style. 47 | D401 48 | 49 | # Missing trailing comma. 50 | C812 51 | 52 | # Missing trailing comma. 53 | S101 54 | 55 | # Allow unindexed string format parameters 56 | P101 57 | P102 58 | P103 59 | 60 | # Consider possible security implications associated with pickle module 61 | S403 62 | # Pickle and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue. 63 | S301 64 | 65 | # per-file-ignores = 66 | 67 | # For flake8-quotes: 68 | inline-quotes = " 69 | 70 | # Use the formatter provided by flake8_formatter_abspath. 71 | # This will output the full path of files with warnings so they will be clickable from PyCharm's terminal. 72 | format = abspath 73 | 74 | # For flake8-spellcheck. Default ends with .txt but .dic means 75 | # we can add the file to PyCharm/IntelliJ. 76 | whitelist=dictionary.dic 77 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | 2 | **_pb2.* linguist-generated=true 3 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2020 The github-release-retry Project Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -x 18 | set -e 19 | set -u 20 | 21 | help | head 22 | 23 | uname 24 | 25 | case "$(uname)" in 26 | "Linux") 27 | ACTIVATE_PATH=".venv/bin/activate" 28 | export PYTHON=python 29 | ;; 30 | 31 | "Darwin") 32 | ACTIVATE_PATH=".venv/bin/activate" 33 | export PYTHON=python 34 | ;; 35 | 36 | "MINGW"*|"MSYS_NT"*) 37 | ACTIVATE_PATH=".venv/Scripts/activate" 38 | export PYTHON=python.exe 39 | ;; 40 | 41 | *) 42 | echo "Unknown OS" 43 | exit 1 44 | ;; 45 | esac 46 | 47 | ./dev_shell.sh.template 48 | # shellcheck disable=SC1090 49 | source "${ACTIVATE_PATH}" 50 | ./check_all.sh 51 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The github-release-retry Project Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | on: 17 | pull_request: 18 | branches: 19 | - main 20 | 21 | jobs: 22 | build: 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | os: 27 | - ubuntu-18.04 28 | - windows-latest 29 | python-version: 30 | - 3.6 31 | runs-on: ${{ matrix.os }} 32 | steps: 33 | 34 | - name: checkout 35 | uses: actions/checkout@v2 36 | 37 | - name: setup_python 38 | uses: actions/setup-python@v2 39 | with: 40 | python-version: ${{ matrix.python-version }} 41 | architecture: x64 42 | 43 | - name: build_step 44 | run: | 45 | .github/workflows/build_and_test.sh 46 | shell: bash 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /build/ 3 | /dist/ 4 | /dev_shell.sh 5 | 6 | .idea/misc.xml 7 | .idea/modules.xml 8 | .idea/vcs.xml 9 | .idea/workspace.xml 10 | .idea/dictionaries/* 11 | .idea/*.iml 12 | 13 | .venv/ 14 | *.egg-info/ 15 | __pycache__/ 16 | .mypy_cache/ 17 | 18 | 19 | .pytest_cache/ 20 | .vscode/ 21 | 22 | pip-wheel-metadata/ 23 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/grr_inspections.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/mypy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /.idea/scopes/python_files.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 24 | 25 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The github-release-retry Project Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This is not a TOML file; no quoting. 16 | 17 | [isort] 18 | multi_line_output = 3 19 | include_trailing_comma = True 20 | force_grid_wrap = 0 21 | use_parentheses = True 22 | line_length = 88 23 | skip_glob = *_pb2.py* 24 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of The github-release-retry Project Authors for copyright 2 | # purposes. This file is distinct from the CONTRIBUTORS files. For example, 3 | # Google employees are listed in CONTRIBUTORS but not here, because Google 4 | # holds the copyright. 5 | 6 | Google LLC 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the list of contributors of code to the github-release-retry project 2 | # repository. The AUTHORS file lists the copyright holders; this file lists 3 | # people. For example, Google employees are listed here but not in AUTHORS, 4 | # because Google holds the copyright. 5 | 6 | Google LLC 7 | Paul Thomson 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The github-release-retry Project Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [[source]] 16 | name = "pypi" 17 | url = "https://pypi.org/simple" 18 | verify_ssl = true 19 | 20 | [packages] 21 | # Add "this" package as a dependency; this ensures all packages listed in 22 | # setup.py are installed into the venv, as well as github_release_retry itself. 23 | github_release_retry = {path = ".", editable = true} 24 | 25 | [dev-packages] 26 | # autoflake = "*" # Tool to auto-remove unused imports; does not seem to work 27 | # well. 28 | 29 | # iPython shell. 30 | ipython = "*" 31 | jedi = "*" 32 | 33 | # Code formatter. Explicit version given because it is a pre-release and 34 | # otherwise we get errors. 35 | black = "==19.10b0" 36 | 37 | # Type checking. 38 | mypy = "*" 39 | 40 | # Dataclass JSON (de)serializing. 41 | dataclasses-json = "*" 42 | 43 | # Testing. 44 | pytest = "*" 45 | atomicwrites = "*" # PyTest depends on this, but only on Windows, which gets missed. 46 | pytest-xdist = "*" # For running tests in parallel. 47 | requests-mock = "*" # For mock requests data 48 | 49 | # PyLint linter. 50 | pylint = "*" 51 | 52 | # Flake8 linter. 53 | flake8 = "*" 54 | flake8_formatter_abspath = "*" 55 | # cohesion = "*" # A tool for measuring Python class cohesion 56 | # flake8-alfred = "*" # Can be used to ban symbols. 57 | # flake8-copyright = "*" # Not maintained. 58 | # flake8-return = "*" # Does not look that good. 59 | # flake8-if-expr = "*" # Disallows ternary expressions. 60 | # flake8-strict = "*" # Just contains two redundant checks. 61 | # flake8-eradicate = "*" # Disallows commented out code, but has false-positives. 62 | flake8-bandit = "*" 63 | flake8-black = "==0.1.0" # Fix to 0.1.0 because otherwise it requires black =>19.3b0 (pre-release) which messes up dependency resolution for some reason. 64 | flake8-breakpoint = "*" 65 | flake8-broken-line = "*" 66 | flake8-bugbear = "*" 67 | flake8-builtins = "*" 68 | flake8-coding = "*" # Checks for a utf-8 comment at top of every file. 69 | flake8-comprehensions = "*" 70 | flake8-commas = "*" 71 | flake8-debugger = "*" 72 | flake8-docstrings = "*" 73 | flake8-isort = "*" 74 | flake8-logging-format = "*" 75 | flake8-mock = "*" 76 | flake8-mutable = "*" 77 | # flake8-mypy = "*" # We run the full mypy; this plugin gives false-positives. 78 | flake8-pep3101 = "*" 79 | flake8-print = "*" 80 | flake8-quotes = "*" 81 | flake8-spellcheck = "*" 82 | flake8-string-format = "*" 83 | flake8-type-annotations = "*" 84 | flake8-variables-names = "*" 85 | mccabe = "*" 86 | pep8-naming = "*" 87 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "3c49bcdce6dcd1a964edb1da1013b0daea5ddbb9870c7e2d641bb9179c74fe73" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "certifi": { 18 | "hashes": [ 19 | "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1", 20 | "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc" 21 | ], 22 | "version": "==2020.4.5.2" 23 | }, 24 | "chardet": { 25 | "hashes": [ 26 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 27 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 28 | ], 29 | "version": "==3.0.4" 30 | }, 31 | "dataclasses": { 32 | "hashes": [ 33 | "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836", 34 | "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6" 35 | ], 36 | "markers": "python_version == '3.6' and python_version == '3.6'", 37 | "version": "==0.7" 38 | }, 39 | "dataclasses-json": { 40 | "hashes": [ 41 | "sha256:2f5fca4f097f9f9727e2100c824ed48153171186a679487f8875eca8d8c05107", 42 | "sha256:6e38b11b178e404124bffd6d213736bc505338e8a4c718596efec8d32eb96f5a" 43 | ], 44 | "markers": "python_version >= '3.6'", 45 | "version": "==0.5.1" 46 | }, 47 | "github-release-retry": { 48 | "editable": true, 49 | "path": "." 50 | }, 51 | "idna": { 52 | "hashes": [ 53 | "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", 54 | "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" 55 | ], 56 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 57 | "version": "==2.9" 58 | }, 59 | "marshmallow": { 60 | "hashes": [ 61 | "sha256:35ee2fb188f0bd9fc1cf9ac35e45fd394bd1c153cee430745a465ea435514bd5", 62 | "sha256:9aa20f9b71c992b4782dad07c51d92884fd0f7c5cb9d3c737bea17ec1bad765f" 63 | ], 64 | "markers": "python_version >= '3.5'", 65 | "version": "==3.6.1" 66 | }, 67 | "marshmallow-enum": { 68 | "hashes": [ 69 | "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58", 70 | "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072" 71 | ], 72 | "version": "==1.5.1" 73 | }, 74 | "mypy-extensions": { 75 | "hashes": [ 76 | "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", 77 | "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" 78 | ], 79 | "version": "==0.4.3" 80 | }, 81 | "requests": { 82 | "hashes": [ 83 | "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", 84 | "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" 85 | ], 86 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 87 | "version": "==2.23.0" 88 | }, 89 | "stringcase": { 90 | "hashes": [ 91 | "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008" 92 | ], 93 | "version": "==1.2.0" 94 | }, 95 | "typing-extensions": { 96 | "hashes": [ 97 | "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", 98 | "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", 99 | "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" 100 | ], 101 | "version": "==3.7.4.2" 102 | }, 103 | "typing-inspect": { 104 | "hashes": [ 105 | "sha256:3b98390df4d999a28cf5b35d8b333425af5da2ece8a4ea9e98f71e7591347b4f", 106 | "sha256:8f1b1dd25908dbfd81d3bebc218011531e7ab614ba6e5bf7826d887c834afab7", 107 | "sha256:de08f50a22955ddec353876df7b2545994d6df08a2f45d54ac8c05e530372ca0" 108 | ], 109 | "version": "==0.6.0" 110 | }, 111 | "urllib3": { 112 | "hashes": [ 113 | "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", 114 | "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" 115 | ], 116 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 117 | "version": "==1.25.9" 118 | } 119 | }, 120 | "develop": { 121 | "apipkg": { 122 | "hashes": [ 123 | "sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6", 124 | "sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c" 125 | ], 126 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 127 | "version": "==1.5" 128 | }, 129 | "appdirs": { 130 | "hashes": [ 131 | "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", 132 | "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" 133 | ], 134 | "version": "==1.4.4" 135 | }, 136 | "astroid": { 137 | "hashes": [ 138 | "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", 139 | "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" 140 | ], 141 | "markers": "python_version >= '3.5'", 142 | "version": "==2.4.2" 143 | }, 144 | "atomicwrites": { 145 | "hashes": [ 146 | "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", 147 | "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" 148 | ], 149 | "index": "pypi", 150 | "version": "==1.4.0" 151 | }, 152 | "attrs": { 153 | "hashes": [ 154 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 155 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 156 | ], 157 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 158 | "version": "==19.3.0" 159 | }, 160 | "backcall": { 161 | "hashes": [ 162 | "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", 163 | "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" 164 | ], 165 | "version": "==0.2.0" 166 | }, 167 | "bandit": { 168 | "hashes": [ 169 | "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952", 170 | "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065" 171 | ], 172 | "version": "==1.6.2" 173 | }, 174 | "black": { 175 | "hashes": [ 176 | "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", 177 | "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" 178 | ], 179 | "index": "pypi", 180 | "version": "==19.10b0" 181 | }, 182 | "click": { 183 | "hashes": [ 184 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 185 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 186 | ], 187 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 188 | "version": "==7.1.2" 189 | }, 190 | "dataclasses": { 191 | "hashes": [ 192 | "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836", 193 | "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6" 194 | ], 195 | "markers": "python_version == '3.6' and python_version == '3.6'", 196 | "version": "==0.7" 197 | }, 198 | "dataclasses-json": { 199 | "hashes": [ 200 | "sha256:2f5fca4f097f9f9727e2100c824ed48153171186a679487f8875eca8d8c05107", 201 | "sha256:6e38b11b178e404124bffd6d213736bc505338e8a4c718596efec8d32eb96f5a" 202 | ], 203 | "markers": "python_version >= '3.6'", 204 | "version": "==0.5.1" 205 | }, 206 | "decorator": { 207 | "hashes": [ 208 | "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", 209 | "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7" 210 | ], 211 | "version": "==4.4.2" 212 | }, 213 | "execnet": { 214 | "hashes": [ 215 | "sha256:cacb9df31c9680ec5f95553976c4da484d407e85e41c83cb812aa014f0eddc50", 216 | "sha256:d4efd397930c46415f62f8a31388d6be4f27a91d7550eb79bc64a756e0056547" 217 | ], 218 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 219 | "version": "==1.7.1" 220 | }, 221 | "flake8": { 222 | "hashes": [ 223 | "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", 224 | "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" 225 | ], 226 | "index": "pypi", 227 | "version": "==3.8.3" 228 | }, 229 | "flake8-bandit": { 230 | "hashes": [ 231 | "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b" 232 | ], 233 | "index": "pypi", 234 | "version": "==2.1.2" 235 | }, 236 | "flake8-black": { 237 | "hashes": [ 238 | "sha256:6b5fe2a609fa750170da8d5b1ed7c11029bceaff025660be7f19307ec6fa0c35" 239 | ], 240 | "index": "pypi", 241 | "version": "==0.1.0" 242 | }, 243 | "flake8-breakpoint": { 244 | "hashes": [ 245 | "sha256:27e0cb132647f9ef348b4a3c3126e7350bedbb22e8e221cd11712a223855ea0b", 246 | "sha256:5bc70d478f0437a3655d094e1d2fca81ddacabaa84d99db45ad3630bf2004064" 247 | ], 248 | "index": "pypi", 249 | "version": "==1.1.0" 250 | }, 251 | "flake8-broken-line": { 252 | "hashes": [ 253 | "sha256:167130fcb4761755e9919c0bc8f984ff5790df1ff7a4447fed5c00b09ea6b4c3", 254 | "sha256:550d217ebcdb1d3febc3a7dd5962b2deb4f809a5b0f10b7632b416c4877d2760" 255 | ], 256 | "index": "pypi", 257 | "version": "==0.2.0" 258 | }, 259 | "flake8-bugbear": { 260 | "hashes": [ 261 | "sha256:a3ddc03ec28ba2296fc6f89444d1c946a6b76460f859795b35b77d4920a51b63", 262 | "sha256:bd02e4b009fb153fe6072c31c52aeab5b133d508095befb2ffcf3b41c4823162" 263 | ], 264 | "index": "pypi", 265 | "version": "==20.1.4" 266 | }, 267 | "flake8-builtins": { 268 | "hashes": [ 269 | "sha256:09998853b2405e98e61d2ff3027c47033adbdc17f9fe44ca58443d876eb00f3b", 270 | "sha256:7706babee43879320376861897e5d1468e396a40b8918ed7bccf70e5f90b8687" 271 | ], 272 | "index": "pypi", 273 | "version": "==1.5.3" 274 | }, 275 | "flake8-coding": { 276 | "hashes": [ 277 | "sha256:79704112c44d09d4ab6c8965e76a20c3f7073d52146db60303bce777d9612260", 278 | "sha256:b8f4d5157a8f74670e6cfea732c3d9f4291a4e994c8701d2c55f787c6e6cb741" 279 | ], 280 | "index": "pypi", 281 | "version": "==1.3.2" 282 | }, 283 | "flake8-commas": { 284 | "hashes": [ 285 | "sha256:d3005899466f51380387df7151fb59afec666a0f4f4a2c6a8995b975de0f44b7", 286 | "sha256:ee2141a3495ef9789a3894ed8802d03eff1eaaf98ce6d8653a7c573ef101935e" 287 | ], 288 | "index": "pypi", 289 | "version": "==2.0.0" 290 | }, 291 | "flake8-comprehensions": { 292 | "hashes": [ 293 | "sha256:44eaae9894aa15f86e0c86df1e218e7917494fab6f96d28f96a029c460f17d92", 294 | "sha256:d5751acc0f7364794c71d06f113f4686d6e2e26146a50fa93130b9f200fe160d" 295 | ], 296 | "index": "pypi", 297 | "version": "==3.2.3" 298 | }, 299 | "flake8-debugger": { 300 | "hashes": [ 301 | "sha256:712d7c1ff69ddf3f0130e94cc88c2519e720760bce45e8c330bfdcb61ab4090d" 302 | ], 303 | "index": "pypi", 304 | "version": "==3.2.1" 305 | }, 306 | "flake8-docstrings": { 307 | "hashes": [ 308 | "sha256:3d5a31c7ec6b7367ea6506a87ec293b94a0a46c0bce2bb4975b7f1d09b6f3717", 309 | "sha256:a256ba91bc52307bef1de59e2a009c3cf61c3d0952dbe035d6ff7208940c2edc" 310 | ], 311 | "index": "pypi", 312 | "version": "==1.5.0" 313 | }, 314 | "flake8-formatter-abspath": { 315 | "hashes": [ 316 | "sha256:694d0874d5d047ed57c82a10213f75604475e4525ee8bbaad53417a7d6f8442c", 317 | "sha256:7ff3d9186eb61b4c78a118acf70cd6ef26fb5e323d9a927b47f062b5e8bf31ed" 318 | ], 319 | "index": "pypi", 320 | "version": "==1.0.1" 321 | }, 322 | "flake8-isort": { 323 | "hashes": [ 324 | "sha256:3ce227b5c5342b6d63937d3863e8de8783ae21863cb035cf992cdb0ba5990aa3", 325 | "sha256:f5322a85cea89998e0df954162fd35a1f1e5b5eb4fc0c79b5975aa2799106baa" 326 | ], 327 | "index": "pypi", 328 | "version": "==3.0.0" 329 | }, 330 | "flake8-logging-format": { 331 | "hashes": [ 332 | "sha256:ca5f2b7fc31c3474a0aa77d227e022890f641a025f0ba664418797d979a779f8" 333 | ], 334 | "index": "pypi", 335 | "version": "==0.6.0" 336 | }, 337 | "flake8-mock": { 338 | "hashes": [ 339 | "sha256:2fa775e7589f4e1ad74f35d60953eb20937f5d7355235e54bf852c6837f2bede" 340 | ], 341 | "index": "pypi", 342 | "version": "==0.3" 343 | }, 344 | "flake8-mutable": { 345 | "hashes": [ 346 | "sha256:38fd9dadcbcda6550a916197bc40ed76908119dabb37fbcca30873666c31d2d5", 347 | "sha256:ee9b77111b867d845177bbc289d87d541445ffcc6029a0c5c65865b42b18c6a6" 348 | ], 349 | "index": "pypi", 350 | "version": "==1.2.0" 351 | }, 352 | "flake8-pep3101": { 353 | "hashes": [ 354 | "sha256:86e3eb4e42de8326dcd98ebdeaf9a3c6854203a48f34aeb3e7e8ed948107f512", 355 | "sha256:a5dae1caca1243b2b40108dce926d97cf5a9f52515c4a4cbb1ffe1ca0c54e343" 356 | ], 357 | "index": "pypi", 358 | "version": "==1.3.0" 359 | }, 360 | "flake8-plugin-utils": { 361 | "hashes": [ 362 | "sha256:305461c4fbf94877bcc9ccf435771b135d72a40eefd92e70a4b5f761ca43b1c8", 363 | "sha256:965931e7c17a760915e38bb10dc60516b414ef8210e987252a8d73dcb196a5f5" 364 | ], 365 | "markers": "python_version >= '3.6' and python_version < '4.0'", 366 | "version": "==1.3.0" 367 | }, 368 | "flake8-polyfill": { 369 | "hashes": [ 370 | "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", 371 | "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda" 372 | ], 373 | "version": "==1.0.2" 374 | }, 375 | "flake8-print": { 376 | "hashes": [ 377 | "sha256:324f9e59a522518daa2461bacd7f82da3c34eb26a4314c2a54bd493f8b394a68" 378 | ], 379 | "index": "pypi", 380 | "version": "==3.1.4" 381 | }, 382 | "flake8-quotes": { 383 | "hashes": [ 384 | "sha256:3f1116e985ef437c130431ac92f9b3155f8f652fda7405ac22ffdfd7a9d1055e" 385 | ], 386 | "index": "pypi", 387 | "version": "==3.2.0" 388 | }, 389 | "flake8-spellcheck": { 390 | "hashes": [ 391 | "sha256:3ebd8b79065d4b596a1db6cbf253ebac815c519b2b7dd6b1bf1c6c166a404f69", 392 | "sha256:e6dbd944c607c917fff15b5e293a61d7d290570bd9cd714b6f4180cc12a7dc5f" 393 | ], 394 | "index": "pypi", 395 | "version": "==0.13.0" 396 | }, 397 | "flake8-string-format": { 398 | "hashes": [ 399 | "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2", 400 | "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af" 401 | ], 402 | "index": "pypi", 403 | "version": "==0.3.0" 404 | }, 405 | "flake8-type-annotations": { 406 | "hashes": [ 407 | "sha256:88775455792ad7bbd63a71bc94e8a077deb5608eacb5add7e5a7a648c7636426", 408 | "sha256:de64de5efef3277d7b6012e8618c37d35b21465fb16292e46e6eec5b87e47a8c" 409 | ], 410 | "index": "pypi", 411 | "version": "==0.1.0" 412 | }, 413 | "flake8-variables-names": { 414 | "hashes": [ 415 | "sha256:d109f5a8fe8c20d64e165287330f1b0160b442d7f96e1527124ba1b63c438347" 416 | ], 417 | "index": "pypi", 418 | "version": "==0.0.3" 419 | }, 420 | "gitdb": { 421 | "hashes": [ 422 | "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", 423 | "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" 424 | ], 425 | "markers": "python_version >= '3.4'", 426 | "version": "==4.0.5" 427 | }, 428 | "gitpython": { 429 | "hashes": [ 430 | "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a", 431 | "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac" 432 | ], 433 | "markers": "python_version >= '3.4'", 434 | "version": "==3.1.3" 435 | }, 436 | "importlib-metadata": { 437 | "hashes": [ 438 | "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545", 439 | "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958" 440 | ], 441 | "markers": "python_version < '3.8' and python_version < '3.8'", 442 | "version": "==1.6.1" 443 | }, 444 | "ipython": { 445 | "hashes": [ 446 | "sha256:0ef1433879816a960cd3ae1ae1dc82c64732ca75cec8dab5a4e29783fb571d0e", 447 | "sha256:1b85d65632211bf5d3e6f1406f3393c8c429a47d7b947b9a87812aa5bce6595c" 448 | ], 449 | "index": "pypi", 450 | "version": "==7.15.0" 451 | }, 452 | "ipython-genutils": { 453 | "hashes": [ 454 | "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", 455 | "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" 456 | ], 457 | "version": "==0.2.0" 458 | }, 459 | "isort": { 460 | "extras": [ 461 | "pyproject" 462 | ], 463 | "hashes": [ 464 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", 465 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" 466 | ], 467 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 468 | "version": "==4.3.21" 469 | }, 470 | "jedi": { 471 | "hashes": [ 472 | "sha256:cd60c93b71944d628ccac47df9a60fec53150de53d42dc10a7fc4b5ba6aae798", 473 | "sha256:df40c97641cb943661d2db4c33c2e1ff75d491189423249e989bcea4464f3030" 474 | ], 475 | "index": "pypi", 476 | "version": "==0.17.0" 477 | }, 478 | "lazy-object-proxy": { 479 | "hashes": [ 480 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", 481 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", 482 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", 483 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", 484 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", 485 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", 486 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", 487 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", 488 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", 489 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", 490 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", 491 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", 492 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", 493 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", 494 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", 495 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", 496 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", 497 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", 498 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", 499 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", 500 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" 501 | ], 502 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 503 | "version": "==1.4.3" 504 | }, 505 | "marshmallow": { 506 | "hashes": [ 507 | "sha256:35ee2fb188f0bd9fc1cf9ac35e45fd394bd1c153cee430745a465ea435514bd5", 508 | "sha256:9aa20f9b71c992b4782dad07c51d92884fd0f7c5cb9d3c737bea17ec1bad765f" 509 | ], 510 | "markers": "python_version >= '3.5'", 511 | "version": "==3.6.1" 512 | }, 513 | "marshmallow-enum": { 514 | "hashes": [ 515 | "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58", 516 | "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072" 517 | ], 518 | "version": "==1.5.1" 519 | }, 520 | "mccabe": { 521 | "hashes": [ 522 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 523 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 524 | ], 525 | "index": "pypi", 526 | "version": "==0.6.1" 527 | }, 528 | "more-itertools": { 529 | "hashes": [ 530 | "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be", 531 | "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982" 532 | ], 533 | "markers": "python_version >= '3.5'", 534 | "version": "==8.3.0" 535 | }, 536 | "mypy": { 537 | "hashes": [ 538 | "sha256:00cb1964a7476e871d6108341ac9c1a857d6bd20bf5877f4773ac5e9d92cd3cd", 539 | "sha256:127de5a9b817a03a98c5ae8a0c46a20dc44442af6dcfa2ae7f96cb519b312efa", 540 | "sha256:1f3976a945ad7f0a0727aafdc5651c2d3278e3c88dee94e2bf75cd3386b7b2f4", 541 | "sha256:2f8c098f12b402c19b735aec724cc9105cc1a9eea405d08814eb4b14a6fb1a41", 542 | "sha256:4ef13b619a289aa025f2273e05e755f8049bb4eaba6d703a425de37d495d178d", 543 | "sha256:5d142f219bf8c7894dfa79ebfb7d352c4c63a325e75f10dfb4c3db9417dcd135", 544 | "sha256:62eb5dd4ea86bda8ce386f26684f7f26e4bfe6283c9f2b6ca6d17faf704dcfad", 545 | "sha256:64c36eb0936d0bfb7d8da49f92c18e312ad2e3ed46e5548ae4ca997b0d33bd59", 546 | "sha256:75eed74d2faf2759f79c5f56f17388defd2fc994222312ec54ee921e37b31ad4", 547 | "sha256:974bebe3699b9b46278a7f076635d219183da26e1a675c1f8243a69221758273", 548 | "sha256:a5e5bb12b7982b179af513dddb06fca12285f0316d74f3964078acbfcf4c68f2", 549 | "sha256:d31291df31bafb997952dc0a17ebb2737f802c754aed31dd155a8bfe75112c57", 550 | "sha256:d3b4941de44341227ece1caaf5b08b23e42ad4eeb8b603219afb11e9d4cfb437", 551 | "sha256:eadb865126da4e3c4c95bdb47fe1bb087a3e3ea14d39a3b13224b8a4d9f9a102" 552 | ], 553 | "index": "pypi", 554 | "version": "==0.780" 555 | }, 556 | "mypy-extensions": { 557 | "hashes": [ 558 | "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", 559 | "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" 560 | ], 561 | "version": "==0.4.3" 562 | }, 563 | "packaging": { 564 | "hashes": [ 565 | "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", 566 | "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" 567 | ], 568 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 569 | "version": "==20.4" 570 | }, 571 | "parso": { 572 | "hashes": [ 573 | "sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0", 574 | "sha256:908e9fae2144a076d72ae4e25539143d40b8e3eafbaeae03c1bfe226f4cdf12c" 575 | ], 576 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 577 | "version": "==0.7.0" 578 | }, 579 | "pathspec": { 580 | "hashes": [ 581 | "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", 582 | "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" 583 | ], 584 | "version": "==0.8.0" 585 | }, 586 | "pbr": { 587 | "hashes": [ 588 | "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c", 589 | "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8" 590 | ], 591 | "version": "==5.4.5" 592 | }, 593 | "pep8-naming": { 594 | "hashes": [ 595 | "sha256:5d9f1056cb9427ce344e98d1a7f5665710e2f20f748438e308995852cfa24164", 596 | "sha256:f3b4a5f9dd72b991bf7d8e2a341d2e1aa3a884a769b5aaac4f56825c1763bf3a" 597 | ], 598 | "index": "pypi", 599 | "version": "==0.10.0" 600 | }, 601 | "pexpect": { 602 | "hashes": [ 603 | "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", 604 | "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" 605 | ], 606 | "markers": "sys_platform != 'win32'", 607 | "version": "==4.8.0" 608 | }, 609 | "pickleshare": { 610 | "hashes": [ 611 | "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", 612 | "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" 613 | ], 614 | "version": "==0.7.5" 615 | }, 616 | "pluggy": { 617 | "hashes": [ 618 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 619 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 620 | ], 621 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 622 | "version": "==0.13.1" 623 | }, 624 | "prompt-toolkit": { 625 | "hashes": [ 626 | "sha256:563d1a4140b63ff9dd587bda9557cffb2fe73650205ab6f4383092fb882e7dc8", 627 | "sha256:df7e9e63aea609b1da3a65641ceaf5bc7d05e0a04de5bd45d05dbeffbabf9e04" 628 | ], 629 | "markers": "python_full_version >= '3.6.1'", 630 | "version": "==3.0.5" 631 | }, 632 | "ptyprocess": { 633 | "hashes": [ 634 | "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", 635 | "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" 636 | ], 637 | "version": "==0.6.0" 638 | }, 639 | "py": { 640 | "hashes": [ 641 | "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", 642 | "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" 643 | ], 644 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 645 | "version": "==1.8.1" 646 | }, 647 | "pycodestyle": { 648 | "hashes": [ 649 | "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", 650 | "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" 651 | ], 652 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 653 | "version": "==2.6.0" 654 | }, 655 | "pydocstyle": { 656 | "hashes": [ 657 | "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", 658 | "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5" 659 | ], 660 | "markers": "python_version >= '3.5'", 661 | "version": "==5.0.2" 662 | }, 663 | "pyflakes": { 664 | "hashes": [ 665 | "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", 666 | "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" 667 | ], 668 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 669 | "version": "==2.2.0" 670 | }, 671 | "pygments": { 672 | "hashes": [ 673 | "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", 674 | "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" 675 | ], 676 | "markers": "python_version >= '3.5'", 677 | "version": "==2.6.1" 678 | }, 679 | "pylint": { 680 | "hashes": [ 681 | "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", 682 | "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" 683 | ], 684 | "index": "pypi", 685 | "version": "==2.5.3" 686 | }, 687 | "pyparsing": { 688 | "hashes": [ 689 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 690 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 691 | ], 692 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", 693 | "version": "==2.4.7" 694 | }, 695 | "pytest": { 696 | "hashes": [ 697 | "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1", 698 | "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8" 699 | ], 700 | "index": "pypi", 701 | "version": "==5.4.3" 702 | }, 703 | "pytest-forked": { 704 | "hashes": [ 705 | "sha256:1805699ed9c9e60cb7a8179b8d4fa2b8898098e82d229b0825d8095f0f261100", 706 | "sha256:1ae25dba8ee2e56fb47311c9638f9e58552691da87e82d25b0ce0e4bf52b7d87" 707 | ], 708 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 709 | "version": "==1.1.3" 710 | }, 711 | "pytest-xdist": { 712 | "hashes": [ 713 | "sha256:1d4166dcac69adb38eeaedb88c8fada8588348258a3492ab49ba9161f2971129", 714 | "sha256:ba5ec9fde3410bd9a116ff7e4f26c92e02fa3d27975ef3ad03f330b3d4b54e91" 715 | ], 716 | "index": "pypi", 717 | "version": "==1.32.0" 718 | }, 719 | "requests-mock": { 720 | "hashes": [ 721 | "sha256:11215c6f4df72702aa357f205cf1e537cffd7392b3e787b58239bde5fb3db53b", 722 | "sha256:e68f46844e4cee9d447150343c9ae875f99fa8037c6dcf5f15bf1fe9ab43d226" 723 | ], 724 | "index": "pypi", 725 | "version": "==1.8.0" 726 | }, 727 | "pyyaml": { 728 | "hashes": [ 729 | "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", 730 | "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", 731 | "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", 732 | "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", 733 | "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", 734 | "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", 735 | "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", 736 | "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", 737 | "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", 738 | "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", 739 | "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" 740 | ], 741 | "version": "==5.3.1" 742 | }, 743 | "regex": { 744 | "hashes": [ 745 | "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a", 746 | "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938", 747 | "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29", 748 | "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae", 749 | "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387", 750 | "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a", 751 | "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf", 752 | "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610", 753 | "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9", 754 | "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5", 755 | "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3", 756 | "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89", 757 | "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded", 758 | "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754", 759 | "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f", 760 | "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868", 761 | "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd", 762 | "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910", 763 | "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3", 764 | "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac", 765 | "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c" 766 | ], 767 | "version": "==2020.6.8" 768 | }, 769 | "six": { 770 | "hashes": [ 771 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 772 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 773 | ], 774 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 775 | "version": "==1.15.0" 776 | }, 777 | "smmap": { 778 | "hashes": [ 779 | "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", 780 | "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" 781 | ], 782 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 783 | "version": "==3.0.4" 784 | }, 785 | "snowballstemmer": { 786 | "hashes": [ 787 | "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", 788 | "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" 789 | ], 790 | "version": "==2.0.0" 791 | }, 792 | "stevedore": { 793 | "hashes": [ 794 | "sha256:001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9", 795 | "sha256:471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691" 796 | ], 797 | "markers": "python_version >= '3.6'", 798 | "version": "==2.0.0" 799 | }, 800 | "stringcase": { 801 | "hashes": [ 802 | "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008" 803 | ], 804 | "version": "==1.2.0" 805 | }, 806 | "testfixtures": { 807 | "hashes": [ 808 | "sha256:30566e24a1b34e4d3f8c13abf62557d01eeb4480bcb8f1745467bfb0d415a7d9", 809 | "sha256:58d2b3146d93bc5ddb0cd24e0ccacb13e29bdb61e5c81235c58f7b8ee4470366" 810 | ], 811 | "version": "==6.14.1" 812 | }, 813 | "toml": { 814 | "hashes": [ 815 | "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", 816 | "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" 817 | ], 818 | "version": "==0.10.1" 819 | }, 820 | "traitlets": { 821 | "hashes": [ 822 | "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44", 823 | "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7" 824 | ], 825 | "version": "==4.3.3" 826 | }, 827 | "typed-ast": { 828 | "hashes": [ 829 | "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", 830 | "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", 831 | "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", 832 | "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", 833 | "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", 834 | "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", 835 | "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", 836 | "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", 837 | "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", 838 | "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", 839 | "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", 840 | "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", 841 | "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", 842 | "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", 843 | "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", 844 | "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", 845 | "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", 846 | "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", 847 | "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", 848 | "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", 849 | "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" 850 | ], 851 | "markers": "implementation_name == 'cpython' and python_version < '3.8'", 852 | "version": "==1.4.1" 853 | }, 854 | "typing-extensions": { 855 | "hashes": [ 856 | "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", 857 | "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", 858 | "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" 859 | ], 860 | "version": "==3.7.4.2" 861 | }, 862 | "typing-inspect": { 863 | "hashes": [ 864 | "sha256:3b98390df4d999a28cf5b35d8b333425af5da2ece8a4ea9e98f71e7591347b4f", 865 | "sha256:8f1b1dd25908dbfd81d3bebc218011531e7ab614ba6e5bf7826d887c834afab7", 866 | "sha256:de08f50a22955ddec353876df7b2545994d6df08a2f45d54ac8c05e530372ca0" 867 | ], 868 | "version": "==0.6.0" 869 | }, 870 | "wcwidth": { 871 | "hashes": [ 872 | "sha256:79375666b9954d4a1a10739315816324c3e73110af9d0e102d906fdb0aec009f", 873 | "sha256:8c6b5b6ee1360b842645f336d9e5d68c55817c26d3050f46b235ef2bc650e48f" 874 | ], 875 | "version": "==0.2.4" 876 | }, 877 | "wrapt": { 878 | "hashes": [ 879 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 880 | ], 881 | "version": "==1.12.1" 882 | }, 883 | "zipp": { 884 | "hashes": [ 885 | "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", 886 | "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" 887 | ], 888 | "markers": "python_version >= '3.6'", 889 | "version": "==3.1.0" 890 | } 891 | } 892 | } 893 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-release-retry 2 | 3 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | 5 | 6 | `github-release-retry` is a 7 | tool for creating GitHub Releases and uploading assets **reliably**. 8 | It differs from other 9 | tools because it uploads 10 | assets reliably by 11 | verifying that the asset 12 | exists, 13 | and retries (deleting partial assets) if not. 14 | 15 | This is not an officially supported Google product. 16 | 17 | ## Install 18 | 19 | Requires Python 3.6+. 20 | 21 | To ensure you use the `pip` module associated with your 22 | preferred Python 3.6+ binary: 23 | 24 | ```bash 25 | python3 -m pip install --user github-release-retry 26 | # where `python3` is your preferred Python 3.6+ binary. 27 | # Omit `--user` to install for all users. 28 | ``` 29 | 30 | Or just: 31 | 32 | ```bash 33 | pip3 install --user github-release-retry 34 | # where `pip3` is your version of pip for Python 3.6+. 35 | # Omit `--user` to install for all users. 36 | ``` 37 | 38 | ## Usage 39 | 40 | If your [Python user scripts directory](https://www.python.org/dev/peps/pep-0370/) 41 | is not on your `PATH`, 42 | you can use: 43 | 44 | ```bash 45 | python3 -m github_release_retry.github_release_retry 46 | # where `python3` is your preferred Python 3.6+ binary. 47 | ``` 48 | 49 | Otherwise: 50 | 51 | ```bash 52 | $ github-release-retry -h 53 | usage: github-release-retry [-h] --user USER --repo REPO --tag_name TAG_NAME 54 | [--target_commitish TARGET_COMMITISH] 55 | [--release_name RELEASE_NAME] 56 | (--body_string BODY_STRING | --body_file BODY_FILE) 57 | [--draft] [--prerelease] 58 | [--github_api_url GITHUB_API_URL] 59 | [--retry_limit RETRY_LIMIT] 60 | [files [files ...]] 61 | 62 | Creates a GitHub release (if it does not already exist) and uploads files to the release. 63 | Please set the GITHUB_TOKEN environment variable. 64 | EXAMPLE: 65 | github-release-retry \ 66 | --user paul \ 67 | --repo hello-world \ 68 | --tag_name v1.0 \ 69 | --target_commitish 448301eb \ 70 | --body_string "My first release." \ 71 | hello-world.zip RELEASE_NOTES.txt 72 | 73 | positional arguments: 74 | files The files to upload to the release. (default: None) 75 | 76 | optional arguments: 77 | -h, --help show this help message and exit 78 | --user USER Required: The GitHub username or organization name in 79 | which the repo resides. (default: None) 80 | --repo REPO Required: The GitHub repo name in which to make the 81 | release. (default: None) 82 | --tag_name TAG_NAME Required: The name of the tag to create or use. 83 | (default: None) 84 | --target_commitish TARGET_COMMITISH 85 | The commit-ish value where the tag will be created. 86 | Unused if the tag already exists. (default: None) 87 | --release_name RELEASE_NAME 88 | The name of the release. Leave unset to use the 89 | tag_name (recommended). (default: None) 90 | --body_string BODY_STRING 91 | Required (or use --body_file): Text describing the 92 | release. Ignored if the release already exists. 93 | (default: None) 94 | --body_file BODY_FILE 95 | Required (or use --body_string): Text describing the 96 | release, which will be read from BODY_FILE. Ignored if 97 | the release already exists. (default: None) 98 | --draft Creates a draft release, which means it is 99 | unpublished. (default: False) 100 | --prerelease Creates a prerelease release, which means it will be 101 | marked as such. (default: False) 102 | --github_api_url GITHUB_API_URL 103 | The GitHub API URL without a trailing slash. (default: 104 | https://api.github.com) 105 | --retry_limit RETRY_LIMIT 106 | The number of times to retry creating/getting the 107 | release and/or uploading each file. (default: 10) 108 | ``` 109 | 110 | ## Development 111 | 112 | > Optional: if you have just done `git pull` 113 | and `Pipfile.lock` was updated, 114 | you can delete `.venv/` to start from a fresh virtual environment. 115 | 116 | > On Windows, you can use the Git Bash shell, or adapt the commands (including those inside `dev_shell.sh.template`) for the Windows command prompt. 117 | 118 | Clone this repo and change to the directory that contains this README file. Execute `./dev_shell.sh.template`. If the default settings don't work, make a copy of the file called `dev_shell.sh` and modify according to the comments before executing. `pip` must be installed for the version of Python you wish to use. Note that you can do e.g. `export PYTHON=python3` first to set your preferred Python binary. 119 | We currently target Python 3.6+. 120 | 121 | > Pip for Python 3.6 may be broken on certain Debian distributions. 122 | > You can just use the newer Python 3.7+ version provided by your 123 | > distribution. 124 | > Alternatively, see "Installing Python" below if you want to use Python 3.6. 125 | 126 | The script generates a Python virtual environment (located at `.venv/`) with all dependencies installed. 127 | Activate the Python virtual environment via: 128 | 129 | * `source .venv/bin/activate` (on Linux) 130 | * `source .venv/Scripts/activate` (on Windows with the Git Bash shell) 131 | * `.venv/Scripts/activate.bat` (on Windows with cmd) 132 | 133 | 134 | ### Presubmit checks 135 | 136 | * Execute `./check_all.sh` to run various presubmit checks, linters, etc. 137 | * Execute `./fix_all.sh` to automatically fix certain issues, such as formatting. 138 | 139 | 140 | ### PyCharm 141 | 142 | Use PyCharm to open the directory containing this README file. 143 | It should pick up the Python virtual environment 144 | (at `.venv/`) automatically 145 | for both the code 146 | and when you open a `Terminal` or `Python Console` tab. 147 | 148 | Install and configure plugins: 149 | 150 | * File Watchers (may already be installed) 151 | * The watcher task should already be under version control. 152 | * Mypy: the built-in PyCharm type checking uses Mypy behind-the-scenes, but this plugin enhances it by using the latest version and allowing the use of stricter settings, matching the settings used by the `./check_all.sh` script. 153 | 154 | Add `dictionary.dic` as a custom dictionary (search for "Spelling" in Actions). Do not add words via PyCharm's "Quick Fixes" feature, as the word will only be added to your personal dictionary. Instead, manually add the word to `dictionary.dic`. 155 | 156 | ## [Coding conventions](docs/conventions.md) 157 | 158 | ## Terminal 159 | 160 | The `Terminal` tab in PyCharm is useful and will use the project's Python virtual environment. 161 | 162 | ## Installing Python 163 | 164 | To manually install Python on your Linux distribution, you can use `pyenv`. 165 | 166 | https://github.com/pyenv/pyenv#basic-github-checkout 167 | 168 | In summary: 169 | 170 | * Install the required packages recommended [here](https://github.com/pyenv/pyenv/wiki/Common-build-problems). 171 | 172 | * Then: 173 | 174 | ```sh 175 | git clone https://github.com/pyenv/pyenv.git ~/.pyenv 176 | 177 | # Add the following two lines to your ~/.bashrc file. 178 | export PYENV_ROOT="$HOME/.pyenv" 179 | export PATH="$PYENV_ROOT/bin:$PATH" 180 | 181 | # In a new terminal: 182 | eval "$(pyenv init -)" 183 | pyenv install 3.6.9 184 | pyenv global 3.6.9 185 | 186 | # Now execute the development shell script, as usual. 187 | export PYTHON="python" 188 | ./dev_shell.sh.template 189 | ``` 190 | 191 | You can reactivate the virtual environment later 192 | using `source .venv/bin/activate`, 193 | without having to re-execute the above `pyenv` commands. 194 | -------------------------------------------------------------------------------- /check_all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2020 The github-release-retry Project Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -x 18 | set -e 19 | set -u 20 | 21 | # Check for some known files for sanity. 22 | test -f ./Pipfile 23 | 24 | if [ -z ${VIRTUAL_ENV+x} ]; then 25 | source .venv/bin/activate 26 | fi 27 | 28 | python ci/check_headers.py 29 | mypy --strict --show-absolute-path github_release_retry github_release_retry_tests 30 | pylint github_release_retry github_release_retry_tests 31 | # Flake checks formatting via black. 32 | flake8 . 33 | 34 | pytest github_release_retry_tests 35 | -------------------------------------------------------------------------------- /ci/check_headers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2020 The github-release-retry Project Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | import io 19 | import sys 20 | import re 21 | 22 | path = os.path.join 23 | 24 | 25 | def exclude_dirname(f: str): 26 | return ( 27 | f.endswith(".egg-info") or f.startswith(".") or f in ["venv", "__pycache__",] 28 | ) 29 | 30 | 31 | def exclude_dirpath(f: str): 32 | parts = os.path.split(f) 33 | return ( 34 | # Ignore /*build*/ 35 | len(parts) == 2 36 | and "build" in parts[1] 37 | or f 38 | in [ 39 | path(os.curdir, "build"), 40 | path(os.curdir, "dist"), 41 | path(os.curdir, "out"), 42 | path(os.curdir, "temp"), 43 | path(os.curdir, "third_party"), 44 | ] 45 | ) 46 | 47 | 48 | def exclude_filepath(f: str): 49 | return f in [] 50 | 51 | 52 | def exclude_filename(f: str): 53 | return ( 54 | f.startswith(".attach_pid") 55 | or f.endswith(".iml") 56 | or f.endswith(".png") 57 | or f.endswith(".md") 58 | or f.endswith(".json") 59 | or f.endswith(".primitives") 60 | or f.endswith(".jar") 61 | or f.endswith(".spv") 62 | or f.endswith(".dic") 63 | or f 64 | in [ 65 | ".clang-format", 66 | ".clang-tidy", 67 | ".editorconfig", 68 | ".gitmodules", 69 | ".gitignore", 70 | ".gitattributes", 71 | "settings.gradle", 72 | "AUTHORS", 73 | "CODEOWNERS", 74 | "CONTRIBUTORS", 75 | "LICENSE", 76 | "LICENSE.TXT", 77 | "OPEN_SOURCE_LICENSES.TXT", 78 | "local.properties", 79 | "gradlew", 80 | "gradlew.bat", 81 | "dependency-reduced-pom.xml", 82 | "gradle-wrapper.properties", 83 | "Pipfile.lock", 84 | ] 85 | ) 86 | 87 | 88 | def go(): 89 | fail = False 90 | copyright_pattern = re.compile( 91 | r"Copyright 20(18|19|20|21) The github-release-retry Project Authors" 92 | ) 93 | 94 | for (dirpath, dirnames, filenames) in os.walk(os.curdir): 95 | 96 | # dirnames[:] = <--- modifies in-place to ignore certain directories 97 | 98 | if exclude_dirpath(dirpath): 99 | dirnames[:] = [] 100 | continue 101 | 102 | dirnames[:] = [d for d in dirnames if not exclude_dirname(d)] 103 | 104 | for file in [path(dirpath, f) for f in filenames if not exclude_filename(f)]: 105 | if exclude_filepath(file): 106 | continue 107 | try: 108 | with io.open(file, "r") as fin: 109 | contents = fin.read() 110 | 111 | first_lines = "\n".join(contents.split("\n")[:10]) 112 | 113 | # Must contain a header for any year within the first few lines. 114 | if copyright_pattern.search(first_lines) is None: 115 | fail = True 116 | print("Missing license header " + file) 117 | continue 118 | 119 | # This file is OK. Continue to the next file. 120 | except Exception as ex: 121 | print("Failed to check license header of file " + file) 122 | print(ex) 123 | fail = True 124 | 125 | if fail: 126 | sys.exit(1) 127 | 128 | 129 | if __name__ == "__main__": 130 | go() 131 | -------------------------------------------------------------------------------- /dev_shell.sh.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2020 The github-release-retry Project Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -x 18 | set -e 19 | set -u 20 | 21 | # Check for some known files for sanity. 22 | test -f ./Pipfile 23 | test -f ./dev_shell.sh.template 24 | 25 | # Sets PYTHON to python3.6, unless already defined. 26 | # Modify if needed; this should be a Python 3.6+ binary. 27 | # E.g. PYTHON=python3.6 28 | # Or, do `export PYTHON=python3.6` before executing this script. 29 | PYTHON=${PYTHON-python3.6} 30 | 31 | # In some cases it seems that pipenv will NOT recognize the Python version 32 | # that is being used, and so pipenv will create a virtual environment using 33 | # some other version of Python. This line ensures the virtual environment 34 | # created uses the correct Python binary. 35 | PIPENV_PYTHON="$(which "${PYTHON}")" 36 | export PIPENV_PYTHON 37 | 38 | # Upgrade/install pip and pipenv if needed. 39 | "${PYTHON}" -m pip install --upgrade --user --no-warn-script-location 'pip>=19.2.3' 'pipenv>=2018.11.26' 40 | 41 | # Place the virtual environment at `.venv/`. 42 | export PIPENV_VENV_IN_PROJECT=1 43 | 44 | # Use the hard-coded versions of packages in Pipfile.lock. 45 | export PIPENV_IGNORE_PIPFILE=1 46 | 47 | # Install project dependencies, including development dependencies, into the 48 | # virtual environment using pipenv. 49 | "${PYTHON}" -m pipenv install --dev 50 | -------------------------------------------------------------------------------- /dictionary.dic: -------------------------------------------------------------------------------- 1 | 2 | id 3 | base64 4 | pathlib 5 | Mixin 6 | dic 7 | pylint 8 | noqa 9 | T001 10 | VNE003 11 | commitish 12 | prerelease 13 | v3 14 | v4 15 | TODO 16 | repo 17 | graphql 18 | metavar 19 | VNE001 20 | Formatter 21 | A003 22 | noinspection 23 | dirname 24 | testcase 25 | unprocessable 26 | S106 27 | endian 28 | exc 29 | -------------------------------------------------------------------------------- /docs/conventions.md: -------------------------------------------------------------------------------- 1 | # Coding conventions 2 | 3 | * Imports: 4 | * We use the `black` Python code formatter and `isort` for sorting imports. 5 | * We also use the following import style, which is not automatically checked: 6 | 7 | ```python 8 | import random # Standard modules come first (isort takes care of this). 9 | import re # Import the module only, not classes or functions. 10 | import subprocess 11 | from dataclasses import dataclass # Exception: dataclass. 12 | from dataclasses_json import DataClassJsonMixin # Exception: DataClassJsonMixin. 13 | from pathlib import Path # Exception: Path. 14 | from typing import Iterable, List, Optional # Exception: typing. 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /docs/releases.md: -------------------------------------------------------------------------------- 1 | # Releases 2 | 3 | 1. Double-check the current version number in setup.py; it should be a version number that is not yet released. 4 | 2. Follow https://packaging.python.org/tutorials/packaging-projects/ 5 | 6 | Rough summary: 7 | 8 | In the developer shell: 9 | 10 | ```sh 11 | python3 -m pip install --upgrade build 12 | python3 -m pip install --upgrade twine 13 | 14 | python3 -m build 15 | python3 -m twine upload --repository testpypi dist/* 16 | # Username: __token__ 17 | # Password: [TestPyPI API token, including the pypi- prefix] 18 | 19 | # Check: https://test.pypi.org/project/github-release-retry/ 20 | # There should be a new version. 21 | ``` 22 | 23 | In some other terminal: 24 | 25 | ```sh 26 | # Test the test package in some temporary virtual environment. 27 | 28 | python3 -m venv .venv 29 | 30 | source .venv/bin/activate 31 | 32 | python3 -m pip install dataclasses-json 33 | python3 -m pip install requests 34 | python3 -m pip install -i https://test.pypi.org/simple/ --no-deps github-release-retry 35 | 36 | github-release-retry -h 37 | ``` 38 | 39 | Now, from the developer shell, repeat the `twine` command above, but omit `--repository testpypi`. Note that the token will be different. There should then be a new package version at: https://pypi.org/project/github-release-retry/ 40 | 41 | Then, in some other terminal: 42 | 43 | ```sh 44 | # Test the package in some temporary virtual environment. 45 | 46 | python3 -m venv .venv 47 | source .venv/bin/activate 48 | python3 -m pip install github-release-retry 49 | github-release-retry -h 50 | ``` 51 | 52 | ## Git tag 53 | Create a git tag of the form "1.0.x" (check the current version number in setup.py) and push it. 54 | 55 | ## Finally: 56 | 57 | Increment the version number in setup.py in preparation for a future release. Do a PR to update the version number. 58 | -------------------------------------------------------------------------------- /fix_all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2020 The github-release-retry Project Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -x 18 | set -e 19 | set -u 20 | 21 | if [ -z ${VIRTUAL_ENV+x} ]; then 22 | source .venv/bin/activate 23 | fi 24 | 25 | isort -rc github_release_retry github_release_retry_tests 26 | black github_release_retry github_release_retry_tests 27 | -------------------------------------------------------------------------------- /github_release_retry/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2020 The github-release-retry Project Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | -------------------------------------------------------------------------------- /github_release_retry/github_release_retry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2020 The github-release-retry Project Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """Creates a GitHub release and uploads files.""" 19 | 20 | import argparse 21 | import base64 22 | import json 23 | import os 24 | import sys 25 | import time 26 | import traceback 27 | import typing 28 | from dataclasses import dataclass 29 | from pathlib import Path 30 | from typing import Any, Dict, List, Optional 31 | 32 | import requests 33 | 34 | if typing.TYPE_CHECKING: 35 | from dataclasses_json.api import DataClassJsonMixin 36 | else: 37 | from dataclasses_json import DataClassJsonMixin 38 | 39 | 40 | def remove_none_fields(dic: Any) -> Any: 41 | return {k: v for k, v in dic.items() if v is not None} 42 | 43 | 44 | def to_dict(obj: DataClassJsonMixin) -> Any: # pylint: disable=used-before-assignment; 45 | return remove_none_fields(obj.to_dict()) 46 | 47 | 48 | def log(message: str) -> None: 49 | print(message, file=sys.stderr) # noqa: T001 50 | 51 | 52 | def log_exception(message: str) -> None: 53 | log(message) 54 | traceback.print_exc(file=sys.stderr) 55 | log("") 56 | 57 | 58 | def log_response(response: requests.Response) -> None: 59 | log(f"status_code: {response.status_code}") 60 | if response.content: 61 | try: 62 | content = response.content.decode(encoding="utf-8", errors="ignore") 63 | log(f"content: {content}") 64 | except Exception: # pylint: disable=broad-except; 65 | log(f"content: {response.content!r}") 66 | log("") 67 | 68 | 69 | def release_asset_node_id_to_asset_id(node_id: str) -> str: 70 | """ 71 | Extracts and returns the asset id from the given Release Asset |node_id|. 72 | 73 | The "id" returned from the GraphQL v4 API is called the "node_id" in the REST API v3. 74 | We can get back to the REST "id" by decoding the "node_id" (it is base64 encoded) 75 | and extracting the id number at the end, but this is undocumented and may change. 76 | 77 | :param node_id: The Release Asset node_id. 78 | :return: The extracted REST API v3 asset id. 79 | """ 80 | # There is a new format and an old format. 81 | 82 | if node_id.startswith("RA_"): 83 | # New format: "RA_[base64 encoded bytes]". 84 | # The last four bytes (big-endian, unsigned) of the base64 encoded bytes are the node id. 85 | 86 | # Strip off the "RA_". 87 | base64_string = node_id[3:] 88 | 89 | asset_id = str(int.from_bytes(base64.b64decode(base64_string)[-4:], "big")) 90 | else: 91 | # Old format: just a base64 encoded string. 92 | # Once decoded, the format is similar to "012:ReleaseAsset18381577". # noqa: SC100 93 | # The asset id part is 18381577. 94 | node_id_decoded: str = base64.b64decode(node_id).decode( 95 | encoding="utf-8", errors="ignore" 96 | ) 97 | if "ReleaseAsset" not in node_id_decoded: 98 | raise AssertionError( 99 | f"Unrecognized node_id format: {node_id}. Decoded (base64) string: {node_id_decoded}." 100 | ) 101 | 102 | asset_id = node_id_decoded.split("ReleaseAsset")[1] 103 | 104 | return asset_id 105 | 106 | 107 | @dataclass 108 | class GithubResourceError(DataClassJsonMixin): 109 | resource: Optional[str] = None 110 | field: Optional[str] = None 111 | code: Optional[str] = None 112 | 113 | 114 | @dataclass 115 | class GithubClientError(DataClassJsonMixin): 116 | message: Optional[str] = None 117 | errors: Optional[List[GithubResourceError]] = None 118 | 119 | 120 | @dataclass 121 | class Asset(DataClassJsonMixin): 122 | url: Optional[str] = None 123 | browser_download_url: Optional[str] = None 124 | id: Optional[str] = None # noqa: VNE003, A003 125 | name: Optional[str] = None 126 | label: Optional[str] = None 127 | state: Optional[str] = None 128 | content_type: Optional[str] = None 129 | size: Optional[int] = None 130 | 131 | 132 | @dataclass 133 | class Release(DataClassJsonMixin): 134 | upload_url: Optional[str] = None 135 | id: Optional[str] = None # noqa: VNE003, A003 136 | tag_name: Optional[str] = None 137 | target_commitish: Optional[str] = None 138 | name: Optional[str] = None 139 | body: Optional[str] = None 140 | draft: Optional[bool] = None 141 | prerelease: Optional[bool] = None 142 | assets: Optional[List[Asset]] = None 143 | 144 | 145 | @dataclass 146 | class GithubApi(DataClassJsonMixin): 147 | github_api_url: str 148 | user: str 149 | repo: str 150 | token: str 151 | retry_limit: int 152 | 153 | def _headers_v3(self) -> Dict[str, str]: 154 | return { 155 | "Accept": "application/vnd.github.v3.text-match+json", 156 | "Authorization": f"token {self.token}", 157 | "User-Agent": f"{self.user} {self.repo}", 158 | } 159 | 160 | def _headers_v4(self) -> Dict[str, str]: 161 | return { 162 | "Authorization": f"bearer {self.token}", 163 | "User-Agent": f"{self.user} {self.repo}", 164 | } 165 | 166 | @staticmethod 167 | def _wait() -> None: 168 | # Don't make too many requests per second. 169 | # We are unlikely to reach official rate limits, BUT repeated polling can look like abuse. 170 | # TODO: revisit this if needed. 171 | time.sleep(1) 172 | 173 | def create_release(self, release: Release) -> requests.Response: 174 | self._wait() 175 | return requests.post( 176 | url=f"{self.github_api_url}/repos/{self.user}/{self.repo}/releases", 177 | json=to_dict(release), 178 | headers=self._headers_v3(), 179 | ) 180 | 181 | def get_release_by_tag(self, tag_name: str) -> requests.Response: 182 | self._wait() 183 | return requests.get( 184 | url=f"{self.github_api_url}/repos/{self.user}/{self.repo}/releases/tags/{tag_name}", 185 | headers=self._headers_v3(), 186 | ) 187 | 188 | def get_asset_by_id(self, asset_id: str) -> requests.Response: 189 | self._wait() 190 | return requests.get( 191 | url=f"{self.github_api_url}/repos/{self.user}/{self.repo}/releases/assets/{asset_id}", 192 | headers=self._headers_v3(), 193 | ) 194 | 195 | def delete_asset(self, asset_id: str) -> requests.Response: 196 | self._wait() 197 | return requests.delete( 198 | url=f"{self.github_api_url}/repos/{self.user}/{self.repo}/releases/assets/{asset_id}", 199 | headers={**self._headers_v3(), "Content-type": "application/json"}, 200 | ) 201 | 202 | def upload_asset(self, file_path: Path, release: Release) -> requests.Response: 203 | if not release.upload_url: 204 | raise AssertionError("Need release object with upload_url.") 205 | 206 | # Upload URL looks like: 207 | # https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label} 208 | # We want the part before {. 209 | upload_url = release.upload_url.split("{")[0] 210 | # Then we add the name. 211 | upload_url = f"{upload_url}?name={file_path.name}" 212 | 213 | self._wait() 214 | with file_path.open(mode="rb") as f: 215 | return requests.post( 216 | url=upload_url, 217 | headers={ 218 | **self._headers_v3(), 219 | "Content-Type": "application/octet-stream", 220 | }, 221 | data=f, 222 | ) 223 | 224 | def graphql_query(self, query: str) -> requests.Response: 225 | self._wait() 226 | return requests.post( 227 | f"{self.github_api_url}/graphql", 228 | headers=self._headers_v4(), 229 | json={"query": query}, 230 | ) 231 | 232 | def find_asset_id_by_file_name( 233 | self, file_name: str, release: Release 234 | ) -> Optional[str]: 235 | """ 236 | Returns the asset id. 237 | 238 | This relies on undocumented behavior; see release_asset_node_id_to_asset_id. 239 | 240 | :returns the asset id or None if the asset was not found. 241 | """ 242 | if not release.tag_name: 243 | raise AssertionError("Expected tag_name") 244 | 245 | # We get the asset id via GitHub's v4 GraphQL API, as this seems to be more reliable. 246 | # But most other operations still require using the REST v3 API. 247 | 248 | log(f"Finding asset id using v4 API of {file_name}.") 249 | 250 | query = f""" 251 | query {{ 252 | repository(owner:"{self.user}", name:"{self.repo}") {{ 253 | release(tagName:"{release.tag_name}") {{ 254 | releaseAssets(first: 1, name:"{file_name}") {{ 255 | nodes {{ 256 | id 257 | }} 258 | }} 259 | }} 260 | }} 261 | }} 262 | """ 263 | response = self.graphql_query(query) 264 | 265 | # Even on errors, the response should be "OK". 266 | if response.status_code != requests.codes.ok: 267 | raise UnexpectedResponseError(response) 268 | 269 | try: 270 | response_json = json.loads(response.content) 271 | except json.JSONDecodeError: 272 | raise UnexpectedResponseError(response) 273 | 274 | # The response should look a bit like this: 275 | # { 276 | # "data": { 277 | # "repository": { 278 | # "release": { 279 | # "releaseAssets": { 280 | # "nodes": [ 281 | # { 282 | # "id": "MDEyOlJlbGVhc2VBc3NldDE4MzgxNTc3" # noqa: SC100 283 | # } 284 | # ] 285 | # } 286 | # } 287 | # } 288 | # } 289 | # } 290 | 291 | try: 292 | assets = response_json["data"]["repository"]["release"]["releaseAssets"][ 293 | "nodes" 294 | ] 295 | except KeyError: 296 | raise UnexpectedResponseError(response) 297 | 298 | if not assets: 299 | # Asset not found. 300 | return None 301 | 302 | try: 303 | node_id: str = assets[0]["id"] 304 | except KeyError: 305 | raise UnexpectedResponseError(response) 306 | 307 | return release_asset_node_id_to_asset_id(node_id) 308 | 309 | def verify_asset_size_and_state_via_v3_api( 310 | self, file_name: str, file_size: int, tag_name: str, release: Optional[Release] 311 | ) -> bool: 312 | if not release: 313 | log("Getting the current release again to check asset status.") 314 | response = self.get_release_by_tag(tag_name) 315 | if response.status_code != requests.codes.ok: 316 | raise UnexpectedResponseError(response) 317 | log("Decoding release info.") 318 | try: 319 | release = Release.from_json(response.content) 320 | except json.JSONDecodeError: 321 | raise UnexpectedResponseError(response) 322 | 323 | if release.assets: 324 | for asset in release.assets: 325 | if ( 326 | asset.name == file_name 327 | and asset.size == file_size 328 | and asset.state == "uploaded" 329 | ): 330 | log("The asset has the correct size and state. Asset done.\n") 331 | return True 332 | return False 333 | 334 | 335 | class MissingTokenError(Exception): 336 | pass 337 | 338 | 339 | class MissingFilesError(Exception): 340 | def __init__(self, missing_paths: List[Path]): 341 | self.missing_paths = missing_paths 342 | missing_paths_str = [str(p) for p in missing_paths] 343 | missing_paths_joined = "\n" + "\n".join(missing_paths_str) + "\n" 344 | super().__init__(f"Missing: {missing_paths_joined}") 345 | 346 | 347 | class UnexpectedResponseError(Exception): 348 | def __init__(self, response: requests.Response): 349 | self.response = response 350 | super().__init__(f"Unexpected response: {response.__dict__}") 351 | 352 | 353 | class ReachedRetryLimitError(Exception): 354 | pass 355 | 356 | 357 | def upload_file( # pylint: disable=too-many-branches,too-many-nested-blocks,too-many-statements; 358 | g: GithubApi, release: Release, file_path: Path # noqa: VNE001 359 | ) -> None: 360 | 361 | log(f"\nUpload: {file_path.name}") 362 | 363 | file_size = file_path.stat().st_size 364 | 365 | retry_count = 0 366 | wait_time = 2 367 | 368 | if not release.tag_name: 369 | raise AssertionError("Expected tag_name") 370 | 371 | # Optimization: 372 | # The v3 API does not always show assets that are in a bad state, but if the asset *does* exist with the correct 373 | # size and state, then we can assume the asset was successfully uploaded. 374 | # We use the existing |release| object, which means we might be able to skip making any further remote API calls. 375 | try: 376 | if g.verify_asset_size_and_state_via_v3_api( 377 | file_name=file_path.name, 378 | file_size=file_size, 379 | tag_name=release.tag_name, 380 | release=release, 381 | ): 382 | return 383 | except Exception: # pylint: disable=broad-except; 384 | log_exception( 385 | "Ignoring exception that occurred when trying to check asset status with the v3 API." 386 | ) 387 | 388 | # Only exit the loop if we manage to verify that the asset has the expected size and state, or if we reach the retry 389 | # limit. 390 | while True: 391 | # We use try-except liberally so that we always at least try to blindly upload the asset (towards the end of the 392 | # loop), because this may well succeed and then the asset checking code may also be more likely to succeed on 393 | # subsequent iterations. 394 | 395 | # Optimization: 396 | # The v3 API does not always show assets that are in a bad state, but if the asset *does* exist with the 397 | # correct size and state, then we can assume the asset was successfully uploaded, without relying on 398 | # undocumented behavior. 399 | # We pass release=None, which forces a fresh fetch of the Release object. 400 | try: 401 | if g.verify_asset_size_and_state_via_v3_api( 402 | file_name=file_path.name, 403 | file_size=file_size, 404 | tag_name=release.tag_name, 405 | release=None, 406 | ): 407 | return 408 | except Exception: # pylint: disable=broad-except; 409 | log_exception( 410 | "Ignoring exception that occurred when trying to check asset status with the v3 API." 411 | ) 412 | 413 | # We now try to get the asset details via the v4 API. 414 | # This allows us to delete the asset if it is in a bad state, but relies on undocumented behavior. 415 | try: 416 | existing_asset_id = g.find_asset_id_by_file_name(file_path.name, release) 417 | if existing_asset_id: 418 | log("Asset exists.") 419 | log("Getting asset info.") 420 | response = g.get_asset_by_id(existing_asset_id) 421 | if response.status_code != requests.codes.ok: 422 | raise UnexpectedResponseError(response) 423 | 424 | log("Decoding asset info.") 425 | try: 426 | existing_asset = Asset.from_json(response.content) 427 | except json.JSONDecodeError: 428 | raise UnexpectedResponseError(response) 429 | 430 | if ( 431 | existing_asset.size == file_size 432 | and existing_asset.state == "uploaded" 433 | ): 434 | log("The asset has the correct size and state. Asset done.\n") 435 | return 436 | 437 | log('The asset looks bad (wrong size or state was not "uploaded").') 438 | 439 | log("Deleting asset.") 440 | 441 | response = g.delete_asset(existing_asset_id) 442 | if response.status_code != requests.codes.no_content: 443 | log("Ignoring failed deletion.") 444 | log_response(response) 445 | else: 446 | log("Asset does not exist.") 447 | except Exception: # pylint: disable=broad-except; 448 | log_exception( 449 | "Ignoring exception that occurred when trying to check asset status with the v4 API." 450 | ) 451 | 452 | # Asset does not exist, has been deleted, or an error occurred. 453 | # Upload the asset, regardless. 454 | 455 | if retry_count >= g.retry_limit: 456 | raise ReachedRetryLimitError("Reached upload retry limit.") 457 | 458 | if retry_count > 0: 459 | log(f"Waiting {wait_time} seconds before retrying upload.") 460 | time.sleep(wait_time) 461 | 462 | retry_count += 1 463 | wait_time = wait_time * 2 464 | 465 | log("Uploading asset.") 466 | try: 467 | response = g.upload_asset(file_path, release) 468 | if response.status_code != requests.codes.created: 469 | log("Ignoring failed upload.") 470 | log_response(response) 471 | except Exception: # pylint: disable=broad-except; 472 | log_exception("Ignoring upload exception.") 473 | 474 | # And now we loop. 475 | 476 | 477 | def make_release( 478 | g: GithubApi, release: Release, file_paths: List[Path] # noqa: VNE001 479 | ) -> None: 480 | 481 | # Some basic sanity checks. 482 | missing_files = list(filter(lambda p: not p.is_file(), file_paths)) 483 | if missing_files: 484 | raise MissingFilesError(missing_files) 485 | 486 | if not release.tag_name: 487 | raise AssertionError("tag_name must be provided") 488 | 489 | retry_count = 0 490 | wait_time = 2 491 | 492 | while True: 493 | try: 494 | log("Creating the release.") 495 | response = g.create_release(release) 496 | if response.status_code != requests.codes.created: 497 | log("Failed...") 498 | # Try to decode the error. 499 | try: 500 | error: GithubClientError = GithubClientError.from_json( 501 | response.content 502 | ) 503 | except json.JSONDecodeError: 504 | raise UnexpectedResponseError(response) 505 | 506 | if not error.errors: 507 | raise UnexpectedResponseError(response) 508 | 509 | if ( 510 | (not error.errors) 511 | or error.errors[0].resource != "Release" 512 | or error.errors[0].code != "already_exists" 513 | ): 514 | raise UnexpectedResponseError(response) 515 | 516 | log("...but this is OK, because the release already exists.") 517 | log("Getting the current release.") 518 | response = g.get_release_by_tag(release.tag_name) 519 | if response.status_code != requests.codes.ok: 520 | raise UnexpectedResponseError(response) 521 | 522 | except UnexpectedResponseError: 523 | log_exception( 524 | "Unexpected response when creating the release or getting the existing release info." 525 | ) 526 | # Note: GitHub will sometimes return a custom error for the Release resource with a message: 527 | # "Published releases must have a valid tag". 528 | # I suspect this is a race condition that occurs when multiple clients try to create the release at the same 529 | # time, or this is a race condition that occurs within GitHub itself when creating the tag as part of 530 | # creating the release. Thus, we retry creating/getting the release. 531 | if retry_count >= g.retry_limit: 532 | raise ReachedRetryLimitError( 533 | "Reached retry limit for creating release." 534 | ) 535 | log("...retrying.") 536 | time.sleep(wait_time) 537 | retry_count += 1 538 | wait_time = wait_time * 2 539 | continue 540 | 541 | # Exit the loop. 542 | break 543 | 544 | log("Decoding release info.") 545 | try: 546 | # noinspection PyUnboundLocalVariable 547 | release = Release.from_json(response.content) 548 | except json.JSONDecodeError: 549 | raise UnexpectedResponseError(response) 550 | 551 | for file_path in file_paths: 552 | upload_file(g, release, file_path) 553 | 554 | 555 | class ArgumentDefaultsWithRawDescriptionHelpFormatter( 556 | argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter 557 | ): 558 | pass 559 | 560 | 561 | def main_with_args(args: List[str]) -> None: 562 | parser = argparse.ArgumentParser( 563 | description="""Creates a GitHub release (if it does not already exist) and uploads files to the release. 564 | Please set the GITHUB_TOKEN environment variable. 565 | EXAMPLE: 566 | github-release-retry \\ 567 | --user paul \\ 568 | --repo hello-world \\ 569 | --tag_name v1.0 \\ 570 | --target_commitish 448301eb \\ 571 | --body_string "My first release." \\ 572 | hello-world.zip RELEASE_NOTES.txt 573 | """, 574 | formatter_class=ArgumentDefaultsWithRawDescriptionHelpFormatter, 575 | ) 576 | 577 | parser.add_argument( 578 | "--user", 579 | help="Required: The GitHub username or organization name in which the repo resides. ", 580 | type=str, 581 | required=True, 582 | ) 583 | 584 | parser.add_argument( 585 | "--repo", 586 | help="Required: The GitHub repo name in which to make the release. ", 587 | type=str, 588 | required=True, 589 | ) 590 | 591 | parser.add_argument( 592 | "--tag_name", 593 | help="Required: The name of the tag to create or use. ", 594 | type=str, 595 | required=True, 596 | ) 597 | 598 | parser.add_argument( 599 | "--target_commitish", 600 | help="The commit-ish value where the tag will be created. Unused if the tag already exists. ", 601 | type=str, 602 | default=None, 603 | ) 604 | 605 | parser.add_argument( 606 | "--release_name", 607 | help="The name of the release. Leave unset to use the tag_name (recommended). ", 608 | type=str, 609 | default=None, 610 | ) 611 | 612 | # --body_string XOR --body_file 613 | body_group = parser.add_mutually_exclusive_group(required=True) 614 | body_group.add_argument( 615 | "--body_string", 616 | help="Required (or use --body_file): Text describing the release. Ignored if the release already exists.", 617 | type=str, 618 | ) 619 | body_group.add_argument( 620 | "--body_file", 621 | help="Required (or use --body_string): Text describing the release, which will be read from BODY_FILE. Ignored if the release already exists.", 622 | type=str, 623 | ) 624 | 625 | parser.add_argument( 626 | "--draft", 627 | help="Creates a draft release, which means it is unpublished. ", 628 | action="store_true", 629 | ) 630 | 631 | parser.add_argument( 632 | "--prerelease", 633 | help="Creates a prerelease release, which means it will be marked as such. ", 634 | action="store_true", 635 | ) 636 | 637 | parser.add_argument( 638 | "--github_api_url", 639 | help="The GitHub API URL without a trailing slash. ", 640 | type=str, 641 | default="https://api.github.com", 642 | ) 643 | 644 | parser.add_argument( 645 | "--retry_limit", 646 | help="The number of times to retry creating/getting the release and/or uploading each file. ", 647 | type=int, 648 | default=10, 649 | ) 650 | 651 | parser.add_argument( 652 | "files", 653 | metavar="files", 654 | type=str, 655 | nargs="*", 656 | help="The files to upload to the release.", 657 | ) 658 | 659 | parsed_args = parser.parse_args(args) 660 | 661 | token: Optional[str] = os.environ.get("GITHUB_TOKEN", None) 662 | 663 | if not token: 664 | raise MissingTokenError("Please set the GITHUB_TOKEN environment variable. ") 665 | 666 | g = GithubApi( # noqa: VNE001 667 | github_api_url=parsed_args.github_api_url, 668 | user=parsed_args.user, 669 | repo=parsed_args.repo, 670 | token=token, 671 | retry_limit=parsed_args.retry_limit, 672 | ) 673 | 674 | if parsed_args.body_string: 675 | body_text = parsed_args.body_string 676 | else: 677 | body_text = Path(parsed_args.body_file).read_text( 678 | encoding="utf-8", errors="ignore" 679 | ) 680 | 681 | release = Release( 682 | tag_name=parsed_args.tag_name, 683 | target_commitish=parsed_args.target_commitish or None, 684 | name=parsed_args.release_name or None, 685 | body=body_text, 686 | draft=parsed_args.draft or None, 687 | prerelease=parsed_args.prerelease or None, 688 | ) 689 | 690 | files_str: List[str] = parsed_args.files 691 | files = [Path(f) for f in files_str] 692 | 693 | make_release(g, release, files) 694 | 695 | 696 | def main() -> None: 697 | main_with_args(sys.argv[1:]) 698 | 699 | 700 | if __name__ == "__main__": 701 | main() 702 | -------------------------------------------------------------------------------- /github_release_retry/py.typed: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The github-release-retry Project Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Marker file for https://www.python.org/dev/peps/pep-0561/ 16 | -------------------------------------------------------------------------------- /github_release_retry_tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2020 The github-release-retry Project Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | -------------------------------------------------------------------------------- /github_release_retry_tests/fixtures/get_release_by_tag.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/repos/google/github-release-retry/releases/27837730", 3 | "assets_url": "https://api.github.com/repos/google/github-release-retry/releases/27837730/assets", 4 | "upload_url": "https://uploads.github.com/repos/google/github-release-retry/releases/27837730/assets{?name,label}", 5 | "html_url": "https://github.com/google/github-release-retry/releases/tag/v1.0", 6 | "id": 27837730, 7 | "node_id": "MDc6UmVsZWFzZTI3ODM3NzMw", 8 | "tag_name": "v1.0", 9 | "target_commitish": "main", 10 | "name": null, 11 | "draft": false, 12 | "author": { 13 | "login": "google", 14 | "id": 6159524, 15 | "node_id": "MDQ6VXNlcjYxNTk1MjQ=", 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/6159524?v=4", 17 | "gravatar_id": "", 18 | "url": "https://api.github.com/users/google", 19 | "html_url": "https://github.com/google", 20 | "followers_url": "https://api.github.com/users/google/followers", 21 | "following_url": "https://api.github.com/users/google/following{/other_user}", 22 | "gists_url": "https://api.github.com/users/google/gists{/gist_id}", 23 | "starred_url": "https://api.github.com/users/google/starred{/owner}{/repo}", 24 | "subscriptions_url": "https://api.github.com/users/google/subscriptions", 25 | "organizations_url": "https://api.github.com/users/google/orgs", 26 | "repos_url": "https://api.github.com/users/google/repos", 27 | "events_url": "https://api.github.com/users/google/events{/privacy}", 28 | "received_events_url": "https://api.github.com/users/google/received_events", 29 | "type": "User", 30 | "site_admin": false 31 | }, 32 | "prerelease": false, 33 | "created_at": "2020-05-10T15:45:17Z", 34 | "published_at": "2020-06-23T16:37:07Z", 35 | "assets": [], 36 | "tarball_url": "https://api.github.com/repos/google/github-release-retry/tarball/v1.0", 37 | "zipball_url": "https://api.github.com/repos/google/github-release-retry/zipball/v1.0", 38 | "body": "Release v1.0." 39 | } 40 | -------------------------------------------------------------------------------- /github_release_retry_tests/fixtures/release_already_exists.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "Validation Failed", 3 | "errors": [ 4 | { 5 | "resource": "Release", 6 | "code": "already_exists", 7 | "field": "tag_name" 8 | } 9 | ], 10 | "documentation_url": "https://developer.github.com/v3/repos/releases/#create-a-release" 11 | } 12 | -------------------------------------------------------------------------------- /github_release_retry_tests/test_github_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2020 The github-release-retry Project Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | import requests 20 | import requests_mock # type: ignore 21 | 22 | from github_release_retry.github_release_retry import GithubApi, Release 23 | from github_release_retry_tests.testcase import GitHubTestCase 24 | 25 | 26 | class TestGithubApi(GitHubTestCase): 27 | @staticmethod 28 | def test_github_api_invalid_token() -> None: 29 | github = GithubApi( # noqa: S106 30 | github_api_url="https://api.github.com", 31 | user="google", 32 | repo="github-release-retry", 33 | token="INVALID_TOKEN", 34 | retry_limit=10, 35 | ) 36 | assert github.token == "INVALID_TOKEN" 37 | 38 | release = Release( 39 | tag_name="v1.0", 40 | target_commitish=None, 41 | name=None, 42 | body="Test", 43 | draft=None, 44 | prerelease=None, 45 | ) 46 | 47 | github_release = github.create_release(release) 48 | 49 | assert github_release.status_code == requests.codes.unauthorized 50 | 51 | def test_create_release_with_mock_requests_already_exists(self) -> None: 52 | github = GithubApi( # noqa: S106 53 | github_api_url="https://api.github.com", 54 | user="google", 55 | repo="github-release-retry", 56 | token="VALID_MOCK_TOKEN", 57 | retry_limit=10, 58 | ) 59 | assert github.token == "VALID_MOCK_TOKEN" 60 | 61 | release = Release( 62 | tag_name="v1.0", 63 | target_commitish=None, 64 | name=None, 65 | body="Test", 66 | draft=None, 67 | prerelease=None, 68 | ) 69 | 70 | with requests_mock.Mocker() as mocker: 71 | mocker.register_uri( 72 | "POST", 73 | f"{github.github_api_url}/repos/{github.user}/{github.repo}/releases", 74 | json=self.get_fixture("release_already_exists.json"), 75 | status_code=requests.codes.unprocessable_entity, 76 | ) 77 | mocker.register_uri( 78 | "GET", 79 | f"{github.github_api_url}/repos/{github.user}/{github.repo}/releases/tags/{release.tag_name}", 80 | json=self.get_fixture("get_release_by_tag.json"), 81 | ) 82 | 83 | github_release = github.create_release(release) 84 | 85 | assert github_release.status_code == requests.codes.unprocessable_entity 86 | 87 | def test_get_release_by_tag_mock_data(self) -> None: 88 | github = GithubApi( # noqa: S106 89 | github_api_url="https://api.github.com", 90 | user="google", 91 | repo="github-release-retry", 92 | token="VALID_MOCK_TOKEN", 93 | retry_limit=10, 94 | ) 95 | assert github.token == "VALID_MOCK_TOKEN" 96 | 97 | release = Release( 98 | tag_name="v1.0", 99 | target_commitish=None, 100 | name=None, 101 | body="Test", 102 | draft=None, 103 | prerelease=None, 104 | ) 105 | 106 | with requests_mock.Mocker() as mocker: 107 | mocker.register_uri( 108 | "GET", 109 | f"{github.github_api_url}/repos/{github.user}/{github.repo}/releases/tags/{release.tag_name}", 110 | json=self.get_fixture("get_release_by_tag.json"), 111 | status_code=requests.codes.ok, 112 | ) 113 | 114 | github_release = github.get_release_by_tag("v1.0") 115 | 116 | assert github_release.status_code == requests.codes.ok 117 | -------------------------------------------------------------------------------- /github_release_retry_tests/test_node_id_extraction.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2021 The github-release-retry Project Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import base64 18 | 19 | from github_release_retry import github_release_retry 20 | 21 | 22 | def test_release_asset_node_id_to_asset_id() -> None: 23 | 24 | old_format_node_id = "MDEyOlJlbGVhc2VBc3NldDQyMTY0NTg2" 25 | old_format_expected_asset_id = "42164586" 26 | 27 | old_format_actual_asset_id = github_release_retry.release_asset_node_id_to_asset_id( 28 | old_format_node_id 29 | ) 30 | 31 | assert old_format_actual_asset_id == old_format_expected_asset_id 32 | 33 | new_format_node_id = "RA_kwDODNhc0c4CrJ0q" 34 | new_format_expected_asset_id = "44866858" 35 | 36 | new_format_actual_asset_id = github_release_retry.release_asset_node_id_to_asset_id( 37 | new_format_node_id 38 | ) 39 | 40 | assert new_format_actual_asset_id == new_format_expected_asset_id 41 | 42 | 43 | def test_release_asset_node_id_to_asset_id_exception() -> None: 44 | # The release_asset_node_id_to_asset_id(...) function expects a node id with "ReleaseAsset", not "ReleaseBadAsset". 45 | node_id_decoded = b"012:ReleaseBadAsset18381577" 46 | node_id_encoded = base64.b64encode(node_id_decoded).decode( 47 | encoding="utf-8", errors="none" 48 | ) 49 | 50 | try: 51 | github_release_retry.release_asset_node_id_to_asset_id(node_id_encoded) 52 | except AssertionError as error: 53 | message = str(error) 54 | assert "format" in message 55 | return 56 | 57 | raise AssertionError("Expected an exception") 58 | -------------------------------------------------------------------------------- /github_release_retry_tests/testcase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2020 The github-release-retry Project Authors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import json 19 | import os 20 | from typing import Any 21 | 22 | 23 | class GitHubTestCase: 24 | @classmethod 25 | def get_fixture(cls, filename: str) -> Any: 26 | location = os.path.realpath( 27 | os.path.join(os.getcwd(), os.path.dirname(__file__)) 28 | ) 29 | fixture = os.path.join(location, "fixtures", filename) 30 | with open(fixture, encoding="utf-8", errors="ignore") as json_file: 31 | data = json.load(json_file) 32 | return data 33 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The github-release-retry Project Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [mypy] 16 | python_version = 3.6 17 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The github-release-retry Project Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | [MASTER] 17 | 18 | # A comma-separated list of package or module names from where C extensions may 19 | # be loaded. Extensions are loading into the active Python interpreter and may 20 | # run arbitrary code. 21 | extension-pkg-whitelist= 22 | 23 | # Add files or directories to the blocklist. They should be base names, not 24 | # paths. 25 | ignore=CVS 26 | 27 | # Add files or directories matching the regex patterns to the blocklist. The 28 | # regex matches against base names, not paths. 29 | ignore-patterns=.*_pb2.* 30 | 31 | # Python code to execute, usually for sys.path manipulation such as 32 | # pygtk.require(). 33 | #init-hook= 34 | 35 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 36 | # number of processors available to use. 37 | jobs=1 38 | 39 | # Control the amount of potential inferred values when inferring a single 40 | # object. This can help the performance when dealing with large functions or 41 | # complex, nested conditions. 42 | limit-inference-results=100 43 | 44 | # List of plugins (as comma separated values of python modules names) to load, 45 | # usually to register additional checkers. 46 | load-plugins= 47 | 48 | # Pickle collected data for later comparisons. 49 | persistent=yes 50 | 51 | # Specify a configuration file. 52 | #rcfile= 53 | 54 | # When enabled, pylint would attempt to guess common misconfiguration and emit 55 | # user-friendly hints instead of false-positive error messages. 56 | suggestion-mode=yes 57 | 58 | # Allow loading of arbitrary C extensions. Extensions are imported into the 59 | # active Python interpreter and may run arbitrary code. 60 | unsafe-load-any-extension=no 61 | 62 | 63 | [MESSAGES CONTROL] 64 | 65 | # Only show warnings with the listed confidence levels. Leave empty to show 66 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 67 | confidence= 68 | 69 | # Disable the message, report, category or checker with the given id(s). You 70 | # can either give multiple identifiers separated by comma (,) or put this 71 | # option multiple times (only on the command line, not in the configuration 72 | # file where it should appear only once). You can also use "--disable=all" to 73 | # disable everything first and then reenable specific checks. For example, if 74 | # you want to run only the similarities checker, you can use "--disable=all 75 | # --enable=similarities". If you want to run only the classes checker, but have 76 | # no Warning level messages displayed, use "--disable=all --enable=classes 77 | # --disable=W". 78 | disable=C0330, 79 | C0111, 80 | W0511, # Allow to-dos. 81 | E1101, # Missing member; mypy (type checking) should cover this. 82 | R0913, # Too many arguments; could re-enable this. 83 | C0301, # Line too long; we use an auto formatter. 84 | R0903, # too-few-public-methods; good, but I want to store data in classes in case I later add methods. 85 | ungrouped-imports, # We use an imports formatter. 86 | cyclic-import, # We allow cyclic imports, but we should import modules, not functions and variables. 87 | duplicate-code, # Unfortunately, disabling this on a case-by-case basis is broken: https://github.com/PyCQA/pylint/issues/214 88 | unsubscriptable-object, # False-positives for type hints. E.g. CompletedProcess[Any]. Mypy should cover this. 89 | too-many-lines, 90 | too-many-instance-attributes, 91 | 92 | 93 | 94 | # Enable the message, report, category or checker with the given id(s). You can 95 | # either give multiple identifier separated by comma (,) or put this option 96 | # multiple time (only on the command line, not in the configuration file where 97 | # it should appear only once). See also the "--disable" option for examples. 98 | enable=c-extension-no-member 99 | 100 | 101 | [REPORTS] 102 | 103 | # Python expression which should return a note less than 10 (10 is the highest 104 | # note). You have access to the variables errors warning, statement which 105 | # respectively contain the number of errors / warnings messages and the total 106 | # number of statements analyzed. This is used by the global evaluation report 107 | # (RP0004). 108 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 109 | 110 | # Template used to display messages. This is a python new-style format string 111 | # used to format the message information. See doc for all details. 112 | msg-template='{abspath}:{line:d}:{column}: {obj}: [{msg_id} {symbol}] {msg}' 113 | 114 | # Set the output format. Available formats are text, parseable, colorized, json 115 | # and msvs (visual studio). You can also give a reporter class, e.g. 116 | # mypackage.mymodule.MyReporterClass. 117 | output-format=text 118 | 119 | # Tells whether to display a full report or only the messages. 120 | reports=no 121 | 122 | # Activate the evaluation score. 123 | score=yes 124 | 125 | 126 | [REFACTORING] 127 | 128 | # Maximum number of nested blocks for function / method body 129 | max-nested-blocks=5 130 | 131 | # Complete name of functions that never returns. When checking for 132 | # inconsistent-return-statements if a never returning function is called then 133 | # it will be considered as an explicit return statement and no message will be 134 | # printed. 135 | never-returning-functions=sys.exit 136 | 137 | 138 | [SPELLING] 139 | 140 | # Limits count of emitted suggestions for spelling mistakes. 141 | max-spelling-suggestions=4 142 | 143 | # Spelling dictionary name. Available dictionaries: none. To make it working 144 | # install python-enchant package.. 145 | spelling-dict= 146 | 147 | # List of comma separated words that should not be checked. 148 | spelling-ignore-words= 149 | 150 | # A path to a file that contains private dictionary; one word per line. 151 | spelling-private-dict-file= 152 | 153 | # Tells whether to store unknown words to indicated private dictionary in 154 | # --spelling-private-dict-file option instead of raising a message. 155 | spelling-store-unknown-words=no 156 | 157 | 158 | [VARIABLES] 159 | 160 | # List of additional names supposed to be defined in builtins. Remember that 161 | # you should avoid defining new builtins when possible. 162 | additional-builtins= 163 | 164 | # Tells whether unused global variables should be treated as a violation. 165 | allow-global-unused-variables=yes 166 | 167 | # List of strings which can identify a callback function by name. A callback 168 | # name must start or end with one of those strings. 169 | callbacks=cb_, 170 | _cb 171 | 172 | # A regular expression matching the name of unused variables (i.e. expected to 173 | # not be used). 174 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|^ignored_|^unused_ 175 | 176 | # Argument names that match this expression will be ignored. Default to name 177 | # with leading underscore. 178 | ignored-argument-names=_.*|^ignored_|^unused_ 179 | 180 | # Tells whether we should check for unused import in __init__ files. 181 | init-import=no 182 | 183 | # List of qualified module names which can have objects that can redefine 184 | # builtins. 185 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 186 | 187 | 188 | [LOGGING] 189 | 190 | # Format style used to check logging format string. `old` means using % 191 | # formatting, while `new` is for `{}` formatting. 192 | logging-format-style=old 193 | 194 | # Logging modules to check that the string format arguments are in logging 195 | # function parameter format. 196 | logging-modules=logging 197 | 198 | 199 | [FORMAT] 200 | 201 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 202 | expected-line-ending-format= 203 | 204 | # Regexp for a line that is allowed to be longer than the limit. 205 | ignore-long-lines=^\s*(# )??$ 206 | 207 | # Number of spaces of indent required inside a hanging or continued line. 208 | indent-after-paren=4 209 | 210 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 211 | # tab). 212 | indent-string=' ' 213 | 214 | # Maximum number of characters on a single line. 215 | max-line-length=100 216 | 217 | # Maximum number of lines in a module. 218 | max-module-lines=1000 219 | 220 | # List of optional constructs for which whitespace checking is disabled. `dict- 221 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 222 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 223 | # `empty-line` allows space-only lines. 224 | no-space-check=trailing-comma, 225 | dict-separator 226 | 227 | # Allow the body of a class to be on the same line as the declaration if body 228 | # contains single statement. 229 | single-line-class-stmt=no 230 | 231 | # Allow the body of an if to be on the same line as the test if there is no 232 | # else. 233 | single-line-if-stmt=no 234 | 235 | 236 | [STRING] 237 | 238 | # This flag controls whether the implicit-str-concat-in-sequence should 239 | # generate a warning on implicit string concatenation in sequences defined over 240 | # several lines. 241 | check-str-concat-over-line-jumps=no 242 | 243 | 244 | [SIMILARITIES] 245 | 246 | # Ignore comments when computing similarities. 247 | ignore-comments=yes 248 | 249 | # Ignore docstrings when computing similarities. 250 | ignore-docstrings=yes 251 | 252 | # Ignore imports when computing similarities. 253 | ignore-imports=yes # This helps. 254 | 255 | # Minimum lines number of a similarity. 256 | min-similarity-lines=4 257 | 258 | 259 | [TYPECHECK] 260 | 261 | # List of decorators that produce context managers, such as 262 | # contextlib.contextmanager. Add to this list to register other decorators that 263 | # produce valid context managers. 264 | contextmanager-decorators=contextlib.contextmanager 265 | 266 | # List of members which are set dynamically and missed by pylint inference 267 | # system, and so shouldn't trigger E1101 when accessed. Python regular 268 | # expressions are accepted. 269 | generated-members= 270 | 271 | # Tells whether missing members accessed in mixin class should be ignored. A 272 | # mixin class is detected if its name ends with "mixin" (case insensitive). 273 | ignore-mixin-members=yes 274 | 275 | # Tells whether to warn about missing members when the owner of the attribute 276 | # is inferred to be None. 277 | ignore-none=yes 278 | 279 | # This flag controls whether pylint should warn about no-member and similar 280 | # checks whenever an opaque object is returned when inferring. The inference 281 | # can return multiple potential results while evaluating a Python object, but 282 | # some branches might not be evaluated, which results in partial inference. In 283 | # that case, it might be useful to still emit no-member and other checks for 284 | # the rest of the inferred objects. 285 | ignore-on-opaque-inference=yes 286 | 287 | # List of class names for which member attributes should not be checked (useful 288 | # for classes with dynamically set attributes). This supports the use of 289 | # qualified names. 290 | ignored-classes=optparse.Values,thread._local,_thread._local 291 | 292 | # List of module names for which member attributes should not be checked 293 | # (useful for modules/projects where namespaces are manipulated during runtime 294 | # and thus existing member attributes cannot be deduced by static analysis. It 295 | # supports qualified module names, as well as Unix pattern matching. 296 | ignored-modules= 297 | 298 | # Show a hint with possible names when a member name was not found. The aspect 299 | # of finding the hint is based on edit distance. 300 | missing-member-hint=yes 301 | 302 | # The minimum edit distance a name should have in order to be considered a 303 | # similar match for a missing member name. 304 | missing-member-hint-distance=1 305 | 306 | # The total number of similar names that should be taken in consideration when 307 | # showing a hint for a missing member. 308 | missing-member-max-choices=1 309 | 310 | 311 | [MISCELLANEOUS] 312 | 313 | # List of note tags to take in consideration, separated by a comma. 314 | notes=FIXME, 315 | XXX, 316 | TODO 317 | 318 | 319 | [BASIC] 320 | 321 | # Naming style matching correct argument names. 322 | argument-naming-style=snake_case 323 | 324 | # Regular expression matching correct argument names. Overrides argument- 325 | # naming-style. 326 | #argument-rgx= 327 | 328 | # Naming style matching correct attribute names. 329 | attr-naming-style=snake_case 330 | 331 | # Regular expression matching correct attribute names. Overrides attr-naming- 332 | # style. 333 | #attr-rgx= 334 | 335 | # Bad variable names which should always be refused, separated by a comma. 336 | bad-names=foo, 337 | bar, 338 | baz, 339 | toto, 340 | tutu, 341 | tata 342 | 343 | # Naming style matching correct class attribute names. 344 | class-attribute-naming-style=any 345 | 346 | # Regular expression matching correct class attribute names. Overrides class- 347 | # attribute-naming-style. 348 | #class-attribute-rgx= 349 | 350 | # Naming style matching correct class names. 351 | class-naming-style=PascalCase 352 | 353 | # Regular expression matching correct class names. Overrides class-naming- 354 | # style. 355 | #class-rgx= 356 | 357 | # Naming style matching correct constant names. 358 | const-naming-style=UPPER_CASE 359 | 360 | # Regular expression matching correct constant names. Overrides const-naming- 361 | # style. 362 | #const-rgx= 363 | 364 | # Minimum line length for functions/classes that require docstrings, shorter 365 | # ones are exempt. 366 | docstring-min-length=-1 367 | 368 | # Naming style matching correct function names. 369 | function-naming-style=snake_case 370 | 371 | # Regular expression matching correct function names. Overrides function- 372 | # naming-style. 373 | #function-rgx= 374 | 375 | # Good variable names which should always be accepted, separated by a comma. 376 | good-names=i, 377 | j, 378 | k, 379 | ex, 380 | Run, 381 | _, 382 | f, # Good for file handles. 383 | g, # GithubApi object. 384 | id, 385 | 386 | # Include a hint for the correct naming format with invalid-name. 387 | include-naming-hint=no 388 | 389 | # Naming style matching correct inline iteration names. 390 | inlinevar-naming-style=any 391 | 392 | # Regular expression matching correct inline iteration names. Overrides 393 | # inlinevar-naming-style. 394 | #inlinevar-rgx= 395 | 396 | # Naming style matching correct method names. 397 | method-naming-style=snake_case 398 | 399 | # Regular expression matching correct method names. Overrides method-naming- 400 | # style. 401 | #method-rgx= 402 | 403 | # Naming style matching correct module names. 404 | module-naming-style=snake_case 405 | 406 | # Regular expression matching correct module names. Overrides module-naming- 407 | # style. 408 | #module-rgx= 409 | 410 | # Colon-delimited sets of names that determine each other's naming style when 411 | # the name regexes allow several styles. 412 | name-group= 413 | 414 | # Regular expression which should only match function or class names that do 415 | # not require a docstring. 416 | no-docstring-rgx=^_ 417 | 418 | # List of decorators that produce properties, such as abc.abstractproperty. Add 419 | # to this list to register other decorators that produce valid properties. 420 | # These decorators are taken in consideration only for invalid-name. 421 | property-classes=abc.abstractproperty 422 | 423 | # Naming style matching correct variable names. 424 | variable-naming-style=snake_case 425 | 426 | # Regular expression matching correct variable names. Overrides variable- 427 | # naming-style. 428 | #variable-rgx= 429 | 430 | 431 | [IMPORTS] 432 | 433 | # Allow wildcard imports from modules that define __all__. 434 | allow-wildcard-with-all=no 435 | 436 | # Analyse import fallback blocks. This can be used to support both Python 2 and 437 | # 3 compatible code, which means that the block might have code that exists 438 | # only in one or another interpreter, leading to false positives when analysed. 439 | analyse-fallback-blocks=no 440 | 441 | # Deprecated modules which should not be used, separated by a comma. 442 | deprecated-modules=optparse,tkinter.tix 443 | 444 | # Create a graph of external dependencies in the given file (report RP0402 must 445 | # not be disabled). 446 | ext-import-graph= 447 | 448 | # Create a graph of every (i.e. internal and external) dependencies in the 449 | # given file (report RP0402 must not be disabled). 450 | import-graph= 451 | 452 | # Create a graph of internal dependencies in the given file (report RP0402 must 453 | # not be disabled). 454 | int-import-graph= 455 | 456 | # Force import order to recognize a module as part of the standard 457 | # compatibility libraries. 458 | known-standard-library= 459 | 460 | # Force import order to recognize a module as part of a third party library. 461 | known-third-party=enchant 462 | 463 | 464 | [CLASSES] 465 | 466 | # List of method names used to declare (i.e. assign) instance attributes. 467 | defining-attr-methods=__init__, 468 | __new__, 469 | setUp 470 | 471 | # List of member names, which should be excluded from the protected access 472 | # warning. 473 | exclude-protected=_asdict, 474 | _fields, 475 | _replace, 476 | _source, 477 | _make 478 | 479 | # List of valid names for the first argument in a class method. 480 | valid-classmethod-first-arg=cls 481 | 482 | # List of valid names for the first argument in a metaclass class method. 483 | valid-metaclass-classmethod-first-arg=cls 484 | 485 | 486 | [DESIGN] 487 | 488 | # Maximum number of arguments for function / method. 489 | max-args=5 490 | 491 | # Maximum number of attributes for a class (see R0902). 492 | max-attributes=7 493 | 494 | # Maximum number of boolean expressions in an if statement. 495 | max-bool-expr=5 496 | 497 | # Maximum number of branch for function / method body. 498 | max-branches=12 499 | 500 | # Maximum number of locals for function / method body. 501 | max-locals=15 502 | 503 | # Maximum number of parents for a class (see R0901). 504 | max-parents=7 505 | 506 | # Maximum number of public methods for a class (see R0904). 507 | max-public-methods=20 508 | 509 | # Maximum number of return / yield for function / method body. 510 | max-returns=6 511 | 512 | # Maximum number of statements in function / method body. 513 | max-statements=50 514 | 515 | # Minimum number of public methods for a class (see R0903). 516 | min-public-methods=2 517 | 518 | 519 | [EXCEPTIONS] 520 | 521 | # Exceptions that will emit a warning when being caught. Defaults to 522 | # "BaseException, Exception". 523 | overgeneral-exceptions=BaseException, 524 | Exception 525 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The github-release-retry Project Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [build-system] 16 | requires = ['wheel', 'setuptools'] 17 | build-backend = 'setuptools.build_meta' 18 | 19 | [tool.black] 20 | line-length = 88 21 | target-version = ['py36', 'py37', 'py38'] 22 | 23 | # Black currently matches against full paths, and can be quite slow at 24 | # filtering files. Thus, we just use the following filters and always 25 | # specify the folders to check. 26 | # https://github.com/python/black/issues/712 27 | 28 | include = '.*[.]py$' 29 | exclude = '__pycache__|_pb2[.]py' 30 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The github-release-retry Project Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [metadata] 16 | description-file = README.md 17 | 18 | # This affects pep8.py, which in turn affects PyCharm/IntelliJ PEP8 warnings. 19 | [pep8] 20 | ignore = E203,W503,E501 21 | 22 | # pycodestyle is the new name and version of pep8.py. I'm not sure if these are 23 | # actually used by flake8 or PyCharm/IntelliJ, but they should be kept in sync 24 | # with [pep8] above and the config in .flake8. 25 | [pycodestyle] 26 | ignore = E203,W503,E501 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2020 The github-release-retry Project Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from pathlib import Path 18 | 19 | try: 20 | from setuptools import setup, Extension 21 | except Exception: 22 | from distutils.core import setup, Extension 23 | 24 | 25 | def get_long_description() -> str: 26 | readme_path = Path(__file__).parent / "README.md" 27 | return readme_path.read_text(encoding="utf-8") 28 | 29 | 30 | setup( 31 | name="github-release-retry", 32 | version="1.0.8", 33 | description="A tool for creating GitHub Releases and uploading assets reliably.", 34 | long_description=get_long_description(), 35 | long_description_content_type="text/markdown", 36 | keywords="GitHub Release Releases reliable retry upload assets", 37 | author="The github-release-retry Project Authors", 38 | author_email="android-graphics-tools-team@google.com", 39 | url="https://github.com/google/github-release-retry", 40 | license="Apache License 2.0", 41 | packages=["github_release_retry"], 42 | python_requires=">=3.6", 43 | install_requires=[ 44 | 'dataclasses;python_version=="3.6"', 45 | "dataclasses-json", 46 | "requests", 47 | ], 48 | package_data={"github_release_retry": ["py.typed"]}, 49 | classifiers=[ 50 | "Environment :: Console", 51 | "Intended Audience :: Developers", 52 | "License :: OSI Approved :: Apache Software License", 53 | "Operating System :: OS Independent", 54 | "Programming Language :: Python", 55 | "Programming Language :: Python :: 3.6", 56 | "Programming Language :: Python :: 3.7", 57 | "Programming Language :: Python :: 3.8", 58 | "Programming Language :: Python :: 3.9", 59 | "Programming Language :: Python :: 3 :: Only", 60 | ], 61 | project_urls={ 62 | "Documentation": "https://github.com/google/github-release-retry", 63 | "Source": "https://github.com/google/github-release-retry", 64 | }, 65 | entry_points={ 66 | "console_scripts": [ 67 | "github-release-retry = github_release_retry.github_release_retry:main", 68 | ] 69 | }, 70 | ) 71 | --------------------------------------------------------------------------------