├── .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 | [](https://pypi.org/project/aiohttp-s3-client) [](https://pypi.org/project/aiohttp-s3-client) []() [](https://pypi.org/project/aiohttp-s3-client) [](https://pypi.org/project/aiohttp-s3-client) [](https://coveralls.io/github/mosquito/aiohttp-s3-client?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 |
--------------------------------------------------------------------------------