├── .editorconfig ├── .github └── workflows │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── LICENSE.md ├── README.md ├── aiohttp_s3_client ├── __init__.py ├── client.py ├── credentials.py ├── py.typed ├── version.py └── xml.py ├── docker-compose.yaml ├── poetry.lock ├── poetry.toml ├── pyproject.toml └── tests ├── conftest.py ├── test_credentials.py ├── test_get_file_parallel.py ├── test_multipart_upload.py ├── test_parallel_operation_failure.py └── test_simple.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | 9 | [*.{py,yml}] 10 | indent_style = space 11 | 12 | [*.py] 13 | indent_size = 4 14 | 15 | [docs/**.py] 16 | max_line_length = 80 17 | 18 | [*.rst] 19 | indent_size = 3 20 | 21 | [Makefile] 22 | indent_style = tab 23 | 24 | [*.yml] 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | sdist: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Setup python3.10 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: "3.10" 17 | 18 | - name: Resetting git to master 19 | run: git reset --hard master 20 | 21 | - name: Resetting git to master 22 | run: git fetch --unshallow --tags || true 23 | 24 | - name: Install poetry 25 | run: python -m pip install poetry 26 | 27 | - name: Install poem-plugins 28 | run: poetry self add poem-plugins 29 | 30 | - name: Install requirements 31 | run: poetry install 32 | 33 | - name: Publishing to pypi 34 | run: poetry publish --build --skip-existing --no-interaction 35 | env: 36 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | pylama: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup python3.10 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: "3.10" 18 | - run: python -m pip install poetry 19 | - run: poetry install 20 | - run: poetry run pylama 21 | env: 22 | FORCE_COLOR: 1 23 | mypy: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Setup python3.10 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: "3.10" 31 | - run: python -m pip install poetry 32 | - run: poetry install 33 | - run: poetry run mypy 34 | env: 35 | FORCE_COLOR: 1 36 | 37 | tests: 38 | runs-on: ubuntu-latest 39 | services: 40 | s3: 41 | image: docker://adobe/s3mock 42 | ports: 43 | - 9090:9090 44 | env: 45 | initialBuckets: test 46 | 47 | strategy: 48 | fail-fast: false 49 | 50 | matrix: 51 | python: 52 | - '3.8' 53 | - '3.9' 54 | - '3.10' 55 | - '3.11' 56 | - '3.12' 57 | 58 | steps: 59 | - uses: actions/checkout@v2 60 | - name: Setup python${{ matrix.python }} 61 | uses: actions/setup-python@v2 62 | with: 63 | python-version: "${{ matrix.python }}" 64 | 65 | - run: python -m pip install poetry 66 | - run: poetry install 67 | - run: >- 68 | poetry run pytest \ 69 | -vv \ 70 | --cov=aiohttp_s3_client \ 71 | --cov-report=term-missing \ 72 | tests 73 | env: 74 | FORCE_COLOR: 1 75 | S3_URL: http://user:hackme@localhost:9090/test/ 76 | 77 | - run: poetry run coveralls 78 | env: 79 | COVERALLS_PARALLEL: 'true' 80 | COVERALLS_SERVICE_NAME: github 81 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 82 | 83 | finish: 84 | needs: tests 85 | runs-on: ubuntu-latest 86 | steps: 87 | - name: Coveralls Finished 88 | uses: coverallsapp/github-action@v1 89 | with: 90 | parallel-finished: true 91 | github-token: ${{ secrets.GITHUB_TOKEN }} 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### VirtualEnv template 3 | # Virtualenv 4 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 5 | .Python 6 | [Bb]in 7 | [Ii]nclude 8 | [Ll]ib 9 | [Ll]ib64 10 | [Ll]ocal 11 | [Ss]cripts 12 | pyvenv.cfg 13 | .venv 14 | pip-selfcheck.json 15 | ### IPythonNotebook template 16 | # Temporary data 17 | .ipynb_checkpoints/ 18 | ### Python template 19 | # Byte-compiled / optimized / DLL files 20 | __pycache__/ 21 | *.py[cod] 22 | *$py.class 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | env/ 30 | build/ 31 | develop-eggs/ 32 | dist/ 33 | downloads/ 34 | eggs/ 35 | .eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .coverage 59 | .coverage.* 60 | .cache 61 | nosetests.xml 62 | coverage.xml 63 | *,cover 64 | .hypothesis/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | docs/source/apidoc 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # IPython Notebook 89 | .ipynb_checkpoints 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # pytest 95 | .pytest_cache 96 | 97 | # celery beat schedule file 98 | celerybeat-schedule 99 | 100 | # dotenv 101 | .env 102 | 103 | # virtualenv 104 | venv/ 105 | ENV/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | ### JetBrains template 113 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 114 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 115 | 116 | # User-specific stuff: 117 | .idea/ 118 | 119 | ## File-based project format: 120 | *.iws 121 | 122 | ## Plugin-specific files: 123 | 124 | # IntelliJ 125 | /out/ 126 | 127 | # mpeltonen/sbt-idea plugin 128 | .idea_modules/ 129 | 130 | # JIRA plugin 131 | atlassian-ide-plugin.xml 132 | 133 | # Crashlytics plugin (for Android Studio and IntelliJ) 134 | com_crashlytics_export_strings.xml 135 | crashlytics.properties 136 | crashlytics-build.properties 137 | fabric.properties 138 | 139 | /htmlcov 140 | /temp 141 | .DS_Store 142 | .*cache 143 | minio 144 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or 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 exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | aiohttp-s3-client 2 | ================ 3 | 4 | [![PyPI - License](https://img.shields.io/pypi/l/aiohttp-s3-client)](https://pypi.org/project/aiohttp-s3-client) [![Wheel](https://img.shields.io/pypi/wheel/aiohttp-s3-client)](https://pypi.org/project/aiohttp-s3-client) [![Mypy](http://www.mypy-lang.org/static/mypy_badge.svg)]() [![PyPI](https://img.shields.io/pypi/v/aiohttp-s3-client)](https://pypi.org/project/aiohttp-s3-client) [![PyPI](https://img.shields.io/pypi/pyversions/aiohttp-s3-client)](https://pypi.org/project/aiohttp-s3-client) [![Coverage Status](https://coveralls.io/repos/github/mosquito/aiohttp-s3-client/badge.svg?branch=master)](https://coveralls.io/github/mosquito/aiohttp-s3-client?branch=master) ![tox](https://github.com/mosquito/aiohttp-s3-client/workflows/tox/badge.svg?branch=master) 5 | 6 | The simple module for putting and getting object from Amazon S3 compatible endpoints 7 | 8 | ## Installation 9 | 10 | ```bash 11 | pip install aiohttp-s3-client 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```python 17 | from http import HTTPStatus 18 | 19 | from aiohttp import ClientSession 20 | from aiohttp_s3_client import S3Client 21 | 22 | 23 | async with ClientSession(raise_for_status=True) as session: 24 | client = S3Client( 25 | url="http://s3-url", 26 | session=session, 27 | access_key_id="key-id", 28 | secret_access_key="hackme", 29 | region="us-east-1" 30 | ) 31 | 32 | # Upload str object to bucket "bucket" and key "str" 33 | async with client.put("bucket/str", "hello, world") as resp: 34 | assert resp.status == HTTPStatus.OK 35 | 36 | # Upload bytes object to bucket "bucket" and key "bytes" 37 | async with await client.put("bucket/bytes", b"hello, world") as resp: 38 | assert resp.status == HTTPStatus.OK 39 | 40 | # Upload AsyncIterable to bucket "bucket" and key "iterable" 41 | async def gen(): 42 | yield b'some bytes' 43 | 44 | async with client.put("bucket/file", gen()) as resp: 45 | assert resp.status == HTTPStatus.OK 46 | 47 | # Upload file to bucket "bucket" and key "file" 48 | async with client.put_file("bucket/file", "/path_to_file") as resp: 49 | assert resp.status == HTTPStatus.OK 50 | 51 | # Check object exists using bucket+key 52 | async with client.head("bucket/key") as resp: 53 | assert resp == HTTPStatus.OK 54 | 55 | # Get object by bucket+key 56 | async with client.get("bucket/key") as resp: 57 | data = await resp.read() 58 | 59 | # Make presigned URL 60 | url = client.presign_url("GET", "bucket/key", expires=60 * 60) 61 | 62 | # Delete object using bucket+key 63 | async with client.delete("bucket/key") as resp: 64 | assert resp == HTTPStatus.NO_CONTENT 65 | 66 | # List objects by prefix 67 | async for result, prefixes in client.list_objects_v2("bucket/", prefix="prefix"): 68 | # Each result is a list of metadata objects representing an object 69 | # stored in the bucket. Each prefixes is a list of common prefixes 70 | do_work(result, prefixes) 71 | ``` 72 | 73 | Bucket may be specified as subdomain or in object name: 74 | 75 | ```python 76 | import aiohttp 77 | from aiohttp_s3_client import S3Client 78 | 79 | 80 | client = S3Client(url="http://bucket.your-s3-host", 81 | session=aiohttp.ClientSession()) 82 | async with client.put("key", gen()) as resp: 83 | ... 84 | 85 | client = S3Client(url="http://your-s3-host", 86 | session=aiohttp.ClientSession()) 87 | async with await client.put("bucket/key", gen()) as resp: 88 | ... 89 | 90 | client = S3Client(url="http://your-s3-host/bucket", 91 | session=aiohttp.ClientSession()) 92 | async with client.put("key", gen()) as resp: 93 | ... 94 | ``` 95 | 96 | Auth may be specified with keywords or in URL: 97 | ```python 98 | import aiohttp 99 | from aiohttp_s3_client import S3Client 100 | 101 | client_credentials_as_kw = S3Client( 102 | url="http://your-s3-host", 103 | access_key_id="key_id", 104 | secret_access_key="access_key", 105 | session=aiohttp.ClientSession(), 106 | ) 107 | 108 | client_credentials_in_url = S3Client( 109 | url="http://key_id:access_key@your-s3-host", 110 | session=aiohttp.ClientSession(), 111 | ) 112 | ``` 113 | 114 | ## Credentials 115 | 116 | By default `S3Client` trying to collect all available credentials from keyword 117 | arguments like `access_key_id=` and `secret_access_key=`, after that from the 118 | username and password from passed `url` argument, so the next step is environment 119 | variables parsing and the last source for collection is the config file. 120 | 121 | You can pass credentials explicitly using `aiohttp_s3_client.credentials` 122 | module. 123 | 124 | ### `aiohttp_s3_client.credentials.StaticCredentials` 125 | 126 | ```python 127 | import aiohttp 128 | from aiohttp_s3_client import S3Client 129 | from aiohttp_s3_client.credentials import StaticCredentials 130 | 131 | credentials = StaticCredentials( 132 | access_key_id='aaaa', 133 | secret_access_key='bbbb', 134 | region='us-east-1', 135 | ) 136 | client = S3Client( 137 | url="http://your-s3-host", 138 | session=aiohttp.ClientSession(), 139 | credentials=credentials, 140 | ) 141 | ``` 142 | 143 | ### `aiohttp_s3_client.credentials.URLCredentials` 144 | 145 | ```python 146 | import aiohttp 147 | from aiohttp_s3_client import S3Client 148 | from aiohttp_s3_client.credentials import URLCredentials 149 | 150 | url = "http://key@hack-me:your-s3-host" 151 | credentials = URLCredentials(url, region="us-east-1") 152 | client = S3Client( 153 | url="http://your-s3-host", 154 | session=aiohttp.ClientSession(), 155 | credentials=credentials, 156 | ) 157 | ``` 158 | 159 | ### `aiohttp_s3_client.credentials.EnvironmentCredentials` 160 | 161 | ```python 162 | import aiohttp 163 | from aiohttp_s3_client import S3Client 164 | from aiohttp_s3_client.credentials import EnvironmentCredentials 165 | 166 | credentials = EnvironmentCredentials(region="us-east-1") 167 | client = S3Client( 168 | url="http://your-s3-host", 169 | session=aiohttp.ClientSession(), 170 | credentials=credentials, 171 | ) 172 | ``` 173 | 174 | ### `aiohttp_s3_client.credentials.ConfigCredentials` 175 | 176 | Using user config file: 177 | 178 | ```python 179 | import aiohttp 180 | from aiohttp_s3_client import S3Client 181 | from aiohttp_s3_client.credentials import ConfigCredentials 182 | 183 | 184 | credentials = ConfigCredentials() # Will be used ~/.aws/credentials config 185 | client = S3Client( 186 | url="http://your-s3-host", 187 | session=aiohttp.ClientSession(), 188 | credentials=credentials, 189 | ) 190 | ``` 191 | 192 | Using the custom config location: 193 | 194 | ```python 195 | import aiohttp 196 | from aiohttp_s3_client import S3Client 197 | from aiohttp_s3_client.credentials import ConfigCredentials 198 | 199 | 200 | credentials = ConfigCredentials("~/.my-custom-aws-credentials") 201 | client = S3Client( 202 | url="http://your-s3-host", 203 | session=aiohttp.ClientSession(), 204 | credentials=credentials, 205 | ) 206 | ``` 207 | 208 | ### `aiohttp_s3_client.credentials.merge_credentials` 209 | 210 | This function collect all passed credentials instances and return a new one 211 | which contains all non-blank fields from passed instances. The first argument 212 | has more priority. 213 | 214 | 215 | ```python 216 | import aiohttp 217 | from aiohttp_s3_client import S3Client 218 | from aiohttp_s3_client.credentials import ( 219 | ConfigCredentials, EnvironmentCredentials, merge_credentials 220 | ) 221 | 222 | credentials = merge_credentials( 223 | EnvironmentCredentials(), 224 | ConfigCredentials(), 225 | ) 226 | client = S3Client( 227 | url="http://your-s3-host", 228 | session=aiohttp.ClientSession(), 229 | credentials=credentials, 230 | ) 231 | ``` 232 | 233 | 234 | ### `aiohttp_s3_client.credentials.MetadataCredentials` 235 | 236 | Trying to get credentials from the metadata service: 237 | 238 | ```python 239 | import aiohttp 240 | from aiohttp_s3_client import S3Client 241 | from aiohttp_s3_client.credentials import MetadataCredentials 242 | 243 | credentials = MetadataCredentials() 244 | 245 | # start refresh credentials from metadata server 246 | await credentials.start() 247 | client = S3Client( 248 | url="http://your-s3-host", 249 | session=aiohttp.ClientSession(), 250 | ) 251 | await credentials.stop() 252 | ``` 253 | 254 | ## Multipart upload 255 | 256 | For uploading large files [multipart uploading](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html) 257 | can be used. It allows you to asynchronously upload multiple parts of a file 258 | to S3. 259 | S3Client handles retries of part uploads and calculates part hash for integrity checks. 260 | 261 | ```python 262 | import aiohttp 263 | from aiohttp_s3_client import S3Client 264 | 265 | 266 | client = S3Client(url="http://your-s3-host", session=aiohttp.ClientSession()) 267 | await client.put_file_multipart( 268 | "test/bigfile.csv", 269 | headers={ 270 | "Content-Type": "text/csv", 271 | }, 272 | workers_count=8, 273 | ) 274 | ``` 275 | 276 | ## Parallel download to file 277 | 278 | S3 supports `GET` requests with `Range` header. It's possible to download 279 | objects in parallel with multiple connections for speedup. 280 | S3Client handles retries of partial requests and makes sure that file won't 281 | be changed during download with `ETag` header. 282 | If your system supports `pwrite` syscall (Linux, macOS, etc.) it will be used to 283 | write simultaneously to a single file. Otherwise, each worker will have own file 284 | which will be concatenated after downloading. 285 | 286 | ```python 287 | import aiohttp 288 | from aiohttp_s3_client import S3Client 289 | 290 | 291 | client = S3Client(url="http://your-s3-host", session=aiohttp.ClientSession()) 292 | 293 | await client.get_file_parallel( 294 | "dump/bigfile.csv", 295 | "/home/user/bigfile.csv", 296 | workers_count=8, 297 | ) 298 | ``` 299 | -------------------------------------------------------------------------------- /aiohttp_s3_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import S3Client 2 | from .version import __version__, version_info 3 | from .xml import AwsObjectMeta 4 | 5 | 6 | __all__ = ( 7 | "__version__", 8 | "version_info", 9 | "AwsObjectMeta", 10 | "S3Client", 11 | ) 12 | -------------------------------------------------------------------------------- /aiohttp_s3_client/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import hashlib 3 | import io 4 | import logging 5 | import os 6 | import sys 7 | import typing as t 8 | from collections import deque 9 | from functools import partial 10 | from http import HTTPStatus 11 | from itertools import chain 12 | from mimetypes import guess_type 13 | from mmap import PAGESIZE 14 | from pathlib import Path 15 | from tempfile import NamedTemporaryFile 16 | from urllib.parse import quote 17 | 18 | from aiohttp import ClientSession, hdrs 19 | # noinspection PyProtectedMember 20 | from aiohttp.client import ( 21 | _RequestContextManager as RequestContextManager, 22 | ClientResponse, 23 | ) 24 | from aiohttp.client_exceptions import ClientError, ClientResponseError 25 | from aiomisc import asyncbackoff, threaded, threaded_iterable 26 | from aws_request_signer import UNSIGNED_PAYLOAD 27 | from multidict import CIMultiDict, CIMultiDictProxy 28 | from yarl import URL 29 | 30 | from aiohttp_s3_client.credentials import ( 31 | AbstractCredentials, collect_credentials, 32 | ) 33 | from aiohttp_s3_client.xml import ( 34 | AwsObjectMeta, create_complete_upload_request, 35 | parse_create_multipart_upload_id, parse_list_objects, 36 | ) 37 | 38 | log = logging.getLogger(__name__) 39 | 40 | CHUNK_SIZE = 2 ** 16 41 | DONE = object() 42 | EMPTY_STR_HASH = hashlib.sha256(b"").hexdigest() 43 | PART_SIZE = 5 * 1024 * 1024 # 5MB 44 | 45 | HeadersType = t.Union[t.Dict, CIMultiDict, CIMultiDictProxy] 46 | 47 | threaded_iterable_constrained = threaded_iterable(max_size=2) 48 | 49 | 50 | class AwsError(ClientResponseError): 51 | def __init__( 52 | self, resp: ClientResponse, message: str, *history: ClientResponse 53 | ): 54 | super().__init__( 55 | headers=resp.headers, 56 | history=(resp, *history), 57 | message=message, 58 | request_info=resp.request_info, 59 | status=resp.status, 60 | ) 61 | 62 | 63 | class AwsUploadError(AwsError): 64 | pass 65 | 66 | 67 | class AwsDownloadError(AwsError): 68 | pass 69 | 70 | 71 | if sys.version_info < (3, 8): 72 | from contextlib import suppress 73 | 74 | @threaded 75 | def unlink_path(path: Path) -> None: 76 | with suppress(FileNotFoundError): 77 | os.unlink(path.resolve()) 78 | else: 79 | @threaded 80 | def unlink_path(path: Path) -> None: 81 | path.unlink(missing_ok=True) 82 | 83 | 84 | @threaded 85 | def concat_files( 86 | target_file: Path, files: t.List[t.IO[bytes]], buffer_size: int, 87 | ) -> None: 88 | with target_file.open("ab") as fp: 89 | for file in files: 90 | file.seek(0) 91 | while True: 92 | chunk = file.read(buffer_size) 93 | if not chunk: 94 | break 95 | fp.write(chunk) 96 | file.close() 97 | 98 | 99 | @threaded 100 | def write_from_start( 101 | file: io.BytesIO, chunk: bytes, range_start: int, pos: int, 102 | ) -> None: 103 | file.seek(pos - range_start) 104 | file.write(chunk) 105 | 106 | 107 | @threaded 108 | def pwrite_absolute_pos( 109 | fd: int, chunk: bytes, range_start: int, pos: int, 110 | ) -> None: 111 | os.pwrite(fd, chunk, pos) 112 | 113 | 114 | @threaded_iterable_constrained 115 | def gen_without_hash( 116 | stream: t.Iterable[bytes], 117 | ) -> t.Generator[t.Tuple[None, bytes], None, None]: 118 | for data in stream: 119 | yield (None, data) 120 | 121 | 122 | @threaded_iterable_constrained 123 | def gen_with_hash( 124 | stream: t.Iterable[bytes], 125 | ) -> t.Generator[t.Tuple[str, bytes], None, None]: 126 | for data in stream: 127 | yield hashlib.sha256(data).hexdigest(), data 128 | 129 | 130 | def file_sender( 131 | file_name: t.Union[str, Path], chunk_size: int = CHUNK_SIZE, 132 | ) -> t.Iterable[bytes]: 133 | with open(file_name, "rb") as fp: 134 | while True: 135 | data = fp.read(chunk_size) 136 | if not data: 137 | break 138 | yield data 139 | 140 | 141 | async_file_sender = threaded_iterable_constrained(file_sender) 142 | 143 | DataType = t.Union[bytes, str, t.AsyncIterable[bytes]] 144 | ParamsType = t.Optional[t.Mapping[str, str]] 145 | 146 | 147 | class S3Client: 148 | def __init__( 149 | self, session: ClientSession, url: t.Union[URL, str], 150 | secret_access_key: t.Optional[str] = None, 151 | access_key_id: t.Optional[str] = None, 152 | session_token: t.Optional[str] = None, 153 | region: str = "", 154 | credentials: t.Optional[AbstractCredentials] = None, 155 | ): 156 | url = URL(url) 157 | if credentials is None: 158 | credentials = collect_credentials( 159 | url=url, 160 | access_key_id=access_key_id, 161 | region=region, 162 | secret_access_key=secret_access_key, 163 | session_token=session_token, 164 | ) 165 | 166 | if not credentials: 167 | raise ValueError( 168 | f"Credentials {credentials!r} is incomplete", 169 | ) 170 | 171 | self._url = URL(url).with_user(None).with_password(None) 172 | self._session = session 173 | self._credentials = credentials 174 | 175 | @property 176 | def url(self) -> URL: 177 | return self._url 178 | 179 | def request( 180 | self, method: str, path: str, 181 | headers: t.Optional[HeadersType] = None, 182 | params: ParamsType = None, 183 | data: t.Optional[DataType] = None, 184 | data_length: t.Optional[int] = None, 185 | content_sha256: t.Optional[str] = None, 186 | **kwargs, 187 | ) -> RequestContextManager: 188 | if isinstance(data, bytes): 189 | data_length = len(data) 190 | elif isinstance(data, str): 191 | data = data.encode() 192 | data_length = len(data) 193 | 194 | headers = self._prepare_headers(headers) 195 | if data_length: 196 | headers[hdrs.CONTENT_LENGTH] = str(data_length) 197 | elif data is not None: 198 | kwargs["chunked"] = True 199 | 200 | if data is not None and content_sha256 is None: 201 | content_sha256 = UNSIGNED_PAYLOAD 202 | 203 | url = (self._url / path.lstrip("/")) 204 | url = url.with_path(quote(url.path), encoded=True).with_query(params) 205 | 206 | headers = self._make_headers(headers) 207 | headers.extend( 208 | self._credentials.signer.sign_with_headers( 209 | method, str(url), headers=headers, content_hash=content_sha256, 210 | ), 211 | ) 212 | return self._session.request( 213 | method, url, headers=headers, data=data, **kwargs, 214 | ) 215 | 216 | def get(self, object_name: str, **kwargs) -> RequestContextManager: 217 | return self.request("GET", object_name, **kwargs) 218 | 219 | def head( 220 | self, object_name: str, 221 | content_sha256=EMPTY_STR_HASH, 222 | **kwargs, 223 | ) -> RequestContextManager: 224 | return self.request( 225 | "HEAD", object_name, content_sha256=content_sha256, **kwargs, 226 | ) 227 | 228 | def delete( 229 | self, object_name: str, 230 | content_sha256=EMPTY_STR_HASH, 231 | **kwargs, 232 | ) -> RequestContextManager: 233 | return self.request( 234 | "DELETE", object_name, content_sha256=content_sha256, **kwargs, 235 | ) 236 | 237 | @staticmethod 238 | def _make_headers(headers: t.Optional[HeadersType]) -> CIMultiDict: 239 | headers = CIMultiDict(headers or {}) 240 | return headers 241 | 242 | def _prepare_headers( 243 | self, headers: t.Optional[HeadersType], 244 | file_path: str = "", 245 | ) -> CIMultiDict: 246 | headers = self._make_headers(headers) 247 | 248 | if hdrs.CONTENT_TYPE not in headers: 249 | content_type = guess_type(file_path)[0] 250 | if content_type is None: 251 | content_type = "application/octet-stream" 252 | 253 | headers[hdrs.CONTENT_TYPE] = content_type 254 | 255 | return headers 256 | 257 | def put( 258 | self, object_name: str, 259 | data: t.Union[bytes, str, t.AsyncIterable[bytes]], **kwargs, 260 | ) -> RequestContextManager: 261 | return self.request("PUT", object_name, data=data, **kwargs) 262 | 263 | def post( 264 | self, object_name: str, 265 | data: t.Union[None, bytes, str, t.AsyncIterable[bytes]] = None, 266 | **kwargs, 267 | ) -> RequestContextManager: 268 | return self.request("POST", object_name, data=data, **kwargs) 269 | 270 | def put_file( 271 | self, object_name: t.Union[str, Path], 272 | file_path: t.Union[str, Path], 273 | *, headers: t.Optional[HeadersType] = None, 274 | chunk_size: int = CHUNK_SIZE, content_sha256: t.Optional[str] = None, 275 | ) -> RequestContextManager: 276 | 277 | headers = self._prepare_headers(headers, str(file_path)) 278 | return self.put( 279 | str(object_name), 280 | headers=headers, 281 | data=async_file_sender( 282 | file_path, 283 | chunk_size=chunk_size, 284 | ), 285 | data_length=os.stat(file_path).st_size, 286 | content_sha256=content_sha256, 287 | ) 288 | 289 | @asyncbackoff( 290 | None, None, 0, 291 | max_tries=3, exceptions=(ClientError,), 292 | ) 293 | async def _create_multipart_upload( 294 | self, 295 | object_name: str, 296 | headers: t.Optional[HeadersType] = None, 297 | ) -> str: 298 | async with self.post( 299 | object_name, 300 | headers=headers, 301 | params={"uploads": 1}, 302 | content_sha256=EMPTY_STR_HASH, 303 | ) as resp: 304 | payload = await resp.read() 305 | if resp.status != HTTPStatus.OK: 306 | raise AwsUploadError( 307 | resp, 308 | ( 309 | f"Wrong status code {resp.status} from s3 " 310 | f"with message {payload.decode()}." 311 | ), 312 | ) 313 | return parse_create_multipart_upload_id(payload) 314 | 315 | @asyncbackoff( 316 | None, None, 0, 317 | max_tries=3, exceptions=(AwsUploadError, ClientError), 318 | ) 319 | async def _complete_multipart_upload( 320 | self, 321 | upload_id: str, 322 | object_name: str, 323 | parts: t.List[t.Tuple[int, str]], 324 | ) -> None: 325 | complete_upload_request = create_complete_upload_request(parts) 326 | async with self.post( 327 | object_name, 328 | headers={"Content-Type": "text/xml"}, 329 | params={"uploadId": upload_id}, 330 | data=complete_upload_request, 331 | content_sha256=hashlib.sha256(complete_upload_request).hexdigest(), 332 | ) as resp: 333 | if resp.status != HTTPStatus.OK: 334 | payload = await resp.text() 335 | raise AwsUploadError( 336 | resp, 337 | ( 338 | f"Wrong status code {resp.status} from s3 " 339 | f"with message {payload}." 340 | ), 341 | ) 342 | 343 | async def _put_part( 344 | self, 345 | upload_id: str, 346 | object_name: str, 347 | part_no: int, 348 | data: bytes, 349 | content_sha256: str, 350 | **kwargs, 351 | ) -> str: 352 | async with self.put( 353 | object_name, 354 | params={"partNumber": part_no, "uploadId": upload_id}, 355 | data=data, 356 | content_sha256=content_sha256, 357 | **kwargs, 358 | ) as resp: 359 | payload = await resp.text() 360 | if resp.status != HTTPStatus.OK: 361 | raise AwsUploadError( 362 | resp, 363 | ( 364 | f"Wrong status code {resp.status} from s3 " 365 | f"with message {payload}." 366 | ), 367 | ) 368 | return resp.headers["Etag"].strip('"') 369 | 370 | async def _part_uploader( 371 | self, 372 | upload_id: str, 373 | object_name: str, 374 | parts_queue: asyncio.Queue, 375 | results_queue: deque, 376 | part_upload_tries: int, 377 | **kwargs, 378 | ) -> None: 379 | backoff = asyncbackoff( 380 | None, None, 381 | max_tries=part_upload_tries, 382 | exceptions=(ClientError,), 383 | ) 384 | while True: 385 | msg = await parts_queue.get() 386 | if msg is DONE: 387 | break 388 | part_no, part_hash, part = msg 389 | etag = await backoff(self._put_part)( 390 | upload_id=upload_id, 391 | object_name=object_name, 392 | part_no=part_no, 393 | data=part, 394 | content_sha256=part_hash, 395 | **kwargs, 396 | ) 397 | log.debug( 398 | "Etag for part %d of %s is %s", part_no, upload_id, etag, 399 | ) 400 | results_queue.append((part_no, etag)) 401 | 402 | async def put_file_multipart( 403 | self, 404 | object_name: t.Union[str, Path], 405 | file_path: t.Union[str, Path], 406 | *, 407 | headers: t.Optional[HeadersType] = None, 408 | part_size: int = PART_SIZE, 409 | workers_count: int = 1, 410 | max_size: t.Optional[int] = None, 411 | part_upload_tries: int = 3, 412 | calculate_content_sha256: bool = True, 413 | **kwargs, 414 | ) -> None: 415 | """ 416 | Upload data from a file with multipart upload 417 | 418 | object_name: key in s3 419 | file_path: path to a file for upload 420 | headers: additional headers, such as Content-Type 421 | part_size: size of a chunk to send (recommended: >5Mb) 422 | workers_count: count of coroutines for asyncronous parts uploading 423 | max_size: maximum size of a queue with data to send (should be 424 | at least `workers_count`) 425 | part_upload_tries: how many times trying to put part to s3 before fail 426 | calculate_content_sha256: whether to calculate sha256 hash of a part 427 | for integrity purposes 428 | """ 429 | log.debug( 430 | "Going to multipart upload %s to %s with part size %d", 431 | file_path, object_name, part_size, 432 | ) 433 | await self.put_multipart( 434 | object_name, 435 | file_sender( 436 | file_path, 437 | chunk_size=part_size, 438 | ), 439 | headers=headers, 440 | workers_count=workers_count, 441 | max_size=max_size, 442 | part_upload_tries=part_upload_tries, 443 | calculate_content_sha256=calculate_content_sha256, 444 | **kwargs, 445 | ) 446 | 447 | async def _parts_generator( 448 | self, gen, workers_count: int, parts_queue: asyncio.Queue, 449 | ) -> int: 450 | part_no = 1 451 | async with gen: 452 | async for part_hash, part in gen: 453 | log.debug( 454 | "Reading part %d (%d bytes)", part_no, len(part), 455 | ) 456 | await parts_queue.put((part_no, part_hash, part)) 457 | part_no += 1 458 | 459 | for _ in range(workers_count): 460 | await parts_queue.put(DONE) 461 | 462 | return part_no 463 | 464 | async def put_multipart( 465 | self, 466 | object_name: t.Union[str, Path], 467 | data: t.Iterable[bytes], 468 | *, 469 | headers: t.Optional[HeadersType] = None, 470 | workers_count: int = 1, 471 | max_size: t.Optional[int] = None, 472 | part_upload_tries: int = 3, 473 | calculate_content_sha256: bool = True, 474 | **kwargs, 475 | ) -> None: 476 | """ 477 | Send data from iterable with multipart upload 478 | 479 | object_name: key in s3 480 | data: any iterable that returns chunks of bytes 481 | headers: additional headers, such as Content-Type 482 | workers_count: count of coroutines for asyncronous parts uploading 483 | max_size: maximum size of a queue with data to send (should be 484 | at least `workers_count`) 485 | part_upload_tries: how many times trying to put part to s3 before fail 486 | calculate_content_sha256: whether to calculate sha256 hash of a part 487 | for integrity purposes 488 | """ 489 | if workers_count < 1: 490 | raise ValueError( 491 | f"Workers count should be > 0. Got {workers_count}", 492 | ) 493 | max_size = max_size or workers_count 494 | 495 | upload_id = await self._create_multipart_upload( 496 | str(object_name), 497 | headers=headers, 498 | ) 499 | log.debug("Got upload id %s for %s", upload_id, object_name) 500 | 501 | parts_queue: asyncio.Queue = asyncio.Queue(maxsize=max_size) 502 | results_queue: deque = deque() 503 | workers = [ 504 | asyncio.create_task( 505 | self._part_uploader( 506 | upload_id, 507 | str(object_name), 508 | parts_queue, 509 | results_queue, 510 | part_upload_tries, 511 | **kwargs, 512 | ), 513 | ) 514 | for _ in range(workers_count) 515 | ] 516 | 517 | if calculate_content_sha256: 518 | gen = gen_with_hash(data) 519 | else: 520 | gen = gen_without_hash(data) 521 | 522 | parts_generator = asyncio.create_task( 523 | self._parts_generator(gen, workers_count, parts_queue), 524 | ) 525 | try: 526 | part_no, *_ = await asyncio.gather( 527 | parts_generator, 528 | *workers, 529 | ) 530 | except Exception: 531 | for task in chain([parts_generator], workers): 532 | if not task.done(): 533 | task.cancel() 534 | raise 535 | 536 | log.debug( 537 | "All parts (#%d) of %s are uploaded to %s", 538 | part_no - 1, upload_id, object_name, # type: ignore 539 | ) 540 | 541 | # Parts should be in ascending order 542 | parts = sorted(results_queue, key=lambda x: x[0]) 543 | await self._complete_multipart_upload( 544 | upload_id, object_name, parts, 545 | ) 546 | 547 | async def _download_range( 548 | self, 549 | object_name: str, 550 | writer: t.Callable[[bytes, int, int], t.Coroutine], 551 | *, 552 | etag: str, 553 | range_start: int, 554 | req_range_start: int, 555 | req_range_end: int, 556 | buffer_size: int, 557 | headers: t.Optional[HeadersType] = None, 558 | **kwargs, 559 | ) -> None: 560 | """ 561 | Downloading range [req_range_start:req_range_end] to `file` 562 | """ 563 | log.debug( 564 | "Downloading %s from %d to %d", 565 | object_name, 566 | req_range_start, 567 | req_range_end, 568 | ) 569 | if not headers: 570 | headers = {} 571 | headers = headers.copy() 572 | headers["Range"] = f"bytes={req_range_start}-{req_range_end}" 573 | headers["If-Match"] = etag 574 | 575 | pos = req_range_start 576 | async with self.get(object_name, headers=headers, **kwargs) as resp: 577 | if resp.status not in (HTTPStatus.PARTIAL_CONTENT, HTTPStatus.OK): 578 | raise AwsDownloadError( 579 | resp, 580 | ( 581 | f"Got wrong status code {resp.status} on " 582 | f"range download of {object_name}" 583 | ), 584 | ) 585 | while True: 586 | chunk = await resp.content.read(buffer_size) 587 | if not chunk: 588 | break 589 | await writer(chunk, range_start, pos) 590 | pos += len(chunk) 591 | 592 | async def _download_worker( 593 | self, 594 | object_name: str, 595 | writer: t.Callable[[bytes, int, int], t.Coroutine], 596 | *, 597 | etag: str, 598 | range_step: int, 599 | range_start: int, 600 | range_end: int, 601 | buffer_size: int, 602 | range_get_tries: int = 3, 603 | headers: t.Optional[HeadersType] = None, 604 | **kwargs, 605 | ) -> None: 606 | """ 607 | Downloads data in range `[range_start, range_end)` 608 | with step `range_step` to file `file_path`. 609 | Uses `etag` to make sure that file wasn't changed in the process. 610 | """ 611 | log.debug( 612 | "Starting download worker for range [%d:%d]", 613 | range_start, 614 | range_end, 615 | ) 616 | backoff = asyncbackoff( 617 | None, None, 618 | max_tries=range_get_tries, 619 | exceptions=(ClientError,), 620 | ) 621 | req_range_end = range_start 622 | for req_range_start in range(range_start, range_end, range_step): 623 | req_range_end += range_step 624 | if req_range_end > range_end: 625 | req_range_end = range_end 626 | await backoff(self._download_range)( 627 | object_name, 628 | writer, 629 | etag=etag, 630 | range_start=range_start, 631 | req_range_start=req_range_start, 632 | req_range_end=req_range_end - 1, 633 | buffer_size=buffer_size, 634 | headers=headers, 635 | **kwargs, 636 | ) 637 | 638 | async def get_file_parallel( 639 | self, 640 | object_name: t.Union[str, Path], 641 | file_path: t.Union[str, Path], 642 | *, 643 | headers: t.Optional[HeadersType] = None, 644 | range_step: int = PART_SIZE, 645 | workers_count: int = 1, 646 | range_get_tries: int = 3, 647 | buffer_size: int = PAGESIZE * 32, 648 | **kwargs, 649 | ) -> None: 650 | """ 651 | Download object in parallel with requests with Range. 652 | If file will change while download is in progress - 653 | error will be raised. 654 | 655 | object_name: s3 key to download 656 | file_path: target file path 657 | headers: additional headers 658 | range_step: how much data will be downloaded in single HTTP request 659 | workers_count: count of parallel workers 660 | range_get_tries: count of tries to download each range 661 | buffer_size: size of a buffer for on the fly data 662 | """ 663 | file_path = Path(file_path) 664 | async with self.head(str(object_name), headers=headers) as resp: 665 | if resp.status != HTTPStatus.OK: 666 | raise AwsDownloadError( 667 | resp, 668 | ( 669 | f"Got response for HEAD request for " 670 | f"{object_name} of a wrong status {resp.status}" 671 | ), 672 | ) 673 | etag = resp.headers["Etag"] 674 | file_size = int(resp.headers["Content-Length"]) 675 | log.debug( 676 | "Object's %s etag is %s and size is %d", 677 | object_name, 678 | etag, 679 | file_size, 680 | ) 681 | 682 | workers = [] 683 | files: t.List[t.IO[bytes]] = [] 684 | worker_range_size = file_size // workers_count 685 | range_end = 0 686 | file: t.IO[bytes] 687 | try: 688 | with file_path.open("w+b") as fp: 689 | for range_start in range(0, file_size, worker_range_size): 690 | range_end += worker_range_size 691 | if range_end > file_size: 692 | range_end = file_size 693 | if hasattr(os, "pwrite"): 694 | writer = partial(pwrite_absolute_pos, fp.fileno()) 695 | else: 696 | if range_start: 697 | file = NamedTemporaryFile(dir=file_path.parent) 698 | files.append(file) 699 | else: 700 | file = fp 701 | writer = partial(write_from_start, file) 702 | workers.append( 703 | self._download_worker( 704 | str(object_name), 705 | writer, # type: ignore 706 | buffer_size=buffer_size, 707 | etag=etag, 708 | headers=headers, 709 | range_end=range_end, 710 | range_get_tries=range_get_tries, 711 | range_start=range_start, 712 | range_step=range_step, 713 | **kwargs, 714 | ), 715 | ) 716 | 717 | await asyncio.gather(*workers) 718 | 719 | if files: 720 | # First part already in `file_path` 721 | log.debug("Joining %d parts to %s", len(files) + 1, file_path) 722 | await concat_files( 723 | file_path, files, buffer_size=buffer_size, 724 | ) 725 | except Exception: 726 | log.exception( 727 | "Error on file download. Removing possibly incomplete file %s", 728 | file_path, 729 | ) 730 | await unlink_path(file_path) 731 | raise 732 | 733 | async def list_objects_v2( 734 | self, 735 | object_name: t.Union[str, Path] = "/", 736 | *, 737 | bucket: t.Optional[str] = None, 738 | prefix: t.Optional[t.Union[str, Path]] = None, 739 | delimiter: t.Optional[str] = None, 740 | max_keys: t.Optional[int] = None, 741 | start_after: t.Optional[str] = None, 742 | ) -> t.AsyncIterator[t.Tuple[t.List[AwsObjectMeta], t.List[str]]]: 743 | """ 744 | List objects in bucket. 745 | 746 | Returns an iterator over lists of metadata objects, each corresponding 747 | to an individual response result (typically limited to 1000 keys). 748 | 749 | object_name: 750 | path to listing endpoint, defaults to '/'; a `bucket` value is 751 | prepended to this value if provided. 752 | prefix: 753 | limits the response to keys that begin with the specified 754 | prefix 755 | delimiter: a delimiter is a character you use to group keys 756 | max_keys: maximum number of keys returned in the response 757 | start_after: keys to start listing after 758 | """ 759 | 760 | params = { 761 | "list-type": "2", 762 | } 763 | 764 | if prefix: 765 | params["prefix"] = str(prefix) 766 | 767 | if delimiter: 768 | params["delimiter"] = delimiter 769 | 770 | if max_keys: 771 | params["max-keys"] = str(max_keys) 772 | 773 | if start_after: 774 | params["start-after"] = start_after 775 | 776 | if bucket is not None: 777 | object_name = f"/{bucket}" 778 | 779 | while True: 780 | async with self.get(str(object_name), params=params) as resp: 781 | if resp.status != HTTPStatus.OK: 782 | raise AwsDownloadError( 783 | resp, 784 | ( 785 | "Got response with wrong status for GET request " 786 | f"for {object_name} with prefix '{prefix}'" 787 | ), 788 | ) 789 | payload = await resp.read() 790 | metadata, prefixes, cont_token = parse_list_objects(payload) 791 | if not metadata and not prefixes: 792 | break 793 | yield metadata, prefixes 794 | if not cont_token: 795 | break 796 | params["continuation-token"] = cont_token 797 | 798 | def presign_url( 799 | self, 800 | method: str, 801 | url: t.Union[str, URL], 802 | headers: t.Optional[HeadersType] = None, 803 | content_sha256: t.Optional[str] = None, 804 | expires: int = 86400, 805 | ) -> URL: 806 | """ 807 | Make presigned url which will expire in specified amount of seconds 808 | 809 | method: HTTP method 810 | url: object key or absolute URL 811 | headers: optional headers you would like to pass 812 | content_sha256: 813 | expires: amount of seconds presigned url would be usable 814 | """ 815 | if content_sha256 is None: 816 | content_sha256 = UNSIGNED_PAYLOAD 817 | 818 | _url = URL(url) 819 | if not _url.is_absolute(): 820 | _url = self._url / str(_url) 821 | 822 | return URL(self._credentials.signer.presign_url( 823 | method=method.upper(), 824 | url=str(_url), 825 | headers=headers, 826 | content_hash=content_sha256, 827 | expires=expires 828 | )) 829 | -------------------------------------------------------------------------------- /aiohttp_s3_client/credentials.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import configparser 3 | import datetime 4 | import logging 5 | import math 6 | import os 7 | from abc import ABC, abstractmethod 8 | from dataclasses import dataclass 9 | from pathlib import Path 10 | from typing import Any, List, Mapping, Optional, Tuple, Union 11 | 12 | import aiohttp 13 | from aws_request_signer import AwsRequestSigner 14 | from yarl import URL 15 | 16 | 17 | try: 18 | from functools import cached_property 19 | except ImportError: 20 | from cached_property import cached_property # type: ignore 21 | 22 | try: 23 | from typing import TypedDict 24 | except ImportError: 25 | from typing_extensions import TypedDict 26 | 27 | 28 | log = logging.getLogger(__name__) 29 | 30 | 31 | class AbstractCredentials(ABC): 32 | @abstractmethod 33 | def __bool__(self) -> bool: 34 | ... 35 | 36 | @property 37 | @abstractmethod 38 | def signer(self) -> AwsRequestSigner: 39 | ... 40 | 41 | 42 | @dataclass(frozen=True) 43 | class StaticCredentials(AbstractCredentials): 44 | access_key_id: str = "" 45 | secret_access_key: str = "" 46 | session_token: Optional[str] = None 47 | region: str = "" 48 | service: str = "s3" 49 | 50 | def __bool__(self) -> bool: 51 | return all((self.access_key_id, self.secret_access_key)) 52 | 53 | def __repr__(self) -> str: 54 | return ( 55 | f"{self.__class__.__name__}(access_key_id={self.access_key_id!r}, " 56 | "secret_access_key=" 57 | f'{"******" if self.secret_access_key else None!r}, ' 58 | f"region={self.region!r}, service={self.service!r})" 59 | ) 60 | 61 | def as_dict(self) -> dict: 62 | return { 63 | "region": self.region, 64 | "access_key_id": self.access_key_id, 65 | "secret_access_key": self.secret_access_key, 66 | "session_token": self.session_token, 67 | "service": self.service, 68 | } 69 | 70 | @cached_property 71 | def signer(self) -> AwsRequestSigner: 72 | return AwsRequestSigner(**self.as_dict()) 73 | 74 | 75 | class URLCredentials(StaticCredentials): 76 | def __init__( 77 | self, url: Union[str, URL], *, region: str = "", service: str = "s3", 78 | ): 79 | url = URL(url) 80 | super().__init__( 81 | access_key_id=url.user or "", 82 | secret_access_key=url.password or "", 83 | region=region, service=service, 84 | ) 85 | 86 | 87 | class EnvironmentCredentials(StaticCredentials): 88 | def __init__(self, region: str = "", service: str = "s3"): 89 | super().__init__( 90 | access_key_id=os.getenv("AWS_ACCESS_KEY_ID", ""), 91 | secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY", ""), 92 | session_token=os.getenv("AWS_SESSION_TOKEN"), 93 | region=os.getenv("AWS_DEFAULT_REGION", region), 94 | service=service, 95 | ) 96 | 97 | 98 | class ConfigCredentials(StaticCredentials): 99 | DEFAULT_CREDENTIALS_PATH = Path.home() / ".aws" / "credentials" 100 | DEFAULT_CONFIG_PATH = Path.home() / ".aws" / "config" 101 | 102 | @staticmethod 103 | def _parse_ini_section(path: Path, section: str) -> Mapping[str, str]: 104 | conf = configparser.ConfigParser() 105 | if not conf.read(path): 106 | return {} 107 | 108 | if section not in conf: 109 | return {} 110 | 111 | return conf[section] 112 | 113 | def __init__( 114 | self, 115 | credentials_path: Union[str, Path, None] = None, 116 | config_path: Union[str, Path, None] = DEFAULT_CONFIG_PATH, *, 117 | region: str = "", service: str = "s3", profile: str = "auto", 118 | ): 119 | if credentials_path is None: 120 | credentials_path = Path( 121 | os.getenv( 122 | "AWS_SHARED_CREDENTIALS_FILE", 123 | self.DEFAULT_CREDENTIALS_PATH, 124 | ), 125 | ) 126 | credentials_path = Path(credentials_path) 127 | 128 | if config_path is None: 129 | config_path = Path( 130 | os.getenv( 131 | "AWS_SHARED_CONFIG_FILE", 132 | self.DEFAULT_CONFIG_PATH, 133 | ), 134 | ) 135 | config_path = Path(config_path) 136 | 137 | try: 138 | credentials_paths_exists = ( 139 | credentials_path.exists() and config_path.exists() 140 | ) 141 | except OSError: 142 | credentials_paths_exists = False 143 | 144 | if not credentials_paths_exists: 145 | super().__init__(region=region, service=service) 146 | return 147 | 148 | if profile == "auto": 149 | profile = os.getenv("AWS_PROFILE", "default") 150 | 151 | section = self._parse_ini_section(credentials_path, profile) 152 | access_key_id = section.get("aws_access_key_id", "") 153 | secret_access_key = section.get("aws_secret_access_key", "") 154 | 155 | section = self._parse_ini_section(config_path, profile) 156 | region = section.get("region", "") 157 | 158 | super().__init__( 159 | access_key_id=access_key_id, 160 | secret_access_key=secret_access_key, 161 | region=region, 162 | service=service, 163 | ) 164 | 165 | 166 | ENVIRONMENT_CREDENTIALS = EnvironmentCredentials() 167 | 168 | 169 | def merge_credentials(*credentials: StaticCredentials) -> StaticCredentials: 170 | result = {} 171 | fields = ( 172 | "access_key_id", "secret_access_key", 173 | "session_token", "region", "service", 174 | ) 175 | 176 | for candidate in credentials: 177 | for field in fields: 178 | if field in result: 179 | continue 180 | value = getattr(candidate, field, None) 181 | if not value: 182 | continue 183 | result[field] = value 184 | 185 | return StaticCredentials(**result) 186 | 187 | 188 | def collect_credentials( 189 | *, url: Optional[URL] = None, **kwargs, 190 | ) -> StaticCredentials: 191 | credentials: List[StaticCredentials] = [] 192 | if kwargs: 193 | credentials.append(StaticCredentials(**kwargs)) 194 | if url: 195 | credentials.append(URLCredentials(url)) 196 | credentials.append(EnvironmentCredentials()) 197 | credentials.append(ConfigCredentials()) 198 | return merge_credentials(*credentials) 199 | 200 | 201 | class MetadataDocument(TypedDict, total=False): 202 | """ 203 | Response example is: 204 | 205 | { 206 | "accountId" : "123123", 207 | "architecture" : "x86_64", 208 | "availabilityZone" : "us-east-1a", 209 | "billingProducts" : null, 210 | "devpayProductCodes" : null, 211 | "marketplaceProductCodes" : null, 212 | "imageId" : "ami-123123", 213 | "instanceId" : "i-11232323", 214 | "instanceType" : "t3a.micro", 215 | "kernelId" : null, 216 | "pendingTime" : "2023-06-13T18:18:58Z", 217 | "privateIp" : "172.33.33.33", 218 | "ramdiskId" : null, 219 | "region" : "us-east-1", 220 | "version" : "2017-09-30" 221 | } 222 | """ 223 | region: str 224 | 225 | 226 | class MetadataSecurityCredentials(TypedDict, total=False): 227 | Code: str 228 | Type: str 229 | AccessKeyId: str 230 | SecretAccessKey: str 231 | Token: str 232 | Expiration: str 233 | 234 | 235 | class MetadataCredentials(AbstractCredentials): 236 | METADATA_ADDRESS = "169.254.169.254" 237 | METADATA_PORT = 80 238 | 239 | def __bool__(self) -> bool: 240 | return self.is_started.is_set() 241 | 242 | def __init__(self, *, service: str = "s3"): 243 | self.session = aiohttp.ClientSession( 244 | base_url=URL.build( 245 | scheme="http", 246 | host=self.METADATA_ADDRESS, 247 | port=self.METADATA_PORT, 248 | ), 249 | headers={}, 250 | ) 251 | self.service = service 252 | self.refresh_lock: asyncio.Lock = asyncio.Lock() 253 | self._signer: Optional[AwsRequestSigner] = None 254 | self._tasks: List[asyncio.Task] = [] 255 | self.is_started = asyncio.Event() 256 | 257 | async def _refresher(self) -> None: 258 | while True: 259 | async with self.refresh_lock: 260 | try: 261 | credentials, expires_at = await self._fetch_credentials() 262 | self._signer = AwsRequestSigner(**credentials.as_dict()) 263 | delta = expires_at - datetime.datetime.utcnow() 264 | sleep_time = math.floor(delta.total_seconds() / 2) 265 | self.is_started.set() 266 | except Exception: 267 | log.exception("Failed to update credentials") 268 | sleep_time = 60 269 | await asyncio.sleep(sleep_time) 270 | 271 | async def start(self) -> None: 272 | self._tasks.append(asyncio.create_task(self._refresher())) 273 | await self.is_started.wait() 274 | 275 | async def stop(self, *_: Any) -> None: 276 | if self._tasks: 277 | await asyncio.gather(*self._tasks, return_exceptions=True) 278 | await self.session.close() 279 | 280 | async def _fetch_credentials( 281 | self, 282 | ) -> Tuple[StaticCredentials, datetime.datetime]: 283 | async with self.session.get( 284 | "/latest/dynamic/instance-identity/document", 285 | ) as response: 286 | document: MetadataDocument = await response.json( 287 | content_type=None, encoding="utf-8", 288 | ) 289 | 290 | async with self.session.get( 291 | "/latest/meta-data/iam/security-credentials/", 292 | ) as response: 293 | iam_role = await response.text(encoding="utf-8") 294 | 295 | async with self.session.get( 296 | f"/latest/meta-data/iam/security-credentials/{iam_role}", 297 | ) as response: 298 | credentials: MetadataSecurityCredentials = await response.json( 299 | content_type=None, encoding="utf-8", 300 | ) 301 | 302 | return ( 303 | StaticCredentials( 304 | region=document["region"], 305 | access_key_id=credentials["AccessKeyId"], 306 | secret_access_key=credentials["SecretAccessKey"], 307 | session_token=credentials["Token"], 308 | ), 309 | datetime.datetime.strptime( 310 | credentials["Expiration"], 311 | "%Y-%m-%dT%H:%M:%SZ", 312 | ), 313 | ) 314 | 315 | @property 316 | def signer(self) -> AwsRequestSigner: 317 | if not self._signer: 318 | raise RuntimeError( 319 | f"{self.__class__.__name__} must be started before using", 320 | ) 321 | return self._signer 322 | 323 | 324 | __all__ = ( 325 | "AbstractCredentials", 326 | "ConfigCredentials", 327 | "EnvironmentCredentials", 328 | "MetadataCredentials", 329 | "StaticCredentials", 330 | "URLCredentials", 331 | "collect_credentials", 332 | "merge_credentials", 333 | ) 334 | -------------------------------------------------------------------------------- /aiohttp_s3_client/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiokitchen/aiohttp-s3-client/7f5dd453af8d73cb9ba7045bf4f09521293f39fe/aiohttp_s3_client/py.typed -------------------------------------------------------------------------------- /aiohttp_s3_client/version.py: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS GENERATED AUTOMATICALLY 2 | # BY: poem-plugins "git" plugin 3 | # NEVER EDIT THIS FILE MANUALLY 4 | 5 | version_info = (0, 8, 16) 6 | __version__ = "0.8.16" 7 | -------------------------------------------------------------------------------- /aiohttp_s3_client/xml.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from sys import intern 3 | from typing import List, NamedTuple, Optional, Tuple 4 | from xml.etree import ElementTree as ET 5 | 6 | 7 | NS = "http://s3.amazonaws.com/doc/2006-03-01/" 8 | 9 | 10 | class AwsObjectMeta(NamedTuple): 11 | etag: str 12 | key: str 13 | last_modified: datetime 14 | size: int 15 | storage_class: str 16 | 17 | 18 | def parse_create_multipart_upload_id(payload: bytes) -> str: 19 | root = ET.fromstring(payload) 20 | uploadid_el = root.find(f"{{{NS}}}UploadId") 21 | if uploadid_el is None: 22 | uploadid_el = root.find("UploadId") 23 | if uploadid_el is None or uploadid_el.text is None: 24 | raise ValueError(f"Upload id not found in {payload!r}") 25 | return uploadid_el.text 26 | 27 | 28 | def create_complete_upload_request(parts: List[Tuple[int, str]]) -> bytes: 29 | ET.register_namespace("", NS) 30 | root = ET.Element(f"{{{NS}}}CompleteMultipartUpload") 31 | 32 | for part_no, etag in parts: 33 | part_el = ET.SubElement(root, "Part") 34 | etag_el = ET.SubElement(part_el, "ETag") 35 | etag_el.text = etag 36 | part_number_el = ET.SubElement(part_el, "PartNumber") 37 | part_number_el.text = str(part_no) 38 | 39 | return ( 40 | b'' + 41 | ET.tostring(root, encoding="UTF-8") 42 | ) 43 | 44 | 45 | def parse_list_objects(payload: bytes) -> Tuple[ 46 | List[AwsObjectMeta], List[str], Optional[str], 47 | ]: 48 | root = ET.fromstring(payload) 49 | result = [] 50 | prefixes = [ 51 | el.text 52 | for el in root.findall(f"{{{NS}}}CommonPrefixes/{{{NS}}}Prefix") 53 | if el.text 54 | ] 55 | for el in root.findall(f"{{{NS}}}Contents"): 56 | etag = key = last_modified = size = storage_class = None 57 | for child in el: 58 | tag = child.tag[child.tag.rfind("}") + 1:] 59 | text = child.text 60 | if text is None: 61 | continue 62 | if tag == "ETag": 63 | etag = text 64 | elif tag == "Key": 65 | key = text 66 | elif tag == "LastModified": 67 | assert text[-1] == "Z" 68 | last_modified = datetime.fromisoformat(text[:-1]).replace( 69 | tzinfo=timezone.utc, 70 | ) 71 | elif tag == "Size": 72 | size = int(text) 73 | elif tag == "StorageClass": 74 | storage_class = intern(text) 75 | if ( 76 | etag and 77 | key and 78 | last_modified and 79 | size is not None and 80 | storage_class 81 | ): 82 | meta = AwsObjectMeta(etag, key, last_modified, size, storage_class) 83 | result.append(meta) 84 | nct_el = root.find(f"{{{NS}}}NextContinuationToken") 85 | continuation_token = nct_el.text if nct_el is not None else None 86 | return result, prefixes, continuation_token 87 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | s3: 4 | restart: always 5 | image: adobe/s3mock 6 | ports: 7 | - 9191:9191 8 | - 9090:9090 9 | environment: 10 | initialBuckets: test 11 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "aiohttp" 5 | version = "3.9.5" 6 | description = "Async http client/server framework (asyncio)" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"}, 11 | {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"}, 12 | {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"}, 13 | {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"}, 14 | {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"}, 15 | {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"}, 16 | {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"}, 17 | {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"}, 18 | {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"}, 19 | {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"}, 20 | {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"}, 21 | {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"}, 22 | {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"}, 23 | {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"}, 24 | {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"}, 25 | {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, 26 | {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, 27 | {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, 28 | {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, 29 | {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, 30 | {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, 31 | {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, 32 | {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, 33 | {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, 34 | {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, 35 | {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, 36 | {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, 37 | {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, 38 | {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, 39 | {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, 40 | {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, 41 | {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, 42 | {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, 43 | {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, 44 | {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, 45 | {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, 46 | {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, 47 | {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, 48 | {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, 49 | {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, 50 | {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, 51 | {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, 52 | {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, 53 | {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, 54 | {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, 55 | {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"}, 56 | {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"}, 57 | {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"}, 58 | {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"}, 59 | {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"}, 60 | {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"}, 61 | {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"}, 62 | {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"}, 63 | {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"}, 64 | {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"}, 65 | {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"}, 66 | {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"}, 67 | {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"}, 68 | {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"}, 69 | {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"}, 70 | {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"}, 71 | {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"}, 72 | {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"}, 73 | {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"}, 74 | {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"}, 75 | {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"}, 76 | {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"}, 77 | {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"}, 78 | {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"}, 79 | {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"}, 80 | {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"}, 81 | {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"}, 82 | {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"}, 83 | {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"}, 84 | {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"}, 85 | {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, 86 | ] 87 | 88 | [package.dependencies] 89 | aiosignal = ">=1.1.2" 90 | async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} 91 | attrs = ">=17.3.0" 92 | frozenlist = ">=1.1.1" 93 | multidict = ">=4.5,<7.0" 94 | yarl = ">=1.0,<2.0" 95 | 96 | [package.extras] 97 | speedups = ["Brotli", "aiodns", "brotlicffi"] 98 | 99 | [[package]] 100 | name = "aiomisc" 101 | version = "17.5.6" 102 | description = "aiomisc - miscellaneous utils for asyncio" 103 | optional = false 104 | python-versions = "<4.0,>=3.8" 105 | files = [ 106 | {file = "aiomisc-17.5.6-py3-none-any.whl", hash = "sha256:2375e3d2bbc0ff8b4c2d072fe67a1864f0874af63215d06a1046833d2afdc566"}, 107 | {file = "aiomisc-17.5.6.tar.gz", hash = "sha256:a2ae33f942cde956245d7e0ecff26a299488879e401cec452552f268d1aa68d0"}, 108 | ] 109 | 110 | [package.dependencies] 111 | colorlog = ">=6.0,<7.0" 112 | logging-journald = {version = "*", markers = "sys_platform == \"linux\""} 113 | typing_extensions = {version = "*", markers = "python_version < \"3.10\""} 114 | 115 | [package.extras] 116 | aiohttp = ["aiohttp (>3)"] 117 | asgi = ["aiohttp-asgi (>=0.5.2,<0.6.0)"] 118 | carbon = ["aiocarbon (>=0.15,<0.16)"] 119 | cron = ["croniter (==2.0)"] 120 | grpc = ["grpcio (>=1.56,<2.0)", "grpcio-reflection (>=1.56,<2.0)", "grpcio-tools (>=1.56,<2.0)"] 121 | raven = ["aiohttp (>3)", "raven"] 122 | rich = ["rich"] 123 | uvicorn = ["asgiref (>=3.7,<4.0)", "uvicorn (>=0.27,<0.28)"] 124 | uvloop = ["uvloop (>=0.19,<1)"] 125 | 126 | [[package]] 127 | name = "aiomisc-pytest" 128 | version = "1.1.2" 129 | description = "pytest integration for aiomisc" 130 | optional = false 131 | python-versions = ">=3.7,<4.0" 132 | files = [ 133 | {file = "aiomisc_pytest-1.1.2-py3-none-any.whl", hash = "sha256:3519cb40a6ce245c26e18f3a77e43979d0d3675d59fa27757f17d09a16b4cc04"}, 134 | {file = "aiomisc_pytest-1.1.2.tar.gz", hash = "sha256:6636b470d16b9fa99416564eb7302d049fc69f6eda903e7da97ea9f3ccad0fac"}, 135 | ] 136 | 137 | [package.dependencies] 138 | aiomisc = ">=17" 139 | pytest = ">=7.2.1,<8.0.0" 140 | 141 | [[package]] 142 | name = "aiosignal" 143 | version = "1.3.1" 144 | description = "aiosignal: a list of registered asynchronous callbacks" 145 | optional = false 146 | python-versions = ">=3.7" 147 | files = [ 148 | {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, 149 | {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, 150 | ] 151 | 152 | [package.dependencies] 153 | frozenlist = ">=1.1.0" 154 | 155 | [[package]] 156 | name = "async-timeout" 157 | version = "4.0.3" 158 | description = "Timeout context manager for asyncio programs" 159 | optional = false 160 | python-versions = ">=3.7" 161 | files = [ 162 | {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, 163 | {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, 164 | ] 165 | 166 | [[package]] 167 | name = "attrs" 168 | version = "23.2.0" 169 | description = "Classes Without Boilerplate" 170 | optional = false 171 | python-versions = ">=3.7" 172 | files = [ 173 | {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, 174 | {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, 175 | ] 176 | 177 | [package.extras] 178 | cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] 179 | dev = ["attrs[tests]", "pre-commit"] 180 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] 181 | tests = ["attrs[tests-no-zope]", "zope-interface"] 182 | tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] 183 | tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] 184 | 185 | [[package]] 186 | name = "aws-request-signer" 187 | version = "1.2.0" 188 | description = "A python library to sign AWS requests using AWS Signature V4." 189 | optional = false 190 | python-versions = ">=3.6.1,<4.0.0" 191 | files = [ 192 | {file = "aws_request_signer-1.2.0-py3-none-any.whl", hash = "sha256:eb5bca05d5055a608078be5f47bb6a333f1e5c383ed7603a75289e04203a10a6"}, 193 | {file = "aws_request_signer-1.2.0.tar.gz", hash = "sha256:0d5a2b0ced30cfde0585db9ac7b56c419e41e529c17ec1d0a0ba16e26e827be5"}, 194 | ] 195 | 196 | [package.extras] 197 | demo = ["requests (>=2.21,<3.0)", "requests_toolbelt (>=0.8.0,<0.9.0)"] 198 | requests = ["requests (>=2.21,<3.0)"] 199 | 200 | [[package]] 201 | name = "cachetools" 202 | version = "5.3.3" 203 | description = "Extensible memoizing collections and decorators" 204 | optional = false 205 | python-versions = ">=3.7" 206 | files = [ 207 | {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, 208 | {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, 209 | ] 210 | 211 | [[package]] 212 | name = "certifi" 213 | version = "2024.2.2" 214 | description = "Python package for providing Mozilla's CA Bundle." 215 | optional = false 216 | python-versions = ">=3.6" 217 | files = [ 218 | {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, 219 | {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, 220 | ] 221 | 222 | [[package]] 223 | name = "chardet" 224 | version = "5.2.0" 225 | description = "Universal encoding detector for Python 3" 226 | optional = false 227 | python-versions = ">=3.7" 228 | files = [ 229 | {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, 230 | {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, 231 | ] 232 | 233 | [[package]] 234 | name = "charset-normalizer" 235 | version = "3.3.2" 236 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 237 | optional = false 238 | python-versions = ">=3.7.0" 239 | files = [ 240 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 241 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 242 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 243 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 244 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 245 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 246 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 247 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 248 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 249 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 250 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 251 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 252 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 253 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 254 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 255 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 256 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 257 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 258 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 259 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 260 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 261 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 262 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 263 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 264 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 265 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 266 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 267 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 268 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 269 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 270 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 271 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 272 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 273 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 274 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 275 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 276 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 277 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 278 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 279 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 280 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 281 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 282 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 283 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 284 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 285 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 286 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 287 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 288 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 289 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 290 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 291 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 292 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 293 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 294 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 295 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 296 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 297 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 298 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 299 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 300 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 301 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 302 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 303 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 304 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 305 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 306 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 307 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 308 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 309 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 310 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 311 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 312 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 313 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 314 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 315 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 316 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 317 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 318 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 319 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 320 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 321 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 322 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 323 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 324 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 325 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 326 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 327 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 328 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 329 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 330 | ] 331 | 332 | [[package]] 333 | name = "colorama" 334 | version = "0.4.6" 335 | description = "Cross-platform colored terminal text." 336 | optional = false 337 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 338 | files = [ 339 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 340 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 341 | ] 342 | 343 | [[package]] 344 | name = "colorlog" 345 | version = "6.8.2" 346 | description = "Add colours to the output of Python's logging module." 347 | optional = false 348 | python-versions = ">=3.6" 349 | files = [ 350 | {file = "colorlog-6.8.2-py3-none-any.whl", hash = "sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33"}, 351 | {file = "colorlog-6.8.2.tar.gz", hash = "sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44"}, 352 | ] 353 | 354 | [package.dependencies] 355 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 356 | 357 | [package.extras] 358 | development = ["black", "flake8", "mypy", "pytest", "types-colorama"] 359 | 360 | [[package]] 361 | name = "coverage" 362 | version = "6.5.0" 363 | description = "Code coverage measurement for Python" 364 | optional = false 365 | python-versions = ">=3.7" 366 | files = [ 367 | {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, 368 | {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, 369 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, 370 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, 371 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, 372 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, 373 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, 374 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, 375 | {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, 376 | {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, 377 | {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, 378 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, 379 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, 380 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, 381 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, 382 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, 383 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, 384 | {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, 385 | {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, 386 | {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, 387 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, 388 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, 389 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, 390 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, 391 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, 392 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, 393 | {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, 394 | {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, 395 | {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, 396 | {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, 397 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, 398 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, 399 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, 400 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, 401 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, 402 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, 403 | {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, 404 | {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, 405 | {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, 406 | {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, 407 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, 408 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, 409 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, 410 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, 411 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, 412 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, 413 | {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, 414 | {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, 415 | {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, 416 | {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, 417 | ] 418 | 419 | [package.dependencies] 420 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 421 | 422 | [package.extras] 423 | toml = ["tomli"] 424 | 425 | [[package]] 426 | name = "coveralls" 427 | version = "3.3.1" 428 | description = "Show coverage stats online via coveralls.io" 429 | optional = false 430 | python-versions = ">= 3.5" 431 | files = [ 432 | {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"}, 433 | {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"}, 434 | ] 435 | 436 | [package.dependencies] 437 | coverage = ">=4.1,<6.0.dev0 || >6.1,<6.1.1 || >6.1.1,<7.0" 438 | docopt = ">=0.6.1" 439 | requests = ">=1.0.0" 440 | 441 | [package.extras] 442 | yaml = ["PyYAML (>=3.10)"] 443 | 444 | [[package]] 445 | name = "distlib" 446 | version = "0.3.8" 447 | description = "Distribution utilities" 448 | optional = false 449 | python-versions = "*" 450 | files = [ 451 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 452 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 453 | ] 454 | 455 | [[package]] 456 | name = "docopt" 457 | version = "0.6.2" 458 | description = "Pythonic argument parser, that will make you smile" 459 | optional = false 460 | python-versions = "*" 461 | files = [ 462 | {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, 463 | ] 464 | 465 | [[package]] 466 | name = "exceptiongroup" 467 | version = "1.2.1" 468 | description = "Backport of PEP 654 (exception groups)" 469 | optional = false 470 | python-versions = ">=3.7" 471 | files = [ 472 | {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, 473 | {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, 474 | ] 475 | 476 | [package.extras] 477 | test = ["pytest (>=6)"] 478 | 479 | [[package]] 480 | name = "filelock" 481 | version = "3.14.0" 482 | description = "A platform independent file lock." 483 | optional = false 484 | python-versions = ">=3.8" 485 | files = [ 486 | {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, 487 | {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, 488 | ] 489 | 490 | [package.extras] 491 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 492 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 493 | typing = ["typing-extensions (>=4.8)"] 494 | 495 | [[package]] 496 | name = "freezegun" 497 | version = "1.5.0" 498 | description = "Let your Python tests travel through time" 499 | optional = false 500 | python-versions = ">=3.7" 501 | files = [ 502 | {file = "freezegun-1.5.0-py3-none-any.whl", hash = "sha256:ec3f4ba030e34eb6cf7e1e257308aee2c60c3d038ff35996d7475760c9ff3719"}, 503 | {file = "freezegun-1.5.0.tar.gz", hash = "sha256:200a64359b363aa3653d8aac289584078386c7c3da77339d257e46a01fb5c77c"}, 504 | ] 505 | 506 | [package.dependencies] 507 | python-dateutil = ">=2.7" 508 | 509 | [[package]] 510 | name = "frozenlist" 511 | version = "1.4.1" 512 | description = "A list-like structure which implements collections.abc.MutableSequence" 513 | optional = false 514 | python-versions = ">=3.8" 515 | files = [ 516 | {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, 517 | {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, 518 | {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, 519 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, 520 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, 521 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, 522 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, 523 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, 524 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, 525 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, 526 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, 527 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, 528 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, 529 | {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, 530 | {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, 531 | {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, 532 | {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, 533 | {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, 534 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, 535 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, 536 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, 537 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, 538 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, 539 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, 540 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, 541 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, 542 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, 543 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, 544 | {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, 545 | {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, 546 | {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, 547 | {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, 548 | {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, 549 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, 550 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, 551 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, 552 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, 553 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, 554 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, 555 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, 556 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, 557 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, 558 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, 559 | {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, 560 | {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, 561 | {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, 562 | {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, 563 | {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, 564 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, 565 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, 566 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, 567 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, 568 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, 569 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, 570 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, 571 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, 572 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, 573 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, 574 | {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, 575 | {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, 576 | {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, 577 | {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, 578 | {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, 579 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, 580 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, 581 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, 582 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, 583 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, 584 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, 585 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, 586 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, 587 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, 588 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, 589 | {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, 590 | {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, 591 | {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, 592 | {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, 593 | ] 594 | 595 | [[package]] 596 | name = "idna" 597 | version = "3.7" 598 | description = "Internationalized Domain Names in Applications (IDNA)" 599 | optional = false 600 | python-versions = ">=3.5" 601 | files = [ 602 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 603 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 604 | ] 605 | 606 | [[package]] 607 | name = "iniconfig" 608 | version = "2.0.0" 609 | description = "brain-dead simple config-ini parsing" 610 | optional = false 611 | python-versions = ">=3.7" 612 | files = [ 613 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 614 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 615 | ] 616 | 617 | [[package]] 618 | name = "logging-journald" 619 | version = "0.6.7" 620 | description = "Pure python logging handler for writing logs to the journald using native protocol" 621 | optional = false 622 | python-versions = ">=3.7,<4.0" 623 | files = [ 624 | {file = "logging_journald-0.6.7-py3-none-any.whl", hash = "sha256:ef9333a84fd64fbe1e18ca6f22624af4fc5d92d519a2e2652aa43358548898eb"}, 625 | {file = "logging_journald-0.6.7.tar.gz", hash = "sha256:5fdb576fff2ff82e98be1c7b4f0cbd87f061de5dbed38030f388dd4ba2d52e7d"}, 626 | ] 627 | 628 | [[package]] 629 | name = "mccabe" 630 | version = "0.7.0" 631 | description = "McCabe checker, plugin for flake8" 632 | optional = false 633 | python-versions = ">=3.6" 634 | files = [ 635 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 636 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 637 | ] 638 | 639 | [[package]] 640 | name = "multidict" 641 | version = "6.0.5" 642 | description = "multidict implementation" 643 | optional = false 644 | python-versions = ">=3.7" 645 | files = [ 646 | {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, 647 | {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, 648 | {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, 649 | {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, 650 | {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, 651 | {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, 652 | {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, 653 | {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, 654 | {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, 655 | {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, 656 | {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, 657 | {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, 658 | {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, 659 | {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, 660 | {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, 661 | {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, 662 | {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, 663 | {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, 664 | {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, 665 | {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, 666 | {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, 667 | {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, 668 | {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, 669 | {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, 670 | {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, 671 | {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, 672 | {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, 673 | {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, 674 | {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, 675 | {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, 676 | {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, 677 | {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, 678 | {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, 679 | {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, 680 | {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, 681 | {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, 682 | {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, 683 | {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, 684 | {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, 685 | {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, 686 | {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, 687 | {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, 688 | {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, 689 | {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, 690 | {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, 691 | {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, 692 | {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, 693 | {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, 694 | {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, 695 | {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, 696 | {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, 697 | {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, 698 | {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, 699 | {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, 700 | {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, 701 | {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, 702 | {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, 703 | {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, 704 | {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, 705 | {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, 706 | {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, 707 | {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, 708 | {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, 709 | {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, 710 | {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, 711 | {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, 712 | {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, 713 | {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, 714 | {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, 715 | {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, 716 | {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, 717 | {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, 718 | {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, 719 | {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, 720 | {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, 721 | {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, 722 | {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, 723 | {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, 724 | {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, 725 | {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, 726 | {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, 727 | {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, 728 | {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, 729 | {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, 730 | {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, 731 | {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, 732 | {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, 733 | {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, 734 | {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, 735 | {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, 736 | ] 737 | 738 | [[package]] 739 | name = "mypy" 740 | version = "1.10.0" 741 | description = "Optional static typing for Python" 742 | optional = false 743 | python-versions = ">=3.8" 744 | files = [ 745 | {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, 746 | {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, 747 | {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, 748 | {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, 749 | {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, 750 | {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, 751 | {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, 752 | {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, 753 | {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, 754 | {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, 755 | {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, 756 | {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, 757 | {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, 758 | {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, 759 | {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, 760 | {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, 761 | {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, 762 | {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, 763 | {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, 764 | {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, 765 | {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, 766 | {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, 767 | {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, 768 | {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, 769 | {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, 770 | {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, 771 | {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, 772 | ] 773 | 774 | [package.dependencies] 775 | mypy-extensions = ">=1.0.0" 776 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 777 | typing-extensions = ">=4.1.0" 778 | 779 | [package.extras] 780 | dmypy = ["psutil (>=4.0)"] 781 | install-types = ["pip"] 782 | mypyc = ["setuptools (>=50)"] 783 | reports = ["lxml"] 784 | 785 | [[package]] 786 | name = "mypy-extensions" 787 | version = "1.0.0" 788 | description = "Type system extensions for programs checked with the mypy type checker." 789 | optional = false 790 | python-versions = ">=3.5" 791 | files = [ 792 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 793 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 794 | ] 795 | 796 | [[package]] 797 | name = "packaging" 798 | version = "24.0" 799 | description = "Core utilities for Python packages" 800 | optional = false 801 | python-versions = ">=3.7" 802 | files = [ 803 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 804 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 805 | ] 806 | 807 | [[package]] 808 | name = "platformdirs" 809 | version = "4.2.1" 810 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 811 | optional = false 812 | python-versions = ">=3.8" 813 | files = [ 814 | {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, 815 | {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, 816 | ] 817 | 818 | [package.extras] 819 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 820 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 821 | type = ["mypy (>=1.8)"] 822 | 823 | [[package]] 824 | name = "pluggy" 825 | version = "1.5.0" 826 | description = "plugin and hook calling mechanisms for python" 827 | optional = false 828 | python-versions = ">=3.8" 829 | files = [ 830 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 831 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 832 | ] 833 | 834 | [package.extras] 835 | dev = ["pre-commit", "tox"] 836 | testing = ["pytest", "pytest-benchmark"] 837 | 838 | [[package]] 839 | name = "pycodestyle" 840 | version = "2.11.1" 841 | description = "Python style guide checker" 842 | optional = false 843 | python-versions = ">=3.8" 844 | files = [ 845 | {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, 846 | {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, 847 | ] 848 | 849 | [[package]] 850 | name = "pydocstyle" 851 | version = "6.3.0" 852 | description = "Python docstring style checker" 853 | optional = false 854 | python-versions = ">=3.6" 855 | files = [ 856 | {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, 857 | {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, 858 | ] 859 | 860 | [package.dependencies] 861 | snowballstemmer = ">=2.2.0" 862 | 863 | [package.extras] 864 | toml = ["tomli (>=1.2.3)"] 865 | 866 | [[package]] 867 | name = "pyflakes" 868 | version = "3.2.0" 869 | description = "passive checker of Python programs" 870 | optional = false 871 | python-versions = ">=3.8" 872 | files = [ 873 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 874 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 875 | ] 876 | 877 | [[package]] 878 | name = "pylama" 879 | version = "8.4.1" 880 | description = "Code audit tool for python" 881 | optional = false 882 | python-versions = ">=3.7" 883 | files = [ 884 | {file = "pylama-8.4.1-py3-none-any.whl", hash = "sha256:5bbdbf5b620aba7206d688ed9fc917ecd3d73e15ec1a89647037a09fa3a86e60"}, 885 | {file = "pylama-8.4.1.tar.gz", hash = "sha256:2d4f7aecfb5b7466216d48610c7d6bad1c3990c29cdd392ad08259b161e486f6"}, 886 | ] 887 | 888 | [package.dependencies] 889 | mccabe = ">=0.7.0" 890 | pycodestyle = ">=2.9.1" 891 | pydocstyle = ">=6.1.1" 892 | pyflakes = ">=2.5.0" 893 | toml = {version = ">=0.10.2", optional = true, markers = "extra == \"toml\""} 894 | 895 | [package.extras] 896 | all = ["eradicate", "mypy", "pylint", "radon", "vulture"] 897 | eradicate = ["eradicate"] 898 | mypy = ["mypy"] 899 | pylint = ["pylint"] 900 | radon = ["radon"] 901 | tests = ["eradicate (>=2.0.0)", "mypy", "pylama-quotes", "pylint (>=2.11.1)", "pytest (>=7.1.2)", "pytest-mypy", "radon (>=5.1.0)", "toml", "types-setuptools", "types-toml", "vulture"] 902 | toml = ["toml (>=0.10.2)"] 903 | vulture = ["vulture"] 904 | 905 | [[package]] 906 | name = "pyproject-api" 907 | version = "1.6.1" 908 | description = "API to interact with the python pyproject.toml based projects" 909 | optional = false 910 | python-versions = ">=3.8" 911 | files = [ 912 | {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"}, 913 | {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"}, 914 | ] 915 | 916 | [package.dependencies] 917 | packaging = ">=23.1" 918 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 919 | 920 | [package.extras] 921 | docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"] 922 | testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"] 923 | 924 | [[package]] 925 | name = "pytest" 926 | version = "7.4.4" 927 | description = "pytest: simple powerful testing with Python" 928 | optional = false 929 | python-versions = ">=3.7" 930 | files = [ 931 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 932 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 933 | ] 934 | 935 | [package.dependencies] 936 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 937 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 938 | iniconfig = "*" 939 | packaging = "*" 940 | pluggy = ">=0.12,<2.0" 941 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 942 | 943 | [package.extras] 944 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 945 | 946 | [[package]] 947 | name = "pytest-aiohttp" 948 | version = "1.0.5" 949 | description = "Pytest plugin for aiohttp support" 950 | optional = false 951 | python-versions = ">=3.7" 952 | files = [ 953 | {file = "pytest-aiohttp-1.0.5.tar.gz", hash = "sha256:880262bc5951e934463b15e3af8bb298f11f7d4d3ebac970aab425aff10a780a"}, 954 | {file = "pytest_aiohttp-1.0.5-py3-none-any.whl", hash = "sha256:63a5360fd2f34dda4ab8e6baee4c5f5be4cd186a403cabd498fced82ac9c561e"}, 955 | ] 956 | 957 | [package.dependencies] 958 | aiohttp = ">=3.8.1" 959 | pytest = ">=6.1.0" 960 | pytest-asyncio = ">=0.17.2" 961 | 962 | [package.extras] 963 | testing = ["coverage (==6.2)", "mypy (==0.931)"] 964 | 965 | [[package]] 966 | name = "pytest-asyncio" 967 | version = "0.23.6" 968 | description = "Pytest support for asyncio" 969 | optional = false 970 | python-versions = ">=3.8" 971 | files = [ 972 | {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, 973 | {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, 974 | ] 975 | 976 | [package.dependencies] 977 | pytest = ">=7.0.0,<9" 978 | 979 | [package.extras] 980 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 981 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 982 | 983 | [[package]] 984 | name = "pytest-cov" 985 | version = "4.1.0" 986 | description = "Pytest plugin for measuring coverage." 987 | optional = false 988 | python-versions = ">=3.7" 989 | files = [ 990 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 991 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 992 | ] 993 | 994 | [package.dependencies] 995 | coverage = {version = ">=5.2.1", extras = ["toml"]} 996 | pytest = ">=4.6" 997 | 998 | [package.extras] 999 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 1000 | 1001 | [[package]] 1002 | name = "pytest-timeout" 1003 | version = "2.3.1" 1004 | description = "pytest plugin to abort hanging tests" 1005 | optional = false 1006 | python-versions = ">=3.7" 1007 | files = [ 1008 | {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, 1009 | {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, 1010 | ] 1011 | 1012 | [package.dependencies] 1013 | pytest = ">=7.0.0" 1014 | 1015 | [[package]] 1016 | name = "python-dateutil" 1017 | version = "2.9.0.post0" 1018 | description = "Extensions to the standard Python datetime module" 1019 | optional = false 1020 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 1021 | files = [ 1022 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 1023 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 1024 | ] 1025 | 1026 | [package.dependencies] 1027 | six = ">=1.5" 1028 | 1029 | [[package]] 1030 | name = "requests" 1031 | version = "2.31.0" 1032 | description = "Python HTTP for Humans." 1033 | optional = false 1034 | python-versions = ">=3.7" 1035 | files = [ 1036 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 1037 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 1038 | ] 1039 | 1040 | [package.dependencies] 1041 | certifi = ">=2017.4.17" 1042 | charset-normalizer = ">=2,<4" 1043 | idna = ">=2.5,<4" 1044 | urllib3 = ">=1.21.1,<3" 1045 | 1046 | [package.extras] 1047 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 1048 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 1049 | 1050 | [[package]] 1051 | name = "setuptools" 1052 | version = "69.5.1" 1053 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 1054 | optional = false 1055 | python-versions = ">=3.8" 1056 | files = [ 1057 | {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, 1058 | {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, 1059 | ] 1060 | 1061 | [package.extras] 1062 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 1063 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 1064 | testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 1065 | 1066 | [[package]] 1067 | name = "six" 1068 | version = "1.16.0" 1069 | description = "Python 2 and 3 compatibility utilities" 1070 | optional = false 1071 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 1072 | files = [ 1073 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1074 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "snowballstemmer" 1079 | version = "2.2.0" 1080 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 1081 | optional = false 1082 | python-versions = "*" 1083 | files = [ 1084 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 1085 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "toml" 1090 | version = "0.10.2" 1091 | description = "Python Library for Tom's Obvious, Minimal Language" 1092 | optional = false 1093 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 1094 | files = [ 1095 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1096 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1097 | ] 1098 | 1099 | [[package]] 1100 | name = "tomli" 1101 | version = "2.0.1" 1102 | description = "A lil' TOML parser" 1103 | optional = false 1104 | python-versions = ">=3.7" 1105 | files = [ 1106 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1107 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "tox" 1112 | version = "4.15.0" 1113 | description = "tox is a generic virtualenv management and test command line tool" 1114 | optional = false 1115 | python-versions = ">=3.8" 1116 | files = [ 1117 | {file = "tox-4.15.0-py3-none-any.whl", hash = "sha256:300055f335d855b2ab1b12c5802de7f62a36d4fd53f30bd2835f6a201dda46ea"}, 1118 | {file = "tox-4.15.0.tar.gz", hash = "sha256:7a0beeef166fbe566f54f795b4906c31b428eddafc0102ac00d20998dd1933f6"}, 1119 | ] 1120 | 1121 | [package.dependencies] 1122 | cachetools = ">=5.3.2" 1123 | chardet = ">=5.2" 1124 | colorama = ">=0.4.6" 1125 | filelock = ">=3.13.1" 1126 | packaging = ">=23.2" 1127 | platformdirs = ">=4.1" 1128 | pluggy = ">=1.3" 1129 | pyproject-api = ">=1.6.1" 1130 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 1131 | virtualenv = ">=20.25" 1132 | 1133 | [package.extras] 1134 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.25.2)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.11)"] 1135 | testing = ["build[virtualenv] (>=1.0.3)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=8.0.2)", "distlib (>=0.3.8)", "flaky (>=3.7)", "hatch-vcs (>=0.4)", "hatchling (>=1.21)", "psutil (>=5.9.7)", "pytest (>=7.4.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-xdist (>=3.5)", "re-assert (>=1.1)", "time-machine (>=2.13)", "wheel (>=0.42)"] 1136 | 1137 | [[package]] 1138 | name = "typing-extensions" 1139 | version = "4.11.0" 1140 | description = "Backported and Experimental Type Hints for Python 3.8+" 1141 | optional = false 1142 | python-versions = ">=3.8" 1143 | files = [ 1144 | {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, 1145 | {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, 1146 | ] 1147 | 1148 | [[package]] 1149 | name = "urllib3" 1150 | version = "2.2.1" 1151 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1152 | optional = false 1153 | python-versions = ">=3.8" 1154 | files = [ 1155 | {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, 1156 | {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, 1157 | ] 1158 | 1159 | [package.extras] 1160 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1161 | h2 = ["h2 (>=4,<5)"] 1162 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1163 | zstd = ["zstandard (>=0.18.0)"] 1164 | 1165 | [[package]] 1166 | name = "virtualenv" 1167 | version = "20.26.1" 1168 | description = "Virtual Python Environment builder" 1169 | optional = false 1170 | python-versions = ">=3.7" 1171 | files = [ 1172 | {file = "virtualenv-20.26.1-py3-none-any.whl", hash = "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"}, 1173 | {file = "virtualenv-20.26.1.tar.gz", hash = "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b"}, 1174 | ] 1175 | 1176 | [package.dependencies] 1177 | distlib = ">=0.3.7,<1" 1178 | filelock = ">=3.12.2,<4" 1179 | platformdirs = ">=3.9.1,<5" 1180 | 1181 | [package.extras] 1182 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1183 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 1184 | 1185 | [[package]] 1186 | name = "yarl" 1187 | version = "1.9.4" 1188 | description = "Yet another URL library" 1189 | optional = false 1190 | python-versions = ">=3.7" 1191 | files = [ 1192 | {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, 1193 | {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, 1194 | {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, 1195 | {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, 1196 | {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, 1197 | {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, 1198 | {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, 1199 | {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, 1200 | {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, 1201 | {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, 1202 | {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, 1203 | {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, 1204 | {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, 1205 | {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, 1206 | {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, 1207 | {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, 1208 | {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, 1209 | {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, 1210 | {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, 1211 | {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, 1212 | {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, 1213 | {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, 1214 | {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, 1215 | {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, 1216 | {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, 1217 | {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, 1218 | {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, 1219 | {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, 1220 | {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, 1221 | {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, 1222 | {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, 1223 | {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, 1224 | {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, 1225 | {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, 1226 | {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, 1227 | {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, 1228 | {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, 1229 | {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, 1230 | {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, 1231 | {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, 1232 | {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, 1233 | {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, 1234 | {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, 1235 | {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, 1236 | {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, 1237 | {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, 1238 | {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, 1239 | {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, 1240 | {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, 1241 | {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, 1242 | {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, 1243 | {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, 1244 | {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, 1245 | {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, 1246 | {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, 1247 | {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, 1248 | {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, 1249 | {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, 1250 | {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, 1251 | {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, 1252 | {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, 1253 | {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, 1254 | {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, 1255 | {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, 1256 | {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, 1257 | {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, 1258 | {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, 1259 | {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, 1260 | {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, 1261 | {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, 1262 | {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, 1263 | {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, 1264 | {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, 1265 | {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, 1266 | {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, 1267 | {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, 1268 | {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, 1269 | {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, 1270 | {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, 1271 | {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, 1272 | {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, 1273 | {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, 1274 | {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, 1275 | {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, 1276 | {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, 1277 | {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, 1278 | {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, 1279 | {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, 1280 | {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, 1281 | {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, 1282 | ] 1283 | 1284 | [package.dependencies] 1285 | idna = ">=2.0" 1286 | multidict = ">=4.0" 1287 | 1288 | [metadata] 1289 | lock-version = "2.0" 1290 | python-versions = "^3.8" 1291 | content-hash = "f0c66fe386c6f35f2227aaf9fed15404625821f8628a6c3d7b2506de38b53a36" 1292 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | cache-dir = ".cache" 2 | 3 | [virtualenvs] 4 | path = ".venv" 5 | in-project = true 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "aiohttp-s3-client" 3 | # Dummy version which will be rewritten with poem-plugins 4 | version = "0.8.16" 5 | description = "The simple module for putting and getting object from Amazon S3 compatible endpoints" 6 | authors = [ 7 | "Dmitry Orlov ", 8 | "Yuri Shikanov ", 9 | "Alexander Vasin " 10 | ] 11 | license = "Apache Software License" 12 | readme = "README.md" 13 | homepage = "https://github.com/aiokitchen/aiohttp-s3-client" 14 | packages = [{ include = "aiohttp_s3_client" }] 15 | classifiers = [ 16 | "Development Status :: 4 - Beta", 17 | "License :: OSI Approved :: Apache Software License", 18 | "Topic :: Internet", 19 | "Topic :: Software Development", 20 | "Topic :: Software Development :: Libraries", 21 | "Intended Audience :: Developers", 22 | "Natural Language :: English", 23 | "Operating System :: MacOS", 24 | "Operating System :: POSIX", 25 | "Operating System :: Microsoft", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3.8", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3.12", 33 | "Programming Language :: Python :: Implementation :: PyPy", 34 | "Programming Language :: Python :: Implementation :: CPython", 35 | ] 36 | 37 | [tool.poetry.urls] 38 | "Source" = "https://github.com/aiokitchen/aiohttp-s3-client" 39 | "Tracker" = "https://github.com/aiokitchen/aiohttp-s3-client/issues" 40 | 41 | [tool.poetry.dependencies] 42 | aiohttp = "^3.8" 43 | aiomisc = "^17.3.4" 44 | aws-request-signer = "1.2.0" 45 | typing_extensions = [{ version = '*', python = "< 3.10" }] 46 | python = "^3.8" 47 | 48 | 49 | [tool.poetry.group.dev.dependencies] 50 | aiomisc-pytest = "^1.1" 51 | coverage = "!=4.3" 52 | coveralls = "^3.3.1" 53 | mypy = "*" 54 | pylama = { extras = ["toml"], version = "^8.4.1" } 55 | pytest-cov = "^4.0.0" 56 | pytest-timeout = "^2.1.0" 57 | tox = "^4.4.6" 58 | freezegun = "^1.5.0" 59 | pytest = "^7.4.4" 60 | setuptools = "^69.5.1" 61 | pytest-aiohttp = "^1.0.5" 62 | yarl = "^1.9.4" 63 | 64 | [build-system] 65 | requires = ["poetry-core"] 66 | build-backend = "poetry.core.masonry.api" 67 | 68 | [tool.poem-plugins.version] 69 | provider = "git" 70 | update_pyproject = true 71 | write_version_file = true 72 | 73 | [tool.mypy] 74 | check_untyped_defs = true 75 | follow_imports = "silent" 76 | ignore_missing_imports = true 77 | no_implicit_reexport = true 78 | warn_redundant_casts = true 79 | warn_unused_ignores = true 80 | warn_unused_configs = true 81 | files = ["aiohttp_s3_client", "tests"] 82 | 83 | [tool.pylama] 84 | max_line_length = 80 85 | skip = ".venv*,.*cache,dist" 86 | 87 | [tool.pylama.linter.mccabe] 88 | max-complexity = 15 89 | 90 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional 3 | 4 | import pytest 5 | from aiohttp import ClientSession 6 | from yarl import URL 7 | 8 | from aiohttp_s3_client import S3Client 9 | 10 | 11 | @pytest.fixture 12 | async def s3_client(event_loop, s3_url: URL): 13 | async with ClientSession( 14 | raise_for_status=False, auto_decompress=False, 15 | ) as session: 16 | yield S3Client( 17 | url=s3_url, 18 | region="us-east-1", 19 | session=session, 20 | ) 21 | 22 | 23 | @pytest.fixture 24 | async def s3_url() -> URL: 25 | return URL(os.getenv("S3_URL", "http://user:hackme@localhost:9090/test")) 26 | 27 | 28 | @pytest.fixture 29 | def object_name() -> str: 30 | return "/test/test" 31 | 32 | 33 | @pytest.fixture 34 | def s3_read(s3_client: S3Client, object_name): 35 | 36 | async def do_read(custom_object_name: Optional[str] = None): 37 | s3_key = custom_object_name or object_name 38 | return await (await s3_client.get(s3_key)).read() 39 | 40 | return do_read 41 | -------------------------------------------------------------------------------- /tests/test_credentials.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | from unittest import mock 4 | 5 | import pytest 6 | from aiohttp import web 7 | from pytest import FixtureRequest 8 | from pytest_aiohttp.plugin import TestServer 9 | 10 | from aiohttp_s3_client.credentials import ( 11 | AbstractCredentials, ConfigCredentials, EnvironmentCredentials, 12 | MetadataCredentials, StaticCredentials, URLCredentials, merge_credentials, 13 | ) 14 | 15 | 16 | def test_static_credentials(): 17 | assert not StaticCredentials(access_key_id="", secret_access_key="") 18 | assert StaticCredentials(access_key_id="foo", secret_access_key="bar") 19 | 20 | 21 | def test_static_credentials_repr(): 22 | assert "hack-me" not in repr( 23 | StaticCredentials(access_key_id="foo", secret_access_key="hack-me"), 24 | ) 25 | 26 | 27 | def test_url_credentials(): 28 | credentials = URLCredentials("http://key:secret@host") 29 | assert isinstance(credentials, AbstractCredentials) 30 | assert credentials.signer.access_key_id == "key" 31 | assert credentials.signer.secret_access_key == "secret" 32 | 33 | 34 | @mock.patch.dict(os.environ, {}) 35 | def test_env_credentials_false(): 36 | assert not EnvironmentCredentials() 37 | 38 | 39 | @mock.patch.dict( 40 | os.environ, { 41 | "AWS_ACCESS_KEY_ID": "key", 42 | "AWS_SECRET_ACCESS_KEY": "hack-me", 43 | "AWS_DEFAULT_REGION": "cc-mid-2", 44 | }, 45 | ) 46 | def test_env_credentials_mock(): 47 | cred = EnvironmentCredentials() 48 | assert isinstance(cred, AbstractCredentials) 49 | assert cred.access_key_id == "key" 50 | assert cred.secret_access_key == "hack-me" 51 | assert cred.region == "cc-mid-2" 52 | 53 | 54 | def test_config_credentials(tmp_path): 55 | with open(tmp_path / "credentials", "w") as fp: 56 | fp.write( 57 | "\n".join([ 58 | "[test-profile]", 59 | " aws_access_key_id = test-key", 60 | " aws_secret_access_key = test-secret", 61 | "[default]", 62 | "aws_access_key_id = default-key", 63 | "aws_secret_access_key = default-secret", 64 | ]), 65 | ) 66 | 67 | with open(tmp_path / "config", "w") as fp: 68 | fp.write( 69 | "\n".join([ 70 | "[test-profile]", 71 | " region = ru-central1", 72 | "[default]", 73 | "region = us-east-1", 74 | ]), 75 | ) 76 | 77 | cred = ConfigCredentials( 78 | credentials_path=tmp_path / "credentials", 79 | config_path=tmp_path / "config", 80 | ) 81 | assert isinstance(cred, AbstractCredentials) 82 | assert cred 83 | assert cred.access_key_id == "default-key" 84 | assert cred.secret_access_key == "default-secret" 85 | 86 | cred = ConfigCredentials( 87 | credentials_path=tmp_path / "credentials", 88 | config_path=tmp_path / "config", 89 | profile="test-profile", 90 | ) 91 | assert isinstance(cred, AbstractCredentials) 92 | assert cred 93 | assert cred.access_key_id == "test-key" 94 | assert cred.secret_access_key == "test-secret" 95 | 96 | 97 | @pytest.fixture 98 | def metadata_server_app() -> web.Application: 99 | app = web.Application() 100 | 101 | async def get_iam_role(request: web.Request) -> web.Response: 102 | """ 103 | $ curl -v \ 104 | 169.254.169.254/latest/meta-data/iam/security-credentials/myiamrole 105 | * Trying 169.254.169.254:80... 106 | * Connected to 169.254.169.254 (169.254.169.254) port 80 (#0) 107 | > GET /latest/meta-data/iam/security-credentials/myiamrole HTTP/1.1 108 | > Host: 169.254.169.254 109 | > User-Agent: curl/7.87.0 110 | > Accept: */* 111 | > 112 | * Mark bundle as not supporting multiuse 113 | < HTTP/1.1 200 OK 114 | < Content-Type: text/plain 115 | < Accept-Ranges: none 116 | < Last-Modified: Fri, 30 Jun 2023 19:06:40 GMT 117 | < Content-Length: 1582 118 | < Date: Fri, 30 Jun 2023 20:00:47 GMT 119 | < Server: EC2ws 120 | < Connection: close 121 | < 122 | { 123 | "Code" : "Success", 124 | "LastUpdated" : "2023-06-30T19:06:42Z", 125 | "Type" : "AWS-HMAC", 126 | "AccessKeyId" : "ANOTATOKEN5345W4RX", 127 | "SecretAccessKey" : "VGJvJ5H34NOTATOKENAJikpQN/Riq", 128 | "Token" : "INOTATOKENQoJb3JpZ2luX2V...SzrAFy", 129 | "Expiration" : "2023-07-01T01:25:35Z" 130 | } 131 | 132 | """ 133 | last_updated = datetime.datetime.utcnow() 134 | 135 | if request.match_info["role"] != "pytest": 136 | raise web.HTTPNotFound() 137 | 138 | return web.json_response( 139 | { 140 | "Code": "Success", 141 | "LastUpdated": last_updated.strftime("%Y-%m-%dT%H:%M:%SZ"), 142 | "Type": "AWS-HMAC", 143 | "AccessKeyId": "PYTESTACCESSKEYID", 144 | "SecretAccessKey": "PYTESTACCESSKEYSECRET", 145 | "Token": "PYTESTACCESSTOKEN", 146 | "Expiration": ( 147 | last_updated + datetime.timedelta(hours=2) 148 | ).strftime( 149 | "%Y-%m-%dT%H:%M:%SZ", 150 | ), 151 | }, 152 | content_type="text/plain", 153 | ) 154 | 155 | app.router.add_get( 156 | "/latest/meta-data/iam/security-credentials/{role}", 157 | get_iam_role, 158 | ) 159 | 160 | async def get_security_credentials(request: web.Request) -> web.Response: 161 | """ 162 | GET /latest/meta-data/iam/security-credentials/ HTTP/1.1 163 | > Host: 169.254.169.254 164 | > User-Agent: curl/7.87.0 165 | > Accept: */* 166 | > 167 | * Mark bundle as not supporting multiuse 168 | < HTTP/1.1 200 OK 169 | < Content-Type: text/plain 170 | < Accept-Ranges: none 171 | < Last-Modified: Fri, 30 Jun 2023 19:06:40 GMT 172 | < Content-Length: 14 173 | < Date: Fri, 30 Jun 2023 20:02:18 GMT 174 | < Server: EC2ws 175 | < Connection: close 176 | < 177 | pytest 178 | """ 179 | return web.Response(body="pytest") 180 | 181 | app.router.add_get( 182 | "/latest/meta-data/iam/security-credentials/", 183 | get_security_credentials, 184 | ) 185 | 186 | async def get_instance_identity(request: web.Request) -> web.Response: 187 | """ 188 | $ curl-v \ 189 | http://169.254.169.254/latest/dynamic/instance-identity/document 190 | * Trying 169.254.169.254:80... 191 | * Connected to 169.254.169.254 (169.254.169.254) port 80 (#0) 192 | > GET /latest/dynamic/instance-identity/document HTTP/1.1 193 | > Host: 169.254.169.254 194 | > User-Agent: curl/7.87.0 195 | > Accept: */* 196 | > 197 | * Mark bundle as not supporting multiuse 198 | < HTTP/1.1 200 OK 199 | < Content-Type: text/plain 200 | < Accept-Ranges: none 201 | < Last-Modified: Fri, 30 Jun 2023 19:06:40 GMT 202 | < Content-Length: 478 203 | < Date: Fri, 30 Jun 2023 20:03:14 GMT 204 | < Server: EC2ws 205 | < Connection: close 206 | < 207 | { 208 | "accountId" : "123123", 209 | "architecture" : "x86_64", 210 | "availabilityZone" : "us-east-1a", 211 | "billingProducts" : null, 212 | "devpayProductCodes" : null, 213 | "marketplaceProductCodes" : null, 214 | "imageId" : "ami-123123", 215 | "instanceId" : "i-11232323", 216 | "instanceType" : "t3a.micro", 217 | "kernelId" : null, 218 | "pendingTime" : "2023-06-13T18:18:58Z", 219 | "privateIp" : "172.33.33.33", 220 | "ramdiskId" : null, 221 | "region" : "us-east-1", 222 | "version" : "2017-09-30" 223 | } 224 | """ 225 | 226 | return web.json_response( 227 | { 228 | "accountId": "123123", 229 | "architecture": "x86_64", 230 | "availabilityZone": "us-east-1a", 231 | "billingProducts": None, 232 | "devpayProductCodes": None, 233 | "marketplaceProductCodes": None, 234 | "imageId": "ami-123123", 235 | "instanceId": "i-11232323", 236 | "instanceType": "t3a.micro", 237 | "kernelId": None, 238 | "pendingTime": "2023-06-13T18:18:58Z", 239 | "privateIp": "172.33.33.33", 240 | "ramdiskId": None, 241 | "region": "us-east-99", 242 | "version": "2017-09-30", 243 | }, 244 | content_type="text/plain", 245 | ) 246 | 247 | app.router.add_get( 248 | "/latest/dynamic/instance-identity/document", 249 | get_instance_identity, 250 | ) 251 | 252 | return app 253 | 254 | 255 | async def test_metadata_credentials( 256 | request: FixtureRequest, 257 | metadata_server_app, 258 | ): 259 | server = TestServer(metadata_server_app) 260 | await server.start_server() 261 | 262 | class TestMetadataCredentials(MetadataCredentials): 263 | METADATA_ADDRESS = server.host 264 | METADATA_PORT = server.port 265 | 266 | credentials = TestMetadataCredentials() 267 | assert isinstance(credentials, AbstractCredentials) 268 | 269 | assert not credentials 270 | 271 | with pytest.raises(RuntimeError): 272 | print(credentials.signer) 273 | 274 | await credentials.start() 275 | request.addfinalizer(credentials.stop) 276 | 277 | assert credentials 278 | 279 | assert credentials.signer 280 | assert credentials.signer.region == "us-east-99" 281 | assert credentials.signer.access_key_id == "PYTESTACCESSKEYID" 282 | assert credentials.signer.secret_access_key == "PYTESTACCESSKEYSECRET" 283 | assert credentials.signer.session_token == "PYTESTACCESSTOKEN" 284 | 285 | 286 | async def test_merge_credentials(): 287 | credentials = [ 288 | StaticCredentials(access_key_id="access_key"), 289 | StaticCredentials(secret_access_key="secret"), 290 | StaticCredentials(session_token="token") 291 | ] 292 | 293 | assert not all(credentials) 294 | 295 | result = merge_credentials(*credentials) 296 | assert result 297 | 298 | assert result.access_key_id == credentials[0].access_key_id 299 | assert result.secret_access_key == credentials[1].secret_access_key 300 | assert result.session_token == credentials[2].session_token 301 | 302 | result = merge_credentials( 303 | StaticCredentials( 304 | access_key_id="overriden", 305 | secret_access_key="overriden", 306 | session_token="overriden" 307 | ), 308 | *credentials 309 | ) 310 | 311 | assert result.access_key_id == "overriden" 312 | assert result.secret_access_key == "overriden" 313 | assert result.session_token == "overriden" 314 | -------------------------------------------------------------------------------- /tests/test_get_file_parallel.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import secrets 4 | 5 | import pytest 6 | 7 | from aiohttp_s3_client import S3Client 8 | from aiohttp_s3_client.client import AwsDownloadError, AwsError 9 | 10 | 11 | async def test_get_file_parallel(s3_client: S3Client, tmpdir): 12 | data = b"Hello world! " * 100 13 | object_name = "foo/bar.txt" 14 | await s3_client.put(object_name, data) 15 | await s3_client.get_file_parallel( 16 | object_name, 17 | tmpdir / "bar.txt", 18 | workers_count=4, 19 | ) 20 | assert (tmpdir / "bar.txt").read_binary() == data 21 | 22 | 23 | async def test_get_file_parallel_without_pwrite( 24 | s3_client: S3Client, 25 | tmpdir, 26 | monkeypatch, 27 | ): 28 | monkeypatch.delattr("os.pwrite") 29 | data = b"Hello world! " * 100 30 | object_name = "foo/bar.txt" 31 | await s3_client.put(object_name, data) 32 | await s3_client.get_file_parallel( 33 | object_name, 34 | tmpdir / "bar.txt", 35 | workers_count=4, 36 | ) 37 | assert (tmpdir / "bar.txt").read_binary() == data 38 | 39 | 40 | async def test_get_file_that_changed_in_process_error( 41 | s3_client: S3Client, 42 | tmpdir, 43 | ): 44 | object_name = "test/test" 45 | 46 | def iterable(): 47 | for _ in range(8): # type: int 48 | yield secrets.token_hex(1024 * 1024 * 5).encode() 49 | 50 | await s3_client.put_multipart( 51 | object_name, 52 | iterable(), 53 | workers_count=4, 54 | ) 55 | 56 | async def upload(): 57 | await asyncio.sleep(0.05) 58 | await s3_client.put_multipart( 59 | object_name, 60 | iterable(), 61 | workers_count=4, 62 | ) 63 | 64 | with pytest.raises(AwsError) as err: 65 | await asyncio.gather( 66 | s3_client.get_file_parallel( 67 | object_name, 68 | tmpdir / "temp.dat", 69 | workers_count=4, 70 | range_step=128, 71 | ), 72 | upload(), 73 | ) 74 | 75 | assert err.type is AwsDownloadError 76 | assert err.value.message.startswith( 77 | "Got wrong status code 412 on range download of test/test", 78 | ) 79 | assert not os.path.exists(tmpdir / "temp.dat") 80 | -------------------------------------------------------------------------------- /tests/test_multipart_upload.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from aiohttp_s3_client import S3Client 4 | 5 | 6 | async def test_multipart_file_upload(s3_client: S3Client, s3_read, tmp_path): 7 | data = b"hello, world" * 1024 * 128 8 | 9 | with (tmp_path / "hello.txt").open("wb") as f: 10 | f.write(data) 11 | f.flush() 12 | 13 | await s3_client.put_file_multipart( 14 | "/test/test_multipart", 15 | f.name, 16 | part_size=5 * (1024 * 1024), 17 | ) 18 | 19 | assert data == (await s3_read("/test/test_multipart")) 20 | 21 | 22 | @pytest.mark.parametrize("calculate_content_sha256", [True, False]) 23 | @pytest.mark.parametrize("workers_count", [1, 2]) 24 | async def test_multipart_stream_upload( 25 | calculate_content_sha256, workers_count, 26 | s3_client: S3Client, s3_read, tmp_path, 27 | ): 28 | 29 | def iterable(): 30 | for _ in range(8): # type: int 31 | yield b"hello world" * 1024 * 1024 32 | 33 | await s3_client.put_multipart( 34 | "/test/test", 35 | iterable(), 36 | calculate_content_sha256=calculate_content_sha256, 37 | workers_count=workers_count, 38 | ) 39 | 40 | assert (await s3_read("/test/test")) == b"hello world" * 1024 * 1024 * 8 41 | -------------------------------------------------------------------------------- /tests/test_parallel_operation_failure.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | import pytest 4 | from aiohttp import hdrs, web 5 | from aiohttp.test_utils import TestServer 6 | from yarl import URL 7 | 8 | from aiohttp_s3_client.client import AwsDownloadError, AwsUploadError 9 | 10 | 11 | @pytest.mark.timeout(1) 12 | async def test_multipart_upload_failure(s3_client, s3_mock_calls): 13 | 14 | def iterable(): 15 | for _ in range(8): # type: int 16 | yield b"hello world" * 1024 17 | 18 | with pytest.raises(AwsUploadError): 19 | await s3_client.put_multipart( 20 | "/test/test", 21 | iterable(), 22 | workers_count=4, 23 | part_upload_tries=3, 24 | ) 25 | 26 | 27 | # @pytest.mark.timeout(1) 28 | async def test_parallel_download_failure(s3_client, s3_mock_calls, tmpdir): 29 | with pytest.raises(AwsDownloadError): 30 | await s3_client.get_file_parallel( 31 | "foo/bar.txt", 32 | tmpdir / "bar.txt", 33 | workers_count=4, 34 | ) 35 | 36 | 37 | @pytest.fixture 38 | def s3_url(s3_mock_url): 39 | return s3_mock_url 40 | 41 | 42 | CREATE_MP_UPLOAD_RESPONSE = """\ 43 | 44 | 45 | EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI- 46 | 47 | """ 48 | 49 | 50 | @pytest.fixture 51 | def s3_mock_port(aiomisc_unused_port_factory) -> int: 52 | return aiomisc_unused_port_factory() 53 | 54 | 55 | @pytest.fixture 56 | def s3_mock_url(s3_mock_port, localhost): 57 | return URL.build( 58 | scheme="http", 59 | host=localhost, 60 | port=s3_mock_port, 61 | user="user", 62 | password="hackme", 63 | ) 64 | 65 | 66 | @pytest.fixture 67 | async def s3_mock_server(s3_mock_port, localhost): 68 | app = web.Application() 69 | app["calls"] = [] 70 | app.router.add_put("/{key:[^{}]+}", s3_mock_put_object_handler) 71 | app.router.add_post("/{key:[^{}]+}", s3_mock_post_object_handler) 72 | app.router.add_head("/{key:[^{}]+}", s3_mock_head_object_handler) 73 | app.router.add_get("/{key:[^{}]+}", s3_mock_get_object_handler) 74 | server = TestServer(app, host=localhost, port=s3_mock_port) 75 | await server.start_server() 76 | try: 77 | yield server 78 | finally: 79 | await server.close() 80 | 81 | 82 | @pytest.fixture 83 | def s3_mock_calls(s3_mock_server): 84 | return s3_mock_server.app["calls"] 85 | 86 | 87 | async def s3_mock_put_object_handler(request): 88 | request.payload = await request.read() 89 | request.app["calls"].append(request) 90 | return web.Response( 91 | body=b"", 92 | status=HTTPStatus.INTERNAL_SERVER_ERROR, 93 | ) 94 | 95 | 96 | async def s3_mock_post_object_handler(request): 97 | request.payload = await request.read() 98 | request.app["calls"].append(request) 99 | 100 | if "uploads" in request.query: 101 | return web.Response(body=CREATE_MP_UPLOAD_RESPONSE) 102 | 103 | return web.Response(body=b"") 104 | 105 | 106 | async def s3_mock_head_object_handler(request): 107 | return web.Response( 108 | headers={ 109 | hdrs.CONTENT_LENGTH: str((1024 ** 2) * 16), 110 | hdrs.ETAG: "7e10e7d25dc4581d89b9285be5f384fd", 111 | }, 112 | ) 113 | 114 | 115 | async def s3_mock_get_object_handler(request): 116 | return web.Response( 117 | status=HTTPStatus.INTERNAL_SERVER_ERROR, 118 | headers={ 119 | hdrs.CONTENT_LENGTH: str((1024 ** 2) * 5), 120 | hdrs.ETAG: "7e10e7d25dc4581d89b9285be5f384fd", 121 | }, 122 | ) 123 | -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import sys 3 | from http import HTTPStatus 4 | from io import BytesIO 5 | from pathlib import Path 6 | 7 | import pytest 8 | from freezegun import freeze_time 9 | from yarl import URL 10 | 11 | from aiohttp_s3_client import S3Client 12 | 13 | 14 | @pytest.mark.parametrize("object_name", ("test/test", "/test/test")) 15 | async def test_put_str(s3_client: S3Client, object_name): 16 | data = "hello, world" 17 | resp = await s3_client.put(object_name, data) 18 | assert resp.status == HTTPStatus.OK 19 | 20 | resp = await s3_client.get(object_name) 21 | result = await resp.text() 22 | assert result == data 23 | 24 | 25 | @pytest.mark.parametrize("object_name", ("test/test", "/test/test")) 26 | async def test_put_bytes(s3_client: S3Client, s3_read, object_name): 27 | data = b"hello, world" 28 | resp = await s3_client.put(object_name, data) 29 | assert resp.status == HTTPStatus.OK 30 | assert (await s3_read()) == data 31 | 32 | 33 | @pytest.mark.parametrize("object_name", ("test/test", "/test/test")) 34 | async def test_put_async_iterable(s3_client: S3Client, s3_read, object_name): 35 | async def async_iterable(iterable: bytes): 36 | for i in iterable: 37 | yield i.to_bytes(1, sys.byteorder) 38 | 39 | data = b"hello, world" 40 | resp = await s3_client.put(object_name, async_iterable(data)) 41 | assert resp.status == HTTPStatus.OK 42 | 43 | assert (await s3_read()) == data 44 | 45 | 46 | async def test_put_file(s3_client: S3Client, s3_read, tmp_path): 47 | data = b"hello, world" 48 | 49 | with (tmp_path / "hello.txt").open("wb") as f: 50 | f.write(data) 51 | f.flush() 52 | 53 | # Test upload by file str path 54 | resp = await s3_client.put_file("/test/test", f.name) 55 | assert resp.status == HTTPStatus.OK 56 | 57 | assert (await s3_read("/test/test")) == data 58 | 59 | # Test upload by file Path 60 | resp = await s3_client.put_file("/test/test2", Path(f.name)) 61 | assert resp.status == HTTPStatus.OK 62 | 63 | assert (await s3_read("/test/test2")) == data 64 | 65 | 66 | async def test_list_objects_v2(s3_client: S3Client, s3_read, tmp_path): 67 | data = b"hello, world" 68 | 69 | with (tmp_path / "hello.txt").open("wb") as f: 70 | f.write(data) 71 | f.flush() 72 | 73 | resp = await s3_client.put_file("/test/list/test1", f.name) 74 | assert resp.status == HTTPStatus.OK 75 | 76 | resp = await s3_client.put_file("/test/list/test2", f.name) 77 | assert resp.status == HTTPStatus.OK 78 | 79 | # Test list file 80 | batch = 0 81 | async for result, prefixes in s3_client.list_objects_v2( 82 | prefix="test/list/", 83 | delimiter="/", 84 | max_keys=1, 85 | ): 86 | batch += 1 87 | assert result[0].key == f"test/list/test{batch}" 88 | assert result[0].size == len(data) 89 | 90 | 91 | async def test_list_objects_v2_prefix(s3_client: S3Client, s3_read, tmp_path): 92 | data = b"hello, world" 93 | 94 | with (tmp_path / "hello.txt").open("wb") as f: 95 | f.write(data) 96 | f.flush() 97 | 98 | resp = await s3_client.put_file("/test2/list1/test1", f.name) 99 | assert resp.status == HTTPStatus.OK 100 | 101 | resp = await s3_client.put_file("/test2/list2/test2", f.name) 102 | assert resp.status == HTTPStatus.OK 103 | 104 | # Test list file 105 | batch = 0 106 | 107 | async for result, prefixes in s3_client.list_objects_v2( 108 | prefix="test2/", 109 | delimiter="/", 110 | ): 111 | batch += 1 112 | assert len(result) == 0 113 | assert prefixes[0] == "test2/list1/" 114 | assert prefixes[1] == "test2/list2/" 115 | 116 | 117 | async def test_url_path_with_colon(s3_client: S3Client, s3_read): 118 | data = b"hello, world" 119 | key = "/some-path:with-colon.txt" 120 | resp = await s3_client.put(key, data) 121 | assert resp.status == HTTPStatus.OK 122 | 123 | assert (await s3_read(key)) == data 124 | 125 | 126 | @pytest.mark.parametrize("object_name", ("test/test", "/test/test")) 127 | async def test_put_compression(s3_client: S3Client, s3_read, object_name): 128 | async def async_iterable(iterable: bytes): 129 | for i in iterable: 130 | yield i.to_bytes(1, sys.byteorder) 131 | 132 | data = b"hello, world" 133 | resp = await s3_client.put( 134 | object_name, async_iterable(data), compress="gzip", 135 | ) 136 | assert resp.status == HTTPStatus.OK 137 | 138 | result = await s3_read() 139 | # assert resp.headers[hdrs.CONTENT_ENCODING] == "gzip" 140 | # FIXME: uncomment after update fakes3 image 141 | actual = gzip.GzipFile(fileobj=BytesIO(result)).read() 142 | assert actual == data 143 | 144 | 145 | @pytest.mark.parametrize('method,given_url', [ 146 | # Simple test 147 | ('GET', 'test/object'), 148 | 149 | # Check method name is ok in lowercase 150 | ('get', 'test/object'), 151 | 152 | # Absolute path 153 | ('GET', '/test/object'), 154 | 155 | # Check URL object with path is given 156 | ('get', URL('./test/object')), 157 | 158 | # Check URL object with path is given 159 | ('get', URL('/test/object')), 160 | ]) 161 | def test_presign_non_absolute_url(s3_client, s3_url, method, given_url): 162 | presigned = s3_client.presign_url('get', 'test/object') 163 | assert presigned.is_absolute() 164 | assert presigned.scheme == s3_url.scheme 165 | assert presigned.host == s3_url.host 166 | assert presigned.port == s3_url.port 167 | assert presigned.path == (s3_url / str(given_url).lstrip('/')).path 168 | 169 | 170 | @pytest.mark.parametrize('method,given_url', [ 171 | # String url 172 | ('GET', 'https://absolute-url:123/path'), 173 | 174 | # URL object 175 | ('GET', URL('https://absolute-url:123/path')), 176 | ]) 177 | def test_presign_absolute_url(s3_client, method, given_url): 178 | presigned = s3_client.presign_url(method, given_url) 179 | assert presigned.is_absolute() 180 | 181 | url_object = URL(given_url) 182 | assert presigned.scheme == url_object.scheme 183 | assert presigned.host == url_object.host 184 | assert presigned.port == url_object.port 185 | assert presigned.path == url_object.path 186 | 187 | 188 | @freeze_time("2024-01-01") 189 | def test_presign_url(s3_client, s3_url): 190 | url = s3_client.presign_url('get', URL('./example')) 191 | assert url.path == (s3_url / 'example').path 192 | assert url.query == { 193 | 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256', 194 | 'X-Amz-Content-Sha256': 'UNSIGNED-PAYLOAD', 195 | 'X-Amz-Credential': 'user/20240101/us-east-1/s3/aws4_request', 196 | 'X-Amz-Date': '20240101T000000Z', 197 | 'X-Amz-Expires': '86400', 198 | 'X-Amz-SignedHeaders': 'host', 199 | 'X-Amz-Signature': ( 200 | '7359f1286edf554b0eab363e3c93ee32855b8d429d975fdb0a5d2cb7ad5c0db0' 201 | ) 202 | } 203 | --------------------------------------------------------------------------------