├── .github └── workflows │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── basic_auth ├── handler.py ├── put_user.py └── test_handler.py ├── poetry.lock ├── pyproject.toml ├── s3pypi ├── __init__.py ├── __main__.py ├── core.py ├── exceptions.py ├── index.py ├── locking.py └── storage.py ├── scripts └── migrate-s3-index.py ├── setup.cfg ├── terraform ├── main.tf └── modules │ └── s3pypi │ ├── basic_auth │ ├── handler.py │ └── main.tf │ └── main.tf ├── tests ├── __init__.py ├── conftest.py ├── data │ ├── dists │ │ ├── foo-0.1.0.tar.gz │ │ ├── hello_world-0.1.0-py3-none-any.whl │ │ ├── hello_world-0.1.0.tar.gz │ │ └── xyz-0.1.0.zip │ └── index │ │ ├── hello_world.html │ │ └── s3pypi.html ├── integration │ ├── conftest.py │ ├── test_locking.py │ ├── test_main.py │ └── test_storage.py └── unit │ ├── test_core.py │ └── test_index.py └── tox.ini /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install poetry tox tox-gh-actions 22 | poetry install 23 | - name: Run tests 24 | run: tox 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | coverage 46 | *,cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | 63 | # PyCharm 64 | .idea/ 65 | 66 | #Ipython Notebook 67 | .ipynb_checkpoints 68 | 69 | # MyPY 70 | .mypy_cache/ 71 | 72 | # Personal 73 | .envrc 74 | .vscode/ 75 | 76 | # Terraform 77 | .terraform/ 78 | .terraform.lock.hcl 79 | *.tfstate 80 | *.tfstate.backup 81 | *.tfstate.*.backup 82 | *.tfstate.lock.info 83 | *.auto.tfvars 84 | handler.zip 85 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [PEP 440](https://www.python.org/dev/peps/pep-0440/). 7 | 8 | 9 | ## 2.0.1 - 2024-01-14 10 | 11 | ### Fixed 12 | 13 | - Correctly parse source distribution names with hyphens, since setuptools does not 14 | produce normalised names. See 15 | [pypa/setuptools#3593](https://github.com/pypa/setuptools/issues/3593). 16 | 17 | 18 | ## 2.0.0 - 2024-01-06 19 | 20 | ### Added 21 | 22 | - `s3pypi delete` command to delete packages from S3. 23 | - `s3pypi force-unlock` command to release a stuck lock in DynamoDB. 24 | - `--locks-table` option to customise the DynamoDB table name used for locking. 25 | 26 | ### Changed 27 | 28 | - Moved default command to `s3pypi upload`. 29 | - Renamed `--unsafe-s3-website` option to `--index.html`. 30 | 31 | ### Removed 32 | 33 | - `--acl` option. Use `--s3-put-args='ACL=...'` instead. [The use of ACLs is discouraged]. 34 | - `--lock-indexes` option. Locking is enabled automatically if a DynamoDB table exists. 35 | 36 | [The use of ACLs is discouraged]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/about-object-ownership.html 37 | 38 | 39 | ## 1.2.1 - 2023-12-31 40 | 41 | ### Fixed 42 | 43 | - `--put-root-index` combined with `--prefix` builds the index page of that prefix. 44 | 45 | 46 | ## 1.2.0 - 2023-12-30 47 | 48 | ### Added 49 | 50 | - `--strict` option to fail when trying to upload existing files. 51 | 52 | ### Changed 53 | 54 | - Require Python 3.8 or greater. 55 | 56 | 57 | ## 1.1.1 - 2023-02-20 58 | 59 | ### Fixed 60 | 61 | - Fail when a distribution package doesn't exist. 62 | 63 | 64 | ## 1.1.0 - 2022-12-19 65 | 66 | ### Added 67 | 68 | - Allow uploading source distributions with `.tar.bz2`, `.tar.xz`, and `.zip` extensions. 69 | 70 | ### Changed 71 | 72 | - Require Python 3.7 or greater. 73 | 74 | 75 | ## 1.0.0 - 2022-03-13 76 | 77 | ### Added 78 | 79 | - SHA-256 checksums in URLs. [@andrei-shabanski](https://github.com/andrei-shabanski) 80 | - `--no-sign-request` option to disable S3 authentication. 81 | [@jaustinpage](https://github.com/jaustinpage) 82 | - Expand glob patterns in case they were not expanded by a shell. 83 | [@jaustinpage](https://github.com/jaustinpage) 84 | 85 | 86 | ## 1.0.0rc3 - 2021-06-05 87 | 88 | ### Added 89 | 90 | - `--s3-endpoint-url` option for targeting a custom S3 endpoint. 91 | 92 | 93 | ## 1.0.0rc2 - 2021-05-20 94 | 95 | ### Added 96 | 97 | - Terraform config for an optional DynamoDB table used for distributed locking. 98 | - `--lock-indexes` option to lock index objects in S3 using said DynamoDB table. 99 | - `--put-root-index` option to write a root index that lists all package names. 100 | 101 | ### Changed 102 | 103 | - Set CloudFront default root object to `index.html`. 104 | 105 | 106 | ## 1.0.0rc1 - 2021-05-19 107 | 108 | ### Added 109 | 110 | - Terraform configuration for S3 and CloudFront, including optional **basic 111 | authentication** using Lambda@Edge and AWS Systems Manager Parameter Store. Instructions 112 | for migrating from CloudFormation are in the [README](README.md). 113 | - `--s3-put-args` option for passing extra arguments to S3 PutObject calls. Example: 114 | `--s3-put-args='ServerSideEncryption=aws:kms,SSEKMSKeyId=1234...'` 115 | 116 | ### Changed 117 | 118 | - CLI arguments have been overhauled. See `s3pypi --help` for details. 119 | - The **default behaviour for uploading index pages** has changed. Previously, they would 120 | be placed under the `/index.html` key in S3, which could be changed to 121 | `/` using the `--bare` option. This has now been reversed: the default key is 122 | `/`, and an option `--unsafe-s3-website` was added to append `index.html`. This 123 | new behaviour assumes that CloudFront uses the S3 REST API endpoint as its origin, not 124 | the S3 website endpoint. This allows the bucket to remain private, with CloudFront 125 | accessing it through an [Origin Access Identity (OAI)]. The new Terraform configuration 126 | includes such an OAI by default. To keep using the old configuration, packages must be 127 | uploaded with `--unsafe-s3-website --acl public-read`. This is **not recommended**, 128 | because the files will be **publicly accessible**! 129 | 130 | [Origin Access Identity (OAI)]: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html 131 | 132 | ### Removed 133 | 134 | - **Python 2 support**. 135 | - Automatic creation of distributions. From now on, **distributions must be created using 136 | a separate build command**. See the [README](README.md) for an example. 137 | - The `--private` option. The **default ACL is now** `private`, and you can pass a 138 | different one using the `--acl` option. 139 | - CloudFormation templates (replaced by Terraform configuration). 140 | - Jinja2 dependency. 141 | 142 | 143 | ## 0.11.0 - 2020-09-01 144 | 145 | ### Added 146 | 147 | - New `--acl` option, mutually exclusive with `--private`. 148 | [@marcelgwerder](https://github.com/marcelgwerder) 149 | 150 | 151 | ## 0.10.1 - 2020-01-14 152 | 153 | ### Fixed 154 | 155 | - Preserve existing files of the same version in the index when uploading with `--force`. 156 | [@natefoo](https://github.com/natefoo) 157 | 158 | 159 | ## 0.10.0 - 2019-09-18 160 | 161 | ### Added 162 | 163 | - Support `--dist-path` with only a wheel package present. 164 | [@takacsd](https://github.com/takacsd) 165 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Getting started 4 | 5 | So you're interested in contributing to s3pypi? That's great! We're excited to 6 | hear your ideas and experiences. 7 | 8 | This file describes all the different ways in which you can contribute. 9 | 10 | 11 | ## Reporting a bug 12 | 13 | Have you encountered a bug? Please let us know by reporting it. 14 | 15 | Before doing so, take a look at the existing [Issues] to make sure the bug you 16 | encountered hasn't already been reported by someone else. If so, we ask you to 17 | reply to the existing Issue rather than creating a new one. Bugs with many 18 | replies will obviously have a higher priority. 19 | 20 | If the bug you encountered has not been reported yet, create a new Issue for it 21 | and make sure to label it as a 'bug'. To allow us to help you as efficiently as 22 | possible, always try to include the following: 23 | 24 | - Which version of s3pypi are you using? 25 | - Which operating system or environment did the bug occur on? 26 | - What went wrong? 27 | - What did you expect to happen? 28 | - Detailed steps to reproduce. 29 | 30 | The maintainer of this repository monitors issues on a regular basis and will 31 | respond to your bug report as soon as possible. 32 | 33 | 34 | ## Requesting an enhancement 35 | 36 | Do you have a great idea that could make s3pypi even better? First take a look 37 | at the **Roadmap** section of our [README](README.md), maybe it is already 38 | planned for a future update. If not, feel free to request an enhancement. 39 | 40 | Before doing so, take a look at the existing [Issues] to make sure your idea 41 | hasn't already been requested by someone else. If so, we ask you to reply or 42 | give a thumbs-up to the existing Issue rather than creating a new one. Requests 43 | with many replies will obviously have a higher priority. 44 | 45 | If your idea has not been requested yet, create a new Issue for it and make sure 46 | to label it as an 'enhancement'. Explain what your idea is in detail and how it 47 | could improve s3pypi. 48 | 49 | The maintainer of this repository monitors issues on a regular basis and will 50 | respond to your request as soon as possible. 51 | 52 | 53 | ## Contributing code 54 | 55 | Would you prefer to contribute directly by writing some code yourself? That's 56 | great. 57 | 58 | **If your contribution is minor**, such as fixing a bug or typo, we encourage 59 | you to open a pull request right away. 60 | 61 | **If your contribution is major**, such as a new feature or a breaking change, 62 | start by opening an issue first. That way, other people can weigh in on the 63 | discussion before you do any work. 64 | 65 | The workflow for creating a pull request: 66 | 67 | 1. Fork the repository. 68 | 2. `clone` your forked repository. 69 | 3. Create a new feature branch from the `master` branch. 70 | 4. Make your contributions to the project's code. Please run the tests before 71 | committing, and keep the code style conform to the project's style guide 72 | (listed below). 73 | 5. Add documentation where required. Please keep the style conform. 74 | 6. Add your changes to the [changelog](CHANGELOG.md) in the "Unreleased" 75 | section. Include a link to your GitHub profile for some internet fame. 76 | 7. `commit` your changes in logical chunks. 77 | 8. `push` your branch to your fork on GitHub. 78 | 9. Create a pull request from your feature branch to the original repository's 79 | `master` branch. 80 | 81 | The maintainer of this repository monitors pull requests on a regular basis and 82 | will respond as soon as possible. The smaller individual pull requests are, the 83 | faster the maintainer will be able to respond. 84 | 85 | 86 | ### Local development 87 | 88 | Install the project's dependencies, and make sure that all tests pass: 89 | 90 | ```bash 91 | make install 92 | make test 93 | ``` 94 | 95 | Run the s3pypi CLI: 96 | 97 | ```bash 98 | poetry run s3pypi 99 | ``` 100 | 101 | This project uses the [Black](https://github.com/psf/black) code style. When 102 | contributing code, please format your changes properly. 103 | 104 | Check for lint & formatting errors: 105 | 106 | ```bash 107 | make lint 108 | ``` 109 | 110 | Reformat all source files: 111 | 112 | ```bash 113 | make format 114 | ``` 115 | 116 | 117 | ## Contact 118 | 119 | Discussions about s3pypi take place on this repository's [Issues] and [Pull 120 | Requests] sections. Anybody is welcome to join the conversation. Wherever 121 | possible, do not take these conversations to private channels, including 122 | contacting the maintainers directly. Keeping communication public means 123 | everybody can benefit and learn. 124 | 125 | 126 | [Issues]: https://github.com/gorilla-co/s3pypi/issues 127 | [Pull Requests]: https://github.com/gorilla-co/s3pypi/pulls 128 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 November Five BVBA 4 | Copyright (c) 2021 Gorillini NV 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | poetry install 3 | 4 | test: 5 | tox 6 | 7 | lint: 8 | tox -e lint 9 | 10 | format: 11 | poetry run isort --apply 12 | poetry run black . 13 | 14 | profile: 15 | poetry run pyinstrument -r html -m pytest tests/integration/test_main.py 16 | 17 | clean: 18 | rm -rf .coverage .eggs/ .pytest_cache/ .tox/ \ 19 | build/ coverage/ dist/ pip-wheel-metadata/ 20 | find . -name '*.egg-info' -exec rm -rf {} + 21 | find . -name '*.egg' -exec rm -f {} + 22 | find . -name '*.pyc' -exec rm -f {} + 23 | find . -name '*.pyo' -exec rm -f {} + 24 | find . -name '*~' -exec rm -f {} + 25 | find . -name '__pycache__' -exec rm -rf {} + 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # S3PyPI 2 | 3 | S3PyPI is a CLI for creating a Python Package Repository in an S3 bucket. 4 | 5 | 6 | ## Why? 7 | 8 | The official [Python Package Index (PyPI)](https://pypi.org) is a public 9 | repository of Python software. It's used by `pip` to download packages. 10 | 11 | If you work at a company, you may wish to publish your packages somewhere 12 | private instead, and still have them be accessible via `pip install`. This 13 | requires hosting your own repository. 14 | 15 | S3PyPI enables hosting a private repository at a low cost. It requires only an 16 | [S3 bucket] for storage, and some way to serve files over HTTPS (e.g. [Amazon 17 | CloudFront]). 18 | 19 | Publishing packages and index pages to S3 is done using the `s3pypi` CLI. 20 | Creating the S3 bucket and CloudFront distribution is done using a provided 21 | [Terraform] configuration, which you can tailor to your own needs. 22 | 23 | [S3 bucket]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html 24 | [Amazon CloudFront]: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html 25 | [Terraform]: https://www.terraform.io/ 26 | 27 | 28 | ## Alternatives 29 | 30 | - [AWS CodeArtifact](https://aws.amazon.com/codeartifact/) is a fully managed 31 | service that integrates with IAM. 32 | 33 | 34 | ## Getting started 35 | 36 | ### Installation 37 | 38 | Install s3pypi using pip: 39 | 40 | ```console 41 | $ pip install s3pypi 42 | ``` 43 | 44 | 45 | ### Setting up S3 and CloudFront 46 | 47 | Before you can start using `s3pypi`, you must set up an S3 bucket for storing 48 | packages, and a CloudFront distribution for serving files over HTTPS. Both of 49 | these can be created using the [Terraform] configuration provided in the 50 | `terraform/` directory: 51 | 52 | ```console 53 | $ git clone https://github.com/gorilla-co/s3pypi.git 54 | $ cd s3pypi/terraform/ 55 | 56 | $ terraform init 57 | $ terraform apply 58 | ``` 59 | 60 | You will be asked to enter your desired AWS region, S3 bucket name, and domain 61 | name for CloudFront. You can also enter these in a file named 62 | `config.auto.tfvars`: 63 | 64 | ```terraform 65 | region = "eu-west-1" 66 | bucket = "example-bucket" 67 | domain = "pypi.example.com" 68 | ``` 69 | 70 | #### DNS and HTTPS 71 | 72 | The Terraform configuration assumes that a [Route 53 hosted zone] exists for 73 | your domain, with a matching (wildcard) certificate in [AWS Certificate 74 | Manager]. If your certificate is a wildcard certificate, add 75 | `use_wildcard_certificate = true` to `config.auto.tfvars`. 76 | 77 | #### Distributed locking with DynamoDB 78 | 79 | To ensure that concurrent invocations of `s3pypi` do not overwrite each other's 80 | changes, the objects in S3 can be locked via an optional DynamoDB table (using 81 | the `--lock-indexes` option). To create this table, add `enable_dynamodb_locking 82 | = true` to `config.auto.tfvars`. 83 | 84 | #### Basic authentication 85 | 86 | To enable basic authentication, add `enable_basic_auth = true` to 87 | `config.auto.tfvars`. This will attach a [Lambda@Edge] function to your 88 | CloudFront distribution that reads user passwords from [AWS Systems Manager 89 | Parameter Store]. Users and passwords can be configured using the `put_user.py` 90 | script: 91 | 92 | ```console 93 | $ basic_auth/put_user.py pypi.example.com alice 94 | Password: 95 | ``` 96 | 97 | This creates a parameter named `/s3pypi/pypi.example.com/users/alice`. Passwords 98 | are hashed with a random salt, and stored as JSON objects: 99 | 100 | ```json 101 | { 102 | "password_hash": "7364151acc6229ec1468f54986a7614a8b215c26", 103 | "password_salt": "RRoCSRzvYJ1xRra2TWzhqS70wn84Sb_ElKxpl49o3Y0" 104 | } 105 | ``` 106 | 107 | #### Terraform module 108 | 109 | The Terraform configuration can also be included in your own project as a 110 | module: 111 | 112 | ```terraform 113 | terraform { 114 | required_providers { 115 | aws = { 116 | source = "hashicorp/aws" 117 | version = "~> 4.0" 118 | } 119 | } 120 | } 121 | 122 | provider "aws" { 123 | region = "eu-west-1" 124 | } 125 | 126 | provider "aws" { 127 | alias = "us_east_1" 128 | region = "us-east-1" 129 | } 130 | 131 | module "s3pypi" { 132 | source = "github.com/gorilla-co/s3pypi//terraform/modules/s3pypi" 133 | 134 | bucket = "example-bucket" 135 | domain = "pypi.example.com" 136 | 137 | use_wildcard_certificate = true 138 | enable_dynamodb_locking = true 139 | enable_basic_auth = true 140 | 141 | providers = { 142 | aws.us_east_1 = aws.us_east_1 143 | } 144 | } 145 | ``` 146 | 147 | #### Migrating from s3pypi 0.x to 1.x 148 | 149 | Existing resources created using the CloudFormation templates from s3pypi 0.x 150 | can be [imported into Terraform] and [removed from CloudFormation]. For example: 151 | 152 | ```console 153 | $ terraform init 154 | $ terraform import module.s3pypi.aws_s3_bucket.pypi example-bucket 155 | $ terraform import module.s3pypi.aws_cloudfront_distribution.cdn EDFDVBD6EXAMPLE 156 | $ terraform apply 157 | ``` 158 | 159 | [imported into Terraform]: https://www.terraform.io/docs/import/index.html 160 | [removed from CloudFormation]: https://aws.amazon.com/premiumsupport/knowledge-center/delete-cf-stack-retain-resources/ 161 | 162 | In this new configuration, CloudFront uses the S3 REST API endpoint as its 163 | origin, not the S3 website endpoint. This allows the bucket to remain private, 164 | with CloudFront accessing it through an [Origin Access Identity (OAI)]. To make 165 | this work with your existing S3 bucket, all `/index.html` objects must 166 | be renamed to `/`. You can do so using the provided script: 167 | 168 | ```console 169 | $ scripts/migrate-s3-index.py example-bucket 170 | ``` 171 | 172 | To instead keep using the old configuration with a publicly accessible S3 173 | website endpoint, pass the following options when uploading packages: 174 | 175 | ```console 176 | $ s3pypi upload ... --index.html --s3-put-args='ACL=public-read' 177 | ``` 178 | 179 | [Origin Access Identity (OAI)]: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html 180 | 181 | 182 | ### Example IAM policy 183 | 184 | The `s3pypi` CLI requires the following IAM permissions to access S3 and 185 | (optionally) DynamoDB. Replace `example-bucket` by your S3 bucket name. 186 | 187 | ```json 188 | { 189 | "Version": "2012-10-17", 190 | "Statement": [ 191 | { 192 | "Effect": "Allow", 193 | "Action": [ 194 | "s3:GetObject", 195 | "s3:PutObject", 196 | "s3:DeleteObject" 197 | ], 198 | "Resource": "arn:aws:s3:::example-bucket/*" 199 | }, 200 | { 201 | "Effect": "Allow", 202 | "Action": [ 203 | "s3:ListBucket" 204 | ], 205 | "Resource": "arn:aws:s3:::example-bucket" 206 | }, 207 | { 208 | "Effect": "Allow", 209 | "Action": [ 210 | "dynamodb:GetItem", 211 | "dynamodb:PutItem", 212 | "dynamodb:DeleteItem" 213 | ], 214 | "Resource": "arn:aws:dynamodb:*:*:table/example-bucket-locks" 215 | } 216 | ] 217 | } 218 | ``` 219 | 220 | 221 | ## Usage 222 | 223 | ### Distributing packages 224 | 225 | You can now use `s3pypi` to upload packages to S3: 226 | 227 | ```console 228 | $ cd /path/to/your-project/ 229 | $ python setup.py sdist bdist_wheel 230 | 231 | $ s3pypi upload dist/* --bucket example-bucket [--prefix PREFIX] 232 | ``` 233 | 234 | See `s3pypi --help` for a description of all options. 235 | 236 | 237 | ### Installing packages 238 | 239 | Install your packages using `pip` by pointing the `--extra-index-url` to your 240 | CloudFront domain. If you used `--prefix` while uploading, then add the prefix 241 | here as well: 242 | 243 | ```console 244 | $ pip install your-project --extra-index-url https://pypi.example.com/PREFIX/ 245 | ``` 246 | 247 | Alternatively, you can configure the index URL in `~/.pip/pip.conf`: 248 | 249 | ``` 250 | [global] 251 | extra-index-url = https://pypi.example.com/PREFIX/ 252 | ``` 253 | 254 | 255 | ## Roadmap 256 | 257 | Currently there are no plans to add new features to s3pypi. If you have any 258 | ideas for new features, check out our [contributing guidelines](CONTRIBUTING.md) 259 | on how to get these on our roadmap. 260 | 261 | 262 | ## Contact 263 | 264 | Got any questions or ideas? We'd love to hear from you. Check out our 265 | [contributing guidelines](CONTRIBUTING.md) for ways to offer feedback and 266 | contribute. 267 | 268 | 269 | ## License 270 | 271 | Copyright (c) [Gorillini NV](https://gorilla.co). 272 | All rights reserved. 273 | 274 | Licensed under the [MIT](LICENSE) License. 275 | 276 | 277 | [Route 53 hosted zone]: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/AboutHZWorkingWith.html 278 | [AWS Certificate Manager]: https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request-public.html 279 | [Lambda@Edge]: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html 280 | [AWS Systems Manager Parameter Store]: https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html 281 | -------------------------------------------------------------------------------- /basic_auth/handler.py: -------------------------------------------------------------------------------- 1 | ../terraform/modules/s3pypi/basic_auth/handler.py -------------------------------------------------------------------------------- /basic_auth/put_user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import getpass 4 | import json 5 | import secrets 6 | import sys 7 | from dataclasses import asdict 8 | 9 | import boto3 10 | 11 | from handler import User, hash_password, region 12 | 13 | 14 | def get_arg_parser(): 15 | p = argparse.ArgumentParser() 16 | p.add_argument("domain", help="S3PyPI domain") 17 | p.add_argument("username", help="Username") 18 | pw = p.add_mutually_exclusive_group() 19 | pw.add_argument( 20 | "--password-stdin", action="store_true", help="Read password from stdin" 21 | ) 22 | pw.add_argument( 23 | "--random-password", 24 | metavar="N", 25 | type=int, 26 | default=0, 27 | help="Generate a random password of N bytes", 28 | ) 29 | p.add_argument( 30 | "--salt-nbytes", 31 | metavar="N", 32 | type=int, 33 | default=32, 34 | help="Length of the random salt in bytes", 35 | ) 36 | p.add_argument("--overwrite", action="store_true", help="Overwrite existing users") 37 | return p 38 | 39 | 40 | def main(): 41 | args = get_arg_parser().parse_args() 42 | 43 | if args.password_stdin: 44 | password = sys.stdin.read().strip("\n") 45 | elif args.random_password: 46 | password = secrets.token_urlsafe(args.random_password) 47 | else: 48 | password = getpass.getpass() 49 | 50 | salt = secrets.token_urlsafe(args.salt_nbytes) 51 | 52 | user = User( 53 | username=args.username, 54 | password_hash=hash_password(password, salt), 55 | password_salt=salt, 56 | ) 57 | put_user(args.domain, user, args.overwrite) 58 | 59 | if args.random_password: 60 | print(password) 61 | 62 | 63 | def put_user(domain: str, user: User, overwrite: bool = False): 64 | data = asdict(user) 65 | username = data.pop("username") 66 | boto3.client("ssm", region_name=region).put_parameter( 67 | Name=f"/s3pypi/{domain}/users/{username}", 68 | Value=json.dumps(data, indent=2), 69 | Type="SecureString", 70 | Overwrite=overwrite, 71 | ) 72 | 73 | 74 | if __name__ == "__main__": 75 | main() 76 | -------------------------------------------------------------------------------- /basic_auth/test_handler.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from unittest.mock import patch 3 | 4 | import handler 5 | 6 | 7 | def test_handle_success(): 8 | resp = call_handler("secret", "secret") 9 | assert resp != handler.unauthorized 10 | assert resp["headers"]["authorization"] 11 | 12 | 13 | def test_handle_unauthorized(): 14 | resp = call_handler("wrong", "secret") 15 | assert resp == handler.unauthorized 16 | 17 | 18 | def call_handler(password: str, expected_password: str, salt: str = "NaCl"): 19 | auth = "Basic " + base64.b64encode(f"alice:{password}".encode()).decode() 20 | headers = { 21 | "host": [{"value": "pypi.example.com"}], 22 | "authorization": [{"value": auth}], 23 | } 24 | event = {"Records": [{"cf": {"request": {"headers": headers}}}]} 25 | 26 | def get_mock_user(_, username: str): 27 | return handler.User( 28 | username=username, 29 | password_hash=handler.hash_password(expected_password, salt), 30 | password_salt=salt, 31 | ) 32 | 33 | with patch.object(handler, "get_user", get_mock_user): 34 | return handler.handle(event, context=None) 35 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "23.12.1" 6 | description = "The uncompromising code formatter." 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, 11 | {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, 12 | {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, 13 | {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, 14 | {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, 15 | {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, 16 | {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, 17 | {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, 18 | {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, 19 | {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, 20 | {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, 21 | {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, 22 | {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, 23 | {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, 24 | {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, 25 | {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, 26 | {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, 27 | {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, 28 | {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, 29 | {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, 30 | {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, 31 | {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, 32 | ] 33 | 34 | [package.dependencies] 35 | click = ">=8.0.0" 36 | mypy-extensions = ">=0.4.3" 37 | packaging = ">=22.0" 38 | pathspec = ">=0.9.0" 39 | platformdirs = ">=2" 40 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 41 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 42 | 43 | [package.extras] 44 | colorama = ["colorama (>=0.4.3)"] 45 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 46 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 47 | uvloop = ["uvloop (>=0.15.2)"] 48 | 49 | [[package]] 50 | name = "boto3" 51 | version = "1.34.19" 52 | description = "The AWS SDK for Python" 53 | optional = false 54 | python-versions = ">= 3.8" 55 | files = [ 56 | {file = "boto3-1.34.19-py3-none-any.whl", hash = "sha256:4c76ef92af7dbdcea21b196a2699671e82e8814d4cfe570c48eda477dd1aeb19"}, 57 | {file = "boto3-1.34.19.tar.gz", hash = "sha256:95d2c2bde86a0934d4c461020c50fc1344b444f167654e215f1de549bc77fc0f"}, 58 | ] 59 | 60 | [package.dependencies] 61 | botocore = ">=1.34.19,<1.35.0" 62 | jmespath = ">=0.7.1,<2.0.0" 63 | s3transfer = ">=0.10.0,<0.11.0" 64 | 65 | [package.extras] 66 | crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] 67 | 68 | [[package]] 69 | name = "boto3-stubs" 70 | version = "1.34.19" 71 | description = "Type annotations for boto3 1.34.19 generated with mypy-boto3-builder 7.23.1" 72 | optional = false 73 | python-versions = ">=3.8" 74 | files = [ 75 | {file = "boto3-stubs-1.34.19.tar.gz", hash = "sha256:67fe5a2fb1d1b2c534fa31c543b773edbc01ce9528010d9cc0ef9c5044aa218a"}, 76 | {file = "boto3_stubs-1.34.19-py3-none-any.whl", hash = "sha256:6651e6ddd0c5cdd77393432e517301096b646aeeea3ade5c305960817dc46aa1"}, 77 | ] 78 | 79 | [package.dependencies] 80 | botocore-stubs = "*" 81 | mypy-boto3-dynamodb = {version = ">=1.34.0,<1.35.0", optional = true, markers = "extra == \"dynamodb\""} 82 | mypy-boto3-s3 = {version = ">=1.34.0,<1.35.0", optional = true, markers = "extra == \"s3\""} 83 | types-s3transfer = "*" 84 | typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} 85 | 86 | [package.extras] 87 | accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.34.0,<1.35.0)"] 88 | account = ["mypy-boto3-account (>=1.34.0,<1.35.0)"] 89 | acm = ["mypy-boto3-acm (>=1.34.0,<1.35.0)"] 90 | acm-pca = ["mypy-boto3-acm-pca (>=1.34.0,<1.35.0)"] 91 | alexaforbusiness = ["mypy-boto3-alexaforbusiness (>=1.34.0,<1.35.0)"] 92 | all = ["mypy-boto3-accessanalyzer (>=1.34.0,<1.35.0)", "mypy-boto3-account (>=1.34.0,<1.35.0)", "mypy-boto3-acm (>=1.34.0,<1.35.0)", "mypy-boto3-acm-pca (>=1.34.0,<1.35.0)", "mypy-boto3-alexaforbusiness (>=1.34.0,<1.35.0)", "mypy-boto3-amp (>=1.34.0,<1.35.0)", "mypy-boto3-amplify (>=1.34.0,<1.35.0)", "mypy-boto3-amplifybackend (>=1.34.0,<1.35.0)", "mypy-boto3-amplifyuibuilder (>=1.34.0,<1.35.0)", "mypy-boto3-apigateway (>=1.34.0,<1.35.0)", "mypy-boto3-apigatewaymanagementapi (>=1.34.0,<1.35.0)", "mypy-boto3-apigatewayv2 (>=1.34.0,<1.35.0)", "mypy-boto3-appconfig (>=1.34.0,<1.35.0)", "mypy-boto3-appconfigdata (>=1.34.0,<1.35.0)", "mypy-boto3-appfabric (>=1.34.0,<1.35.0)", "mypy-boto3-appflow (>=1.34.0,<1.35.0)", "mypy-boto3-appintegrations (>=1.34.0,<1.35.0)", "mypy-boto3-application-autoscaling (>=1.34.0,<1.35.0)", "mypy-boto3-application-insights (>=1.34.0,<1.35.0)", "mypy-boto3-applicationcostprofiler (>=1.34.0,<1.35.0)", "mypy-boto3-appmesh (>=1.34.0,<1.35.0)", "mypy-boto3-apprunner (>=1.34.0,<1.35.0)", "mypy-boto3-appstream (>=1.34.0,<1.35.0)", "mypy-boto3-appsync (>=1.34.0,<1.35.0)", "mypy-boto3-arc-zonal-shift (>=1.34.0,<1.35.0)", "mypy-boto3-athena (>=1.34.0,<1.35.0)", "mypy-boto3-auditmanager (>=1.34.0,<1.35.0)", "mypy-boto3-autoscaling (>=1.34.0,<1.35.0)", "mypy-boto3-autoscaling-plans (>=1.34.0,<1.35.0)", "mypy-boto3-b2bi (>=1.34.0,<1.35.0)", "mypy-boto3-backup (>=1.34.0,<1.35.0)", "mypy-boto3-backup-gateway (>=1.34.0,<1.35.0)", "mypy-boto3-backupstorage (>=1.34.0,<1.35.0)", "mypy-boto3-batch (>=1.34.0,<1.35.0)", "mypy-boto3-bcm-data-exports (>=1.34.0,<1.35.0)", "mypy-boto3-bedrock (>=1.34.0,<1.35.0)", "mypy-boto3-bedrock-agent (>=1.34.0,<1.35.0)", "mypy-boto3-bedrock-agent-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-bedrock-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-billingconductor (>=1.34.0,<1.35.0)", "mypy-boto3-braket (>=1.34.0,<1.35.0)", "mypy-boto3-budgets (>=1.34.0,<1.35.0)", "mypy-boto3-ce (>=1.34.0,<1.35.0)", "mypy-boto3-chime (>=1.34.0,<1.35.0)", "mypy-boto3-chime-sdk-identity (>=1.34.0,<1.35.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.34.0,<1.35.0)", "mypy-boto3-chime-sdk-meetings (>=1.34.0,<1.35.0)", "mypy-boto3-chime-sdk-messaging (>=1.34.0,<1.35.0)", "mypy-boto3-chime-sdk-voice (>=1.34.0,<1.35.0)", "mypy-boto3-cleanrooms (>=1.34.0,<1.35.0)", "mypy-boto3-cleanroomsml (>=1.34.0,<1.35.0)", "mypy-boto3-cloud9 (>=1.34.0,<1.35.0)", "mypy-boto3-cloudcontrol (>=1.34.0,<1.35.0)", "mypy-boto3-clouddirectory (>=1.34.0,<1.35.0)", "mypy-boto3-cloudformation (>=1.34.0,<1.35.0)", "mypy-boto3-cloudfront (>=1.34.0,<1.35.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.34.0,<1.35.0)", "mypy-boto3-cloudhsm (>=1.34.0,<1.35.0)", "mypy-boto3-cloudhsmv2 (>=1.34.0,<1.35.0)", "mypy-boto3-cloudsearch (>=1.34.0,<1.35.0)", "mypy-boto3-cloudsearchdomain (>=1.34.0,<1.35.0)", "mypy-boto3-cloudtrail (>=1.34.0,<1.35.0)", "mypy-boto3-cloudtrail-data (>=1.34.0,<1.35.0)", "mypy-boto3-cloudwatch (>=1.34.0,<1.35.0)", "mypy-boto3-codeartifact (>=1.34.0,<1.35.0)", "mypy-boto3-codebuild (>=1.34.0,<1.35.0)", "mypy-boto3-codecatalyst (>=1.34.0,<1.35.0)", "mypy-boto3-codecommit (>=1.34.0,<1.35.0)", "mypy-boto3-codedeploy (>=1.34.0,<1.35.0)", "mypy-boto3-codeguru-reviewer (>=1.34.0,<1.35.0)", "mypy-boto3-codeguru-security (>=1.34.0,<1.35.0)", "mypy-boto3-codeguruprofiler (>=1.34.0,<1.35.0)", "mypy-boto3-codepipeline (>=1.34.0,<1.35.0)", "mypy-boto3-codestar (>=1.34.0,<1.35.0)", "mypy-boto3-codestar-connections (>=1.34.0,<1.35.0)", "mypy-boto3-codestar-notifications (>=1.34.0,<1.35.0)", "mypy-boto3-cognito-identity (>=1.34.0,<1.35.0)", "mypy-boto3-cognito-idp (>=1.34.0,<1.35.0)", "mypy-boto3-cognito-sync (>=1.34.0,<1.35.0)", "mypy-boto3-comprehend (>=1.34.0,<1.35.0)", "mypy-boto3-comprehendmedical (>=1.34.0,<1.35.0)", "mypy-boto3-compute-optimizer (>=1.34.0,<1.35.0)", "mypy-boto3-config (>=1.34.0,<1.35.0)", "mypy-boto3-connect (>=1.34.0,<1.35.0)", "mypy-boto3-connect-contact-lens (>=1.34.0,<1.35.0)", "mypy-boto3-connectcampaigns (>=1.34.0,<1.35.0)", "mypy-boto3-connectcases (>=1.34.0,<1.35.0)", "mypy-boto3-connectparticipant (>=1.34.0,<1.35.0)", "mypy-boto3-controltower (>=1.34.0,<1.35.0)", "mypy-boto3-cost-optimization-hub (>=1.34.0,<1.35.0)", "mypy-boto3-cur (>=1.34.0,<1.35.0)", "mypy-boto3-customer-profiles (>=1.34.0,<1.35.0)", "mypy-boto3-databrew (>=1.34.0,<1.35.0)", "mypy-boto3-dataexchange (>=1.34.0,<1.35.0)", "mypy-boto3-datapipeline (>=1.34.0,<1.35.0)", "mypy-boto3-datasync (>=1.34.0,<1.35.0)", "mypy-boto3-datazone (>=1.34.0,<1.35.0)", "mypy-boto3-dax (>=1.34.0,<1.35.0)", "mypy-boto3-detective (>=1.34.0,<1.35.0)", "mypy-boto3-devicefarm (>=1.34.0,<1.35.0)", "mypy-boto3-devops-guru (>=1.34.0,<1.35.0)", "mypy-boto3-directconnect (>=1.34.0,<1.35.0)", "mypy-boto3-discovery (>=1.34.0,<1.35.0)", "mypy-boto3-dlm (>=1.34.0,<1.35.0)", "mypy-boto3-dms (>=1.34.0,<1.35.0)", "mypy-boto3-docdb (>=1.34.0,<1.35.0)", "mypy-boto3-docdb-elastic (>=1.34.0,<1.35.0)", "mypy-boto3-drs (>=1.34.0,<1.35.0)", "mypy-boto3-ds (>=1.34.0,<1.35.0)", "mypy-boto3-dynamodb (>=1.34.0,<1.35.0)", "mypy-boto3-dynamodbstreams (>=1.34.0,<1.35.0)", "mypy-boto3-ebs (>=1.34.0,<1.35.0)", "mypy-boto3-ec2 (>=1.34.0,<1.35.0)", "mypy-boto3-ec2-instance-connect (>=1.34.0,<1.35.0)", "mypy-boto3-ecr (>=1.34.0,<1.35.0)", "mypy-boto3-ecr-public (>=1.34.0,<1.35.0)", "mypy-boto3-ecs (>=1.34.0,<1.35.0)", "mypy-boto3-efs (>=1.34.0,<1.35.0)", "mypy-boto3-eks (>=1.34.0,<1.35.0)", "mypy-boto3-eks-auth (>=1.34.0,<1.35.0)", "mypy-boto3-elastic-inference (>=1.34.0,<1.35.0)", "mypy-boto3-elasticache (>=1.34.0,<1.35.0)", "mypy-boto3-elasticbeanstalk (>=1.34.0,<1.35.0)", "mypy-boto3-elastictranscoder (>=1.34.0,<1.35.0)", "mypy-boto3-elb (>=1.34.0,<1.35.0)", "mypy-boto3-elbv2 (>=1.34.0,<1.35.0)", "mypy-boto3-emr (>=1.34.0,<1.35.0)", "mypy-boto3-emr-containers (>=1.34.0,<1.35.0)", "mypy-boto3-emr-serverless (>=1.34.0,<1.35.0)", "mypy-boto3-entityresolution (>=1.34.0,<1.35.0)", "mypy-boto3-es (>=1.34.0,<1.35.0)", "mypy-boto3-events (>=1.34.0,<1.35.0)", "mypy-boto3-evidently (>=1.34.0,<1.35.0)", "mypy-boto3-finspace (>=1.34.0,<1.35.0)", "mypy-boto3-finspace-data (>=1.34.0,<1.35.0)", "mypy-boto3-firehose (>=1.34.0,<1.35.0)", "mypy-boto3-fis (>=1.34.0,<1.35.0)", "mypy-boto3-fms (>=1.34.0,<1.35.0)", "mypy-boto3-forecast (>=1.34.0,<1.35.0)", "mypy-boto3-forecastquery (>=1.34.0,<1.35.0)", "mypy-boto3-frauddetector (>=1.34.0,<1.35.0)", "mypy-boto3-freetier (>=1.34.0,<1.35.0)", "mypy-boto3-fsx (>=1.34.0,<1.35.0)", "mypy-boto3-gamelift (>=1.34.0,<1.35.0)", "mypy-boto3-glacier (>=1.34.0,<1.35.0)", "mypy-boto3-globalaccelerator (>=1.34.0,<1.35.0)", "mypy-boto3-glue (>=1.34.0,<1.35.0)", "mypy-boto3-grafana (>=1.34.0,<1.35.0)", "mypy-boto3-greengrass (>=1.34.0,<1.35.0)", "mypy-boto3-greengrassv2 (>=1.34.0,<1.35.0)", "mypy-boto3-groundstation (>=1.34.0,<1.35.0)", "mypy-boto3-guardduty (>=1.34.0,<1.35.0)", "mypy-boto3-health (>=1.34.0,<1.35.0)", "mypy-boto3-healthlake (>=1.34.0,<1.35.0)", "mypy-boto3-honeycode (>=1.34.0,<1.35.0)", "mypy-boto3-iam (>=1.34.0,<1.35.0)", "mypy-boto3-identitystore (>=1.34.0,<1.35.0)", "mypy-boto3-imagebuilder (>=1.34.0,<1.35.0)", "mypy-boto3-importexport (>=1.34.0,<1.35.0)", "mypy-boto3-inspector (>=1.34.0,<1.35.0)", "mypy-boto3-inspector-scan (>=1.34.0,<1.35.0)", "mypy-boto3-inspector2 (>=1.34.0,<1.35.0)", "mypy-boto3-internetmonitor (>=1.34.0,<1.35.0)", "mypy-boto3-iot (>=1.34.0,<1.35.0)", "mypy-boto3-iot-data (>=1.34.0,<1.35.0)", "mypy-boto3-iot-jobs-data (>=1.34.0,<1.35.0)", "mypy-boto3-iot-roborunner (>=1.34.0,<1.35.0)", "mypy-boto3-iot1click-devices (>=1.34.0,<1.35.0)", "mypy-boto3-iot1click-projects (>=1.34.0,<1.35.0)", "mypy-boto3-iotanalytics (>=1.34.0,<1.35.0)", "mypy-boto3-iotdeviceadvisor (>=1.34.0,<1.35.0)", "mypy-boto3-iotevents (>=1.34.0,<1.35.0)", "mypy-boto3-iotevents-data (>=1.34.0,<1.35.0)", "mypy-boto3-iotfleethub (>=1.34.0,<1.35.0)", "mypy-boto3-iotfleetwise (>=1.34.0,<1.35.0)", "mypy-boto3-iotsecuretunneling (>=1.34.0,<1.35.0)", "mypy-boto3-iotsitewise (>=1.34.0,<1.35.0)", "mypy-boto3-iotthingsgraph (>=1.34.0,<1.35.0)", "mypy-boto3-iottwinmaker (>=1.34.0,<1.35.0)", "mypy-boto3-iotwireless (>=1.34.0,<1.35.0)", "mypy-boto3-ivs (>=1.34.0,<1.35.0)", "mypy-boto3-ivs-realtime (>=1.34.0,<1.35.0)", "mypy-boto3-ivschat (>=1.34.0,<1.35.0)", "mypy-boto3-kafka (>=1.34.0,<1.35.0)", "mypy-boto3-kafkaconnect (>=1.34.0,<1.35.0)", "mypy-boto3-kendra (>=1.34.0,<1.35.0)", "mypy-boto3-kendra-ranking (>=1.34.0,<1.35.0)", "mypy-boto3-keyspaces (>=1.34.0,<1.35.0)", "mypy-boto3-kinesis (>=1.34.0,<1.35.0)", "mypy-boto3-kinesis-video-archived-media (>=1.34.0,<1.35.0)", "mypy-boto3-kinesis-video-media (>=1.34.0,<1.35.0)", "mypy-boto3-kinesis-video-signaling (>=1.34.0,<1.35.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.34.0,<1.35.0)", "mypy-boto3-kinesisanalytics (>=1.34.0,<1.35.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.34.0,<1.35.0)", "mypy-boto3-kinesisvideo (>=1.34.0,<1.35.0)", "mypy-boto3-kms (>=1.34.0,<1.35.0)", "mypy-boto3-lakeformation (>=1.34.0,<1.35.0)", "mypy-boto3-lambda (>=1.34.0,<1.35.0)", "mypy-boto3-launch-wizard (>=1.34.0,<1.35.0)", "mypy-boto3-lex-models (>=1.34.0,<1.35.0)", "mypy-boto3-lex-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-lexv2-models (>=1.34.0,<1.35.0)", "mypy-boto3-lexv2-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-license-manager (>=1.34.0,<1.35.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.34.0,<1.35.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.34.0,<1.35.0)", "mypy-boto3-lightsail (>=1.34.0,<1.35.0)", "mypy-boto3-location (>=1.34.0,<1.35.0)", "mypy-boto3-logs (>=1.34.0,<1.35.0)", "mypy-boto3-lookoutequipment (>=1.34.0,<1.35.0)", "mypy-boto3-lookoutmetrics (>=1.34.0,<1.35.0)", "mypy-boto3-lookoutvision (>=1.34.0,<1.35.0)", "mypy-boto3-m2 (>=1.34.0,<1.35.0)", "mypy-boto3-machinelearning (>=1.34.0,<1.35.0)", "mypy-boto3-macie2 (>=1.34.0,<1.35.0)", "mypy-boto3-managedblockchain (>=1.34.0,<1.35.0)", "mypy-boto3-managedblockchain-query (>=1.34.0,<1.35.0)", "mypy-boto3-marketplace-agreement (>=1.34.0,<1.35.0)", "mypy-boto3-marketplace-catalog (>=1.34.0,<1.35.0)", "mypy-boto3-marketplace-deployment (>=1.34.0,<1.35.0)", "mypy-boto3-marketplace-entitlement (>=1.34.0,<1.35.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.34.0,<1.35.0)", "mypy-boto3-mediaconnect (>=1.34.0,<1.35.0)", "mypy-boto3-mediaconvert (>=1.34.0,<1.35.0)", "mypy-boto3-medialive (>=1.34.0,<1.35.0)", "mypy-boto3-mediapackage (>=1.34.0,<1.35.0)", "mypy-boto3-mediapackage-vod (>=1.34.0,<1.35.0)", "mypy-boto3-mediapackagev2 (>=1.34.0,<1.35.0)", "mypy-boto3-mediastore (>=1.34.0,<1.35.0)", "mypy-boto3-mediastore-data (>=1.34.0,<1.35.0)", "mypy-boto3-mediatailor (>=1.34.0,<1.35.0)", "mypy-boto3-medical-imaging (>=1.34.0,<1.35.0)", "mypy-boto3-memorydb (>=1.34.0,<1.35.0)", "mypy-boto3-meteringmarketplace (>=1.34.0,<1.35.0)", "mypy-boto3-mgh (>=1.34.0,<1.35.0)", "mypy-boto3-mgn (>=1.34.0,<1.35.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.34.0,<1.35.0)", "mypy-boto3-migrationhub-config (>=1.34.0,<1.35.0)", "mypy-boto3-migrationhuborchestrator (>=1.34.0,<1.35.0)", "mypy-boto3-migrationhubstrategy (>=1.34.0,<1.35.0)", "mypy-boto3-mobile (>=1.34.0,<1.35.0)", "mypy-boto3-mq (>=1.34.0,<1.35.0)", "mypy-boto3-mturk (>=1.34.0,<1.35.0)", "mypy-boto3-mwaa (>=1.34.0,<1.35.0)", "mypy-boto3-neptune (>=1.34.0,<1.35.0)", "mypy-boto3-neptune-graph (>=1.34.0,<1.35.0)", "mypy-boto3-neptunedata (>=1.34.0,<1.35.0)", "mypy-boto3-network-firewall (>=1.34.0,<1.35.0)", "mypy-boto3-networkmanager (>=1.34.0,<1.35.0)", "mypy-boto3-networkmonitor (>=1.34.0,<1.35.0)", "mypy-boto3-nimble (>=1.34.0,<1.35.0)", "mypy-boto3-oam (>=1.34.0,<1.35.0)", "mypy-boto3-omics (>=1.34.0,<1.35.0)", "mypy-boto3-opensearch (>=1.34.0,<1.35.0)", "mypy-boto3-opensearchserverless (>=1.34.0,<1.35.0)", "mypy-boto3-opsworks (>=1.34.0,<1.35.0)", "mypy-boto3-opsworkscm (>=1.34.0,<1.35.0)", "mypy-boto3-organizations (>=1.34.0,<1.35.0)", "mypy-boto3-osis (>=1.34.0,<1.35.0)", "mypy-boto3-outposts (>=1.34.0,<1.35.0)", "mypy-boto3-panorama (>=1.34.0,<1.35.0)", "mypy-boto3-payment-cryptography (>=1.34.0,<1.35.0)", "mypy-boto3-payment-cryptography-data (>=1.34.0,<1.35.0)", "mypy-boto3-pca-connector-ad (>=1.34.0,<1.35.0)", "mypy-boto3-personalize (>=1.34.0,<1.35.0)", "mypy-boto3-personalize-events (>=1.34.0,<1.35.0)", "mypy-boto3-personalize-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-pi (>=1.34.0,<1.35.0)", "mypy-boto3-pinpoint (>=1.34.0,<1.35.0)", "mypy-boto3-pinpoint-email (>=1.34.0,<1.35.0)", "mypy-boto3-pinpoint-sms-voice (>=1.34.0,<1.35.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.34.0,<1.35.0)", "mypy-boto3-pipes (>=1.34.0,<1.35.0)", "mypy-boto3-polly (>=1.34.0,<1.35.0)", "mypy-boto3-pricing (>=1.34.0,<1.35.0)", "mypy-boto3-privatenetworks (>=1.34.0,<1.35.0)", "mypy-boto3-proton (>=1.34.0,<1.35.0)", "mypy-boto3-qbusiness (>=1.34.0,<1.35.0)", "mypy-boto3-qconnect (>=1.34.0,<1.35.0)", "mypy-boto3-qldb (>=1.34.0,<1.35.0)", "mypy-boto3-qldb-session (>=1.34.0,<1.35.0)", "mypy-boto3-quicksight (>=1.34.0,<1.35.0)", "mypy-boto3-ram (>=1.34.0,<1.35.0)", "mypy-boto3-rbin (>=1.34.0,<1.35.0)", "mypy-boto3-rds (>=1.34.0,<1.35.0)", "mypy-boto3-rds-data (>=1.34.0,<1.35.0)", "mypy-boto3-redshift (>=1.34.0,<1.35.0)", "mypy-boto3-redshift-data (>=1.34.0,<1.35.0)", "mypy-boto3-redshift-serverless (>=1.34.0,<1.35.0)", "mypy-boto3-rekognition (>=1.34.0,<1.35.0)", "mypy-boto3-repostspace (>=1.34.0,<1.35.0)", "mypy-boto3-resiliencehub (>=1.34.0,<1.35.0)", "mypy-boto3-resource-explorer-2 (>=1.34.0,<1.35.0)", "mypy-boto3-resource-groups (>=1.34.0,<1.35.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.34.0,<1.35.0)", "mypy-boto3-robomaker (>=1.34.0,<1.35.0)", "mypy-boto3-rolesanywhere (>=1.34.0,<1.35.0)", "mypy-boto3-route53 (>=1.34.0,<1.35.0)", "mypy-boto3-route53-recovery-cluster (>=1.34.0,<1.35.0)", "mypy-boto3-route53-recovery-control-config (>=1.34.0,<1.35.0)", "mypy-boto3-route53-recovery-readiness (>=1.34.0,<1.35.0)", "mypy-boto3-route53domains (>=1.34.0,<1.35.0)", "mypy-boto3-route53resolver (>=1.34.0,<1.35.0)", "mypy-boto3-rum (>=1.34.0,<1.35.0)", "mypy-boto3-s3 (>=1.34.0,<1.35.0)", "mypy-boto3-s3control (>=1.34.0,<1.35.0)", "mypy-boto3-s3outposts (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker-edge (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker-geospatial (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker-metrics (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-savingsplans (>=1.34.0,<1.35.0)", "mypy-boto3-scheduler (>=1.34.0,<1.35.0)", "mypy-boto3-schemas (>=1.34.0,<1.35.0)", "mypy-boto3-sdb (>=1.34.0,<1.35.0)", "mypy-boto3-secretsmanager (>=1.34.0,<1.35.0)", "mypy-boto3-securityhub (>=1.34.0,<1.35.0)", "mypy-boto3-securitylake (>=1.34.0,<1.35.0)", "mypy-boto3-serverlessrepo (>=1.34.0,<1.35.0)", "mypy-boto3-service-quotas (>=1.34.0,<1.35.0)", "mypy-boto3-servicecatalog (>=1.34.0,<1.35.0)", "mypy-boto3-servicecatalog-appregistry (>=1.34.0,<1.35.0)", "mypy-boto3-servicediscovery (>=1.34.0,<1.35.0)", "mypy-boto3-ses (>=1.34.0,<1.35.0)", "mypy-boto3-sesv2 (>=1.34.0,<1.35.0)", "mypy-boto3-shield (>=1.34.0,<1.35.0)", "mypy-boto3-signer (>=1.34.0,<1.35.0)", "mypy-boto3-simspaceweaver (>=1.34.0,<1.35.0)", "mypy-boto3-sms (>=1.34.0,<1.35.0)", "mypy-boto3-sms-voice (>=1.34.0,<1.35.0)", "mypy-boto3-snow-device-management (>=1.34.0,<1.35.0)", "mypy-boto3-snowball (>=1.34.0,<1.35.0)", "mypy-boto3-sns (>=1.34.0,<1.35.0)", "mypy-boto3-sqs (>=1.34.0,<1.35.0)", "mypy-boto3-ssm (>=1.34.0,<1.35.0)", "mypy-boto3-ssm-contacts (>=1.34.0,<1.35.0)", "mypy-boto3-ssm-incidents (>=1.34.0,<1.35.0)", "mypy-boto3-ssm-sap (>=1.34.0,<1.35.0)", "mypy-boto3-sso (>=1.34.0,<1.35.0)", "mypy-boto3-sso-admin (>=1.34.0,<1.35.0)", "mypy-boto3-sso-oidc (>=1.34.0,<1.35.0)", "mypy-boto3-stepfunctions (>=1.34.0,<1.35.0)", "mypy-boto3-storagegateway (>=1.34.0,<1.35.0)", "mypy-boto3-sts (>=1.34.0,<1.35.0)", "mypy-boto3-supplychain (>=1.34.0,<1.35.0)", "mypy-boto3-support (>=1.34.0,<1.35.0)", "mypy-boto3-support-app (>=1.34.0,<1.35.0)", "mypy-boto3-swf (>=1.34.0,<1.35.0)", "mypy-boto3-synthetics (>=1.34.0,<1.35.0)", "mypy-boto3-textract (>=1.34.0,<1.35.0)", "mypy-boto3-timestream-query (>=1.34.0,<1.35.0)", "mypy-boto3-timestream-write (>=1.34.0,<1.35.0)", "mypy-boto3-tnb (>=1.34.0,<1.35.0)", "mypy-boto3-transcribe (>=1.34.0,<1.35.0)", "mypy-boto3-transfer (>=1.34.0,<1.35.0)", "mypy-boto3-translate (>=1.34.0,<1.35.0)", "mypy-boto3-trustedadvisor (>=1.34.0,<1.35.0)", "mypy-boto3-verifiedpermissions (>=1.34.0,<1.35.0)", "mypy-boto3-voice-id (>=1.34.0,<1.35.0)", "mypy-boto3-vpc-lattice (>=1.34.0,<1.35.0)", "mypy-boto3-waf (>=1.34.0,<1.35.0)", "mypy-boto3-waf-regional (>=1.34.0,<1.35.0)", "mypy-boto3-wafv2 (>=1.34.0,<1.35.0)", "mypy-boto3-wellarchitected (>=1.34.0,<1.35.0)", "mypy-boto3-wisdom (>=1.34.0,<1.35.0)", "mypy-boto3-workdocs (>=1.34.0,<1.35.0)", "mypy-boto3-worklink (>=1.34.0,<1.35.0)", "mypy-boto3-workmail (>=1.34.0,<1.35.0)", "mypy-boto3-workmailmessageflow (>=1.34.0,<1.35.0)", "mypy-boto3-workspaces (>=1.34.0,<1.35.0)", "mypy-boto3-workspaces-thin-client (>=1.34.0,<1.35.0)", "mypy-boto3-workspaces-web (>=1.34.0,<1.35.0)", "mypy-boto3-xray (>=1.34.0,<1.35.0)"] 93 | amp = ["mypy-boto3-amp (>=1.34.0,<1.35.0)"] 94 | amplify = ["mypy-boto3-amplify (>=1.34.0,<1.35.0)"] 95 | amplifybackend = ["mypy-boto3-amplifybackend (>=1.34.0,<1.35.0)"] 96 | amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.34.0,<1.35.0)"] 97 | apigateway = ["mypy-boto3-apigateway (>=1.34.0,<1.35.0)"] 98 | apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.34.0,<1.35.0)"] 99 | apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.34.0,<1.35.0)"] 100 | appconfig = ["mypy-boto3-appconfig (>=1.34.0,<1.35.0)"] 101 | appconfigdata = ["mypy-boto3-appconfigdata (>=1.34.0,<1.35.0)"] 102 | appfabric = ["mypy-boto3-appfabric (>=1.34.0,<1.35.0)"] 103 | appflow = ["mypy-boto3-appflow (>=1.34.0,<1.35.0)"] 104 | appintegrations = ["mypy-boto3-appintegrations (>=1.34.0,<1.35.0)"] 105 | application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.34.0,<1.35.0)"] 106 | application-insights = ["mypy-boto3-application-insights (>=1.34.0,<1.35.0)"] 107 | applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.34.0,<1.35.0)"] 108 | appmesh = ["mypy-boto3-appmesh (>=1.34.0,<1.35.0)"] 109 | apprunner = ["mypy-boto3-apprunner (>=1.34.0,<1.35.0)"] 110 | appstream = ["mypy-boto3-appstream (>=1.34.0,<1.35.0)"] 111 | appsync = ["mypy-boto3-appsync (>=1.34.0,<1.35.0)"] 112 | arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.34.0,<1.35.0)"] 113 | athena = ["mypy-boto3-athena (>=1.34.0,<1.35.0)"] 114 | auditmanager = ["mypy-boto3-auditmanager (>=1.34.0,<1.35.0)"] 115 | autoscaling = ["mypy-boto3-autoscaling (>=1.34.0,<1.35.0)"] 116 | autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.34.0,<1.35.0)"] 117 | b2bi = ["mypy-boto3-b2bi (>=1.34.0,<1.35.0)"] 118 | backup = ["mypy-boto3-backup (>=1.34.0,<1.35.0)"] 119 | backup-gateway = ["mypy-boto3-backup-gateway (>=1.34.0,<1.35.0)"] 120 | backupstorage = ["mypy-boto3-backupstorage (>=1.34.0,<1.35.0)"] 121 | batch = ["mypy-boto3-batch (>=1.34.0,<1.35.0)"] 122 | bcm-data-exports = ["mypy-boto3-bcm-data-exports (>=1.34.0,<1.35.0)"] 123 | bedrock = ["mypy-boto3-bedrock (>=1.34.0,<1.35.0)"] 124 | bedrock-agent = ["mypy-boto3-bedrock-agent (>=1.34.0,<1.35.0)"] 125 | bedrock-agent-runtime = ["mypy-boto3-bedrock-agent-runtime (>=1.34.0,<1.35.0)"] 126 | bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.34.0,<1.35.0)"] 127 | billingconductor = ["mypy-boto3-billingconductor (>=1.34.0,<1.35.0)"] 128 | boto3 = ["boto3 (==1.34.19)", "botocore (==1.34.19)"] 129 | braket = ["mypy-boto3-braket (>=1.34.0,<1.35.0)"] 130 | budgets = ["mypy-boto3-budgets (>=1.34.0,<1.35.0)"] 131 | ce = ["mypy-boto3-ce (>=1.34.0,<1.35.0)"] 132 | chime = ["mypy-boto3-chime (>=1.34.0,<1.35.0)"] 133 | chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.34.0,<1.35.0)"] 134 | chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.34.0,<1.35.0)"] 135 | chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.34.0,<1.35.0)"] 136 | chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.34.0,<1.35.0)"] 137 | chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.34.0,<1.35.0)"] 138 | cleanrooms = ["mypy-boto3-cleanrooms (>=1.34.0,<1.35.0)"] 139 | cleanroomsml = ["mypy-boto3-cleanroomsml (>=1.34.0,<1.35.0)"] 140 | cloud9 = ["mypy-boto3-cloud9 (>=1.34.0,<1.35.0)"] 141 | cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.34.0,<1.35.0)"] 142 | clouddirectory = ["mypy-boto3-clouddirectory (>=1.34.0,<1.35.0)"] 143 | cloudformation = ["mypy-boto3-cloudformation (>=1.34.0,<1.35.0)"] 144 | cloudfront = ["mypy-boto3-cloudfront (>=1.34.0,<1.35.0)"] 145 | cloudfront-keyvaluestore = ["mypy-boto3-cloudfront-keyvaluestore (>=1.34.0,<1.35.0)"] 146 | cloudhsm = ["mypy-boto3-cloudhsm (>=1.34.0,<1.35.0)"] 147 | cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.34.0,<1.35.0)"] 148 | cloudsearch = ["mypy-boto3-cloudsearch (>=1.34.0,<1.35.0)"] 149 | cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.34.0,<1.35.0)"] 150 | cloudtrail = ["mypy-boto3-cloudtrail (>=1.34.0,<1.35.0)"] 151 | cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.34.0,<1.35.0)"] 152 | cloudwatch = ["mypy-boto3-cloudwatch (>=1.34.0,<1.35.0)"] 153 | codeartifact = ["mypy-boto3-codeartifact (>=1.34.0,<1.35.0)"] 154 | codebuild = ["mypy-boto3-codebuild (>=1.34.0,<1.35.0)"] 155 | codecatalyst = ["mypy-boto3-codecatalyst (>=1.34.0,<1.35.0)"] 156 | codecommit = ["mypy-boto3-codecommit (>=1.34.0,<1.35.0)"] 157 | codedeploy = ["mypy-boto3-codedeploy (>=1.34.0,<1.35.0)"] 158 | codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.34.0,<1.35.0)"] 159 | codeguru-security = ["mypy-boto3-codeguru-security (>=1.34.0,<1.35.0)"] 160 | codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.34.0,<1.35.0)"] 161 | codepipeline = ["mypy-boto3-codepipeline (>=1.34.0,<1.35.0)"] 162 | codestar = ["mypy-boto3-codestar (>=1.34.0,<1.35.0)"] 163 | codestar-connections = ["mypy-boto3-codestar-connections (>=1.34.0,<1.35.0)"] 164 | codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.34.0,<1.35.0)"] 165 | cognito-identity = ["mypy-boto3-cognito-identity (>=1.34.0,<1.35.0)"] 166 | cognito-idp = ["mypy-boto3-cognito-idp (>=1.34.0,<1.35.0)"] 167 | cognito-sync = ["mypy-boto3-cognito-sync (>=1.34.0,<1.35.0)"] 168 | comprehend = ["mypy-boto3-comprehend (>=1.34.0,<1.35.0)"] 169 | comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.34.0,<1.35.0)"] 170 | compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.34.0,<1.35.0)"] 171 | config = ["mypy-boto3-config (>=1.34.0,<1.35.0)"] 172 | connect = ["mypy-boto3-connect (>=1.34.0,<1.35.0)"] 173 | connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.34.0,<1.35.0)"] 174 | connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.34.0,<1.35.0)"] 175 | connectcases = ["mypy-boto3-connectcases (>=1.34.0,<1.35.0)"] 176 | connectparticipant = ["mypy-boto3-connectparticipant (>=1.34.0,<1.35.0)"] 177 | controltower = ["mypy-boto3-controltower (>=1.34.0,<1.35.0)"] 178 | cost-optimization-hub = ["mypy-boto3-cost-optimization-hub (>=1.34.0,<1.35.0)"] 179 | cur = ["mypy-boto3-cur (>=1.34.0,<1.35.0)"] 180 | customer-profiles = ["mypy-boto3-customer-profiles (>=1.34.0,<1.35.0)"] 181 | databrew = ["mypy-boto3-databrew (>=1.34.0,<1.35.0)"] 182 | dataexchange = ["mypy-boto3-dataexchange (>=1.34.0,<1.35.0)"] 183 | datapipeline = ["mypy-boto3-datapipeline (>=1.34.0,<1.35.0)"] 184 | datasync = ["mypy-boto3-datasync (>=1.34.0,<1.35.0)"] 185 | datazone = ["mypy-boto3-datazone (>=1.34.0,<1.35.0)"] 186 | dax = ["mypy-boto3-dax (>=1.34.0,<1.35.0)"] 187 | detective = ["mypy-boto3-detective (>=1.34.0,<1.35.0)"] 188 | devicefarm = ["mypy-boto3-devicefarm (>=1.34.0,<1.35.0)"] 189 | devops-guru = ["mypy-boto3-devops-guru (>=1.34.0,<1.35.0)"] 190 | directconnect = ["mypy-boto3-directconnect (>=1.34.0,<1.35.0)"] 191 | discovery = ["mypy-boto3-discovery (>=1.34.0,<1.35.0)"] 192 | dlm = ["mypy-boto3-dlm (>=1.34.0,<1.35.0)"] 193 | dms = ["mypy-boto3-dms (>=1.34.0,<1.35.0)"] 194 | docdb = ["mypy-boto3-docdb (>=1.34.0,<1.35.0)"] 195 | docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.34.0,<1.35.0)"] 196 | drs = ["mypy-boto3-drs (>=1.34.0,<1.35.0)"] 197 | ds = ["mypy-boto3-ds (>=1.34.0,<1.35.0)"] 198 | dynamodb = ["mypy-boto3-dynamodb (>=1.34.0,<1.35.0)"] 199 | dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.34.0,<1.35.0)"] 200 | ebs = ["mypy-boto3-ebs (>=1.34.0,<1.35.0)"] 201 | ec2 = ["mypy-boto3-ec2 (>=1.34.0,<1.35.0)"] 202 | ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.34.0,<1.35.0)"] 203 | ecr = ["mypy-boto3-ecr (>=1.34.0,<1.35.0)"] 204 | ecr-public = ["mypy-boto3-ecr-public (>=1.34.0,<1.35.0)"] 205 | ecs = ["mypy-boto3-ecs (>=1.34.0,<1.35.0)"] 206 | efs = ["mypy-boto3-efs (>=1.34.0,<1.35.0)"] 207 | eks = ["mypy-boto3-eks (>=1.34.0,<1.35.0)"] 208 | eks-auth = ["mypy-boto3-eks-auth (>=1.34.0,<1.35.0)"] 209 | elastic-inference = ["mypy-boto3-elastic-inference (>=1.34.0,<1.35.0)"] 210 | elasticache = ["mypy-boto3-elasticache (>=1.34.0,<1.35.0)"] 211 | elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.34.0,<1.35.0)"] 212 | elastictranscoder = ["mypy-boto3-elastictranscoder (>=1.34.0,<1.35.0)"] 213 | elb = ["mypy-boto3-elb (>=1.34.0,<1.35.0)"] 214 | elbv2 = ["mypy-boto3-elbv2 (>=1.34.0,<1.35.0)"] 215 | emr = ["mypy-boto3-emr (>=1.34.0,<1.35.0)"] 216 | emr-containers = ["mypy-boto3-emr-containers (>=1.34.0,<1.35.0)"] 217 | emr-serverless = ["mypy-boto3-emr-serverless (>=1.34.0,<1.35.0)"] 218 | entityresolution = ["mypy-boto3-entityresolution (>=1.34.0,<1.35.0)"] 219 | es = ["mypy-boto3-es (>=1.34.0,<1.35.0)"] 220 | essential = ["mypy-boto3-cloudformation (>=1.34.0,<1.35.0)", "mypy-boto3-dynamodb (>=1.34.0,<1.35.0)", "mypy-boto3-ec2 (>=1.34.0,<1.35.0)", "mypy-boto3-lambda (>=1.34.0,<1.35.0)", "mypy-boto3-rds (>=1.34.0,<1.35.0)", "mypy-boto3-s3 (>=1.34.0,<1.35.0)", "mypy-boto3-sqs (>=1.34.0,<1.35.0)"] 221 | events = ["mypy-boto3-events (>=1.34.0,<1.35.0)"] 222 | evidently = ["mypy-boto3-evidently (>=1.34.0,<1.35.0)"] 223 | finspace = ["mypy-boto3-finspace (>=1.34.0,<1.35.0)"] 224 | finspace-data = ["mypy-boto3-finspace-data (>=1.34.0,<1.35.0)"] 225 | firehose = ["mypy-boto3-firehose (>=1.34.0,<1.35.0)"] 226 | fis = ["mypy-boto3-fis (>=1.34.0,<1.35.0)"] 227 | fms = ["mypy-boto3-fms (>=1.34.0,<1.35.0)"] 228 | forecast = ["mypy-boto3-forecast (>=1.34.0,<1.35.0)"] 229 | forecastquery = ["mypy-boto3-forecastquery (>=1.34.0,<1.35.0)"] 230 | frauddetector = ["mypy-boto3-frauddetector (>=1.34.0,<1.35.0)"] 231 | freetier = ["mypy-boto3-freetier (>=1.34.0,<1.35.0)"] 232 | fsx = ["mypy-boto3-fsx (>=1.34.0,<1.35.0)"] 233 | gamelift = ["mypy-boto3-gamelift (>=1.34.0,<1.35.0)"] 234 | glacier = ["mypy-boto3-glacier (>=1.34.0,<1.35.0)"] 235 | globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.34.0,<1.35.0)"] 236 | glue = ["mypy-boto3-glue (>=1.34.0,<1.35.0)"] 237 | grafana = ["mypy-boto3-grafana (>=1.34.0,<1.35.0)"] 238 | greengrass = ["mypy-boto3-greengrass (>=1.34.0,<1.35.0)"] 239 | greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.34.0,<1.35.0)"] 240 | groundstation = ["mypy-boto3-groundstation (>=1.34.0,<1.35.0)"] 241 | guardduty = ["mypy-boto3-guardduty (>=1.34.0,<1.35.0)"] 242 | health = ["mypy-boto3-health (>=1.34.0,<1.35.0)"] 243 | healthlake = ["mypy-boto3-healthlake (>=1.34.0,<1.35.0)"] 244 | honeycode = ["mypy-boto3-honeycode (>=1.34.0,<1.35.0)"] 245 | iam = ["mypy-boto3-iam (>=1.34.0,<1.35.0)"] 246 | identitystore = ["mypy-boto3-identitystore (>=1.34.0,<1.35.0)"] 247 | imagebuilder = ["mypy-boto3-imagebuilder (>=1.34.0,<1.35.0)"] 248 | importexport = ["mypy-boto3-importexport (>=1.34.0,<1.35.0)"] 249 | inspector = ["mypy-boto3-inspector (>=1.34.0,<1.35.0)"] 250 | inspector-scan = ["mypy-boto3-inspector-scan (>=1.34.0,<1.35.0)"] 251 | inspector2 = ["mypy-boto3-inspector2 (>=1.34.0,<1.35.0)"] 252 | internetmonitor = ["mypy-boto3-internetmonitor (>=1.34.0,<1.35.0)"] 253 | iot = ["mypy-boto3-iot (>=1.34.0,<1.35.0)"] 254 | iot-data = ["mypy-boto3-iot-data (>=1.34.0,<1.35.0)"] 255 | iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.34.0,<1.35.0)"] 256 | iot-roborunner = ["mypy-boto3-iot-roborunner (>=1.34.0,<1.35.0)"] 257 | iot1click-devices = ["mypy-boto3-iot1click-devices (>=1.34.0,<1.35.0)"] 258 | iot1click-projects = ["mypy-boto3-iot1click-projects (>=1.34.0,<1.35.0)"] 259 | iotanalytics = ["mypy-boto3-iotanalytics (>=1.34.0,<1.35.0)"] 260 | iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.34.0,<1.35.0)"] 261 | iotevents = ["mypy-boto3-iotevents (>=1.34.0,<1.35.0)"] 262 | iotevents-data = ["mypy-boto3-iotevents-data (>=1.34.0,<1.35.0)"] 263 | iotfleethub = ["mypy-boto3-iotfleethub (>=1.34.0,<1.35.0)"] 264 | iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.34.0,<1.35.0)"] 265 | iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.34.0,<1.35.0)"] 266 | iotsitewise = ["mypy-boto3-iotsitewise (>=1.34.0,<1.35.0)"] 267 | iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.34.0,<1.35.0)"] 268 | iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.34.0,<1.35.0)"] 269 | iotwireless = ["mypy-boto3-iotwireless (>=1.34.0,<1.35.0)"] 270 | ivs = ["mypy-boto3-ivs (>=1.34.0,<1.35.0)"] 271 | ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.34.0,<1.35.0)"] 272 | ivschat = ["mypy-boto3-ivschat (>=1.34.0,<1.35.0)"] 273 | kafka = ["mypy-boto3-kafka (>=1.34.0,<1.35.0)"] 274 | kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.34.0,<1.35.0)"] 275 | kendra = ["mypy-boto3-kendra (>=1.34.0,<1.35.0)"] 276 | kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.34.0,<1.35.0)"] 277 | keyspaces = ["mypy-boto3-keyspaces (>=1.34.0,<1.35.0)"] 278 | kinesis = ["mypy-boto3-kinesis (>=1.34.0,<1.35.0)"] 279 | kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.34.0,<1.35.0)"] 280 | kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.34.0,<1.35.0)"] 281 | kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.34.0,<1.35.0)"] 282 | kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.34.0,<1.35.0)"] 283 | kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.34.0,<1.35.0)"] 284 | kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.34.0,<1.35.0)"] 285 | kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.34.0,<1.35.0)"] 286 | kms = ["mypy-boto3-kms (>=1.34.0,<1.35.0)"] 287 | lakeformation = ["mypy-boto3-lakeformation (>=1.34.0,<1.35.0)"] 288 | lambda = ["mypy-boto3-lambda (>=1.34.0,<1.35.0)"] 289 | launch-wizard = ["mypy-boto3-launch-wizard (>=1.34.0,<1.35.0)"] 290 | lex-models = ["mypy-boto3-lex-models (>=1.34.0,<1.35.0)"] 291 | lex-runtime = ["mypy-boto3-lex-runtime (>=1.34.0,<1.35.0)"] 292 | lexv2-models = ["mypy-boto3-lexv2-models (>=1.34.0,<1.35.0)"] 293 | lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.34.0,<1.35.0)"] 294 | license-manager = ["mypy-boto3-license-manager (>=1.34.0,<1.35.0)"] 295 | license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.34.0,<1.35.0)"] 296 | license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.34.0,<1.35.0)"] 297 | lightsail = ["mypy-boto3-lightsail (>=1.34.0,<1.35.0)"] 298 | location = ["mypy-boto3-location (>=1.34.0,<1.35.0)"] 299 | logs = ["mypy-boto3-logs (>=1.34.0,<1.35.0)"] 300 | lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.34.0,<1.35.0)"] 301 | lookoutmetrics = ["mypy-boto3-lookoutmetrics (>=1.34.0,<1.35.0)"] 302 | lookoutvision = ["mypy-boto3-lookoutvision (>=1.34.0,<1.35.0)"] 303 | m2 = ["mypy-boto3-m2 (>=1.34.0,<1.35.0)"] 304 | machinelearning = ["mypy-boto3-machinelearning (>=1.34.0,<1.35.0)"] 305 | macie2 = ["mypy-boto3-macie2 (>=1.34.0,<1.35.0)"] 306 | managedblockchain = ["mypy-boto3-managedblockchain (>=1.34.0,<1.35.0)"] 307 | managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.34.0,<1.35.0)"] 308 | marketplace-agreement = ["mypy-boto3-marketplace-agreement (>=1.34.0,<1.35.0)"] 309 | marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.34.0,<1.35.0)"] 310 | marketplace-deployment = ["mypy-boto3-marketplace-deployment (>=1.34.0,<1.35.0)"] 311 | marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.34.0,<1.35.0)"] 312 | marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.34.0,<1.35.0)"] 313 | mediaconnect = ["mypy-boto3-mediaconnect (>=1.34.0,<1.35.0)"] 314 | mediaconvert = ["mypy-boto3-mediaconvert (>=1.34.0,<1.35.0)"] 315 | medialive = ["mypy-boto3-medialive (>=1.34.0,<1.35.0)"] 316 | mediapackage = ["mypy-boto3-mediapackage (>=1.34.0,<1.35.0)"] 317 | mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.34.0,<1.35.0)"] 318 | mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.34.0,<1.35.0)"] 319 | mediastore = ["mypy-boto3-mediastore (>=1.34.0,<1.35.0)"] 320 | mediastore-data = ["mypy-boto3-mediastore-data (>=1.34.0,<1.35.0)"] 321 | mediatailor = ["mypy-boto3-mediatailor (>=1.34.0,<1.35.0)"] 322 | medical-imaging = ["mypy-boto3-medical-imaging (>=1.34.0,<1.35.0)"] 323 | memorydb = ["mypy-boto3-memorydb (>=1.34.0,<1.35.0)"] 324 | meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.34.0,<1.35.0)"] 325 | mgh = ["mypy-boto3-mgh (>=1.34.0,<1.35.0)"] 326 | mgn = ["mypy-boto3-mgn (>=1.34.0,<1.35.0)"] 327 | migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.34.0,<1.35.0)"] 328 | migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.34.0,<1.35.0)"] 329 | migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.34.0,<1.35.0)"] 330 | migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.34.0,<1.35.0)"] 331 | mobile = ["mypy-boto3-mobile (>=1.34.0,<1.35.0)"] 332 | mq = ["mypy-boto3-mq (>=1.34.0,<1.35.0)"] 333 | mturk = ["mypy-boto3-mturk (>=1.34.0,<1.35.0)"] 334 | mwaa = ["mypy-boto3-mwaa (>=1.34.0,<1.35.0)"] 335 | neptune = ["mypy-boto3-neptune (>=1.34.0,<1.35.0)"] 336 | neptune-graph = ["mypy-boto3-neptune-graph (>=1.34.0,<1.35.0)"] 337 | neptunedata = ["mypy-boto3-neptunedata (>=1.34.0,<1.35.0)"] 338 | network-firewall = ["mypy-boto3-network-firewall (>=1.34.0,<1.35.0)"] 339 | networkmanager = ["mypy-boto3-networkmanager (>=1.34.0,<1.35.0)"] 340 | networkmonitor = ["mypy-boto3-networkmonitor (>=1.34.0,<1.35.0)"] 341 | nimble = ["mypy-boto3-nimble (>=1.34.0,<1.35.0)"] 342 | oam = ["mypy-boto3-oam (>=1.34.0,<1.35.0)"] 343 | omics = ["mypy-boto3-omics (>=1.34.0,<1.35.0)"] 344 | opensearch = ["mypy-boto3-opensearch (>=1.34.0,<1.35.0)"] 345 | opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.34.0,<1.35.0)"] 346 | opsworks = ["mypy-boto3-opsworks (>=1.34.0,<1.35.0)"] 347 | opsworkscm = ["mypy-boto3-opsworkscm (>=1.34.0,<1.35.0)"] 348 | organizations = ["mypy-boto3-organizations (>=1.34.0,<1.35.0)"] 349 | osis = ["mypy-boto3-osis (>=1.34.0,<1.35.0)"] 350 | outposts = ["mypy-boto3-outposts (>=1.34.0,<1.35.0)"] 351 | panorama = ["mypy-boto3-panorama (>=1.34.0,<1.35.0)"] 352 | payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.34.0,<1.35.0)"] 353 | payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.34.0,<1.35.0)"] 354 | pca-connector-ad = ["mypy-boto3-pca-connector-ad (>=1.34.0,<1.35.0)"] 355 | personalize = ["mypy-boto3-personalize (>=1.34.0,<1.35.0)"] 356 | personalize-events = ["mypy-boto3-personalize-events (>=1.34.0,<1.35.0)"] 357 | personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.34.0,<1.35.0)"] 358 | pi = ["mypy-boto3-pi (>=1.34.0,<1.35.0)"] 359 | pinpoint = ["mypy-boto3-pinpoint (>=1.34.0,<1.35.0)"] 360 | pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.34.0,<1.35.0)"] 361 | pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.34.0,<1.35.0)"] 362 | pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.34.0,<1.35.0)"] 363 | pipes = ["mypy-boto3-pipes (>=1.34.0,<1.35.0)"] 364 | polly = ["mypy-boto3-polly (>=1.34.0,<1.35.0)"] 365 | pricing = ["mypy-boto3-pricing (>=1.34.0,<1.35.0)"] 366 | privatenetworks = ["mypy-boto3-privatenetworks (>=1.34.0,<1.35.0)"] 367 | proton = ["mypy-boto3-proton (>=1.34.0,<1.35.0)"] 368 | qbusiness = ["mypy-boto3-qbusiness (>=1.34.0,<1.35.0)"] 369 | qconnect = ["mypy-boto3-qconnect (>=1.34.0,<1.35.0)"] 370 | qldb = ["mypy-boto3-qldb (>=1.34.0,<1.35.0)"] 371 | qldb-session = ["mypy-boto3-qldb-session (>=1.34.0,<1.35.0)"] 372 | quicksight = ["mypy-boto3-quicksight (>=1.34.0,<1.35.0)"] 373 | ram = ["mypy-boto3-ram (>=1.34.0,<1.35.0)"] 374 | rbin = ["mypy-boto3-rbin (>=1.34.0,<1.35.0)"] 375 | rds = ["mypy-boto3-rds (>=1.34.0,<1.35.0)"] 376 | rds-data = ["mypy-boto3-rds-data (>=1.34.0,<1.35.0)"] 377 | redshift = ["mypy-boto3-redshift (>=1.34.0,<1.35.0)"] 378 | redshift-data = ["mypy-boto3-redshift-data (>=1.34.0,<1.35.0)"] 379 | redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.34.0,<1.35.0)"] 380 | rekognition = ["mypy-boto3-rekognition (>=1.34.0,<1.35.0)"] 381 | repostspace = ["mypy-boto3-repostspace (>=1.34.0,<1.35.0)"] 382 | resiliencehub = ["mypy-boto3-resiliencehub (>=1.34.0,<1.35.0)"] 383 | resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.34.0,<1.35.0)"] 384 | resource-groups = ["mypy-boto3-resource-groups (>=1.34.0,<1.35.0)"] 385 | resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.34.0,<1.35.0)"] 386 | robomaker = ["mypy-boto3-robomaker (>=1.34.0,<1.35.0)"] 387 | rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.34.0,<1.35.0)"] 388 | route53 = ["mypy-boto3-route53 (>=1.34.0,<1.35.0)"] 389 | route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.34.0,<1.35.0)"] 390 | route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.34.0,<1.35.0)"] 391 | route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.34.0,<1.35.0)"] 392 | route53domains = ["mypy-boto3-route53domains (>=1.34.0,<1.35.0)"] 393 | route53resolver = ["mypy-boto3-route53resolver (>=1.34.0,<1.35.0)"] 394 | rum = ["mypy-boto3-rum (>=1.34.0,<1.35.0)"] 395 | s3 = ["mypy-boto3-s3 (>=1.34.0,<1.35.0)"] 396 | s3control = ["mypy-boto3-s3control (>=1.34.0,<1.35.0)"] 397 | s3outposts = ["mypy-boto3-s3outposts (>=1.34.0,<1.35.0)"] 398 | sagemaker = ["mypy-boto3-sagemaker (>=1.34.0,<1.35.0)"] 399 | sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.34.0,<1.35.0)"] 400 | sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.34.0,<1.35.0)"] 401 | sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.34.0,<1.35.0)"] 402 | sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.34.0,<1.35.0)"] 403 | sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.34.0,<1.35.0)"] 404 | sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.34.0,<1.35.0)"] 405 | savingsplans = ["mypy-boto3-savingsplans (>=1.34.0,<1.35.0)"] 406 | scheduler = ["mypy-boto3-scheduler (>=1.34.0,<1.35.0)"] 407 | schemas = ["mypy-boto3-schemas (>=1.34.0,<1.35.0)"] 408 | sdb = ["mypy-boto3-sdb (>=1.34.0,<1.35.0)"] 409 | secretsmanager = ["mypy-boto3-secretsmanager (>=1.34.0,<1.35.0)"] 410 | securityhub = ["mypy-boto3-securityhub (>=1.34.0,<1.35.0)"] 411 | securitylake = ["mypy-boto3-securitylake (>=1.34.0,<1.35.0)"] 412 | serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.34.0,<1.35.0)"] 413 | service-quotas = ["mypy-boto3-service-quotas (>=1.34.0,<1.35.0)"] 414 | servicecatalog = ["mypy-boto3-servicecatalog (>=1.34.0,<1.35.0)"] 415 | servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.34.0,<1.35.0)"] 416 | servicediscovery = ["mypy-boto3-servicediscovery (>=1.34.0,<1.35.0)"] 417 | ses = ["mypy-boto3-ses (>=1.34.0,<1.35.0)"] 418 | sesv2 = ["mypy-boto3-sesv2 (>=1.34.0,<1.35.0)"] 419 | shield = ["mypy-boto3-shield (>=1.34.0,<1.35.0)"] 420 | signer = ["mypy-boto3-signer (>=1.34.0,<1.35.0)"] 421 | simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.34.0,<1.35.0)"] 422 | sms = ["mypy-boto3-sms (>=1.34.0,<1.35.0)"] 423 | sms-voice = ["mypy-boto3-sms-voice (>=1.34.0,<1.35.0)"] 424 | snow-device-management = ["mypy-boto3-snow-device-management (>=1.34.0,<1.35.0)"] 425 | snowball = ["mypy-boto3-snowball (>=1.34.0,<1.35.0)"] 426 | sns = ["mypy-boto3-sns (>=1.34.0,<1.35.0)"] 427 | sqs = ["mypy-boto3-sqs (>=1.34.0,<1.35.0)"] 428 | ssm = ["mypy-boto3-ssm (>=1.34.0,<1.35.0)"] 429 | ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.34.0,<1.35.0)"] 430 | ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.34.0,<1.35.0)"] 431 | ssm-sap = ["mypy-boto3-ssm-sap (>=1.34.0,<1.35.0)"] 432 | sso = ["mypy-boto3-sso (>=1.34.0,<1.35.0)"] 433 | sso-admin = ["mypy-boto3-sso-admin (>=1.34.0,<1.35.0)"] 434 | sso-oidc = ["mypy-boto3-sso-oidc (>=1.34.0,<1.35.0)"] 435 | stepfunctions = ["mypy-boto3-stepfunctions (>=1.34.0,<1.35.0)"] 436 | storagegateway = ["mypy-boto3-storagegateway (>=1.34.0,<1.35.0)"] 437 | sts = ["mypy-boto3-sts (>=1.34.0,<1.35.0)"] 438 | supplychain = ["mypy-boto3-supplychain (>=1.34.0,<1.35.0)"] 439 | support = ["mypy-boto3-support (>=1.34.0,<1.35.0)"] 440 | support-app = ["mypy-boto3-support-app (>=1.34.0,<1.35.0)"] 441 | swf = ["mypy-boto3-swf (>=1.34.0,<1.35.0)"] 442 | synthetics = ["mypy-boto3-synthetics (>=1.34.0,<1.35.0)"] 443 | textract = ["mypy-boto3-textract (>=1.34.0,<1.35.0)"] 444 | timestream-query = ["mypy-boto3-timestream-query (>=1.34.0,<1.35.0)"] 445 | timestream-write = ["mypy-boto3-timestream-write (>=1.34.0,<1.35.0)"] 446 | tnb = ["mypy-boto3-tnb (>=1.34.0,<1.35.0)"] 447 | transcribe = ["mypy-boto3-transcribe (>=1.34.0,<1.35.0)"] 448 | transfer = ["mypy-boto3-transfer (>=1.34.0,<1.35.0)"] 449 | translate = ["mypy-boto3-translate (>=1.34.0,<1.35.0)"] 450 | trustedadvisor = ["mypy-boto3-trustedadvisor (>=1.34.0,<1.35.0)"] 451 | verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.34.0,<1.35.0)"] 452 | voice-id = ["mypy-boto3-voice-id (>=1.34.0,<1.35.0)"] 453 | vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.34.0,<1.35.0)"] 454 | waf = ["mypy-boto3-waf (>=1.34.0,<1.35.0)"] 455 | waf-regional = ["mypy-boto3-waf-regional (>=1.34.0,<1.35.0)"] 456 | wafv2 = ["mypy-boto3-wafv2 (>=1.34.0,<1.35.0)"] 457 | wellarchitected = ["mypy-boto3-wellarchitected (>=1.34.0,<1.35.0)"] 458 | wisdom = ["mypy-boto3-wisdom (>=1.34.0,<1.35.0)"] 459 | workdocs = ["mypy-boto3-workdocs (>=1.34.0,<1.35.0)"] 460 | worklink = ["mypy-boto3-worklink (>=1.34.0,<1.35.0)"] 461 | workmail = ["mypy-boto3-workmail (>=1.34.0,<1.35.0)"] 462 | workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.34.0,<1.35.0)"] 463 | workspaces = ["mypy-boto3-workspaces (>=1.34.0,<1.35.0)"] 464 | workspaces-thin-client = ["mypy-boto3-workspaces-thin-client (>=1.34.0,<1.35.0)"] 465 | workspaces-web = ["mypy-boto3-workspaces-web (>=1.34.0,<1.35.0)"] 466 | xray = ["mypy-boto3-xray (>=1.34.0,<1.35.0)"] 467 | 468 | [[package]] 469 | name = "botocore" 470 | version = "1.34.19" 471 | description = "Low-level, data-driven core of boto 3." 472 | optional = false 473 | python-versions = ">= 3.8" 474 | files = [ 475 | {file = "botocore-1.34.19-py3-none-any.whl", hash = "sha256:a4a39c7092960f5da2439efc5f6220730dab634aaff4c1444bbd1dfa43bc28cc"}, 476 | {file = "botocore-1.34.19.tar.gz", hash = "sha256:64352b2f05de5c6ab025c1d5232880c22775356dcc5a53d798a6f65db847e826"}, 477 | ] 478 | 479 | [package.dependencies] 480 | jmespath = ">=0.7.1,<2.0.0" 481 | python-dateutil = ">=2.1,<3.0.0" 482 | urllib3 = [ 483 | {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, 484 | {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""}, 485 | ] 486 | 487 | [package.extras] 488 | crt = ["awscrt (==0.19.19)"] 489 | 490 | [[package]] 491 | name = "botocore-stubs" 492 | version = "1.34.19" 493 | description = "Type annotations and code completion for botocore" 494 | optional = false 495 | python-versions = ">=3.8,<4.0" 496 | files = [ 497 | {file = "botocore_stubs-1.34.19-py3-none-any.whl", hash = "sha256:141b6727a7258fc16826f21c7369ca7d38a83848a03ae13fe50c4d1e677a7545"}, 498 | {file = "botocore_stubs-1.34.19.tar.gz", hash = "sha256:dd1bff75bfe0a64e6704a8d74e80f94a1978d58cb2810f7549fa1c83ac8e458c"}, 499 | ] 500 | 501 | [package.dependencies] 502 | types-awscrt = "*" 503 | typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.9\""} 504 | 505 | [package.extras] 506 | botocore = ["botocore"] 507 | 508 | [[package]] 509 | name = "bump2version" 510 | version = "1.0.1" 511 | description = "Version-bump your software with a single command!" 512 | optional = false 513 | python-versions = ">=3.5" 514 | files = [ 515 | {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"}, 516 | {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"}, 517 | ] 518 | 519 | [[package]] 520 | name = "certifi" 521 | version = "2023.11.17" 522 | description = "Python package for providing Mozilla's CA Bundle." 523 | optional = false 524 | python-versions = ">=3.6" 525 | files = [ 526 | {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, 527 | {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, 528 | ] 529 | 530 | [[package]] 531 | name = "cffi" 532 | version = "1.16.0" 533 | description = "Foreign Function Interface for Python calling C code." 534 | optional = false 535 | python-versions = ">=3.8" 536 | files = [ 537 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 538 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 539 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 540 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 541 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 542 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 543 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 544 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 545 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 546 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 547 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 548 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 549 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 550 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 551 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 552 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 553 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 554 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 555 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 556 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 557 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 558 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 559 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 560 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 561 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 562 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 563 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 564 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 565 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 566 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 567 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 568 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 569 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 570 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 571 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 572 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 573 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 574 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 575 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 576 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 577 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 578 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 579 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 580 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 581 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 582 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 583 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 584 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 585 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 586 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 587 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 588 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 589 | ] 590 | 591 | [package.dependencies] 592 | pycparser = "*" 593 | 594 | [[package]] 595 | name = "charset-normalizer" 596 | version = "3.3.2" 597 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 598 | optional = false 599 | python-versions = ">=3.7.0" 600 | files = [ 601 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 602 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 603 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 604 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 605 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 606 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 607 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 608 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 609 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 610 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 611 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 612 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 613 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 614 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 615 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 616 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 617 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 618 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 619 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 620 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 621 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 622 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 623 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 624 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 625 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 626 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 627 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 628 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 629 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 630 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 631 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 632 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 633 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 634 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 635 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 636 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 637 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 638 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 639 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 640 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 641 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 642 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 643 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 644 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 645 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 646 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 647 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 648 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 649 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 650 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 651 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 652 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 653 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 654 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 655 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 656 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 657 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 658 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 659 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 660 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 661 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 662 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 663 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 664 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 665 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 666 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 667 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 668 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 669 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 670 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 671 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 672 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 673 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 674 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 675 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 676 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 677 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 678 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 679 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 680 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 681 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 682 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 683 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 684 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 685 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 686 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 687 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 688 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 689 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 690 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 691 | ] 692 | 693 | [[package]] 694 | name = "click" 695 | version = "8.1.7" 696 | description = "Composable command line interface toolkit" 697 | optional = false 698 | python-versions = ">=3.7" 699 | files = [ 700 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 701 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 702 | ] 703 | 704 | [package.dependencies] 705 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 706 | 707 | [[package]] 708 | name = "colorama" 709 | version = "0.4.6" 710 | description = "Cross-platform colored terminal text." 711 | optional = false 712 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 713 | files = [ 714 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 715 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 716 | ] 717 | 718 | [[package]] 719 | name = "coverage" 720 | version = "7.4.0" 721 | description = "Code coverage measurement for Python" 722 | optional = false 723 | python-versions = ">=3.8" 724 | files = [ 725 | {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, 726 | {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, 727 | {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, 728 | {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, 729 | {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, 730 | {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, 731 | {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, 732 | {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, 733 | {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, 734 | {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, 735 | {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, 736 | {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, 737 | {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, 738 | {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, 739 | {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, 740 | {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, 741 | {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, 742 | {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, 743 | {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, 744 | {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, 745 | {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, 746 | {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, 747 | {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, 748 | {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, 749 | {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, 750 | {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, 751 | {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, 752 | {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, 753 | {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, 754 | {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, 755 | {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, 756 | {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, 757 | {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, 758 | {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, 759 | {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, 760 | {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, 761 | {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, 762 | {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, 763 | {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, 764 | {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, 765 | {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, 766 | {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, 767 | {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, 768 | {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, 769 | {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, 770 | {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, 771 | {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, 772 | {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, 773 | {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, 774 | {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, 775 | {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, 776 | {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, 777 | ] 778 | 779 | [package.dependencies] 780 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 781 | 782 | [package.extras] 783 | toml = ["tomli"] 784 | 785 | [[package]] 786 | name = "cryptography" 787 | version = "41.0.7" 788 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 789 | optional = false 790 | python-versions = ">=3.7" 791 | files = [ 792 | {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, 793 | {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, 794 | {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, 795 | {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, 796 | {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, 797 | {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, 798 | {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, 799 | {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, 800 | {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, 801 | {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, 802 | {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, 803 | {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, 804 | {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, 805 | {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, 806 | {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, 807 | {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, 808 | {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, 809 | {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, 810 | {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, 811 | {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, 812 | {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, 813 | {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, 814 | {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, 815 | ] 816 | 817 | [package.dependencies] 818 | cffi = ">=1.12" 819 | 820 | [package.extras] 821 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 822 | docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] 823 | nox = ["nox"] 824 | pep8test = ["black", "check-sdist", "mypy", "ruff"] 825 | sdist = ["build"] 826 | ssh = ["bcrypt (>=3.1.5)"] 827 | test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 828 | test-randomorder = ["pytest-randomly"] 829 | 830 | [[package]] 831 | name = "exceptiongroup" 832 | version = "1.2.0" 833 | description = "Backport of PEP 654 (exception groups)" 834 | optional = false 835 | python-versions = ">=3.7" 836 | files = [ 837 | {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, 838 | {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, 839 | ] 840 | 841 | [package.extras] 842 | test = ["pytest (>=6)"] 843 | 844 | [[package]] 845 | name = "flake8" 846 | version = "5.0.4" 847 | description = "the modular source code checker: pep8 pyflakes and co" 848 | optional = false 849 | python-versions = ">=3.6.1" 850 | files = [ 851 | {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, 852 | {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, 853 | ] 854 | 855 | [package.dependencies] 856 | mccabe = ">=0.7.0,<0.8.0" 857 | pycodestyle = ">=2.9.0,<2.10.0" 858 | pyflakes = ">=2.5.0,<2.6.0" 859 | 860 | [[package]] 861 | name = "idna" 862 | version = "3.6" 863 | description = "Internationalized Domain Names in Applications (IDNA)" 864 | optional = false 865 | python-versions = ">=3.5" 866 | files = [ 867 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, 868 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, 869 | ] 870 | 871 | [[package]] 872 | name = "iniconfig" 873 | version = "2.0.0" 874 | description = "brain-dead simple config-ini parsing" 875 | optional = false 876 | python-versions = ">=3.7" 877 | files = [ 878 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 879 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 880 | ] 881 | 882 | [[package]] 883 | name = "isort" 884 | version = "5.13.2" 885 | description = "A Python utility / library to sort Python imports." 886 | optional = false 887 | python-versions = ">=3.8.0" 888 | files = [ 889 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 890 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 891 | ] 892 | 893 | [package.extras] 894 | colors = ["colorama (>=0.4.6)"] 895 | 896 | [[package]] 897 | name = "jinja2" 898 | version = "3.1.3" 899 | description = "A very fast and expressive template engine." 900 | optional = false 901 | python-versions = ">=3.7" 902 | files = [ 903 | {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, 904 | {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, 905 | ] 906 | 907 | [package.dependencies] 908 | MarkupSafe = ">=2.0" 909 | 910 | [package.extras] 911 | i18n = ["Babel (>=2.7)"] 912 | 913 | [[package]] 914 | name = "jmespath" 915 | version = "1.0.1" 916 | description = "JSON Matching Expressions" 917 | optional = false 918 | python-versions = ">=3.7" 919 | files = [ 920 | {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, 921 | {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, 922 | ] 923 | 924 | [[package]] 925 | name = "markupsafe" 926 | version = "2.1.3" 927 | description = "Safely add untrusted strings to HTML/XML markup." 928 | optional = false 929 | python-versions = ">=3.7" 930 | files = [ 931 | {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, 932 | {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, 933 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, 934 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, 935 | {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, 936 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, 937 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, 938 | {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, 939 | {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, 940 | {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, 941 | {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, 942 | {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, 943 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, 944 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, 945 | {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, 946 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, 947 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, 948 | {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, 949 | {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, 950 | {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, 951 | {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, 952 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, 953 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, 954 | {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, 955 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, 956 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, 957 | {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, 958 | {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, 959 | {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, 960 | {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, 961 | {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, 962 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, 963 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, 964 | {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, 965 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, 966 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, 967 | {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, 968 | {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, 969 | {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, 970 | {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, 971 | {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, 972 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, 973 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, 974 | {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, 975 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, 976 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, 977 | {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, 978 | {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, 979 | {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, 980 | {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, 981 | ] 982 | 983 | [[package]] 984 | name = "mccabe" 985 | version = "0.7.0" 986 | description = "McCabe checker, plugin for flake8" 987 | optional = false 988 | python-versions = ">=3.6" 989 | files = [ 990 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 991 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 992 | ] 993 | 994 | [[package]] 995 | name = "moto" 996 | version = "4.2.13" 997 | description = "" 998 | optional = false 999 | python-versions = ">=3.7" 1000 | files = [ 1001 | {file = "moto-4.2.13-py2.py3-none-any.whl", hash = "sha256:93e0fd13b624bd79115494f833308c3641b2be0fc9f4f18aa9264aa01f6168e0"}, 1002 | {file = "moto-4.2.13.tar.gz", hash = "sha256:01aef6a489a725c8d725bd3dc6f70ff1bedaee3e2641752e4b471ff0ede4b4d7"}, 1003 | ] 1004 | 1005 | [package.dependencies] 1006 | boto3 = ">=1.9.201" 1007 | botocore = ">=1.12.201" 1008 | cryptography = ">=3.3.1" 1009 | Jinja2 = ">=2.10.1" 1010 | python-dateutil = ">=2.1,<3.0.0" 1011 | requests = ">=2.5" 1012 | responses = ">=0.13.0" 1013 | werkzeug = ">=0.5,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1" 1014 | xmltodict = "*" 1015 | 1016 | [package.extras] 1017 | all = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.0)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] 1018 | apigateway = ["PyYAML (>=5.1)", "ecdsa (!=0.15)", "openapi-spec-validator (>=0.5.0)", "python-jose[cryptography] (>=3.1.0,<4.0.0)"] 1019 | apigatewayv2 = ["PyYAML (>=5.1)"] 1020 | appsync = ["graphql-core"] 1021 | awslambda = ["docker (>=3.0.0)"] 1022 | batch = ["docker (>=3.0.0)"] 1023 | cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.0)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] 1024 | cognitoidp = ["ecdsa (!=0.15)", "python-jose[cryptography] (>=3.1.0,<4.0.0)"] 1025 | ds = ["sshpubkeys (>=3.1.0)"] 1026 | dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.0)"] 1027 | dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.0)"] 1028 | ebs = ["sshpubkeys (>=3.1.0)"] 1029 | ec2 = ["sshpubkeys (>=3.1.0)"] 1030 | efs = ["sshpubkeys (>=3.1.0)"] 1031 | eks = ["sshpubkeys (>=3.1.0)"] 1032 | glue = ["pyparsing (>=3.0.7)"] 1033 | iotdata = ["jsondiff (>=1.1.2)"] 1034 | proxy = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=2.5.1)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.0)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] 1035 | resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.0)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "sshpubkeys (>=3.1.0)"] 1036 | route53resolver = ["sshpubkeys (>=3.1.0)"] 1037 | s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.5.0)"] 1038 | s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.5.0)"] 1039 | server = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "ecdsa (!=0.15)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.0)", "pyparsing (>=3.0.7)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "setuptools", "sshpubkeys (>=3.1.0)"] 1040 | ssm = ["PyYAML (>=5.1)"] 1041 | xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] 1042 | 1043 | [[package]] 1044 | name = "mypy" 1045 | version = "1.8.0" 1046 | description = "Optional static typing for Python" 1047 | optional = false 1048 | python-versions = ">=3.8" 1049 | files = [ 1050 | {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, 1051 | {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, 1052 | {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, 1053 | {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, 1054 | {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, 1055 | {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, 1056 | {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, 1057 | {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, 1058 | {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, 1059 | {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, 1060 | {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, 1061 | {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, 1062 | {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, 1063 | {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, 1064 | {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, 1065 | {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, 1066 | {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, 1067 | {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, 1068 | {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, 1069 | {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, 1070 | {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, 1071 | {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, 1072 | {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, 1073 | {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, 1074 | {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, 1075 | {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, 1076 | {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, 1077 | ] 1078 | 1079 | [package.dependencies] 1080 | mypy-extensions = ">=1.0.0" 1081 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 1082 | typing-extensions = ">=4.1.0" 1083 | 1084 | [package.extras] 1085 | dmypy = ["psutil (>=4.0)"] 1086 | install-types = ["pip"] 1087 | mypyc = ["setuptools (>=50)"] 1088 | reports = ["lxml"] 1089 | 1090 | [[package]] 1091 | name = "mypy-boto3-dynamodb" 1092 | version = "1.34.0" 1093 | description = "Type annotations for boto3.DynamoDB 1.34.0 service generated with mypy-boto3-builder 7.21.0" 1094 | optional = false 1095 | python-versions = ">=3.7" 1096 | files = [ 1097 | {file = "mypy-boto3-dynamodb-1.34.0.tar.gz", hash = "sha256:c0d98d7e83b0bc22e5039f703889fb96202d818171c4206fd31e665a37654e84"}, 1098 | {file = "mypy_boto3_dynamodb-1.34.0-py3-none-any.whl", hash = "sha256:76869c3fec882ddeeaca485074e302bf38c3b61103664d665dfed9425234ff75"}, 1099 | ] 1100 | 1101 | [package.dependencies] 1102 | typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} 1103 | 1104 | [[package]] 1105 | name = "mypy-boto3-s3" 1106 | version = "1.34.14" 1107 | description = "Type annotations for boto3.S3 1.34.14 service generated with mypy-boto3-builder 7.21.0" 1108 | optional = false 1109 | python-versions = ">=3.8" 1110 | files = [ 1111 | {file = "mypy-boto3-s3-1.34.14.tar.gz", hash = "sha256:71c39ab0623cdb442d225b71c1783f6a513cff4c4a13505a2efbb2e3aff2e965"}, 1112 | {file = "mypy_boto3_s3-1.34.14-py3-none-any.whl", hash = "sha256:f9669ecd182d5bf3532f5f2dcc5e5237776afe157ad5a0b37b26d6bec5fcc432"}, 1113 | ] 1114 | 1115 | [package.dependencies] 1116 | typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} 1117 | 1118 | [[package]] 1119 | name = "mypy-extensions" 1120 | version = "1.0.0" 1121 | description = "Type system extensions for programs checked with the mypy type checker." 1122 | optional = false 1123 | python-versions = ">=3.5" 1124 | files = [ 1125 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 1126 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "packaging" 1131 | version = "23.2" 1132 | description = "Core utilities for Python packages" 1133 | optional = false 1134 | python-versions = ">=3.7" 1135 | files = [ 1136 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 1137 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 1138 | ] 1139 | 1140 | [[package]] 1141 | name = "pathspec" 1142 | version = "0.12.1" 1143 | description = "Utility library for gitignore style pattern matching of file paths." 1144 | optional = false 1145 | python-versions = ">=3.8" 1146 | files = [ 1147 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 1148 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "platformdirs" 1153 | version = "4.1.0" 1154 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 1155 | optional = false 1156 | python-versions = ">=3.8" 1157 | files = [ 1158 | {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, 1159 | {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, 1160 | ] 1161 | 1162 | [package.extras] 1163 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] 1164 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] 1165 | 1166 | [[package]] 1167 | name = "pluggy" 1168 | version = "1.3.0" 1169 | description = "plugin and hook calling mechanisms for python" 1170 | optional = false 1171 | python-versions = ">=3.8" 1172 | files = [ 1173 | {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, 1174 | {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, 1175 | ] 1176 | 1177 | [package.extras] 1178 | dev = ["pre-commit", "tox"] 1179 | testing = ["pytest", "pytest-benchmark"] 1180 | 1181 | [[package]] 1182 | name = "pycodestyle" 1183 | version = "2.9.1" 1184 | description = "Python style guide checker" 1185 | optional = false 1186 | python-versions = ">=3.6" 1187 | files = [ 1188 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, 1189 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, 1190 | ] 1191 | 1192 | [[package]] 1193 | name = "pycparser" 1194 | version = "2.21" 1195 | description = "C parser in Python" 1196 | optional = false 1197 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 1198 | files = [ 1199 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 1200 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "pyflakes" 1205 | version = "2.5.0" 1206 | description = "passive checker of Python programs" 1207 | optional = false 1208 | python-versions = ">=3.6" 1209 | files = [ 1210 | {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, 1211 | {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, 1212 | ] 1213 | 1214 | [[package]] 1215 | name = "pyinstrument" 1216 | version = "4.6.1" 1217 | description = "Call stack profiler for Python. Shows you why your code is slow!" 1218 | optional = false 1219 | python-versions = ">=3.7" 1220 | files = [ 1221 | {file = "pyinstrument-4.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:73476e4bc6e467ac1b2c3c0dd1f0b71c9061d4de14626676adfdfbb14aa342b4"}, 1222 | {file = "pyinstrument-4.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4d1da8efd974cf9df52ee03edaee2d3875105ddd00de35aa542760f7c612bdf7"}, 1223 | {file = "pyinstrument-4.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:507be1ee2f2b0c9fba74d622a272640dd6d1b0c9ec3388b2cdeb97ad1e77125f"}, 1224 | {file = "pyinstrument-4.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cee6de08eb45754ef4f602ce52b640d1c535d934a6a8733a974daa095def37"}, 1225 | {file = "pyinstrument-4.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7873e8cec92321251fdf894a72b3c78f4c5c20afdd1fef0baf9042ec843bb04"}, 1226 | {file = "pyinstrument-4.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a242f6cac40bc83e1f3002b6b53681846dfba007f366971db0bf21e02dbb1903"}, 1227 | {file = "pyinstrument-4.6.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:97c9660cdb4bd2a43cf4f3ab52cffd22f3ac9a748d913b750178fb34e5e39e64"}, 1228 | {file = "pyinstrument-4.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e304cd0723e2b18ada5e63c187abf6d777949454c734f5974d64a0865859f0f4"}, 1229 | {file = "pyinstrument-4.6.1-cp310-cp310-win32.whl", hash = "sha256:cee21a2d78187dd8a80f72f5d0f1ddb767b2d9800f8bb4d94b6d11f217c22cdb"}, 1230 | {file = "pyinstrument-4.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:2000712f71d693fed2f8a1c1638d37b7919124f367b37976d07128d49f1445eb"}, 1231 | {file = "pyinstrument-4.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a366c6f3dfb11f1739bdc1dee75a01c1563ad0bf4047071e5e77598087df457f"}, 1232 | {file = "pyinstrument-4.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6be327be65d934796558aa9cb0f75ce62ebd207d49ad1854610c97b0579ad47"}, 1233 | {file = "pyinstrument-4.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e160d9c5d20d3e4ef82269e4e8b246ff09bdf37af5fb8cb8ccca97936d95ad6"}, 1234 | {file = "pyinstrument-4.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ffbf56605ef21c2fcb60de2fa74ff81f417d8be0c5002a407e414d6ef6dee43"}, 1235 | {file = "pyinstrument-4.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c92cc4924596d6e8f30a16182bbe90893b1572d847ae12652f72b34a9a17c24a"}, 1236 | {file = "pyinstrument-4.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f4b48a94d938cae981f6948d9ec603bab2087b178d2095d042d5a48aabaecaab"}, 1237 | {file = "pyinstrument-4.6.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7a386392275bdef4a1849712dc5b74f0023483fca14ef93d0ca27d453548982"}, 1238 | {file = "pyinstrument-4.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:871b131b83e9b1122f2325061c68ed1e861eebcb568c934d2fb193652f077f77"}, 1239 | {file = "pyinstrument-4.6.1-cp311-cp311-win32.whl", hash = "sha256:8d8515156dd91f5652d13b5fcc87e634f8fe1c07b68d1d0840348cdd50bf5ace"}, 1240 | {file = "pyinstrument-4.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb868fbe089036e9f32525a249f4c78b8dc46967612393f204b8234f439c9cc4"}, 1241 | {file = "pyinstrument-4.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a18cd234cce4f230f1733807f17a134e64a1f1acabf74a14d27f583cf2b183df"}, 1242 | {file = "pyinstrument-4.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:574cfca69150be4ce4461fb224712fbc0722a49b0dc02fa204d02807adf6b5a0"}, 1243 | {file = "pyinstrument-4.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e02cf505e932eb8ccf561b7527550a67ec14fcae1fe0e25319b09c9c166e914"}, 1244 | {file = "pyinstrument-4.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832fb2acef9d53701c1ab546564c45fb70a8770c816374f8dd11420d399103c9"}, 1245 | {file = "pyinstrument-4.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13cb57e9607545623ebe462345b3d0c4caee0125d2d02267043ece8aca8f4ea0"}, 1246 | {file = "pyinstrument-4.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9be89e7419bcfe8dd6abb0d959d6d9c439c613a4a873514c43d16b48dae697c9"}, 1247 | {file = "pyinstrument-4.6.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:476785cfbc44e8e1b1ad447398aa3deae81a8df4d37eb2d8bbb0c404eff979cd"}, 1248 | {file = "pyinstrument-4.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e9cebd90128a3d2fee36d3ccb665c1b9dce75261061b2046203e45c4a8012d54"}, 1249 | {file = "pyinstrument-4.6.1-cp312-cp312-win32.whl", hash = "sha256:1d0b76683df2ad5c40eff73607dc5c13828c92fbca36aff1ddf869a3c5a55fa6"}, 1250 | {file = "pyinstrument-4.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:c4b7af1d9d6a523cfbfedebcb69202242d5bd0cb89c4e094cc73d5d6e38279bd"}, 1251 | {file = "pyinstrument-4.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:79ae152f8c6a680a188fb3be5e0f360ac05db5bbf410169a6c40851dfaebcce9"}, 1252 | {file = "pyinstrument-4.6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07cad2745964c174c65aa75f1bf68a4394d1b4d28f33894837cfd315d1e836f0"}, 1253 | {file = "pyinstrument-4.6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb81f66f7f94045d723069cf317453d42375de9ff3c69089cf6466b078ac1db4"}, 1254 | {file = "pyinstrument-4.6.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab30ae75969da99e9a529e21ff497c18fdf958e822753db4ae7ed1e67094040"}, 1255 | {file = "pyinstrument-4.6.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f36cb5b644762fb3c86289324bbef17e95f91cd710603ac19444a47f638e8e96"}, 1256 | {file = "pyinstrument-4.6.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8b45075d9dbbc977dbc7007fb22bb0054c6990fbe91bf48dd80c0b96c6307ba7"}, 1257 | {file = "pyinstrument-4.6.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:475ac31477f6302e092463896d6a2055f3e6abcd293bad16ff94fc9185308a88"}, 1258 | {file = "pyinstrument-4.6.1-cp37-cp37m-win32.whl", hash = "sha256:29172ab3d8609fdf821c3f2562dc61e14f1a8ff5306607c32ca743582d3a760e"}, 1259 | {file = "pyinstrument-4.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:bd176f297c99035127b264369d2bb97a65255f65f8d4e843836baf55ebb3cee4"}, 1260 | {file = "pyinstrument-4.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:23e9b4526978432e9999021da9a545992cf2ac3df5ee82db7beb6908fc4c978c"}, 1261 | {file = "pyinstrument-4.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2dbcaccc9f456ef95557ec501caeb292119c24446d768cb4fb43578b0f3d572c"}, 1262 | {file = "pyinstrument-4.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2097f63c66c2bc9678c826b9ff0c25acde3ed455590d9dcac21220673fe74fbf"}, 1263 | {file = "pyinstrument-4.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:205ac2e76bd65d61b9611a9ce03d5f6393e34ec5b41dd38808f25d54e6b3e067"}, 1264 | {file = "pyinstrument-4.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f414ddf1161976a40fc0a333000e6a4ad612719eac0b8c9bb73f47153187148"}, 1265 | {file = "pyinstrument-4.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:65e62ebfa2cd8fb57eda90006f4505ac4c70da00fc2f05b6d8337d776ea76d41"}, 1266 | {file = "pyinstrument-4.6.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d96309df4df10be7b4885797c5f69bb3a89414680ebaec0722d8156fde5268c3"}, 1267 | {file = "pyinstrument-4.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f3d1ad3bc8ebb4db925afa706aa865c4bfb40d52509f143491ac0df2440ee5d2"}, 1268 | {file = "pyinstrument-4.6.1-cp38-cp38-win32.whl", hash = "sha256:dc37cb988c8854eb42bda2e438aaf553536566657d157c4473cc8aad5692a779"}, 1269 | {file = "pyinstrument-4.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:2cd4ce750c34a0318fc2d6c727cc255e9658d12a5cf3f2d0473f1c27157bdaeb"}, 1270 | {file = "pyinstrument-4.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ca95b21f022e995e062b371d1f42d901452bcbedd2c02f036de677119503355"}, 1271 | {file = "pyinstrument-4.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ac1e1d7e1f1b64054c4eb04eb4869a7a5eef2261440e73943cc1b1bc3c828c18"}, 1272 | {file = "pyinstrument-4.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0711845e953fce6ab781221aacffa2a66dbc3289f8343e5babd7b2ea34da6c90"}, 1273 | {file = "pyinstrument-4.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b7d28582017de35cb64eb4e4fa603e753095108ca03745f5d17295970ee631f"}, 1274 | {file = "pyinstrument-4.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7be57db08bd366a37db3aa3a6187941ee21196e8b14975db337ddc7d1490649d"}, 1275 | {file = "pyinstrument-4.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9a0ac0f56860398d2628ce389826ce83fb3a557d0c9a2351e8a2eac6eb869983"}, 1276 | {file = "pyinstrument-4.6.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a9045186ff13bc826fef16be53736a85029aae3c6adfe52e666cad00d7ca623b"}, 1277 | {file = "pyinstrument-4.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6c4c56b6eab9004e92ad8a48bb54913fdd71fc8a748ae42a27b9e26041646f8b"}, 1278 | {file = "pyinstrument-4.6.1-cp39-cp39-win32.whl", hash = "sha256:37e989c44b51839d0c97466fa2b623638b9470d56d79e329f359f0e8fa6d83db"}, 1279 | {file = "pyinstrument-4.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:5494c5a84fee4309d7d973366ca6b8b9f8ba1d6b254e93b7c506264ef74f2cef"}, 1280 | {file = "pyinstrument-4.6.1.tar.gz", hash = "sha256:f4731b27121350f5a983d358d2272fe3df2f538aed058f57217eef7801a89288"}, 1281 | ] 1282 | 1283 | [package.extras] 1284 | bin = ["click", "nox"] 1285 | docs = ["furo (==2021.6.18b36)", "myst-parser (==0.15.1)", "sphinx (==4.2.0)", "sphinxcontrib-programoutput (==0.17)"] 1286 | examples = ["django", "numpy"] 1287 | test = ["flaky", "greenlet (>=3.0.0a1)", "ipython", "pytest", "pytest-asyncio (==0.12.0)", "sphinx-autobuild (==2021.3.14)", "trio"] 1288 | types = ["typing-extensions"] 1289 | 1290 | [[package]] 1291 | name = "pytest" 1292 | version = "7.4.4" 1293 | description = "pytest: simple powerful testing with Python" 1294 | optional = false 1295 | python-versions = ">=3.7" 1296 | files = [ 1297 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 1298 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 1299 | ] 1300 | 1301 | [package.dependencies] 1302 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 1303 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 1304 | iniconfig = "*" 1305 | packaging = "*" 1306 | pluggy = ">=0.12,<2.0" 1307 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 1308 | 1309 | [package.extras] 1310 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 1311 | 1312 | [[package]] 1313 | name = "pytest-cov" 1314 | version = "4.1.0" 1315 | description = "Pytest plugin for measuring coverage." 1316 | optional = false 1317 | python-versions = ">=3.7" 1318 | files = [ 1319 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 1320 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 1321 | ] 1322 | 1323 | [package.dependencies] 1324 | coverage = {version = ">=5.2.1", extras = ["toml"]} 1325 | pytest = ">=4.6" 1326 | 1327 | [package.extras] 1328 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 1329 | 1330 | [[package]] 1331 | name = "python-dateutil" 1332 | version = "2.8.2" 1333 | description = "Extensions to the standard Python datetime module" 1334 | optional = false 1335 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 1336 | files = [ 1337 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 1338 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 1339 | ] 1340 | 1341 | [package.dependencies] 1342 | six = ">=1.5" 1343 | 1344 | [[package]] 1345 | name = "pyyaml" 1346 | version = "6.0.1" 1347 | description = "YAML parser and emitter for Python" 1348 | optional = false 1349 | python-versions = ">=3.6" 1350 | files = [ 1351 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 1352 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 1353 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 1354 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 1355 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 1356 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 1357 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 1358 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 1359 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 1360 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 1361 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 1362 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 1363 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 1364 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 1365 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 1366 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 1367 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 1368 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 1369 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 1370 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 1371 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 1372 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 1373 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 1374 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 1375 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 1376 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 1377 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 1378 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 1379 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 1380 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 1381 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 1382 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 1383 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 1384 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 1385 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 1386 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 1387 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 1388 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 1389 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 1390 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 1391 | ] 1392 | 1393 | [[package]] 1394 | name = "requests" 1395 | version = "2.31.0" 1396 | description = "Python HTTP for Humans." 1397 | optional = false 1398 | python-versions = ">=3.7" 1399 | files = [ 1400 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 1401 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 1402 | ] 1403 | 1404 | [package.dependencies] 1405 | certifi = ">=2017.4.17" 1406 | charset-normalizer = ">=2,<4" 1407 | idna = ">=2.5,<4" 1408 | urllib3 = ">=1.21.1,<3" 1409 | 1410 | [package.extras] 1411 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 1412 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 1413 | 1414 | [[package]] 1415 | name = "responses" 1416 | version = "0.24.1" 1417 | description = "A utility library for mocking out the `requests` Python library." 1418 | optional = false 1419 | python-versions = ">=3.8" 1420 | files = [ 1421 | {file = "responses-0.24.1-py3-none-any.whl", hash = "sha256:a2b43f4c08bfb9c9bd242568328c65a34b318741d3fab884ac843c5ceeb543f9"}, 1422 | {file = "responses-0.24.1.tar.gz", hash = "sha256:b127c6ca3f8df0eb9cc82fd93109a3007a86acb24871834c47b77765152ecf8c"}, 1423 | ] 1424 | 1425 | [package.dependencies] 1426 | pyyaml = "*" 1427 | requests = ">=2.30.0,<3.0" 1428 | urllib3 = ">=1.25.10,<3.0" 1429 | 1430 | [package.extras] 1431 | tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] 1432 | 1433 | [[package]] 1434 | name = "s3transfer" 1435 | version = "0.10.0" 1436 | description = "An Amazon S3 Transfer Manager" 1437 | optional = false 1438 | python-versions = ">= 3.8" 1439 | files = [ 1440 | {file = "s3transfer-0.10.0-py3-none-any.whl", hash = "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e"}, 1441 | {file = "s3transfer-0.10.0.tar.gz", hash = "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b"}, 1442 | ] 1443 | 1444 | [package.dependencies] 1445 | botocore = ">=1.33.2,<2.0a.0" 1446 | 1447 | [package.extras] 1448 | crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] 1449 | 1450 | [[package]] 1451 | name = "six" 1452 | version = "1.16.0" 1453 | description = "Python 2 and 3 compatibility utilities" 1454 | optional = false 1455 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 1456 | files = [ 1457 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1458 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1459 | ] 1460 | 1461 | [[package]] 1462 | name = "tomli" 1463 | version = "2.0.1" 1464 | description = "A lil' TOML parser" 1465 | optional = false 1466 | python-versions = ">=3.7" 1467 | files = [ 1468 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1469 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1470 | ] 1471 | 1472 | [[package]] 1473 | name = "types-awscrt" 1474 | version = "0.20.0" 1475 | description = "Type annotations and code completion for awscrt" 1476 | optional = false 1477 | python-versions = ">=3.7,<4.0" 1478 | files = [ 1479 | {file = "types_awscrt-0.20.0-py3-none-any.whl", hash = "sha256:e872b65d041687ec7fb49fb4dcb871ff10ade5efeca02722e037a03bff81db7e"}, 1480 | {file = "types_awscrt-0.20.0.tar.gz", hash = "sha256:99778c952e1eae10cc7a53468413001177026c9434345bf00120bb2ea5b79109"}, 1481 | ] 1482 | 1483 | [[package]] 1484 | name = "types-s3transfer" 1485 | version = "0.10.0" 1486 | description = "Type annotations and code completion for s3transfer" 1487 | optional = false 1488 | python-versions = ">=3.7,<4.0" 1489 | files = [ 1490 | {file = "types_s3transfer-0.10.0-py3-none-any.whl", hash = "sha256:44fcdf0097b924a9aab1ee4baa1179081a9559ca62a88c807e2b256893ce688f"}, 1491 | {file = "types_s3transfer-0.10.0.tar.gz", hash = "sha256:35e4998c25df7f8985ad69dedc8e4860e8af3b43b7615e940d53c00d413bdc69"}, 1492 | ] 1493 | 1494 | [[package]] 1495 | name = "typing-extensions" 1496 | version = "4.9.0" 1497 | description = "Backported and Experimental Type Hints for Python 3.8+" 1498 | optional = false 1499 | python-versions = ">=3.8" 1500 | files = [ 1501 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, 1502 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, 1503 | ] 1504 | 1505 | [[package]] 1506 | name = "urllib3" 1507 | version = "1.26.18" 1508 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1509 | optional = false 1510 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 1511 | files = [ 1512 | {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, 1513 | {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, 1514 | ] 1515 | 1516 | [package.extras] 1517 | brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 1518 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 1519 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 1520 | 1521 | [[package]] 1522 | name = "urllib3" 1523 | version = "2.0.7" 1524 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1525 | optional = false 1526 | python-versions = ">=3.7" 1527 | files = [ 1528 | {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, 1529 | {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, 1530 | ] 1531 | 1532 | [package.extras] 1533 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1534 | secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] 1535 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1536 | zstd = ["zstandard (>=0.18.0)"] 1537 | 1538 | [[package]] 1539 | name = "werkzeug" 1540 | version = "3.0.1" 1541 | description = "The comprehensive WSGI web application library." 1542 | optional = false 1543 | python-versions = ">=3.8" 1544 | files = [ 1545 | {file = "werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"}, 1546 | {file = "werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"}, 1547 | ] 1548 | 1549 | [package.dependencies] 1550 | MarkupSafe = ">=2.1.1" 1551 | 1552 | [package.extras] 1553 | watchdog = ["watchdog (>=2.3)"] 1554 | 1555 | [[package]] 1556 | name = "xmltodict" 1557 | version = "0.13.0" 1558 | description = "Makes working with XML feel like you are working with JSON" 1559 | optional = false 1560 | python-versions = ">=3.4" 1561 | files = [ 1562 | {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, 1563 | {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, 1564 | ] 1565 | 1566 | [metadata] 1567 | lock-version = "2.0" 1568 | python-versions = "^3.8" 1569 | content-hash = "2ee9ed408e9f645610fc785103e51d3e985f609921d1f13e596ecc8da2bcf8d4" 1570 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "s3pypi" 3 | version = "2.0.1" 4 | description = "CLI for creating a Python Package Repository in an S3 bucket" 5 | authors = [ 6 | "Matteo De Wint ", 7 | "Ruben Van den Bossche ", 8 | ] 9 | 10 | [tool.poetry.scripts] 11 | s3pypi = "s3pypi.__main__:main" 12 | 13 | [tool.poetry.dependencies] 14 | boto3 = "^1.34.11" 15 | boto3-stubs = {extras = ["dynamodb", "s3"], version = "^1.34.11"} 16 | python = "^3.8" 17 | 18 | [tool.poetry.group.dev.dependencies] 19 | black = "^23.12.1" 20 | bump2version = "^1.0.1" 21 | flake8 = "^5.0.4" 22 | isort = "^5.13.2" 23 | moto = "^4.2.12" 24 | mypy = "^1.8.0" 25 | pyinstrument = "^4.6.1" 26 | pytest = "^7.4.3" 27 | pytest-cov = "^4.1.0" 28 | 29 | [build-system] 30 | requires = ["poetry>=0.12"] 31 | build-backend = "poetry.masonry.api" 32 | -------------------------------------------------------------------------------- /s3pypi/__init__.py: -------------------------------------------------------------------------------- 1 | __prog__ = "s3pypi" 2 | __version__ = "2.0.1" 3 | -------------------------------------------------------------------------------- /s3pypi/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import logging 4 | import sys 5 | from argparse import ArgumentParser, Namespace 6 | from pathlib import Path 7 | from typing import Callable, Dict 8 | 9 | from s3pypi import __prog__, __version__, core 10 | 11 | logging.basicConfig() 12 | log = logging.getLogger(__prog__) 13 | 14 | 15 | def string_dict(text: str) -> Dict[str, str]: 16 | return dict(tuple(item.strip().split("=", 1)) for item in text.split(",")) # type: ignore 17 | 18 | 19 | def build_arg_parser() -> ArgumentParser: 20 | p = ArgumentParser(prog=__prog__) 21 | p.add_argument("-V", "--version", action="version", version=__version__) 22 | p.add_argument("-v", "--verbose", action="store_true", help="Verbose output.") 23 | 24 | commands = p.add_subparsers(help="Commands", required=True) 25 | 26 | def add_command( 27 | func: Callable[[core.Config, Namespace], None], help: str 28 | ) -> ArgumentParser: 29 | name = func.__name__.replace("_", "-") 30 | cmd = commands.add_parser(name, help=help) 31 | cmd.set_defaults(func=func) 32 | return cmd 33 | 34 | up = add_command(upload, help="Upload packages to S3.") 35 | up.add_argument( 36 | "dist", 37 | nargs="+", 38 | type=Path, 39 | help="The distribution files to upload to S3. Usually `dist/*`.", 40 | ) 41 | build_s3_args(up) 42 | up.add_argument( 43 | "--put-root-index", 44 | action="store_true", 45 | help="Write a root index that lists all available package names.", 46 | ) 47 | g = up.add_mutually_exclusive_group() 48 | g.add_argument( 49 | "--strict", 50 | action="store_true", 51 | help="Fail when trying to upload existing files.", 52 | ) 53 | g.add_argument( 54 | "-f", "--force", action="store_true", help="Overwrite existing files." 55 | ) 56 | 57 | d = add_command(delete, help="Delete packages from S3.") 58 | d.add_argument("name", help="Package name.") 59 | d.add_argument("version", help="Package version.") 60 | build_s3_args(d) 61 | 62 | ul = add_command(force_unlock, help="Release a stuck lock in DynamoDB.") 63 | ul.add_argument("table", help="DynamoDB table.") 64 | ul.add_argument("lock_id", help="ID of the lock to release.") 65 | build_aws_args(ul) 66 | 67 | return p 68 | 69 | 70 | def build_aws_args(p: ArgumentParser) -> None: 71 | p.add_argument("--profile", help="Optional AWS profile to use.") 72 | p.add_argument("--region", help="Optional AWS region to target.") 73 | 74 | 75 | def build_s3_args(p: ArgumentParser) -> None: 76 | p.add_argument("-b", "--bucket", required=True, help="The S3 bucket to upload to.") 77 | p.add_argument("--prefix", help="Optional prefix to use for S3 object names.") 78 | 79 | build_aws_args(p) 80 | p.add_argument( 81 | "--no-sign-request", 82 | action="store_true", 83 | help="Don't use authentication when communicating with S3.", 84 | ) 85 | p.add_argument( 86 | "--s3-endpoint-url", metavar="URL", help="Optional custom S3 endpoint URL." 87 | ) 88 | p.add_argument( 89 | "--s3-put-args", 90 | metavar="ARGS", 91 | type=string_dict, 92 | default={}, 93 | help=( 94 | "Optional extra arguments to S3 PutObject calls. Example: " 95 | "'ACL=public-read,ServerSideEncryption=aws:kms,SSEKMSKeyId=1234...'" 96 | ), 97 | ) 98 | p.add_argument( 99 | "--index.html", 100 | dest="index_html", 101 | action="store_true", 102 | help=( 103 | "Store index pages with suffix `/index.html` instead of `/`. " 104 | "This provides compatibility with custom HTTPS proxies or S3 website endpoints." 105 | ), 106 | ) 107 | p.add_argument( 108 | "--locks-table", 109 | metavar="TABLE", 110 | help="DynamoDB table to use for locking (default: `-locks`).", 111 | ) 112 | 113 | 114 | def upload(cfg: core.Config, args: Namespace) -> None: 115 | core.upload_packages( 116 | cfg, 117 | args.dist, 118 | put_root_index=args.put_root_index, 119 | strict=args.strict, 120 | force=args.force, 121 | ) 122 | 123 | 124 | def delete(cfg: core.Config, args: Namespace) -> None: 125 | core.delete_package(cfg, name=args.name, version=args.version) 126 | 127 | 128 | def force_unlock(cfg: core.Config, args: Namespace) -> None: 129 | core.force_unlock(cfg, args.table, args.lock_id) 130 | 131 | 132 | def main(*raw_args: str) -> None: 133 | args = build_arg_parser().parse_args(raw_args or sys.argv[1:]) 134 | log.setLevel(logging.DEBUG if args.verbose else logging.INFO) 135 | 136 | cfg = core.Config( 137 | s3=core.S3Config( 138 | bucket=args.bucket, 139 | prefix=args.prefix, 140 | profile=args.profile, 141 | region=args.region, 142 | no_sign_request=args.no_sign_request, 143 | endpoint_url=args.s3_endpoint_url, 144 | put_kwargs=args.s3_put_args, 145 | index_html=args.index_html, 146 | locks_table=args.locks_table, 147 | ) 148 | if hasattr(args, "bucket") 149 | else core.S3Config( 150 | bucket="", 151 | profile=args.profile, 152 | region=args.region, 153 | ), 154 | ) 155 | 156 | try: 157 | args.func(cfg, args) 158 | except core.S3PyPiError as e: 159 | sys.exit(f"ERROR: {e}") 160 | 161 | 162 | if __name__ == "__main__": 163 | main() 164 | -------------------------------------------------------------------------------- /s3pypi/core.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from contextlib import suppress 4 | from dataclasses import dataclass 5 | from itertools import groupby 6 | from operator import attrgetter 7 | from pathlib import Path 8 | from typing import List 9 | 10 | import boto3 11 | 12 | from s3pypi import __prog__ 13 | from s3pypi.exceptions import S3PyPiError 14 | from s3pypi.index import Hash 15 | from s3pypi.locking import DynamoDBLocker 16 | from s3pypi.storage import S3Config, S3Storage 17 | 18 | log = logging.getLogger(__prog__) 19 | 20 | 21 | @dataclass 22 | class Config: 23 | s3: S3Config 24 | 25 | 26 | @dataclass 27 | class DistributionId: 28 | name: str 29 | version: str 30 | 31 | 32 | @dataclass 33 | class Distribution(DistributionId): 34 | local_path: Path 35 | 36 | 37 | def normalize_package_name(name: str) -> str: 38 | return re.sub(r"[-_.]+", "-", name.lower()) 39 | 40 | 41 | def upload_packages( 42 | cfg: Config, 43 | dist: List[Path], 44 | put_root_index: bool = False, 45 | strict: bool = False, 46 | force: bool = False, 47 | ) -> None: 48 | storage = S3Storage(cfg.s3) 49 | distributions = parse_distributions(dist) 50 | 51 | get_name = attrgetter("name") 52 | existing_files = [] 53 | 54 | for name, group in groupby(sorted(distributions, key=get_name), get_name): 55 | directory = normalize_package_name(name) 56 | 57 | with storage.locked_index(directory) as index: 58 | for distr in group: 59 | filename = distr.local_path.name 60 | 61 | if not force and filename in index.filenames: 62 | existing_files.append(filename) 63 | msg = "%s already exists! (use --force to overwrite)" 64 | log.warning(msg, filename) 65 | else: 66 | log.info("Uploading %s", distr.local_path) 67 | storage.put_distribution(directory, distr.local_path) 68 | index.filenames[filename] = Hash.of("sha256", distr.local_path) 69 | 70 | if put_root_index: 71 | with storage.locked_index(storage.root) as root_index: 72 | root_index.filenames = dict.fromkeys(storage.list_directories()) 73 | 74 | if strict and existing_files: 75 | raise S3PyPiError(f"Found {len(existing_files)} existing files on S3") 76 | 77 | 78 | def parse_distribution(path: Path) -> Distribution: 79 | d = parse_distribution_id(path.name) 80 | return Distribution(d.name, d.version, path) 81 | 82 | 83 | def parse_distribution_id(filename: str) -> DistributionId: 84 | extensions = (".whl", ".tar.gz", ".tar.bz2", ".tar.xz", ".zip") 85 | 86 | ext = next((ext for ext in extensions if filename.endswith(ext)), "") 87 | if not ext: 88 | raise S3PyPiError(f"Unknown file type: {filename}") 89 | 90 | stem = filename[: -len(ext)] 91 | 92 | if ext == ".whl": 93 | name, version = stem.split("-", 2)[:2] 94 | else: 95 | name, version = stem.rsplit("-", 1) 96 | name = name.replace("-", "_") 97 | 98 | return DistributionId(name, version) 99 | 100 | 101 | def parse_distributions(paths: List[Path]) -> List[Distribution]: 102 | dists = [] 103 | for path in paths: 104 | if path.is_file(): 105 | dists.append(parse_distribution(path)) 106 | elif not path.exists(): 107 | new_dists = [] 108 | expanded_paths = Path(".").glob(str(path)) 109 | for expanded_path in (f for f in expanded_paths if f.is_file()): 110 | with suppress(S3PyPiError): 111 | new_dists.append(parse_distribution(expanded_path)) 112 | if not new_dists: 113 | raise S3PyPiError(f"No valid files found matching: {path}") 114 | dists.extend(new_dists) 115 | else: 116 | raise S3PyPiError(f"Not a file: {path}") 117 | return dists 118 | 119 | 120 | def delete_package(cfg: Config, name: str, version: str) -> None: 121 | storage = S3Storage(cfg.s3) 122 | directory = normalize_package_name(name) 123 | 124 | with storage.locked_index(directory) as index: 125 | filenames = [ 126 | filename 127 | for filename in index.filenames 128 | if parse_distribution_id(filename).version == version 129 | ] 130 | if not filenames: 131 | raise S3PyPiError(f"Package not found: {name} {version}") 132 | 133 | for filename in filenames: 134 | log.info("Deleting %s", filename) 135 | storage.delete(directory, filename) 136 | del index.filenames[filename] 137 | 138 | if not index.filenames: 139 | with storage.locked_index(storage.root) as root_index: 140 | root_index.filenames.pop(directory, None) 141 | 142 | 143 | def force_unlock(cfg: Config, table: str, lock_id: str) -> None: 144 | session = boto3.Session(profile_name=cfg.s3.profile, region_name=cfg.s3.region) 145 | DynamoDBLocker.build(session, table)._unlock(lock_id) 146 | log.info("Released lock %s", lock_id) 147 | -------------------------------------------------------------------------------- /s3pypi/exceptions.py: -------------------------------------------------------------------------------- 1 | class S3PyPiError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /s3pypi/index.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import hashlib 4 | import re 5 | import urllib.parse 6 | from dataclasses import dataclass, field 7 | from pathlib import Path 8 | from textwrap import indent 9 | from typing import Dict, Optional 10 | 11 | 12 | @dataclass 13 | class Hash: 14 | name: str 15 | value: str 16 | 17 | @classmethod 18 | def of(cls, name: str, path: Path) -> Hash: 19 | h = hashlib.new(name) 20 | with open(path, "rb") as file: 21 | while True: 22 | block = file.read(65536) 23 | if not block: 24 | break 25 | h.update(block) 26 | return cls(name, h.hexdigest()) 27 | 28 | 29 | @dataclass 30 | class Index: 31 | filenames: Dict[str, Optional[Hash]] = field(default_factory=dict) 32 | 33 | @classmethod 34 | def parse(cls, html: str) -> Index: 35 | matches = re.findall(r'(.+)', html) 36 | filenames = { 37 | fname: Hash(hash_name, hash_value) if hash_name else None 38 | for _, hash_name, hash_value, fname in matches 39 | } 40 | return cls(filenames) 41 | 42 | def to_html(self) -> str: 43 | links = "
\n".join( 44 | f'{fname.rstrip("/")}' 47 | for fname, hash_ in sorted(self.filenames.items()) 48 | ) 49 | return index_html.format(body=indent(links, " " * 4)) 50 | 51 | 52 | index_html = """ 53 | 54 | 55 | 56 | 57 | Package Index 58 | 59 | 60 | {body} 61 | 62 | 63 | """.strip() 64 | -------------------------------------------------------------------------------- /s3pypi/locking.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import datetime as dt 3 | import getpass 4 | import hashlib 5 | import json 6 | import logging 7 | import socket 8 | import time 9 | from contextlib import contextmanager 10 | from dataclasses import dataclass 11 | from typing import Iterator 12 | 13 | import boto3 14 | from mypy_boto3_dynamodb.service_resource import Table 15 | 16 | from s3pypi import __prog__, exceptions as exc 17 | 18 | log = logging.getLogger(__prog__) 19 | 20 | 21 | class Locker(abc.ABC): 22 | @contextmanager 23 | def __call__(self, key: str) -> Iterator[None]: 24 | lock_id = hashlib.sha1(key.encode()).hexdigest() 25 | self._lock(lock_id) 26 | try: 27 | yield 28 | finally: 29 | self._unlock(lock_id) 30 | 31 | @abc.abstractmethod 32 | def _lock(self, lock_id: str) -> None: 33 | ... 34 | 35 | @abc.abstractmethod 36 | def _unlock(self, lock_id: str) -> None: 37 | ... 38 | 39 | 40 | class DummyLocker(Locker): 41 | def _lock(self, lock_id: str) -> None: 42 | pass 43 | 44 | _unlock = _lock 45 | 46 | 47 | @dataclass 48 | class LockerConfig: 49 | retry_delay: int = 1 50 | max_attempts: int = 10 51 | 52 | 53 | class DynamoDBLocker(Locker): 54 | @staticmethod 55 | def build( 56 | session: boto3.session.Session, 57 | table_name: str, 58 | discover: bool = False, 59 | cfg: LockerConfig = LockerConfig(), 60 | ) -> Locker: 61 | db = session.resource("dynamodb") 62 | table = db.Table(table_name) 63 | 64 | if discover: 65 | try: 66 | table.get_item(Key={"LockID": "?"}) 67 | except table.meta.client.exceptions.ClientError: 68 | log.debug("No locks table found. Locking disabled.") 69 | return DummyLocker() 70 | 71 | owner = f"{getpass.getuser()}@{socket.gethostname()}" 72 | return DynamoDBLocker(table, owner, cfg) 73 | 74 | def __init__(self, table: Table, owner: str, cfg: LockerConfig): 75 | self.table = table 76 | self.exc = self.table.meta.client.exceptions 77 | self.owner = owner 78 | self.cfg = cfg 79 | 80 | def _lock(self, lock_id: str) -> None: 81 | for attempt in range(1, self.cfg.max_attempts + 1): 82 | now = dt.datetime.now(dt.timezone.utc) 83 | try: 84 | self.table.put_item( 85 | Item={ 86 | "LockID": lock_id, 87 | "AcquiredAt": now.isoformat(), 88 | "Owner": self.owner, 89 | }, 90 | ConditionExpression="attribute_not_exists(LockID)", 91 | ) 92 | return 93 | except self.exc.ConditionalCheckFailedException: 94 | if attempt == 1: 95 | log.info("Waiting to acquire lock... (%s)", lock_id) 96 | if attempt < self.cfg.max_attempts: 97 | time.sleep(self.cfg.retry_delay) 98 | 99 | item = self.table.get_item(Key={"LockID": lock_id})["Item"] 100 | raise DynamoDBLockTimeoutError(self.table.name, item) 101 | 102 | def _unlock(self, lock_id: str) -> None: 103 | self.table.delete_item(Key={"LockID": lock_id}) 104 | 105 | 106 | class DynamoDBLockTimeoutError(exc.S3PyPiError): 107 | def __init__(self, table: str, item: dict): 108 | super().__init__( 109 | f"Timed out trying to acquire lock:\n\n{json.dumps(item, indent=2)}\n\n" 110 | "Another instance of s3pypi may currently be holding the lock.\n" 111 | "If this is not the case, you may release the lock as follows:\n\n" 112 | f"$ s3pypi force-unlock {table} {item['LockID']}\n" 113 | ) 114 | -------------------------------------------------------------------------------- /s3pypi/storage.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from dataclasses import dataclass, field 3 | from pathlib import Path 4 | from typing import Dict, Iterator, List, Optional 5 | 6 | import boto3 7 | import botocore 8 | from botocore.config import Config as BotoConfig 9 | from mypy_boto3_s3.service_resource import Object 10 | 11 | from s3pypi.index import Index 12 | from s3pypi.locking import DynamoDBLocker 13 | 14 | 15 | @dataclass 16 | class S3Config: 17 | bucket: str 18 | prefix: Optional[str] = None 19 | profile: Optional[str] = None 20 | region: Optional[str] = None 21 | no_sign_request: bool = False 22 | endpoint_url: Optional[str] = None 23 | put_kwargs: Dict[str, str] = field(default_factory=dict) 24 | index_html: bool = False 25 | locks_table: Optional[str] = None 26 | 27 | 28 | class S3Storage: 29 | root = "/" 30 | _index = "index.html" 31 | 32 | def __init__(self, cfg: S3Config): 33 | session = boto3.Session(profile_name=cfg.profile, region_name=cfg.region) 34 | 35 | config = None 36 | if cfg.no_sign_request: 37 | config = BotoConfig(signature_version=botocore.session.UNSIGNED) # type: ignore 38 | 39 | self.s3 = session.resource("s3", endpoint_url=cfg.endpoint_url, config=config) 40 | self.index_name = self._index if cfg.index_html else "" 41 | self.cfg = cfg 42 | 43 | self.lock = DynamoDBLocker.build( 44 | session, 45 | table_name=cfg.locks_table or f"{cfg.bucket}-locks", 46 | discover=not cfg.locks_table, 47 | ) 48 | 49 | def _object(self, directory: str, filename: str) -> Object: 50 | parts = [directory, filename] 51 | if parts == [self.root, self.index_name]: 52 | parts = [p, self.index_name] if (p := self.cfg.prefix) else [self._index] 53 | elif self.cfg.prefix: 54 | parts.insert(0, self.cfg.prefix) 55 | return self.s3.Object(self.cfg.bucket, key="/".join(parts)) 56 | 57 | def get_index(self, directory: str) -> Index: 58 | try: 59 | html = self._object(directory, self.index_name).get()["Body"].read() 60 | except botocore.exceptions.ClientError: 61 | return Index() 62 | return Index.parse(html.decode()) 63 | 64 | @contextmanager 65 | def locked_index(self, directory: str) -> Iterator[Index]: 66 | with self.lock(directory): 67 | index = self.get_index(directory) 68 | yield index 69 | 70 | if index.filenames: 71 | self.put_index(directory, index) 72 | else: 73 | self.delete(directory, self.index_name) 74 | 75 | def list_directories(self) -> List[str]: 76 | prefix = f"{p}/" if (p := self.cfg.prefix) else "" 77 | return [ 78 | d[len(prefix) :] 79 | for item in self.s3.meta.client.get_paginator("list_objects_v2") 80 | .paginate(Bucket=self.cfg.bucket, Delimiter="/", Prefix=prefix) 81 | .search("CommonPrefixes") 82 | if item and (d := item.get("Prefix")) 83 | ] 84 | 85 | def put_index(self, directory: str, index: Index) -> None: 86 | self._object(directory, self.index_name).put( 87 | Body=index.to_html(), 88 | ContentType="text/html", 89 | CacheControl="public, must-revalidate, proxy-revalidate, max-age=0", 90 | **self.cfg.put_kwargs, # type: ignore 91 | ) 92 | 93 | def put_distribution(self, directory: str, local_path: Path) -> None: 94 | with open(local_path, mode="rb") as f: 95 | self._object(directory, local_path.name).put( 96 | Body=f, 97 | ContentType="application/x-gzip", 98 | **self.cfg.put_kwargs, # type: ignore 99 | ) 100 | 101 | def delete(self, directory: str, filename: str) -> None: 102 | self._object(directory, filename).delete() 103 | -------------------------------------------------------------------------------- /scripts/migrate-s3-index.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | 4 | import boto3 5 | 6 | 7 | def confirm(prompt: str, accepted_answer: str = "yes") -> bool: 8 | answer = input( 9 | f"{prompt}\n" 10 | f"Only '{accepted_answer}' will be accepted to confirm.\n\n" 11 | "Enter a value: " 12 | ) 13 | return answer == accepted_answer 14 | 15 | 16 | def rename_index_html_objects(bucket: str): 17 | s3 = boto3.client("s3") 18 | INDEX_HTML = "/index.html" 19 | 20 | indexes = [ 21 | obj["Key"] 22 | for page in s3.get_paginator("list_objects_v2").paginate(Bucket=bucket) 23 | for obj in page["Contents"] 24 | if obj["Key"].endswith(INDEX_HTML) 25 | ] 26 | if not indexes: 27 | print(f"No `*{INDEX_HTML}` objects found. Nothing to migrate.") 28 | return 29 | 30 | to_rename = [(key, key.replace(INDEX_HTML, "/")) for key in indexes] 31 | 32 | print(f"{len(to_rename)} objects will be renamed:") 33 | for src_key, dst_key in to_rename: 34 | print(f" {src_key} -> {dst_key}") 35 | 36 | if confirm("\nRename the objects listed above?"): 37 | print("\nRenaming...") 38 | for src_key, dst_key in to_rename: 39 | print(f" {src_key} -> {dst_key}") 40 | src_obj = dict(Bucket=bucket, Key=src_key) 41 | s3.copy_object( 42 | CopySource=src_obj, 43 | Bucket=bucket, 44 | Key=dst_key, 45 | ) 46 | s3.delete_object(**src_obj) 47 | 48 | 49 | def main(): 50 | p = argparse.ArgumentParser() 51 | p.add_argument("bucket", help="S3 bucket name") 52 | args = p.parse_args() 53 | 54 | rename_index_html_objects(args.bucket) 55 | 56 | 57 | if __name__ == "__main__": 58 | main() 59 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.0.1 3 | commit = True 4 | message = chore: bump version to {new_version} 5 | 6 | [tool:pytest] 7 | addopts = 8 | --tb=short 9 | testpaths = tests/unit/ tests/integration/ 10 | 11 | [flake8] 12 | max-line-length = 80 13 | max-complexity = 18 14 | exclude = .tox/ .venv/ build/ dist/ 15 | select = B,C,E,F,W,T4,B9 16 | ignore = E203,E501,W503 17 | show_source = True 18 | 19 | [isort] 20 | line_length = 88 21 | multi_line_output = 3 22 | include_trailing_comma = True 23 | force_grid_wrap = 0 24 | combine_as_imports = True 25 | default_section = THIRDPARTY 26 | known_first_party = s3pypi,tests,handler 27 | 28 | [mypy] 29 | warn_redundant_casts = True 30 | warn_unused_ignores = True 31 | warn_unreachable = True 32 | 33 | [mypy-s3pypi.*] 34 | disallow_untyped_defs = True 35 | 36 | [mypy-moto.*] 37 | ignore_missing_imports = True 38 | 39 | [bumpversion:file:pyproject.toml] 40 | search = version = "{current_version}" 41 | replace = version = "{new_version}" 42 | 43 | [bumpversion:file:s3pypi/__init__.py] 44 | search = __version__ = "{current_version}" 45 | replace = __version__ = "{new_version}" 46 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | } 8 | } 9 | 10 | variable "region" { 11 | type = string 12 | description = "AWS region" 13 | } 14 | 15 | variable "bucket" { 16 | type = string 17 | description = "S3 bucket name" 18 | } 19 | 20 | variable "domain" { 21 | type = string 22 | description = "Domain name" 23 | } 24 | 25 | variable "use_wildcard_certificate" { 26 | type = bool 27 | default = false 28 | description = "Use a wildcard certificate (*.example.com)" 29 | } 30 | 31 | variable "enable_dynamodb_locking" { 32 | type = bool 33 | default = false 34 | description = "Create a DynamoDB table for locking" 35 | } 36 | 37 | variable "enable_basic_auth" { 38 | type = bool 39 | default = false 40 | description = "Enable basic authentication using Lambda@Edge" 41 | } 42 | 43 | provider "aws" { 44 | region = var.region 45 | } 46 | 47 | provider "aws" { 48 | alias = "us_east_1" 49 | region = "us-east-1" 50 | } 51 | 52 | module "s3pypi" { 53 | source = "./modules/s3pypi" 54 | 55 | bucket = var.bucket 56 | domain = var.domain 57 | use_wildcard_certificate = var.use_wildcard_certificate 58 | enable_dynamodb_locking = var.enable_dynamodb_locking 59 | enable_basic_auth = var.enable_basic_auth 60 | 61 | providers = { 62 | aws.us_east_1 = aws.us_east_1 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /terraform/modules/s3pypi/basic_auth/handler.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import json 4 | import logging 5 | from dataclasses import dataclass 6 | 7 | import boto3 8 | 9 | log = logging.getLogger() 10 | 11 | region = "us-east-1" 12 | 13 | 14 | def handle(event: dict, context): 15 | request = event["Records"][0]["cf"]["request"] 16 | try: 17 | authenticate(request["headers"]) 18 | except Exception as e: 19 | log.error(repr(e)) 20 | return unauthorized 21 | return request 22 | 23 | 24 | def authenticate(headers: dict): 25 | domain = headers["host"][0]["value"] 26 | auth = headers["authorization"][0]["value"] 27 | auth_type, creds = auth.split(" ") 28 | 29 | if auth_type != "Basic": 30 | raise ValueError("Invalid auth type: " + auth_type) 31 | 32 | username, password = base64.b64decode(creds).decode().split(":") 33 | user = get_user(domain, username) 34 | 35 | if hash_password(password, user.password_salt) != user.password_hash: 36 | raise ValueError("Invalid password for " + username) 37 | 38 | 39 | @dataclass 40 | class User: 41 | username: str 42 | password_hash: str 43 | password_salt: str 44 | 45 | 46 | def get_user(domain: str, username: str) -> User: 47 | data = boto3.client("ssm", region_name=region).get_parameter( 48 | Name=f"/s3pypi/{domain}/users/{username}", 49 | WithDecryption=True, 50 | )["Parameter"]["Value"] 51 | return User(username, **json.loads(data)) 52 | 53 | 54 | def hash_password(password: str, salt: str) -> str: 55 | return hashlib.sha1((password + salt).encode()).hexdigest() 56 | 57 | 58 | unauthorized = dict( 59 | status="401", 60 | statusDescription="Unauthorized", 61 | headers={ 62 | "www-authenticate": [ 63 | {"key": "WWW-Authenticate", "value": 'Basic realm="Login"'} 64 | ] 65 | }, 66 | ) 67 | -------------------------------------------------------------------------------- /terraform/modules/s3pypi/basic_auth/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0" 6 | } 7 | } 8 | } 9 | 10 | variable "domain" { 11 | type = string 12 | description = "Domain name" 13 | } 14 | 15 | output "lambda_function_arn" { 16 | value = aws_lambda_function.basic_auth.qualified_arn 17 | description = "Lambda function ARN to attach to a CloudFront distribution" 18 | } 19 | 20 | data "archive_file" "basic_auth" { 21 | type = "zip" 22 | source_file = "${path.module}/handler.py" 23 | output_path = "${path.module}/handler.zip" 24 | } 25 | 26 | resource "aws_lambda_function" "basic_auth" { 27 | function_name = "s3pypi-basic-auth-${replace(var.domain, ".", "-")}" 28 | 29 | runtime = "python3.8" 30 | timeout = 5 31 | publish = true 32 | 33 | filename = data.archive_file.basic_auth.output_path 34 | source_code_hash = data.archive_file.basic_auth.output_base64sha256 35 | handler = "handler.handle" 36 | 37 | role = aws_iam_role.basic_auth.arn 38 | } 39 | 40 | resource "aws_iam_role" "basic_auth" { 41 | assume_role_policy = < 2 | 3 | 4 | 5 | Package Index 6 | 7 | 8 | hello-world-0.1.0.tar.gz
9 | hello_world-0.1.0-py3-none-any.whl
10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/data/index/s3pypi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Package Index 6 | 7 | 8 | s3pypi-0!0-py2-none-any.whl
9 | s3pypi-0!0.tar.gz
10 | s3pypi-0+local-py2-none-any.whl
11 | s3pypi-0+local.tar.gz
12 | s3pypi-0-py2-none-any.whl
13 | s3pypi-0.0-py2-none-any.whl
14 | s3pypi-0.0.tar.gz
15 | s3pypi-0.1.1-py2-none-any.whl
16 | s3pypi-0.1.1.tar.gz
17 | s3pypi-0.1.2-py2-none-any.whl
18 | s3pypi-0.1.2.tar.gz
19 | s3pypi-0.dev0-py2-none-any.whl
20 | s3pypi-0.dev0.tar.gz
21 | s3pypi-0.post0-py2-none-any.whl
22 | s3pypi-0.post0.tar.gz
23 | s3pypi-0.tar.gz
24 | s3pypi-0a0-py2-none-any.whl
25 | s3pypi-0a0.tar.gz
26 | s3pypi-0rc0-py2-none-any.whl
27 | s3pypi-0rc0.tar.gz 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/integration/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from contextlib import contextmanager 3 | 4 | import boto3 5 | import moto 6 | import pytest 7 | 8 | 9 | @pytest.fixture(scope="session") 10 | def chdir(): 11 | @contextmanager 12 | def _chdir(path): 13 | orig = os.getcwd() 14 | os.chdir(path) 15 | try: 16 | yield 17 | finally: 18 | os.chdir(orig) 19 | 20 | return _chdir 21 | 22 | 23 | @pytest.fixture 24 | def aws_credentials(): 25 | os.environ["AWS_ACCESS_KEY_ID"] = "testing" 26 | os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" 27 | os.environ["AWS_SECURITY_TOKEN"] = "testing" 28 | os.environ["AWS_SESSION_TOKEN"] = "testing" 29 | os.environ["AWS_DEFAULT_REGION"] = "us-east-1" 30 | 31 | 32 | @pytest.fixture 33 | def s3_bucket(aws_credentials): 34 | with moto.mock_s3(): 35 | s3 = boto3.resource("s3") 36 | bucket = s3.Bucket("s3pypi-test") 37 | bucket.create() 38 | yield bucket 39 | 40 | 41 | @pytest.fixture 42 | def dynamodb_table(s3_bucket): 43 | name = f"{s3_bucket.name}-locks" 44 | with moto.mock_dynamodb(), moto.mock_sts(): 45 | db = boto3.resource("dynamodb") 46 | db.create_table( 47 | TableName=name, 48 | AttributeDefinitions=[{"AttributeName": "LockID", "AttributeType": "S"}], 49 | KeySchema=[{"AttributeName": "LockID", "KeyType": "HASH"}], 50 | BillingMode="PAY_PER_REQUEST", 51 | ) 52 | yield db.Table(name) 53 | 54 | 55 | @pytest.fixture 56 | def boto3_session(s3_bucket): 57 | return boto3.session.Session() 58 | -------------------------------------------------------------------------------- /tests/integration/test_locking.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from s3pypi.locking import ( 4 | DummyLocker, 5 | DynamoDBLocker, 6 | DynamoDBLockTimeoutError, 7 | LockerConfig, 8 | ) 9 | 10 | 11 | def test_dynamodb_discover_found(boto3_session, dynamodb_table): 12 | lock = DynamoDBLocker.build(boto3_session, dynamodb_table.name, discover=True) 13 | 14 | assert isinstance(lock, DynamoDBLocker) 15 | assert lock.table == dynamodb_table 16 | 17 | 18 | def test_dynamodb_discover_not_found(boto3_session): 19 | lock = DynamoDBLocker.build( 20 | boto3_session, table_name="does-not-exist", discover=True 21 | ) 22 | assert isinstance(lock, DummyLocker) 23 | 24 | 25 | def test_dynamodb_lock_timeout(dynamodb_table): 26 | cfg = LockerConfig(retry_delay=0, max_attempts=3) 27 | lock = DynamoDBLocker(dynamodb_table, owner="pytest", cfg=cfg) 28 | key = "example" 29 | 30 | with lock(key): 31 | with pytest.raises(DynamoDBLockTimeoutError): 32 | with lock(key): 33 | pass 34 | -------------------------------------------------------------------------------- /tests/integration/test_main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest 4 | 5 | from s3pypi import __prog__ 6 | from s3pypi.__main__ import main as s3pypi, string_dict 7 | from s3pypi.index import Hash, Index 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "text, expected", 12 | [ 13 | ( 14 | "ServerSideEncryption=aws:kms,SSEKMSKeyId=1234..., foo=bar", 15 | dict(ServerSideEncryption="aws:kms", SSEKMSKeyId="1234...", foo="bar"), 16 | ) 17 | ], 18 | ) 19 | def test_string_dict(text, expected): 20 | assert string_dict(text) == expected 21 | 22 | 23 | @pytest.mark.parametrize("prefix", ["", "packages", "packages/abc"]) 24 | def test_main_upload_package(chdir, data_dir, s3_bucket, dynamodb_table, prefix): 25 | args = ["dists/*", "--bucket", s3_bucket.name, "--put-root-index"] 26 | if prefix: 27 | args.extend(["--prefix", prefix]) 28 | 29 | with chdir(data_dir): 30 | s3pypi("upload", *args) 31 | 32 | def read(key: str) -> bytes: 33 | return s3_bucket.Object(key).get()["Body"].read() 34 | 35 | root_index = read(f"{prefix}/" if prefix else "index.html").decode() 36 | 37 | def assert_pkg_exists(pkg: str, filename: str): 38 | path = (f"{prefix}/" if prefix else "") + f"{pkg}/" 39 | assert read(path + filename) 40 | assert f">{filename}" in read(path).decode() 41 | assert f">{pkg}" in root_index 42 | 43 | assert_pkg_exists("foo", "foo-0.1.0.tar.gz") 44 | assert_pkg_exists("hello-world", "hello_world-0.1.0-py3-none-any.whl") 45 | assert_pkg_exists("xyz", "xyz-0.1.0.zip") 46 | 47 | 48 | def test_main_upload_package_exists(chdir, data_dir, s3_bucket, caplog): 49 | dist = "dists/foo-0.1.0.tar.gz" 50 | 51 | with chdir(data_dir): 52 | s3pypi("upload", dist, "--bucket", s3_bucket.name) 53 | s3pypi("upload", dist, "--bucket", s3_bucket.name) 54 | 55 | with pytest.raises(SystemExit, match="ERROR: Found 1 existing files on S3"): 56 | s3pypi("upload", dist, "--strict", "--bucket", s3_bucket.name) 57 | 58 | s3pypi("upload", dist, "--force", "--bucket", s3_bucket.name) 59 | 60 | msg = "foo-0.1.0.tar.gz already exists! (use --force to overwrite)" 61 | success = (__prog__, logging.INFO, "Uploading " + dist) 62 | warning = (__prog__, logging.WARNING, msg) 63 | 64 | assert caplog.record_tuples == [success, warning, warning, success] 65 | 66 | 67 | @pytest.mark.parametrize( 68 | ["dists", "error_msg"], 69 | [ 70 | (["dists"], r"Not a file: dists"), 71 | (["dists/invalid.whl"], r"No valid files found matching: dists/invalid\.whl"), 72 | ( 73 | ["dists/foo-0.1.0.tar.gz", "dists/*.invalid"], 74 | r"No valid files found matching: dists/\*\.invalid", 75 | ), 76 | ], 77 | ) 78 | def test_main_upload_package_invalid( 79 | dists, error_msg, chdir, data_dir, s3_bucket, caplog 80 | ): 81 | with chdir(data_dir): 82 | with pytest.raises(SystemExit, match=f"ERROR: {error_msg}"): 83 | s3pypi("upload", *dists, "--bucket", s3_bucket.name) 84 | 85 | 86 | def test_main_upload_package_with_force_updates_hash(chdir, data_dir, s3_bucket): 87 | with open(data_dir / "index" / "hello_world.html", "rb") as index_file: 88 | s3_bucket.Object("hello-world/").put(Body=index_file) 89 | 90 | def get_index(): 91 | html = s3_bucket.Object("hello-world/").get()["Body"].read() 92 | return Index.parse(html.decode()) 93 | 94 | assert get_index().filenames == { 95 | "hello-world-0.1.0.tar.gz": None, 96 | "hello_world-0.1.0-py3-none-any.whl": None, 97 | } 98 | 99 | with chdir(data_dir): 100 | dist = "dists/hello_world-0.1.0-py3-none-any.whl" 101 | s3pypi("upload", dist, "--force", "--bucket", s3_bucket.name) 102 | 103 | assert get_index().filenames == { 104 | "hello-world-0.1.0.tar.gz": None, 105 | "hello_world-0.1.0-py3-none-any.whl": Hash( 106 | "sha256", "c5a2633aecf5adc5ae49b868e12faf01f2199b914d4296399b52dec62cb70fb3" 107 | ), 108 | } 109 | 110 | 111 | def test_main_delete_package(chdir, data_dir, s3_bucket): 112 | with chdir(data_dir): 113 | s3pypi("upload", "dists/*", "--bucket", s3_bucket.name, "--put-root-index") 114 | s3pypi("delete", "hello-world", "0.1.0", "--bucket", s3_bucket.name) 115 | 116 | def read(key: str) -> bytes: 117 | return s3_bucket.Object(key).get()["Body"].read() 118 | 119 | root_index = read("index.html").decode() 120 | 121 | def assert_pkg_exists(pkg: str, filename: str): 122 | path = f"{pkg}/" 123 | assert read(path + filename) 124 | assert f">{filename}" in read(path).decode() 125 | assert f">{pkg}" in root_index 126 | 127 | for deleted_key in [ 128 | "hello-world/", 129 | "hello-world/hello_world-0.1.0-py3-none-any.whl", 130 | "hello-world/hello_world-0.1.0.tar.gz", 131 | ]: 132 | with pytest.raises(s3_bucket.meta.client.exceptions.NoSuchKey): 133 | s3_bucket.Object(deleted_key).get() 134 | 135 | assert ">hello-world" not in root_index 136 | assert_pkg_exists("foo", "foo-0.1.0.tar.gz") 137 | assert_pkg_exists("xyz", "xyz-0.1.0.zip") 138 | 139 | 140 | def test_main_force_unlock(dynamodb_table): 141 | s3pypi("force-unlock", dynamodb_table.name, "12345") 142 | -------------------------------------------------------------------------------- /tests/integration/test_storage.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from s3pypi.index import Index 4 | from s3pypi.storage import S3Config, S3Storage 5 | 6 | 7 | def test_index_storage_roundtrip(s3_bucket): 8 | directory = "foo" 9 | index = Index({"bar": None}) 10 | 11 | cfg = S3Config(bucket=s3_bucket.name) 12 | s = S3Storage(cfg) 13 | 14 | s.put_index(directory, index) 15 | got = s.get_index(directory) 16 | 17 | assert got == index 18 | 19 | 20 | index = object() 21 | 22 | 23 | @pytest.mark.parametrize( 24 | "cfg, directory, filename, expected_key", 25 | [ 26 | (S3Config(""), "/", index, "index.html"), 27 | (S3Config(""), "foo", "bar", "foo/bar"), 28 | (S3Config("", prefix="P"), "/", index, "P/"), 29 | (S3Config("", prefix="P"), "foo", "bar", "P/foo/bar"), 30 | (S3Config("", prefix="P", index_html=True), "/", index, "P/index.html"), 31 | (S3Config("", index_html=True), "/", index, "index.html"), 32 | ], 33 | ) 34 | def test_s3_key(cfg, directory, filename, expected_key): 35 | s = S3Storage(cfg) 36 | if filename is index: 37 | filename = s.index_name 38 | 39 | obj = s._object(directory, filename) 40 | 41 | assert obj.key == expected_key 42 | 43 | 44 | def test_list_directories(s3_bucket): 45 | cfg = S3Config(bucket=s3_bucket.name, prefix="AA") 46 | s = S3Storage(cfg) 47 | s.put_index("one", Index()) 48 | s.put_index("two", Index()) 49 | s.put_index("three", Index()) 50 | 51 | assert s.list_directories() == ["one/", "three/", "two/"] 52 | 53 | cfg = S3Config(bucket=s3_bucket.name, prefix="BBBB") 54 | s = S3Storage(cfg) 55 | s.put_index("xxx", Index()) 56 | s.put_index("yyy", Index()) 57 | 58 | assert s.list_directories() == ["xxx/", "yyy/"] 59 | 60 | cfg = S3Config(bucket=s3_bucket.name) 61 | s = S3Storage(cfg) 62 | 63 | assert s.list_directories() == ["AA/", "BBBB/"] 64 | -------------------------------------------------------------------------------- /tests/unit/test_core.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from s3pypi import core 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "name, normalized", 8 | [ 9 | ("company.test", "company-test"), 10 | ("company---test.1", "company-test-1"), 11 | ("company___test.2", "company-test-2"), 12 | ], 13 | ) 14 | def test_normalize_package_name(name, normalized): 15 | assert core.normalize_package_name(name) == normalized 16 | 17 | 18 | @pytest.mark.parametrize( 19 | "filename, dist", 20 | [ 21 | ("hello-world-0.1.0.tar.gz", core.DistributionId("hello_world", "0.1.0")), 22 | ("hello_world-0.1.0.tar.gz", core.DistributionId("hello_world", "0.1.0")), 23 | ("foo_bar-1.2.3-py3-none-any.whl", core.DistributionId("foo_bar", "1.2.3")), 24 | ], 25 | ) 26 | def test_parse_distribution_id(filename, dist): 27 | assert core.parse_distribution_id(filename) == dist 28 | -------------------------------------------------------------------------------- /tests/unit/test_index.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from s3pypi.index import Hash, Index 4 | 5 | 6 | @pytest.fixture( 7 | scope="function", 8 | params=[ 9 | ( 10 | "s3pypi", 11 | [ 12 | f"s3pypi-{version}{suffix}" 13 | for version in ( 14 | "0", 15 | "0!0", 16 | "0+local", 17 | "0.0", 18 | "0.1.1", 19 | "0.1.2", 20 | "0.dev0", 21 | "0.post0", 22 | "0a0", 23 | "0rc0", 24 | ) 25 | for suffix in ( 26 | ".tar.gz", 27 | "-py2-none-any.whl", 28 | ) 29 | ], 30 | [Hash("sha256", "1234" * 16) if i % 3 == 0 else None for i in range(0, 20)], 31 | ) 32 | ], 33 | ) 34 | def index_html(request, data_dir): 35 | index_name, names, hashes = request.param 36 | filenames = dict(zip(names, hashes)) 37 | with open(data_dir / "index" / f"{index_name}.html") as f: 38 | html = f.read().strip() 39 | yield html, filenames 40 | 41 | 42 | def test_parse_index(index_html): 43 | html, expected_filenames = index_html 44 | index = Index.parse(html) 45 | assert index.filenames == expected_filenames 46 | 47 | 48 | def test_render_index(index_html): 49 | expected_html, filenames = index_html 50 | html = Index(filenames).to_html() 51 | assert html == expected_html 52 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{38,39,310,311,312}, py38-lambda, lint 3 | skip_missing_interpreters = true 4 | isolated_build = True 5 | 6 | [gh-actions] 7 | python = 8 | 3.8: py38, py38-lambda 9 | 3.9: py39, lint 10 | 3.10: py310 11 | 3.11: py311 12 | 3.12: py312 13 | 14 | [testenv] 15 | deps = 16 | boto3-stubs 17 | moto 18 | mypy 19 | pytest 20 | pytest-cov 21 | commands = 22 | mypy s3pypi/ tests/ 23 | pytest {posargs} \ 24 | --cov=s3pypi \ 25 | --cov-report term \ 26 | --cov-report html:coverage \ 27 | --no-cov-on-fail 28 | 29 | [testenv:py38-lambda] 30 | deps = 31 | pytest 32 | commands = 33 | pytest basic_auth/ {posargs} 34 | 35 | [testenv:lint] 36 | skip_install = True 37 | deps = 38 | flake8 39 | black 40 | isort 41 | commands = 42 | flake8 43 | black --check --diff . 44 | isort --check-only . 45 | --------------------------------------------------------------------------------