├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/mypy.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/scopes/python_files.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/watcherTasks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 | [](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 |
--------------------------------------------------------------------------------