├── tests
├── __init__.py
├── data
│ ├── dists
│ │ ├── xyz-0.1.0.zip
│ │ ├── foo-0.1.0.tar.gz
│ │ ├── hello_world-0.1.0.tar.gz
│ │ └── hello_world-0.1.0-py3-none-any.whl
│ └── index
│ │ ├── hello_world.html
│ │ └── s3pypi.html
├── conftest.py
├── unit
│ ├── test_core.py
│ └── test_index.py
└── integration
│ ├── test_locking.py
│ ├── conftest.py
│ ├── test_storage.py
│ └── test_main.py
├── s3pypi
├── __init__.py
├── exceptions.py
├── index.py
├── locking.py
├── storage.py
├── core.py
└── __main__.py
├── basic_auth
├── handler.py
├── test_handler.py
└── put_user.py
├── Makefile
├── .github
└── workflows
│ └── tests.yml
├── pyproject.toml
├── tox.ini
├── setup.cfg
├── LICENSE
├── .gitignore
├── terraform
├── main.tf
└── modules
│ └── s3pypi
│ ├── basic_auth
│ ├── handler.py
│ └── main.tf
│ └── main.tf
├── scripts
└── migrate-s3-index.py
├── CONTRIBUTING.md
├── CHANGELOG.md
├── README.md
└── poetry.lock
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/data/dists/xyz-0.1.0.zip:
--------------------------------------------------------------------------------
1 | xyz
2 |
--------------------------------------------------------------------------------
/tests/data/dists/foo-0.1.0.tar.gz:
--------------------------------------------------------------------------------
1 | foo
2 |
--------------------------------------------------------------------------------
/tests/data/dists/hello_world-0.1.0.tar.gz:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/s3pypi/__init__.py:
--------------------------------------------------------------------------------
1 | __prog__ = "s3pypi"
2 | __version__ = "2.0.1"
3 |
--------------------------------------------------------------------------------
/s3pypi/exceptions.py:
--------------------------------------------------------------------------------
1 | class S3PyPiError(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/basic_auth/handler.py:
--------------------------------------------------------------------------------
1 | ../terraform/modules/s3pypi/basic_auth/handler.py
--------------------------------------------------------------------------------
/tests/data/dists/hello_world-0.1.0-py3-none-any.whl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gorilla-co/s3pypi/HEAD/tests/data/dists/hello_world-0.1.0-py3-none-any.whl
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import pytest
4 |
5 |
6 | @pytest.fixture(scope="session")
7 | def data_dir():
8 | return Path(__file__).parent.resolve() / "data"
9 |
--------------------------------------------------------------------------------
/tests/data/index/hello_world.html:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 = < 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 |
--------------------------------------------------------------------------------
/terraform/modules/s3pypi/main.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | version = "~> 4.0"
6 | configuration_aliases = [
7 | aws.us_east_1,
8 | ]
9 | }
10 | }
11 | }
12 |
13 | variable "bucket" {
14 | type = string
15 | description = "S3 bucket name"
16 | }
17 |
18 | variable "domain" {
19 | type = string
20 | description = "Domain name"
21 | }
22 |
23 | variable "use_wildcard_certificate" {
24 | type = bool
25 | default = false
26 | description = "Use a wildcard certificate (*.example.com)"
27 | }
28 |
29 | variable "enable_dynamodb_locking" {
30 | type = bool
31 | default = false
32 | description = "Create a DynamoDB table for locking"
33 | }
34 |
35 | variable "enable_basic_auth" {
36 | type = bool
37 | default = false
38 | description = "Enable basic authentication using Lambda@Edge"
39 | }
40 |
41 | locals {
42 | hosted_zone = replace(var.domain, "/^[^.]+\\./", "")
43 | }
44 |
45 | data "aws_acm_certificate" "viewer" {
46 | provider = aws.us_east_1
47 | domain = var.use_wildcard_certificate ? "*.${local.hosted_zone}" : var.domain
48 | }
49 |
50 | data "aws_route53_zone" "dns" {
51 | name = local.hosted_zone
52 | }
53 |
54 | resource "aws_cloudfront_distribution" "cdn" {
55 | aliases = [var.domain]
56 | comment = var.domain
57 |
58 | viewer_certificate {
59 | acm_certificate_arn = data.aws_acm_certificate.viewer.arn
60 | minimum_protocol_version = "TLSv1.2_2021"
61 | ssl_support_method = "sni-only"
62 | }
63 |
64 | price_class = "PriceClass_100"
65 | enabled = true
66 | is_ipv6_enabled = true
67 |
68 | origin {
69 | domain_name = aws_s3_bucket.pypi.bucket_regional_domain_name
70 | origin_id = "s3"
71 |
72 | s3_origin_config {
73 | origin_access_identity = aws_cloudfront_origin_access_identity.oai.cloudfront_access_identity_path
74 | }
75 | }
76 |
77 | custom_error_response {
78 | error_code = 403
79 | #response_code = 404
80 | #response_page_path = "/404.html"
81 | error_caching_min_ttl = 0
82 | }
83 |
84 | default_root_object = "index.html"
85 |
86 | default_cache_behavior {
87 | target_origin_id = "s3"
88 | viewer_protocol_policy = "redirect-to-https"
89 |
90 | allowed_methods = ["GET", "HEAD", "OPTIONS"]
91 | cached_methods = ["GET", "HEAD"]
92 |
93 | forwarded_values {
94 | query_string = false
95 |
96 | cookies {
97 | forward = "none"
98 | }
99 | }
100 |
101 | min_ttl = 0
102 | default_ttl = 0
103 |
104 | compress = true
105 |
106 | dynamic "lambda_function_association" {
107 | for_each = module.basic_auth
108 | iterator = mod
109 | content {
110 | event_type = "viewer-request"
111 | lambda_arn = mod.value.lambda_function_arn
112 | }
113 | }
114 | }
115 |
116 | restrictions {
117 | geo_restriction {
118 | restriction_type = "none"
119 | }
120 | }
121 | }
122 |
123 | resource "aws_route53_record" "alias" {
124 | zone_id = data.aws_route53_zone.dns.zone_id
125 | name = var.domain
126 | type = "A"
127 |
128 | alias {
129 | name = aws_cloudfront_distribution.cdn.domain_name
130 | zone_id = aws_cloudfront_distribution.cdn.hosted_zone_id
131 |
132 | evaluate_target_health = false
133 | }
134 | }
135 |
136 | resource "aws_cloudfront_origin_access_identity" "oai" {}
137 |
138 | resource "aws_s3_bucket" "pypi" {
139 | bucket = var.bucket
140 | }
141 |
142 | resource "aws_s3_bucket_policy" "s3_policy" {
143 | bucket = aws_s3_bucket.pypi.id
144 | policy = data.aws_iam_policy_document.s3_policy.json
145 | }
146 |
147 | data "aws_iam_policy_document" "s3_policy" {
148 | statement {
149 | actions = ["s3:GetObject", "s3:ListBucket"]
150 | resources = ["${aws_s3_bucket.pypi.arn}/*", aws_s3_bucket.pypi.arn]
151 |
152 | principals {
153 | type = "AWS"
154 | identifiers = [aws_cloudfront_origin_access_identity.oai.iam_arn]
155 | }
156 | }
157 | }
158 |
159 | resource "aws_dynamodb_table" "locks" {
160 | count = var.enable_dynamodb_locking ? 1 : 0
161 |
162 | name = "${var.bucket}-locks"
163 | billing_mode = "PAY_PER_REQUEST"
164 | hash_key = "LockID"
165 |
166 | attribute {
167 | name = "LockID"
168 | type = "S"
169 | }
170 | }
171 |
172 | module "basic_auth" {
173 | count = var.enable_basic_auth ? 1 : 0
174 |
175 | source = "./basic_auth"
176 | domain = var.domain
177 |
178 | providers = {
179 | aws = aws.us_east_1
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------