├── infrastructure ├── __init__.py ├── readme.md ├── cdk-requirements.txt ├── cdk │ ├── components │ │ ├── ec2.py │ │ ├── ecs.py │ │ ├── ecr.py │ │ ├── secretsmanager.py │ │ ├── elasticache.py │ │ ├── rds.py │ │ └── airflow_service_components.py │ └── stack.py ├── app.py ├── config.py └── setup.py ├── poetry.toml ├── .flake8 ├── package.json ├── airflow ├── airflow-requirements.txt ├── dags │ └── mercado_bitcoin │ │ ├── README.md │ │ └── mercado_bitcoin_dag.py └── docker-compose.yml ├── scripts ├── dev_clean_local.sh ├── dev_test_local.sh └── dev_deploy_local.sh ├── .pre-commit-config.yaml ├── cdk.json ├── pyproject.toml ├── tests ├── conftest.py ├── test_infrastructure.py └── test_stack.py ├── Readme.md ├── Makefile ├── .gitignore └── poetry.lock /infrastructure/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /infrastructure/readme.md: -------------------------------------------------------------------------------- 1 | # Managed Infrastructure using AWS CDK -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | create = true 3 | in-project = true 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501, W503, F403 3 | max-line-length = 79 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "aws-cdk": "^1.124.0", 4 | "aws-cdk-local": "^1.65.8" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /airflow/airflow-requirements.txt: -------------------------------------------------------------------------------- 1 | apache-airflow==2.0.1 2 | requests==2.25.1 3 | backoff==1.10.0 4 | ratelimit==2.2.1 5 | boto3==1.17.31 -------------------------------------------------------------------------------- /scripts/dev_clean_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -rf .venv cdk.out */*/*.egg-info .pytest_cache 3 | docker kill $(docker ps --filter name=airflow_ --quiet) || true 4 | docker kill $(docker ps --filter name=localstack_main --quiet) || true -------------------------------------------------------------------------------- /scripts/dev_test_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker kill $(docker ps --filter name=localstack --quiet) || true 3 | ENTRYPOINT=-d localstack start --docker 4 | export AWS_ENDPOINT=http://localhost:4566 5 | export DEPLOY_ENV=test 6 | poetry run python -m pytest -v -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | language_version: python3.8 7 | 8 | # - repo: https://gitlab.com/pycqa/flake8 9 | # rev: 3.7.9 10 | # hooks: 11 | # - id: flake8 12 | -------------------------------------------------------------------------------- /scripts/dev_deploy_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker kill $(docker ps --filter name=localstack --quiet) || true 3 | ENTRYPOINT=-d localstack start --docker 4 | export AWS_ENDPOINT=http://localhost:4566 5 | export DEPLOY_ENV=test 6 | cdklocal bootstrap 7 | cdklocal deploy "*" --verbose --require-approval never 8 | exit 0 -------------------------------------------------------------------------------- /airflow/dags/mercado_bitcoin/README.md: -------------------------------------------------------------------------------- 1 | Precisa criar uma variável no airflow: 2 | 3 | ``` 4 | { 5 | "mercado_bitcoin_dag": { 6 | "bucket": "s3-belisco-dev-data-lake-raw", 7 | "coins": ["BCH", "BTC", "ETH", "LTC"] 8 | } 9 | } 10 | ``` 11 | 12 | E alterar a conexão `aws_default` com as suas credenciais -------------------------------------------------------------------------------- /infrastructure/cdk-requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk.core==1.124.0 2 | aws-cdk.aws_iam==1.124.0 3 | aws-cdk.aws-logs==1.124.0 4 | aws-cdk.aws-ec2==1.124.0 5 | aws-cdk.aws-ssm==1.124.0 6 | aws-cdk.aws-rds==1.124.0 7 | aws-cdk.aws-elasticache==1.124.0 8 | aws-cdk.aws-ecs==1.124.0 9 | aws-cdk.aws_ecs_patterns==1.124.0 10 | aws-cdk.aws_elasticloadbalancingv2==1.124.0 11 | aws-cdk.aws_route53==1.124.0 12 | aws-cdk.aws_certificatemanager==1.124.0 -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 infrastructure/app.py", 3 | "context": { 4 | "@aws-cdk/core:enableStackNameDuplicates": "true", 5 | "aws-cdk:enableDiffNoFail": "true", 6 | "@aws-cdk/core:stackRelativeExports": "true", 7 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 8 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 9 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 10 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true 11 | } 12 | } -------------------------------------------------------------------------------- /infrastructure/cdk/components/ec2.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import aws_cdk.core as core 4 | from aws_cdk import aws_ec2 as ec2 5 | 6 | # To avoid circular dependency when importing AirflowStack 7 | from typing import TYPE_CHECKING 8 | 9 | if TYPE_CHECKING: 10 | from stack import AirflowStack 11 | 12 | 13 | class VpcAirflow(ec2.Vpc): 14 | """ 15 | Creates VPC 16 | """ 17 | 18 | def __init__(self, stack: AirflowStack, **kwargs) -> None: 19 | self.object_name = f"{stack.deploy_env}-airflow-vpc" 20 | super().__init__(stack, id=self.object_name) 21 | -------------------------------------------------------------------------------- /infrastructure/cdk/components/ecs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from aws_cdk import core, aws_iam as iam, aws_logs as logs, aws_ecs as ecs 4 | 5 | # To avoid circular dependency when importing AirflowStack 6 | from typing import TYPE_CHECKING 7 | 8 | if TYPE_CHECKING: 9 | from stack import AirflowStack 10 | 11 | 12 | class EcsAirflowCluster(ecs.Cluster): 13 | """ 14 | Creates ECS cluster to be used by Airflow 15 | """ 16 | 17 | def __init__(self, stack: AirflowStack, **kwargs) -> None: 18 | self.object_name = f"{stack.deploy_env}-airflow-ecs-cluster" 19 | super().__init__( 20 | stack, 21 | id=self.object_name, 22 | cluster_name=self.object_name, 23 | container_insights=True, 24 | vpc=stack.vpc_airflow, 25 | ) 26 | -------------------------------------------------------------------------------- /infrastructure/app.py: -------------------------------------------------------------------------------- 1 | from cdk.stack import AirflowStack 2 | from aws_cdk import core 3 | 4 | from config import deploy_env, default_tags, whitelisted_ips, default_removal_policy 5 | 6 | 7 | class AirflowApp(core.App): 8 | def __init__(self): 9 | super().__init__() 10 | self.deploy_env = deploy_env 11 | self.whitelisted_ips = whitelisted_ips 12 | self.default_removal_policy = default_removal_policy 13 | self.default_tags = default_tags 14 | 15 | def apply_default_tags(self, stack): 16 | for tag in self.default_tags: 17 | core.Tags.of(stack).add(key=tag.get("key"), value=tag.get("value")) 18 | 19 | 20 | airflow_app = AirflowApp() 21 | 22 | airflow_stack = AirflowStack(airflow_app) 23 | airflow_app.apply_default_tags(airflow_stack) 24 | 25 | airflow_app.synth() 26 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "airflow-fargate-cdk" 3 | description = "Deploy of Airflow using ECS Fargate and AWS CDK" 4 | authors = ["Andre Sionek"] 5 | version = "0.0.1" 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | boto3 = "^1.17.33" 10 | cryptography = "^3.4.6" 11 | 12 | [tool.poetry.dev-dependencies] 13 | pre-commit = "^2.11.1" 14 | black = "^20.8b1" 15 | flake8 = "^3.9.0" 16 | pytest = "^6.2.2" 17 | localstack = "^0.12.7" 18 | moto = "^2.0.2" 19 | localstack-client = "^1.14" 20 | awscli-local = "^0.14" 21 | 22 | [tool.black] 23 | target-version = ['py38'] 24 | line-length = 90 25 | include = '\.pyi?$' 26 | exclude = ''' 27 | /( 28 | \.git 29 | | \.github 30 | | \.circleci 31 | | \.hg 32 | | \.mypy_cache 33 | | \.tox 34 | | \venv 35 | | \.venv 36 | | \.env 37 | | \.eggs 38 | | _build 39 | | buck-out 40 | | build 41 | | dist 42 | | venv 43 | )/ 44 | ''' -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import aws_cdk.core as core 2 | from stack import AirflowStack 3 | import pytest 4 | import boto3 5 | import localstack_client.session 6 | 7 | 8 | @pytest.fixture(scope="function") 9 | def app_fixture(): 10 | return core.App() 11 | 12 | 13 | @pytest.fixture(scope="function") 14 | def airflow_stack_fixture(app_fixture): 15 | return AirflowStack(app_fixture, deploy_env="test") 16 | 17 | 18 | @pytest.fixture(scope="function") 19 | def template_fixture(): 20 | app = core.App() 21 | AirflowStack(app, deploy_env="test") 22 | return app.synth().get_stack("test-airflow-stack").template 23 | 24 | 25 | @pytest.fixture(autouse=True) 26 | def boto3_localstack_patch(monkeypatch): 27 | local_session = localstack_client.session.Session() 28 | monkeypatch.setattr(boto3, "client", local_session.client) 29 | monkeypatch.setattr(boto3, "resource", local_session.resource) 30 | -------------------------------------------------------------------------------- /infrastructure/cdk/components/ecr.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from aws_cdk import aws_ecr as ecr 4 | 5 | # To avoid circular dependency when importing AirflowStack 6 | from typing import TYPE_CHECKING 7 | 8 | if TYPE_CHECKING: 9 | from stack import AirflowStack 10 | 11 | 12 | class EcrAirflowDockerRepository(ecr.Repository): 13 | """ 14 | Creates a Redis backend to run celery 15 | """ 16 | 17 | def __init__(self, stack: AirflowStack, **kwargs) -> None: 18 | self.object_name = f"{stack.deploy_env}-airflow-docker-repository" 19 | 20 | super().__init__( 21 | stack, 22 | id=self.object_name, 23 | repository_name=self.object_name, 24 | lifecycle_rules=[ 25 | ecr.LifecycleRule( 26 | description="Keep only the latest 10 images", max_image_count=10 27 | ) 28 | ], 29 | removal_policy=stack.default_removal_policy, 30 | ) 31 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Airflow 2.0 Fargate CDK 2 | 3 | Deploy of Airflow 2.0 using ECS Fargate and AWS CDK. 4 | 5 | Uses Airflow image from [Bitnami](https://github.com/bitnami/bitnami-docker-airflow). 6 | 7 | ## Makefile 8 | 9 | A comprehensive Makefile is available to execute common tasks. Run the following for help: 10 | 11 | ``` 12 | make help 13 | ``` 14 | 15 | ### Local Development 16 | To bring Airflow up do `make airflow-local-up` 17 | 18 | It will be available on `0.0.0.0:8080` 19 | 20 | Credentials are set with environment variables in `docker-compose.yml` 21 | ``` 22 | AIRFLOW_USERNAME: Airflow application username. Default: user 23 | AIRFLOW_PASSWORD: Airflow application password. Default: Sionek123 24 | AIRFLOW_EMAIL: Airflow application email. Default: user@example.com 25 | ``` 26 | 27 | To shut it down do `make airflow-local-down` 28 | 29 | ## AWS CDK Development 30 | If you wish to make changes to the infrastructure, you might need to have AWS CDK installed. 31 | Please follow the [AWS guide](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) to install it. -------------------------------------------------------------------------------- /infrastructure/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | from aws_cdk import core 4 | 5 | deploy_env: str = os.environ.get("DEPLOY_ENV", default="test") 6 | 7 | default_tags: List[dict] = [ 8 | dict(key="deploy_env", value=deploy_env), 9 | dict(key="owner", value="Data Engineering"), 10 | dict(key="service", value="Airflow"), 11 | ] 12 | 13 | # Please change this and select the specific IP you wish to whitelist before deploying. 0.0.0.0/0 is public accessible. 14 | whitelisted_ips: List[str] = ["0.0.0.0/0"] 15 | 16 | default_removal_policy = core.RemovalPolicy.DESTROY 17 | 18 | airflow_environment = { 19 | "AIRFLOW_USERNAME": "user", 20 | "AIRFLOW_PASSWORD": "", 21 | "AIRFLOW_EMAIL": "test@email.com", 22 | "AIRFLOW_EXECUTOR": "CeleryExecutor", 23 | "AIRFLOW_FERNET_KEY": "", 24 | "AIRFLOW_LOAD_EXAMPLES": "no", 25 | "AIRFLOW_BASE_URL": "http://localhost:8080", 26 | "AIRFLOW_DATABASE_HOST": "", 27 | "AIRFLOW_DATABASE_PORT_NUMBER": "", 28 | "AIRFLOW_DATABASE_NAME": "", 29 | "AIRFLOW_DATABASE_USERNAME": "", 30 | "AIRFLOW_DATABASE_PASSWORD": "", 31 | "AIRFLOW_DATABASE_USE_SSL": "no", 32 | "REDIS_HOST": "", 33 | "REDIS_PORT_NUMBER": "", 34 | } 35 | -------------------------------------------------------------------------------- /infrastructure/setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | from os import path 3 | 4 | this_directory = path.abspath(path.dirname(__file__)) 5 | 6 | 7 | with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: 8 | long_description = f.read() 9 | 10 | 11 | with open(path.join(this_directory, "cdk-requirements.txt"), encoding="utf-8") as f: 12 | install_requires = f.readlines() 13 | 14 | setuptools.setup( 15 | name="airflow-fargate-cdk", 16 | version="0.0.1", 17 | description="A Deployment of Airflow on ECS Fargate using AWS CDK", 18 | long_description=long_description, 19 | long_description_content_type="text/markdown", 20 | author="author", 21 | package_dir={"": "cdk"}, 22 | packages=setuptools.find_packages(where="cdk"), 23 | install_requires=install_requires, 24 | python_requires=">=3.8", 25 | classifiers=[ 26 | "Development Status :: 4 - Beta", 27 | "Intended Audience :: Developers", 28 | "License :: OSI Approved :: Apache Software License", 29 | "Programming Language :: JavaScript", 30 | "Programming Language :: Python :: 3 :: Only", 31 | "Programming Language :: Python :: 3.8", 32 | "Topic :: Software Development :: Code Generators", 33 | "Topic :: Utilities", 34 | "Typing :: Typed", 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /tests/test_infrastructure.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | 4 | 5 | @pytest.fixture() 6 | def policies_resources_fixture(template_fixture): 7 | for resource_name, resource in template_fixture["Resources"].items(): 8 | if resource["Type"] in ["AWS::IAM::Policy", "AWS::IAM::ManagedPolicy"]: 9 | yield resource 10 | 11 | 12 | @pytest.mark.parametrize( 13 | "resource_name, expected", 14 | [ 15 | ("test-airflow-ecs-task-role", True), 16 | ("test-airflow-ecs-task-policy", True), 17 | ("test-airflow-ecs-log-group", True), 18 | ("test-airflow-vpc", True), 19 | ("test-airflow-fernet-key-parameter", True), 20 | ("non-existent-resource", False), 21 | ], 22 | ) 23 | def test_resource_in_template(resource_name, expected, template_fixture): 24 | assert (resource_name in json.dumps(template_fixture)) == expected 25 | 26 | 27 | def test_if_actions_in_policies_are_open(policies_resources_fixture): 28 | """ 29 | Will 30 | """ 31 | for statement in policies_resources_fixture["Properties"]["PolicyDocument"][ 32 | "Statement" 33 | ]: 34 | if statement["Action"] == "*": 35 | raise AssertionError( 36 | f"The following policy contains a '*' " 37 | f"action, please revise and specify actions one by one. \n" 38 | f"{policies_resources_fixture}" 39 | ) 40 | -------------------------------------------------------------------------------- /tests/test_stack.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from aws_cdk.aws_ssm import StringParameter 4 | from stack import AirflowStack 5 | from aws_cdk.aws_ec2 import Vpc 6 | from aws_cdk.aws_iam import Role, ManagedPolicy 7 | from aws_cdk.aws_logs import LogGroup 8 | import pytest 9 | 10 | 11 | class TestAirflowStack: 12 | @pytest.mark.parametrize( 13 | "resource_name, resource_type", 14 | [ 15 | ("ecs_task_role", Role), 16 | ("ecs_task_policy", ManagedPolicy), 17 | ("ecs_log_group", LogGroup), 18 | ("airflow_vpc", Vpc), 19 | ("fernet_key_secure_parameter", StringParameter), 20 | ], 21 | ) 22 | def test_resource_is_instance( 23 | self, resource_name, resource_type, airflow_stack_fixture 24 | ): 25 | assert isinstance(getattr(airflow_stack_fixture, resource_name), resource_type) 26 | 27 | @patch.object(AirflowStack, "_apply_default_tags") 28 | def test_apply_default_tags_called(self, mock, app_fixture): 29 | AirflowStack(app_fixture, deploy_env="test") 30 | mock.assert_called_with(app_fixture) 31 | 32 | @pytest.mark.xfail( 33 | raises=NotImplementedError, 34 | strict=True, 35 | reason="Importing vpc needs to be implemented", 36 | ) 37 | def test_import_vpc(self, app_fixture): 38 | AirflowStack(app_fixture, deploy_env="test", import_vpc=True) 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | 3 | .DEFAULT_GOAL := help 4 | 5 | .PHONY: help 6 | help: ## Shows this help text 7 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 8 | 9 | .PHONY: airflow-local-up 10 | airflow-local-up: ## Runs airflow containers locally using docker-compose. Available on 0.0.0.0:8080. Usename: user, Password: Sionek123 11 | docker compose -f airflow/docker-compose.yml up -d 12 | 13 | .PHONY: airflow-local-down 14 | airflow-local-down: ## Kill all airflow containers created with docker-compose. 15 | docker compose -f airflow/docker-compose.yml down -v 16 | 17 | .PHONY: init 18 | init: dev-clean-local dev-install-local dev-test-local ## Clean environment and reinstall all dependencies 19 | 20 | .PHONY: dev-clean-local 21 | dev-clean-local: ## Removes project virtual env 22 | bash scripts/dev_clean_local.sh 23 | 24 | .PHONY: dev-install-local 25 | dev-install-local: ## Local install of the project and pre-commit using Poetry. Install AWS CDK package for development. 26 | npm install aws-cdk-local aws-cdk 27 | poetry install 28 | poetry run pre-commit install 29 | poetry run pip install -e infrastructure 30 | poetry run pip install -r airflow/airflow-requirements.txt 31 | ENTRYPOINT=-d poetry run localstack start --docker 32 | 33 | .PHONY: dev-test-local 34 | dev-test-local: ## Run local tests 35 | bash scripts/dev_test_local.sh 36 | 37 | .PHONY: dev-deploy-local 38 | dev-deploy-local: ## Deploy the infrastructure stack to localstack 39 | bash scripts/dev_deploy_local.sh -------------------------------------------------------------------------------- /infrastructure/cdk/components/secretsmanager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import aws_cdk.core as core 4 | from aws_cdk import aws_secretsmanager as secrets_manager 5 | import boto3 6 | from cryptography.fernet import Fernet 7 | import os 8 | import json 9 | 10 | # To avoid circular dependency when importing AirflowStack 11 | from typing import TYPE_CHECKING 12 | 13 | if TYPE_CHECKING: 14 | from stack import AirflowStack 15 | 16 | 17 | class SecretManagerFernetKeySecret(secrets_manager.Secret): 18 | """ 19 | Creates Fernet key to be used to encrypt data in RDS 20 | """ 21 | 22 | def __init__(self, stack: AirflowStack, **kwargs) -> None: 23 | self.object_name = f"{stack.deploy_env}-airflow-fernet-key-secret" 24 | super().__init__( 25 | stack, 26 | id=self.object_name, 27 | secret_name=self.object_name, 28 | description="Airflow Fernet Key used to encrypt secrets in Airflow Metadata DB", 29 | generate_secret_string=secrets_manager.SecretStringGenerator( 30 | password_length=32 31 | ), 32 | ) 33 | 34 | 35 | class SecretManagerAirflowPasswordSecret(secrets_manager.Secret): 36 | """ 37 | Creates Fernet key to be used to encrypt data in RDS 38 | """ 39 | 40 | def __init__(self, stack: AirflowStack, **kwargs) -> None: 41 | self.object_name = f"{stack.deploy_env}-airflow-master-user-secret" 42 | super().__init__( 43 | stack, 44 | id=self.object_name, 45 | secret_name=self.object_name, 46 | description="Airflow Password to access UI", 47 | generate_secret_string=secrets_manager.SecretStringGenerator( 48 | secret_string_template=json.dumps(dict(username="master_user")), 49 | generate_string_key="password", 50 | password_length=32, 51 | ), 52 | ) 53 | -------------------------------------------------------------------------------- /infrastructure/cdk/components/elasticache.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from aws_cdk import core, aws_elasticache as elasticache, aws_ec2 as ec2 4 | 5 | # To avoid circular dependency when importing AirflowStack 6 | from typing import TYPE_CHECKING 7 | 8 | if TYPE_CHECKING: 9 | from stack import AirflowStack 10 | 11 | 12 | class ElasticacheAirflowCeleryBackendCluster(elasticache.CfnCacheCluster): 13 | """ 14 | Creates a Redis backend to run celery 15 | """ 16 | 17 | def __init__(self, stack: AirflowStack, **kwargs) -> None: 18 | self.object_name = f"{stack.deploy_env}-airflow-celery-cluster" 19 | 20 | self.security_group = Ec2AirflowCeleryBackendSecurityGroup(stack) 21 | self.subnet_group = ElasticacheAirflowCeleryBackendSubnetGroup(stack) 22 | 23 | super().__init__( 24 | stack, 25 | id=self.object_name, 26 | auto_minor_version_upgrade=True, 27 | az_mode="single-az", 28 | cache_node_type="cache.t3.micro", 29 | cluster_name=self.object_name, 30 | engine="redis", 31 | engine_version="4.0.10", 32 | num_cache_nodes=1, 33 | port=6379, 34 | vpc_security_group_ids=[self.security_group.security_group_id], 35 | cache_subnet_group_name=self.subnet_group.cache_subnet_group_name, 36 | ) 37 | 38 | self.node.add_dependency(self.subnet_group) 39 | self.node.add_dependency(self.security_group) 40 | 41 | 42 | class Ec2AirflowCeleryBackendSecurityGroup(ec2.SecurityGroup): 43 | def __init__(self, stack: AirflowStack, **kwargs) -> None: 44 | self.object_name = f"{stack.deploy_env}-airflow-celery-backend-security-group" 45 | super().__init__( 46 | stack, 47 | id=self.object_name, 48 | vpc=stack.vpc_airflow, 49 | allow_all_outbound=True, 50 | security_group_name=self.object_name, 51 | ) 52 | 53 | for subnet in stack.vpc_airflow.private_subnets: 54 | self.add_ingress_rule( 55 | peer=ec2.Peer.ipv4(subnet.ipv4_cidr_block), connection=ec2.Port.tcp(6379) 56 | ) 57 | 58 | 59 | class ElasticacheAirflowCeleryBackendSubnetGroup(elasticache.CfnSubnetGroup): 60 | def __init__(self, stack: AirflowStack, **kwargs) -> None: 61 | self.object_name = f"{stack.deploy_env}-airflow-celery-backend-subnet-group" 62 | super().__init__( 63 | stack, 64 | id=self.object_name, 65 | cache_subnet_group_name=self.object_name, 66 | description="Place airflow celery backend on private subnet", 67 | subnet_ids=[subnet.subnet_id for subnet in stack.vpc_airflow.private_subnets], 68 | ) 69 | -------------------------------------------------------------------------------- /airflow/dags/mercado_bitcoin/mercado_bitcoin_dag.py: -------------------------------------------------------------------------------- 1 | from airflow import DAG 2 | from airflow.operators.python_operator import PythonOperator 3 | from airflow.hooks.S3_hook import S3Hook 4 | import requests 5 | from backoff import on_exception, constant 6 | from ratelimit import limits, RateLimitException 7 | from datetime import datetime 8 | import logging 9 | import json 10 | 11 | logger = logging.getLogger(__name__) 12 | logger.setLevel(logging.INFO) 13 | 14 | config = { 15 | "bucket": "s3-belisquito-turma-5-production-data-lake-raw", 16 | "coins": ["BCH", "BTC", "ETH", "LTC"], 17 | } 18 | 19 | default_args = { 20 | "owner": "andresionek91", 21 | "start_date": datetime(2021, 1, 1), 22 | "depends_on_past": False, 23 | "provide_context": True, 24 | } 25 | 26 | dag = DAG( 27 | "mercado_bitcoin_dag", 28 | description="Extrai dados do sumario diario do mercado bitcoin.", 29 | schedule_interval="0 0 * * *", 30 | catchup=True, 31 | default_args=default_args, 32 | ) 33 | 34 | 35 | @on_exception(constant, RateLimitException, interval=60, max_tries=3) 36 | @limits(calls=20, period=60) 37 | @on_exception(constant, requests.exceptions.HTTPError, max_tries=3, interval=10) 38 | def get_daily_summary(date, coin): 39 | year, month, day = date.split("-") 40 | endpoint = ( 41 | f"https://www.mercadobitcoin.net/api/{coin}/day-summary/{year}/{month}/{day}" 42 | ) 43 | 44 | logger.info(f"Getting data from API with: {endpoint}") 45 | 46 | response = requests.get(endpoint) 47 | response.raise_for_status() 48 | logger.info(f"Data downloaded from API: {response.text}") 49 | 50 | return response.json() 51 | 52 | 53 | def upload_to_s3(date, coin, **context): 54 | logger.info(f"Getting context from previous task") 55 | json_data = context["ti"].xcom_pull(task_ids=f"get_daily_summary_{coin}") 56 | string_data = json.dumps(json_data) 57 | now_string = datetime.now().strftime("%Y-%m-%d-%H-%M-%S-%f") 58 | logger.info(f"Uploading to S3") 59 | S3Hook(aws_conn_id="aws_default").load_string( 60 | string_data=string_data, 61 | key=f"mercado_bitcoin/{coin}/execution_date={date}/mercado_bitcoin{coin}_{now_string}.json", 62 | bucket_name=config["bucket"], 63 | ) 64 | 65 | 66 | for coin in config["coins"]: 67 | logger.info(f"Starting extractions tasks for {coin}") 68 | 69 | task_1 = PythonOperator( 70 | task_id=f"get_daily_summary_{coin}", 71 | dag=dag, 72 | python_callable=get_daily_summary, 73 | op_kwargs={"date": "{{ ds }}", "coin": coin}, 74 | ) 75 | 76 | task_2 = PythonOperator( 77 | task_id=f"upload_to_s3_{coin}", 78 | dag=dag, 79 | python_callable=upload_to_s3, 80 | op_kwargs={"date": "{{ ds }}", "coin": coin}, 81 | provide_context=True, 82 | ) 83 | 84 | task_1 >> task_2 85 | -------------------------------------------------------------------------------- /airflow/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | postgresql: 5 | image: docker.io/bitnami/postgresql:10 6 | volumes: 7 | - 'postgresql_data:/bitnami/postgresql' 8 | environment: 9 | - POSTGRESQL_DATABASE=bitnami_airflow 10 | - POSTGRESQL_USERNAME=bn_airflow 11 | - POSTGRESQL_PASSWORD=bitnami1 12 | - ALLOW_EMPTY_PASSWORD=yes 13 | 14 | redis: 15 | image: docker.io/bitnami/redis:6.0 16 | volumes: 17 | - 'redis_data:/bitnami' 18 | environment: 19 | - ALLOW_EMPTY_PASSWORD=yes 20 | 21 | airflow-scheduler: 22 | image: docker.io/bitnami/airflow-scheduler:2 23 | environment: 24 | - AIRFLOW_DATABASE_NAME=bitnami_airflow 25 | - AIRFLOW_DATABASE_USERNAME=bn_airflow 26 | - AIRFLOW_DATABASE_PASSWORD=bitnami1 27 | - AIRFLOW_EXECUTOR=CeleryExecutor 28 | - AIRFLOW_WEBSERVER_HOST=airflow 29 | - AIRFLOW_PASSWORD=Sionek123 30 | - AIRFLOW_USERNAME=user 31 | - AIRFLOW_EMAIL=user@example.com 32 | - AIRFLOW_LOAD_EXAMPLES=no 33 | - AIRFLOW_FERNET_KEY=OcMYZQUouMHsQMsfZMi8Sa8f1lEEeLja8cV65_sWqko= 34 | volumes: 35 | - airflow_scheduler_data:/bitnami 36 | - ./dags:/opt/bitnami/airflow/dags 37 | - ./airflow-requirements.txt:/bitnami/python/requirements.txt 38 | 39 | airflow-worker: 40 | image: docker.io/bitnami/airflow-worker:2 41 | environment: 42 | - AIRFLOW_DATABASE_NAME=bitnami_airflow 43 | - AIRFLOW_DATABASE_USERNAME=bn_airflow 44 | - AIRFLOW_DATABASE_PASSWORD=bitnami1 45 | - AIRFLOW_EXECUTOR=CeleryExecutor 46 | - AIRFLOW_WEBSERVER_HOST=airflow 47 | - AIRFLOW_PASSWORD=Sionek123 48 | - AIRFLOW_USERNAME=user 49 | - AIRFLOW_EMAIL=user@example.com 50 | - AIRFLOW_LOAD_EXAMPLES=no 51 | - AIRFLOW_FERNET_KEY=OcMYZQUouMHsQMsfZMi8Sa8f1lEEeLja8cV65_sWqko= 52 | volumes: 53 | - airflow_worker_data:/bitnami 54 | - ./dags:/opt/bitnami/airflow/dags 55 | - ./airflow-requirements.txt:/bitnami/python/requirements.txt 56 | 57 | 58 | airflow: 59 | image: docker.io/bitnami/airflow:2 60 | environment: 61 | - AIRFLOW_DATABASE_NAME=bitnami_airflow 62 | - AIRFLOW_DATABASE_USERNAME=bn_airflow 63 | - AIRFLOW_DATABASE_PASSWORD=bitnami1 64 | - AIRFLOW_EXECUTOR=CeleryExecutor 65 | - AIRFLOW_PASSWORD=Sionek123 66 | - AIRFLOW_USERNAME=user 67 | - AIRFLOW_EMAIL=user@example.com 68 | - AIRFLOW_LOAD_EXAMPLES=no 69 | - AIRFLOW_FERNET_KEY=OcMYZQUouMHsQMsfZMi8Sa8f1lEEeLja8cV65_sWqko= 70 | ports: 71 | - '8080:8080' 72 | volumes: 73 | - airflow_data:/bitnami 74 | - ./dags:/opt/bitnami/airflow/dags 75 | - ./airflow-requirements.txt:/bitnami/python/requirements.txt 76 | 77 | volumes: 78 | airflow_scheduler_data: 79 | driver: local 80 | airflow_worker_data: 81 | driver: local 82 | airflow_data: 83 | driver: local 84 | postgresql_data: 85 | driver: local 86 | redis_data: 87 | driver: local -------------------------------------------------------------------------------- /infrastructure/cdk/components/rds.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from aws_cdk import core, aws_rds as rds, aws_ec2 as ec2 4 | 5 | # To avoid circular dependency when importing AirflowStack 6 | from typing import TYPE_CHECKING 7 | 8 | if TYPE_CHECKING: 9 | from stack import AirflowStack 10 | 11 | 12 | class RdsAirflowMetadataDb(rds.DatabaseInstance): 13 | """ 14 | Creates a Postgres database to be used for Airflow Metadata 15 | """ 16 | 17 | def __init__(self, stack: AirflowStack, **kwargs) -> None: 18 | self.object_name = f"{stack.deploy_env}-airflow-metadata-rds-instance" 19 | 20 | super().__init__( 21 | stack, 22 | id=self.object_name, 23 | engine=rds.DatabaseInstanceEngine.postgres( 24 | version=rds.PostgresEngineVersion.VER_12_4 25 | ), 26 | database_name="airflow", 27 | instance_type=ec2.InstanceType("t3.micro"), 28 | vpc=stack.vpc_airflow, 29 | instance_identifier=self.object_name, 30 | port=5432, 31 | vpc_placement=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC), 32 | subnet_group=RdsAirflowMetadataDbSubnetGroup(stack), 33 | parameter_group=RdsAirflowMetadataDbParameterGroup(stack), 34 | security_groups=[Ec2AirflowMetadataDbSecurityGroup(stack)], 35 | removal_policy=stack.default_removal_policy, 36 | **kwargs, 37 | ) 38 | 39 | 40 | class Ec2AirflowMetadataDbSecurityGroup(ec2.SecurityGroup): 41 | def __init__(self, stack: AirflowStack, **kwargs) -> None: 42 | self.object_name = f"{stack.deploy_env}-airflow-metadata-rds-security-group" 43 | super().__init__( 44 | stack, 45 | id=self.object_name, 46 | vpc=stack.vpc_airflow, 47 | allow_all_outbound=True, 48 | security_group_name=self.object_name, 49 | ) 50 | 51 | # Whitelist Database Access to IPs 52 | for ip in stack.whitelisted_ips: 53 | self.add_ingress_rule(peer=ec2.Peer.ipv4(ip), connection=ec2.Port.tcp(5432)) 54 | 55 | for subnet in stack.vpc_airflow.private_subnets: 56 | self.add_ingress_rule( 57 | peer=ec2.Peer.ipv4(subnet.ipv4_cidr_block), connection=ec2.Port.tcp(5432) 58 | ) 59 | 60 | 61 | class RdsAirflowMetadataDbSubnetGroup(rds.SubnetGroup): 62 | def __init__(self, stack: AirflowStack, **kwargs) -> None: 63 | self.object_name = f"{stack.deploy_env}-airflow-metadata-rds-subnet-group" 64 | super().__init__( 65 | stack, 66 | id=self.object_name, 67 | description="Place RDS on public subnet", 68 | vpc=stack.vpc_airflow, 69 | vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC), 70 | ) 71 | 72 | 73 | class RdsAirflowMetadataDbParameterGroup(rds.ParameterGroup): 74 | def __init__(self, stack: AirflowStack, **kwargs) -> None: 75 | self.object_name = f"{stack.deploy_env}-airflow-metadata-rds-parameter-group" 76 | super().__init__( 77 | stack, 78 | id=self.object_name, 79 | description="Parameter group of Airflow Metadata DB.", 80 | engine=rds.DatabaseInstanceEngine.postgres( 81 | version=rds.PostgresEngineVersion.VER_12_4 82 | ), 83 | parameters={"max_connections": "100"}, 84 | ) 85 | -------------------------------------------------------------------------------- /infrastructure/cdk/stack.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | from aws_cdk import core, aws_ec2 as ec2 5 | from components.ecs import EcsAirflowCluster 6 | from components.ec2 import VpcAirflow 7 | from components.secretsmanager import ( 8 | SecretManagerFernetKeySecret, 9 | SecretManagerAirflowPasswordSecret, 10 | ) 11 | from components.rds import RdsAirflowMetadataDb 12 | from components.elasticache import ElasticacheAirflowCeleryBackendCluster 13 | from components.ecr import EcrAirflowDockerRepository 14 | from components.airflow_service_components import ( 15 | EcsAirflowTaskDefinition, 16 | EcsAirflowFargateService, 17 | EcsAirflowLoadBalancedFargateService, 18 | EcsAirflowAutoscalingFargateService, 19 | ) 20 | 21 | 22 | class AirflowStack(core.Stack): 23 | def __init__( 24 | self, 25 | scope: core.Construct, 26 | import_vpc: bool = False, 27 | **kwargs, 28 | ) -> None: 29 | self.import_vpc = import_vpc 30 | self.deploy_env = scope.deploy_env 31 | self.default_removal_policy = scope.default_removal_policy 32 | self.whitelisted_ips = scope.whitelisted_ips 33 | super().__init__(scope, id=f"{self.deploy_env}-airflow-stack", **kwargs) 34 | 35 | if self.import_vpc: 36 | raise NotImplementedError() 37 | else: 38 | self.vpc_airflow = VpcAirflow(self) 39 | 40 | self.fernet_key_secret = SecretManagerFernetKeySecret(self) 41 | self.master_user_secret = SecretManagerAirflowPasswordSecret(self) 42 | self.rds_metadata_db = RdsAirflowMetadataDb(self) 43 | self.celery_backend = ElasticacheAirflowCeleryBackendCluster(self) 44 | self.ecr_repository = EcrAirflowDockerRepository(self) 45 | self.ecs_cluster = EcsAirflowCluster(self) 46 | 47 | # Flower Definition 48 | self.flower_task_definition = EcsAirflowTaskDefinition( 49 | self, 50 | service_name="flower", 51 | service_port=5555, 52 | cpu=512, 53 | memory=1024, 54 | image="docker.io/bitnami/airflow:2-debian-10", 55 | ) 56 | self.flower = EcsAirflowLoadBalancedFargateService( 57 | self, 58 | service_name="flower", 59 | task_definition=self.flower_task_definition, 60 | cluster=self.ecs_cluster, 61 | desired_count=1, 62 | ) 63 | 64 | # Worker Definition 65 | self.worker_task_definition = EcsAirflowTaskDefinition( 66 | self, 67 | service_name="worker", 68 | service_port=8793, 69 | cpu=512, 70 | memory=1024, 71 | image="docker.io/bitnami/airflow-worker:2-debian-10", 72 | ) 73 | self.worker = EcsAirflowAutoscalingFargateService( 74 | self, 75 | service_name="worker", 76 | task_definition=self.worker_task_definition, 77 | cluster=self.ecs_cluster, 78 | desired_count=2, 79 | max_worker_count=4, 80 | target_memory_utilization=80, 81 | memory_scale_in_cooldown=10, 82 | memory_scale_out_cooldown=20, 83 | target_cpu_utilization=80, 84 | cpu_scale_in_cooldown=10, 85 | cpu_scale_out_cooldown=20, 86 | ) 87 | 88 | # Webserver Definition 89 | self.webserver_task_definition = EcsAirflowTaskDefinition( 90 | self, 91 | service_name="webserver", 92 | service_port=8080, 93 | cpu=1024, 94 | memory=2048, 95 | image="docker.io/bitnami/airflow:2-debian-10", 96 | ) 97 | self.webserver = EcsAirflowLoadBalancedFargateService( 98 | self, 99 | service_name="webserver", 100 | task_definition=self.webserver_task_definition, 101 | cluster=self.ecs_cluster, 102 | desired_count=1, 103 | ) 104 | 105 | # Scheduler Definition 106 | self.scheduler_task_definition = EcsAirflowTaskDefinition( 107 | self, 108 | service_name="scheduler", 109 | service_port=None, 110 | cpu=512, 111 | memory=1024, 112 | image="docker.io/bitnami/airflow-scheduler:2-debian-10", 113 | ) 114 | self.scheduler = EcsAirflowFargateService( 115 | self, 116 | service_name="scheduler", 117 | task_definition=self.scheduler_task_definition, 118 | cluster=self.ecs_cluster, 119 | desired_count=1, 120 | ) 121 | 122 | for service in ( 123 | self.scheduler, 124 | self.webserver.service, 125 | self.worker, 126 | self.flower.service, 127 | ): 128 | service.connections.allow_to( 129 | self.rds_metadata_db, 130 | ec2.Port.tcp(5432), 131 | description="Allow connection to Metadata DB", 132 | ) 133 | 134 | service.connections.allow_to( 135 | self.celery_backend.security_group, 136 | ec2.Port.tcp(6379), 137 | description="Allow connection to celery backend / message broker", 138 | ) 139 | 140 | for ip in self.whitelisted_ips: 141 | for service in (self.webserver.service, self.flower.service): 142 | service.connections.allow_to( 143 | ec2.Peer.ipv4(ip), 144 | ec2.Port.tcp(80), 145 | description="Allow HTTP connections", 146 | ) 147 | 148 | service.connections.allow_to( 149 | ec2.Peer.ipv4(ip), 150 | ec2.Port.tcp(443), 151 | description="Allow HTTPS connections", 152 | ) 153 | -------------------------------------------------------------------------------- /infrastructure/cdk/components/airflow_service_components.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from aws_cdk import ( 4 | core, 5 | aws_iam as iam, 6 | aws_logs as logs, 7 | aws_ecs as ecs, 8 | aws_ec2 as ec2, 9 | aws_ecs_patterns as ecs_patterns, 10 | aws_elasticloadbalancingv2 as elb, 11 | aws_route53 as route53, 12 | aws_certificatemanager as certificate_manager, 13 | ) 14 | 15 | from infrastructure.config import airflow_environment 16 | import base64 17 | 18 | # To avoid circular dependency when importing AirflowStack 19 | from typing import TYPE_CHECKING 20 | 21 | if TYPE_CHECKING: 22 | from stack import AirflowStack 23 | 24 | 25 | class EcsAirflowTaskDefinition(ecs.FargateTaskDefinition): 26 | """ 27 | Task Definition for Airflow Services 28 | """ 29 | 30 | def __init__( 31 | self, 32 | stack: AirflowStack, 33 | service_name: str, 34 | service_port: int, 35 | cpu: int, 36 | memory: int, 37 | image: str, 38 | **kwargs, 39 | ) -> None: 40 | self.service_name = service_name 41 | self.service_port = service_port 42 | self.object_name = ( 43 | f"{stack.deploy_env}-airflow-{self.service_name}-task-definition" 44 | ) 45 | self.airflow_environment = airflow_environment 46 | self._update_environment(stack=stack) 47 | super().__init__( 48 | stack, 49 | id=self.object_name, 50 | cpu=cpu, 51 | memory_limit_mib=memory, 52 | family=self.object_name, 53 | ) 54 | 55 | self.container = self.add_container( 56 | id=f"{stack.deploy_env}-airflow-{self.service_name}-container", 57 | image=ecs.ContainerImage.from_registry(image), 58 | environment=self.airflow_environment, 59 | logging=ecs.LogDriver.aws_logs( 60 | stream_prefix=f"{stack.deploy_env}-airflow-{self.service_name}" 61 | ), 62 | command=[self.service_name], 63 | ) 64 | if self.service_port: 65 | self.container.add_port_mappings( 66 | ecs.PortMapping(container_port=self.service_port) 67 | ) 68 | 69 | def _update_environment(self, stack: AirflowStack): 70 | self.airflow_environment.update( 71 | AIRFLOW_USERNAME=stack.master_user_secret.secret_value_from_json( 72 | "username" 73 | ).to_string() 74 | ) 75 | self.airflow_environment.update( 76 | AIRFLOW_PASSWORD=stack.master_user_secret.secret_value_from_json( 77 | "password" 78 | ).to_string() 79 | ) 80 | self.airflow_environment.update( 81 | AIRFLOW_FERNET_KEY=base64.urlsafe_b64encode( 82 | stack.fernet_key_secret.secret_value.to_string().encode() 83 | ).decode() 84 | ) 85 | self.airflow_environment.update( 86 | AIRFLOW_DATABASE_HOST=stack.rds_metadata_db.db_instance_endpoint_address 87 | ) 88 | self.airflow_environment.update( 89 | AIRFLOW_DATABASE_PORT_NUMBER=stack.rds_metadata_db.db_instance_endpoint_port 90 | ) 91 | self.airflow_environment.update( 92 | AIRFLOW_DATABASE_NAME=stack.rds_metadata_db.secret.secret_value_from_json( 93 | "dbname" 94 | ).to_string() 95 | ) 96 | self.airflow_environment.update( 97 | AIRFLOW_DATABASE_USERNAME=stack.rds_metadata_db.secret.secret_value_from_json( 98 | "username" 99 | ).to_string() 100 | ) 101 | self.airflow_environment.update( 102 | AIRFLOW_DATABASE_PASSWORD=stack.rds_metadata_db.secret.secret_value_from_json( 103 | "password" 104 | ).to_string() 105 | ) 106 | self.airflow_environment.update( 107 | REDIS_HOST=stack.celery_backend.attr_redis_endpoint_address 108 | ) 109 | self.airflow_environment.update( 110 | REDIS_PORT_NUMBER=stack.celery_backend.attr_redis_endpoint_port 111 | ) 112 | 113 | 114 | class EcsAirflowLoadBalancedFargateService( 115 | ecs_patterns.ApplicationLoadBalancedFargateService 116 | ): 117 | """ 118 | Task Definition for Airflow Services 119 | """ 120 | 121 | def __init__( 122 | self, 123 | stack: AirflowStack, 124 | service_name: str, 125 | task_definition: ecs.FargateTaskDefinition, 126 | desired_count: int, 127 | cluster: ecs.Cluster, 128 | **kwargs, 129 | ) -> None: 130 | self.object_name = f"{stack.deploy_env}-airflow-{service_name}-ecs-service" 131 | 132 | super().__init__( 133 | stack, 134 | id=self.object_name, 135 | service_name=self.object_name, 136 | task_definition=task_definition, 137 | desired_count=desired_count, 138 | cluster=cluster, 139 | ) 140 | 141 | self.target_group.configure_health_check(healthy_http_codes="200-399") 142 | 143 | 144 | class EcsAirflowAutoscalingFargateService(ecs.FargateService): 145 | """ 146 | Task Definition for Airflow Services 147 | """ 148 | 149 | def __init__( 150 | self, 151 | stack: AirflowStack, 152 | service_name: str, 153 | task_definition: ecs.FargateTaskDefinition, 154 | desired_count: int, 155 | cluster: ecs.Cluster, 156 | max_worker_count: int, 157 | target_memory_utilization: int, 158 | memory_scale_in_cooldown: int, 159 | memory_scale_out_cooldown: int, 160 | target_cpu_utilization: int, 161 | cpu_scale_in_cooldown: int, 162 | cpu_scale_out_cooldown: int, 163 | **kwargs, 164 | ) -> None: 165 | self.object_name = f"{stack.deploy_env}-airflow-{service_name}-ecs-service" 166 | 167 | super().__init__( 168 | stack, 169 | id=self.object_name, 170 | service_name=self.object_name, 171 | task_definition=task_definition, 172 | desired_count=desired_count, 173 | cluster=cluster, 174 | ) 175 | 176 | self.scalable_task_count = self.auto_scale_task_count( 177 | max_capacity=max_worker_count 178 | ) 179 | 180 | self.scalable_task_count.scale_on_memory_utilization( 181 | f"{stack.deploy_env}-airflow-{service_name}-memory-utilization-scaler", 182 | policy_name=f"{stack.deploy_env}-airflow-{service_name}-memory-utilization-scaler", 183 | target_utilization_percent=target_memory_utilization, 184 | scale_in_cooldown=core.Duration.seconds(memory_scale_in_cooldown), 185 | scale_out_cooldown=core.Duration.seconds(memory_scale_out_cooldown), 186 | ) 187 | 188 | self.scalable_task_count.scale_on_cpu_utilization( 189 | f"{stack.deploy_env}-airflow-{service_name}-cpu-utilization-scaler", 190 | policy_name=f"{stack.deploy_env}-airflow-{service_name}-cpu-utilization-scaler", 191 | target_utilization_percent=target_cpu_utilization, 192 | scale_in_cooldown=core.Duration.seconds(cpu_scale_in_cooldown), 193 | scale_out_cooldown=core.Duration.seconds(cpu_scale_out_cooldown), 194 | ) 195 | 196 | 197 | class EcsAirflowFargateService(ecs.FargateService): 198 | """ 199 | Task Definition for Airflow Services 200 | """ 201 | 202 | def __init__( 203 | self, 204 | stack: AirflowStack, 205 | service_name: str, 206 | task_definition: ecs.FargateTaskDefinition, 207 | desired_count: int, 208 | cluster: ecs.Cluster, 209 | **kwargs, 210 | ) -> None: 211 | self.object_name = f"{stack.deploy_env}-airflow-{service_name}-ecs-service" 212 | 213 | super().__init__( 214 | stack, 215 | id=self.object_name, 216 | service_name=self.object_name, 217 | task_definition=task_definition, 218 | desired_count=desired_count, 219 | cluster=cluster, 220 | ) 221 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/**/usage.statistics.xml 10 | .idea/**/dictionaries 11 | .idea/**/shelf 12 | 13 | node_modules/* 14 | node_modules 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/artifacts 37 | # .idea/compiler.xml 38 | # .idea/jarRepositories.xml 39 | # .idea/modules.xml 40 | # .idea/*.iml 41 | # .idea/modules 42 | # *.iml 43 | # *.ipr 44 | 45 | # CMake 46 | cmake-build-*/ 47 | 48 | # Mongo Explorer plugin 49 | .idea/**/mongoSettings.xml 50 | 51 | # File-based project format 52 | *.iws 53 | 54 | # IntelliJ 55 | out/ 56 | 57 | # mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Cursive Clojure plugin 64 | .idea/replstate.xml 65 | 66 | # Crashlytics plugin (for Android Studio and IntelliJ) 67 | com_crashlytics_export_strings.xml 68 | crashlytics.properties 69 | crashlytics-build.properties 70 | fabric.properties 71 | 72 | # Editor-based Rest Client 73 | .idea/httpRequests 74 | 75 | # Android studio 3.1+ serialized cache file 76 | .idea/caches/build_file_checksums.ser 77 | 78 | ### VirtualEnv template 79 | # Virtualenv 80 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 81 | .Python 82 | [Bb]in 83 | [Ii]nclude 84 | [Ll]ib 85 | [Ll]ib64 86 | [Ll]ocal 87 | [Ss]cripts 88 | pyvenv.cfg 89 | .venv 90 | pip-selfcheck.json 91 | 92 | ### macOS template 93 | # General 94 | .DS_Store 95 | .AppleDouble 96 | .LSOverride 97 | 98 | # Icon must end with two \r 99 | Icon 100 | 101 | # Thumbnails 102 | ._* 103 | 104 | # Files that might appear in the root of a volume 105 | .DocumentRevisions-V100 106 | .fseventsd 107 | .Spotlight-V100 108 | .TemporaryItems 109 | .Trashes 110 | .VolumeIcon.icns 111 | .com.apple.timemachine.donotpresent 112 | 113 | # Directories potentially created on remote AFP share 114 | .AppleDB 115 | .AppleDesktop 116 | Network Trash Folder 117 | Temporary Items 118 | .apdisk 119 | 120 | ### macOS template 121 | # General 122 | .DS_Store 123 | .AppleDouble 124 | .LSOverride 125 | 126 | # Icon must end with two \r 127 | Icon 128 | 129 | # Thumbnails 130 | ._* 131 | 132 | # Files that might appear in the root of a volume 133 | .DocumentRevisions-V100 134 | .fseventsd 135 | .Spotlight-V100 136 | .TemporaryItems 137 | .Trashes 138 | .VolumeIcon.icns 139 | .com.apple.timemachine.donotpresent 140 | 141 | # Directories potentially created on remote AFP share 142 | .AppleDB 143 | .AppleDesktop 144 | Network Trash Folder 145 | Temporary Items 146 | .apdisk 147 | 148 | ### Python template 149 | # Byte-compiled / optimized / DLL files 150 | __pycache__/ 151 | *.py[cod] 152 | *$py.class 153 | 154 | # C extensions 155 | *.so 156 | 157 | # Distribution / packaging 158 | .Python 159 | build/ 160 | develop-eggs/ 161 | dist/ 162 | downloads/ 163 | eggs/ 164 | .eggs/ 165 | lib/ 166 | lib64/ 167 | parts/ 168 | sdist/ 169 | var/ 170 | wheels/ 171 | share/python-wheels/ 172 | *.egg-info/ 173 | .installed.cfg 174 | *.egg 175 | MANIFEST 176 | 177 | # PyInstaller 178 | # Usually these files are written by a python script from a template 179 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 180 | *.manifest 181 | *.spec 182 | 183 | # Installer logs 184 | pip-log.txt 185 | pip-delete-this-directory.txt 186 | 187 | # Unit test / coverage reports 188 | htmlcov/ 189 | .tox/ 190 | .nox/ 191 | .coverage 192 | .coverage.* 193 | .cache 194 | nosetests.xml 195 | coverage.xml 196 | *.cover 197 | *.py,cover 198 | .hypothesis/ 199 | .pytest_cache/ 200 | cover/ 201 | 202 | # Translations 203 | *.mo 204 | *.pot 205 | 206 | # Django stuff: 207 | *.log 208 | local_settings.py 209 | db.sqlite3 210 | db.sqlite3-journal 211 | 212 | # Flask stuff: 213 | instance/ 214 | .webassets-cache 215 | 216 | # Scrapy stuff: 217 | .scrapy 218 | 219 | # Sphinx documentation 220 | docs/_build/ 221 | 222 | # PyBuilder 223 | .pybuilder/ 224 | target/ 225 | 226 | # Jupyter Notebook 227 | .ipynb_checkpoints 228 | 229 | # IPython 230 | profile_default/ 231 | ipython_config.py 232 | 233 | # pyenv 234 | # For a library or package, you might want to ignore these files since the code is 235 | # intended to run in multiple environments; otherwise, check them in: 236 | # .python-version 237 | 238 | # pipenv 239 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 240 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 241 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 242 | # install all needed dependencies. 243 | #Pipfile.lock 244 | 245 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 246 | __pypackages__/ 247 | 248 | # Celery stuff 249 | celerybeat-schedule 250 | celerybeat.pid 251 | 252 | # SageMath parsed files 253 | *.sage.py 254 | 255 | # Environments 256 | .env 257 | .venv 258 | env/ 259 | venv/ 260 | ENV/ 261 | env.bak/ 262 | venv.bak/ 263 | 264 | # Spyder project settings 265 | .spyderproject 266 | .spyproject 267 | 268 | # Rope project settings 269 | .ropeproject 270 | 271 | # mkdocs documentation 272 | /site 273 | 274 | # mypy 275 | .mypy_cache/ 276 | .dmypy.json 277 | dmypy.json 278 | 279 | # Pyre type checker 280 | .pyre/ 281 | 282 | # pytype static type analyzer 283 | .pytype/ 284 | 285 | # Cython debug symbols 286 | cython_debug/ 287 | 288 | ### JetBrains template 289 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 290 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 291 | 292 | # User-specific stuff 293 | .idea/**/workspace.xml 294 | .idea/**/tasks.xml 295 | .idea/**/usage.statistics.xml 296 | .idea/**/dictionaries 297 | .idea/**/shelf 298 | 299 | # Generated files 300 | .idea/**/contentModel.xml 301 | 302 | # Sensitive or high-churn files 303 | .idea/**/dataSources/ 304 | .idea/**/dataSources.ids 305 | .idea/**/dataSources.local.xml 306 | .idea/**/sqlDataSources.xml 307 | .idea/**/dynamic.xml 308 | .idea/**/uiDesigner.xml 309 | .idea/**/dbnavigator.xml 310 | 311 | # Gradle 312 | .idea/**/gradle.xml 313 | .idea/**/libraries 314 | 315 | # Gradle and Maven with auto-import 316 | # When using Gradle or Maven with auto-import, you should exclude module files, 317 | # since they will be recreated, and may cause churn. Uncomment if using 318 | # auto-import. 319 | # .idea/artifacts 320 | # .idea/compiler.xml 321 | # .idea/jarRepositories.xml 322 | # .idea/modules.xml 323 | # .idea/*.iml 324 | # .idea/modules 325 | # *.iml 326 | # *.ipr 327 | 328 | # CMake 329 | cmake-build-*/ 330 | 331 | # Mongo Explorer plugin 332 | .idea/**/mongoSettings.xml 333 | 334 | # File-based project format 335 | *.iws 336 | 337 | # IntelliJ 338 | out/ 339 | 340 | # mpeltonen/sbt-idea plugin 341 | .idea_modules/ 342 | 343 | # JIRA plugin 344 | atlassian-ide-plugin.xml 345 | 346 | # Cursive Clojure plugin 347 | .idea/replstate.xml 348 | 349 | # Crashlytics plugin (for Android Studio and IntelliJ) 350 | com_crashlytics_export_strings.xml 351 | crashlytics.properties 352 | crashlytics-build.properties 353 | fabric.properties 354 | 355 | # Editor-based Rest Client 356 | .idea/httpRequests 357 | 358 | # Android studio 3.1+ serialized cache file 359 | .idea/caches/build_file_checksums.ser 360 | 361 | ### VisualStudioCode template 362 | .vscode/* 363 | !.vscode/settings.json 364 | !.vscode/tasks.json 365 | !.vscode/launch.json 366 | !.vscode/extensions.json 367 | *.code-workspace 368 | 369 | # Local History for Visual Studio Code 370 | .history/ 371 | 372 | ### Python template 373 | # Byte-compiled / optimized / DLL files 374 | __pycache__/ 375 | *.py[cod] 376 | *$py.class 377 | 378 | # C extensions 379 | *.so 380 | 381 | # Distribution / packaging 382 | .Python 383 | build/ 384 | develop-eggs/ 385 | dist/ 386 | downloads/ 387 | eggs/ 388 | .eggs/ 389 | lib/ 390 | lib64/ 391 | parts/ 392 | sdist/ 393 | var/ 394 | wheels/ 395 | share/python-wheels/ 396 | *.egg-info/ 397 | .installed.cfg 398 | *.egg 399 | MANIFEST 400 | 401 | # PyInstaller 402 | # Usually these files are written by a python script from a template 403 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 404 | *.manifest 405 | *.spec 406 | 407 | # Installer logs 408 | pip-log.txt 409 | pip-delete-this-directory.txt 410 | 411 | # Unit test / coverage reports 412 | htmlcov/ 413 | .tox/ 414 | .nox/ 415 | .coverage 416 | .coverage.* 417 | .cache 418 | nosetests.xml 419 | coverage.xml 420 | *.cover 421 | *.py,cover 422 | .hypothesis/ 423 | .pytest_cache/ 424 | cover/ 425 | 426 | # Translations 427 | *.mo 428 | *.pot 429 | 430 | # Django stuff: 431 | *.log 432 | local_settings.py 433 | db.sqlite3 434 | db.sqlite3-journal 435 | 436 | # Flask stuff: 437 | instance/ 438 | .webassets-cache 439 | 440 | # Scrapy stuff: 441 | .scrapy 442 | 443 | # Sphinx documentation 444 | docs/_build/ 445 | 446 | # PyBuilder 447 | .pybuilder/ 448 | target/ 449 | 450 | # Jupyter Notebook 451 | .ipynb_checkpoints 452 | 453 | # IPython 454 | profile_default/ 455 | ipython_config.py 456 | 457 | # pyenv 458 | # For a library or package, you might want to ignore these files since the code is 459 | # intended to run in multiple environments; otherwise, check them in: 460 | # .python-version 461 | 462 | # pipenv 463 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 464 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 465 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 466 | # install all needed dependencies. 467 | #Pipfile.lock 468 | 469 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 470 | __pypackages__/ 471 | 472 | # Celery stuff 473 | celerybeat-schedule 474 | celerybeat.pid 475 | 476 | # SageMath parsed files 477 | *.sage.py 478 | 479 | # Environments 480 | .env 481 | .venv 482 | env/ 483 | venv/ 484 | ENV/ 485 | env.bak/ 486 | venv.bak/ 487 | 488 | # Spyder project settings 489 | .spyderproject 490 | .spyproject 491 | 492 | # Rope project settings 493 | .ropeproject 494 | 495 | # mkdocs documentation 496 | /site 497 | 498 | # mypy 499 | .mypy_cache/ 500 | .dmypy.json 501 | dmypy.json 502 | 503 | # Pyre type checker 504 | .pyre/ 505 | 506 | # pytype static type analyzer 507 | .pytype/ 508 | 509 | # Cython debug symbols 510 | cython_debug/ 511 | 512 | # Aws Cdk 513 | .cdk.staging 514 | cdk.out -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "atomicwrites" 11 | version = "1.4.0" 12 | description = "Atomic file writes." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | 17 | [[package]] 18 | name = "attrs" 19 | version = "20.3.0" 20 | description = "Classes Without Boilerplate" 21 | category = "dev" 22 | optional = false 23 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 24 | 25 | [package.extras] 26 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] 27 | docs = ["furo", "sphinx", "zope.interface"] 28 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 29 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] 30 | 31 | [[package]] 32 | name = "awscli-local" 33 | version = "0.14" 34 | description = "Thin wrapper around the \"aws\" command line interface for use with LocalStack" 35 | category = "dev" 36 | optional = false 37 | python-versions = "*" 38 | 39 | [package.dependencies] 40 | localstack-client = "*" 41 | 42 | [package.extras] 43 | ver1 = ["awscli"] 44 | 45 | [[package]] 46 | name = "black" 47 | version = "20.8b1" 48 | description = "The uncompromising code formatter." 49 | category = "dev" 50 | optional = false 51 | python-versions = ">=3.6" 52 | 53 | [package.dependencies] 54 | appdirs = "*" 55 | click = ">=7.1.2" 56 | mypy-extensions = ">=0.4.3" 57 | pathspec = ">=0.6,<1" 58 | regex = ">=2020.1.8" 59 | toml = ">=0.10.1" 60 | typed-ast = ">=1.4.0" 61 | typing-extensions = ">=3.7.4" 62 | 63 | [package.extras] 64 | colorama = ["colorama (>=0.4.3)"] 65 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 66 | 67 | [[package]] 68 | name = "boto3" 69 | version = "1.17.33" 70 | description = "The AWS SDK for Python" 71 | category = "main" 72 | optional = false 73 | python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 74 | 75 | [package.dependencies] 76 | botocore = ">=1.20.33,<1.21.0" 77 | jmespath = ">=0.7.1,<1.0.0" 78 | s3transfer = ">=0.3.0,<0.4.0" 79 | 80 | [[package]] 81 | name = "botocore" 82 | version = "1.20.33" 83 | description = "Low-level, data-driven core of boto 3." 84 | category = "main" 85 | optional = false 86 | python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 87 | 88 | [package.dependencies] 89 | jmespath = ">=0.7.1,<1.0.0" 90 | python-dateutil = ">=2.1,<3.0.0" 91 | urllib3 = ">=1.25.4,<1.27" 92 | 93 | [package.extras] 94 | crt = ["awscrt (==0.10.8)"] 95 | 96 | [[package]] 97 | name = "certifi" 98 | version = "2020.12.5" 99 | description = "Python package for providing Mozilla's CA Bundle." 100 | category = "dev" 101 | optional = false 102 | python-versions = "*" 103 | 104 | [[package]] 105 | name = "cffi" 106 | version = "1.14.5" 107 | description = "Foreign Function Interface for Python calling C code." 108 | category = "main" 109 | optional = false 110 | python-versions = "*" 111 | 112 | [package.dependencies] 113 | pycparser = "*" 114 | 115 | [[package]] 116 | name = "cfgv" 117 | version = "3.2.0" 118 | description = "Validate configuration and produce human readable error messages." 119 | category = "dev" 120 | optional = false 121 | python-versions = ">=3.6.1" 122 | 123 | [[package]] 124 | name = "chardet" 125 | version = "4.0.0" 126 | description = "Universal encoding detector for Python 2 and 3" 127 | category = "dev" 128 | optional = false 129 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 130 | 131 | [[package]] 132 | name = "click" 133 | version = "7.1.2" 134 | description = "Composable command line interface toolkit" 135 | category = "dev" 136 | optional = false 137 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 138 | 139 | [[package]] 140 | name = "colorama" 141 | version = "0.4.4" 142 | description = "Cross-platform colored terminal text." 143 | category = "dev" 144 | optional = false 145 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 146 | 147 | [[package]] 148 | name = "cryptography" 149 | version = "3.4.6" 150 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 151 | category = "main" 152 | optional = false 153 | python-versions = ">=3.6" 154 | 155 | [package.dependencies] 156 | cffi = ">=1.12" 157 | 158 | [package.extras] 159 | docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] 160 | docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] 161 | pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 162 | sdist = ["setuptools-rust (>=0.11.4)"] 163 | ssh = ["bcrypt (>=3.1.5)"] 164 | test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] 165 | 166 | [[package]] 167 | name = "distlib" 168 | version = "0.3.1" 169 | description = "Distribution utilities" 170 | category = "dev" 171 | optional = false 172 | python-versions = "*" 173 | 174 | [[package]] 175 | name = "dnslib" 176 | version = "0.9.14" 177 | description = "Simple library to encode/decode DNS wire-format packets" 178 | category = "dev" 179 | optional = false 180 | python-versions = "*" 181 | 182 | [[package]] 183 | name = "dnspython" 184 | version = "1.16.0" 185 | description = "DNS toolkit" 186 | category = "dev" 187 | optional = false 188 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 189 | 190 | [package.extras] 191 | DNSSEC = ["pycryptodome", "ecdsa (>=0.13)"] 192 | IDNA = ["idna (>=2.1)"] 193 | 194 | [[package]] 195 | name = "docopt" 196 | version = "0.6.2" 197 | description = "Pythonic argument parser, that will make you smile" 198 | category = "dev" 199 | optional = false 200 | python-versions = "*" 201 | 202 | [[package]] 203 | name = "dulwich" 204 | version = "0.20.20" 205 | description = "Python Git Library" 206 | category = "dev" 207 | optional = false 208 | python-versions = ">=3.5" 209 | 210 | [package.dependencies] 211 | certifi = "*" 212 | urllib3 = ">=1.24.1" 213 | 214 | [package.extras] 215 | fastimport = ["fastimport"] 216 | https = ["urllib3[secure] (>=1.24.1)"] 217 | pgp = ["gpg"] 218 | watch = ["pyinotify"] 219 | 220 | [[package]] 221 | name = "filelock" 222 | version = "3.0.12" 223 | description = "A platform independent file lock." 224 | category = "dev" 225 | optional = false 226 | python-versions = "*" 227 | 228 | [[package]] 229 | name = "flake8" 230 | version = "3.9.0" 231 | description = "the modular source code checker: pep8 pyflakes and co" 232 | category = "dev" 233 | optional = false 234 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 235 | 236 | [package.dependencies] 237 | mccabe = ">=0.6.0,<0.7.0" 238 | pycodestyle = ">=2.7.0,<2.8.0" 239 | pyflakes = ">=2.3.0,<2.4.0" 240 | 241 | [[package]] 242 | name = "identify" 243 | version = "2.1.3" 244 | description = "File identification library for Python" 245 | category = "dev" 246 | optional = false 247 | python-versions = ">=3.6.1" 248 | 249 | [package.extras] 250 | license = ["editdistance"] 251 | 252 | [[package]] 253 | name = "idna" 254 | version = "2.10" 255 | description = "Internationalized Domain Names in Applications (IDNA)" 256 | category = "dev" 257 | optional = false 258 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 259 | 260 | [[package]] 261 | name = "iniconfig" 262 | version = "1.1.1" 263 | description = "iniconfig: brain-dead simple config-ini parsing" 264 | category = "dev" 265 | optional = false 266 | python-versions = "*" 267 | 268 | [[package]] 269 | name = "jinja2" 270 | version = "2.11.3" 271 | description = "A very fast and expressive template engine." 272 | category = "dev" 273 | optional = false 274 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 275 | 276 | [package.dependencies] 277 | MarkupSafe = ">=0.23" 278 | 279 | [package.extras] 280 | i18n = ["Babel (>=0.8)"] 281 | 282 | [[package]] 283 | name = "jmespath" 284 | version = "0.10.0" 285 | description = "JSON Matching Expressions" 286 | category = "main" 287 | optional = false 288 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 289 | 290 | [[package]] 291 | name = "localstack" 292 | version = "0.12.7" 293 | description = "An easy-to-use test/mocking framework for developing Cloud applications" 294 | category = "dev" 295 | optional = false 296 | python-versions = "*" 297 | 298 | [package.dependencies] 299 | boto3 = ">=1.14.33" 300 | dnspython = "1.16.0" 301 | docopt = ">=0.6.2" 302 | localstack-client = ">=1.12" 303 | localstack-ext = ">=0.12.5" 304 | requests = ">=2.20.0" 305 | six = ">=1.12.0" 306 | 307 | [package.extras] 308 | full = ["airspeed (>=0.5.14)", "amazon_kclpy-ext (==1.5.1)", "aws-sam-translator (>=1.15.1)", "awscli (>=1.14.18)", "boto (>=2.49.0)", "botocore (>=1.12.13)", "cachetools (>=3.1.1,<4.0.0)", "cbor2 (>=5.2.0)", "coverage (==4.5.4)", "crontab (>=0.22.6)", "cryptography (<3.4)", "elasticsearch (>=7.0.0,<8.0.0)", "flask (>=1.0.2)", "flask-cors (>=3.0.3,<3.1.0)", "flask_swagger (==0.2.12)", "forbiddenfruit (==0.1.3)", "jsondiff (>=1.2.0)", "jsonpatch (>=1.24,<2.0)", "jsonpath-rw (>=1.4.0,<2.0.0)", "localstack-ext[full] (>=0.12.5)", "moto-ext[all] (>=1.3.15.12)", "nose (>=1.3.7)", "nose-timer (>=0.7.5)", "psutil (>=5.4.8,<6.0.0)", "pympler (>=0.6)", "pyopenssl (==17.5.0)", "python-coveralls (>=2.9.1)", "pyyaml (>=3.13,<=5.1)", "Quart (>=0.6.15)", "requests-aws4auth (==0.9)", "sasl (>=0.2.1)", "xmltodict (>=0.11.0)"] 309 | 310 | [[package]] 311 | name = "localstack-client" 312 | version = "1.14" 313 | description = "A lightweight Python client for LocalStack." 314 | category = "dev" 315 | optional = false 316 | python-versions = "*" 317 | 318 | [package.dependencies] 319 | boto3 = "*" 320 | 321 | [[package]] 322 | name = "localstack-ext" 323 | version = "0.12.5.34" 324 | description = "Extensions for LocalStack" 325 | category = "dev" 326 | optional = false 327 | python-versions = "*" 328 | 329 | [package.dependencies] 330 | dnslib = ">=0.9.10" 331 | dnspython = ">=1.16.0" 332 | dulwich = ">=0.19.16" 333 | pyaes = ">=1.6.0" 334 | requests = ">=2.20.0" 335 | 336 | [package.extras] 337 | full = ["amazon.ion (<0.6)", "dill (==0.3.2)", "dirtyjson (==1.0.7)", "docker (>=3.7.2)", "docker-registry-client (>=0.5.2)", "ecdsa (<0.14)", "graphql-core (>=3.0.3)", "hbmqtt (>=0.9.5)", "janus (>=0.5.0)", "kubernetes (>=10.0.1)", "pg8000 (>=1.10)", "postgres (>=2.2.2)", "postgresql-proxy (>=0.0.1)", "presto-python-client (>=0.7.0)", "pyftpdlib (>=1.5.6)", "pyhive[hive] (>=0.6.1)", "pyion2json (>=0.0.2)", "PyJWT (>=1.7.0)", "pyqldb (<3)", "pysiddhi-ext (>=5.1.0)", "readerwriterlock (>=1.0.7)", "rsa (>=4.0)", "srp-ext (==1.0.7.1)", "tabulate", "testing.common.database (>=1.1.0)", "warrant-ext (>=0.2.0.5)", "websockets (>=8.1)", "Whoosh (>=2.7.4)"] 338 | 339 | [[package]] 340 | name = "markupsafe" 341 | version = "1.1.1" 342 | description = "Safely add untrusted strings to HTML/XML markup." 343 | category = "dev" 344 | optional = false 345 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 346 | 347 | [[package]] 348 | name = "mccabe" 349 | version = "0.6.1" 350 | description = "McCabe checker, plugin for flake8" 351 | category = "dev" 352 | optional = false 353 | python-versions = "*" 354 | 355 | [[package]] 356 | name = "mock" 357 | version = "4.0.3" 358 | description = "Rolling backport of unittest.mock for all Pythons" 359 | category = "dev" 360 | optional = false 361 | python-versions = ">=3.6" 362 | 363 | [package.extras] 364 | build = ["twine", "wheel", "blurb"] 365 | docs = ["sphinx"] 366 | test = ["pytest (<5.4)", "pytest-cov"] 367 | 368 | [[package]] 369 | name = "more-itertools" 370 | version = "8.7.0" 371 | description = "More routines for operating on iterables, beyond itertools" 372 | category = "dev" 373 | optional = false 374 | python-versions = ">=3.5" 375 | 376 | [[package]] 377 | name = "moto" 378 | version = "2.0.2" 379 | description = "A library that allows your python tests to easily mock out the boto library" 380 | category = "dev" 381 | optional = false 382 | python-versions = "*" 383 | 384 | [package.dependencies] 385 | boto3 = ">=1.9.201" 386 | botocore = ">=1.12.201" 387 | cryptography = ">=3.3.1" 388 | Jinja2 = ">=2.10.1" 389 | MarkupSafe = "<2.0" 390 | mock = "*" 391 | more-itertools = "*" 392 | python-dateutil = ">=2.1,<3.0.0" 393 | pytz = "*" 394 | requests = ">=2.5" 395 | responses = ">=0.9.0" 396 | six = ">1.9" 397 | werkzeug = "*" 398 | xmltodict = "*" 399 | zipp = "*" 400 | 401 | [package.extras] 402 | all = ["PyYAML (>=5.1)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)", "docker (>=2.5.1)", "jsondiff (>=1.1.2)", "aws-xray-sdk (>=0.93,!=0.96)", "idna (>=2.5,<3)", "cfn-lint (>=0.4.0)", "sshpubkeys (==3.1.0)", "sshpubkeys (>=3.1.0)"] 403 | apigateway = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)"] 404 | awslambda = ["docker (>=2.5.1)"] 405 | batch = ["docker (>=2.5.1)"] 406 | cloudformation = ["docker (>=2.5.1)", "PyYAML (>=5.1)", "cfn-lint (>=0.4.0)"] 407 | cognitoidp = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)"] 408 | dynamodb2 = ["docker (>=2.5.1)"] 409 | dynamodbstreams = ["docker (>=2.5.1)"] 410 | ec2 = ["docker (>=2.5.1)", "sshpubkeys (==3.1.0)", "sshpubkeys (>=3.1.0)"] 411 | iotdata = ["jsondiff (>=1.1.2)"] 412 | s3 = ["PyYAML (>=5.1)"] 413 | server = ["PyYAML (>=5.1)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)", "docker (>=2.5.1)", "jsondiff (>=1.1.2)", "aws-xray-sdk (>=0.93,!=0.96)", "idna (>=2.5,<3)", "cfn-lint (>=0.4.0)", "flask", "flask-cors", "sshpubkeys (==3.1.0)", "sshpubkeys (>=3.1.0)"] 414 | ses = ["docker (>=2.5.1)"] 415 | sns = ["docker (>=2.5.1)"] 416 | sqs = ["docker (>=2.5.1)"] 417 | ssm = ["docker (>=2.5.1)", "PyYAML (>=5.1)", "cfn-lint (>=0.4.0)"] 418 | xray = ["aws-xray-sdk (>=0.93,!=0.96)"] 419 | 420 | [[package]] 421 | name = "mypy-extensions" 422 | version = "0.4.3" 423 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 424 | category = "dev" 425 | optional = false 426 | python-versions = "*" 427 | 428 | [[package]] 429 | name = "nodeenv" 430 | version = "1.5.0" 431 | description = "Node.js virtual environment builder" 432 | category = "dev" 433 | optional = false 434 | python-versions = "*" 435 | 436 | [[package]] 437 | name = "packaging" 438 | version = "20.9" 439 | description = "Core utilities for Python packages" 440 | category = "dev" 441 | optional = false 442 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 443 | 444 | [package.dependencies] 445 | pyparsing = ">=2.0.2" 446 | 447 | [[package]] 448 | name = "pathspec" 449 | version = "0.8.1" 450 | description = "Utility library for gitignore style pattern matching of file paths." 451 | category = "dev" 452 | optional = false 453 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 454 | 455 | [[package]] 456 | name = "pluggy" 457 | version = "0.13.1" 458 | description = "plugin and hook calling mechanisms for python" 459 | category = "dev" 460 | optional = false 461 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 462 | 463 | [package.extras] 464 | dev = ["pre-commit", "tox"] 465 | 466 | [[package]] 467 | name = "pre-commit" 468 | version = "2.11.1" 469 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 470 | category = "dev" 471 | optional = false 472 | python-versions = ">=3.6.1" 473 | 474 | [package.dependencies] 475 | cfgv = ">=2.0.0" 476 | identify = ">=1.0.0" 477 | nodeenv = ">=0.11.1" 478 | pyyaml = ">=5.1" 479 | toml = "*" 480 | virtualenv = ">=20.0.8" 481 | 482 | [[package]] 483 | name = "py" 484 | version = "1.10.0" 485 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 486 | category = "dev" 487 | optional = false 488 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 489 | 490 | [[package]] 491 | name = "pyaes" 492 | version = "1.6.1" 493 | description = "Pure-Python Implementation of the AES block-cipher and common modes of operation" 494 | category = "dev" 495 | optional = false 496 | python-versions = "*" 497 | 498 | [[package]] 499 | name = "pycodestyle" 500 | version = "2.7.0" 501 | description = "Python style guide checker" 502 | category = "dev" 503 | optional = false 504 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 505 | 506 | [[package]] 507 | name = "pycparser" 508 | version = "2.20" 509 | description = "C parser in Python" 510 | category = "main" 511 | optional = false 512 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 513 | 514 | [[package]] 515 | name = "pyflakes" 516 | version = "2.3.0" 517 | description = "passive checker of Python programs" 518 | category = "dev" 519 | optional = false 520 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 521 | 522 | [[package]] 523 | name = "pyparsing" 524 | version = "2.4.7" 525 | description = "Python parsing module" 526 | category = "dev" 527 | optional = false 528 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 529 | 530 | [[package]] 531 | name = "pytest" 532 | version = "6.2.2" 533 | description = "pytest: simple powerful testing with Python" 534 | category = "dev" 535 | optional = false 536 | python-versions = ">=3.6" 537 | 538 | [package.dependencies] 539 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 540 | attrs = ">=19.2.0" 541 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 542 | iniconfig = "*" 543 | packaging = "*" 544 | pluggy = ">=0.12,<1.0.0a1" 545 | py = ">=1.8.2" 546 | toml = "*" 547 | 548 | [package.extras] 549 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 550 | 551 | [[package]] 552 | name = "python-dateutil" 553 | version = "2.8.1" 554 | description = "Extensions to the standard Python datetime module" 555 | category = "main" 556 | optional = false 557 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 558 | 559 | [package.dependencies] 560 | six = ">=1.5" 561 | 562 | [[package]] 563 | name = "pytz" 564 | version = "2021.1" 565 | description = "World timezone definitions, modern and historical" 566 | category = "dev" 567 | optional = false 568 | python-versions = "*" 569 | 570 | [[package]] 571 | name = "pyyaml" 572 | version = "5.4.1" 573 | description = "YAML parser and emitter for Python" 574 | category = "dev" 575 | optional = false 576 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 577 | 578 | [[package]] 579 | name = "regex" 580 | version = "2021.3.17" 581 | description = "Alternative regular expression module, to replace re." 582 | category = "dev" 583 | optional = false 584 | python-versions = "*" 585 | 586 | [[package]] 587 | name = "requests" 588 | version = "2.25.1" 589 | description = "Python HTTP for Humans." 590 | category = "dev" 591 | optional = false 592 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 593 | 594 | [package.dependencies] 595 | certifi = ">=2017.4.17" 596 | chardet = ">=3.0.2,<5" 597 | idna = ">=2.5,<3" 598 | urllib3 = ">=1.21.1,<1.27" 599 | 600 | [package.extras] 601 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 602 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 603 | 604 | [[package]] 605 | name = "responses" 606 | version = "0.13.1" 607 | description = "A utility library for mocking out the `requests` Python library." 608 | category = "dev" 609 | optional = false 610 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 611 | 612 | [package.dependencies] 613 | requests = ">=2.0" 614 | six = "*" 615 | urllib3 = ">=1.25.10" 616 | 617 | [package.extras] 618 | tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"] 619 | 620 | [[package]] 621 | name = "s3transfer" 622 | version = "0.3.6" 623 | description = "An Amazon S3 Transfer Manager" 624 | category = "main" 625 | optional = false 626 | python-versions = "*" 627 | 628 | [package.dependencies] 629 | botocore = ">=1.12.36,<2.0a.0" 630 | 631 | [[package]] 632 | name = "six" 633 | version = "1.15.0" 634 | description = "Python 2 and 3 compatibility utilities" 635 | category = "main" 636 | optional = false 637 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 638 | 639 | [[package]] 640 | name = "toml" 641 | version = "0.10.2" 642 | description = "Python Library for Tom's Obvious, Minimal Language" 643 | category = "dev" 644 | optional = false 645 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 646 | 647 | [[package]] 648 | name = "typed-ast" 649 | version = "1.4.2" 650 | description = "a fork of Python 2 and 3 ast modules with type comment support" 651 | category = "dev" 652 | optional = false 653 | python-versions = "*" 654 | 655 | [[package]] 656 | name = "typing-extensions" 657 | version = "3.7.4.3" 658 | description = "Backported and Experimental Type Hints for Python 3.5+" 659 | category = "dev" 660 | optional = false 661 | python-versions = "*" 662 | 663 | [[package]] 664 | name = "urllib3" 665 | version = "1.26.4" 666 | description = "HTTP library with thread-safe connection pooling, file post, and more." 667 | category = "main" 668 | optional = false 669 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 670 | 671 | [package.extras] 672 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 673 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 674 | brotli = ["brotlipy (>=0.6.0)"] 675 | 676 | [[package]] 677 | name = "virtualenv" 678 | version = "20.4.3" 679 | description = "Virtual Python Environment builder" 680 | category = "dev" 681 | optional = false 682 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 683 | 684 | [package.dependencies] 685 | appdirs = ">=1.4.3,<2" 686 | distlib = ">=0.3.1,<1" 687 | filelock = ">=3.0.0,<4" 688 | six = ">=1.9.0,<2" 689 | 690 | [package.extras] 691 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] 692 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] 693 | 694 | [[package]] 695 | name = "werkzeug" 696 | version = "1.0.1" 697 | description = "The comprehensive WSGI web application library." 698 | category = "dev" 699 | optional = false 700 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 701 | 702 | [package.extras] 703 | dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] 704 | watchdog = ["watchdog"] 705 | 706 | [[package]] 707 | name = "xmltodict" 708 | version = "0.12.0" 709 | description = "Makes working with XML feel like you are working with JSON" 710 | category = "dev" 711 | optional = false 712 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 713 | 714 | [[package]] 715 | name = "zipp" 716 | version = "3.4.1" 717 | description = "Backport of pathlib-compatible object wrapper for zip files" 718 | category = "dev" 719 | optional = false 720 | python-versions = ">=3.6" 721 | 722 | [package.extras] 723 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 724 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 725 | 726 | [metadata] 727 | lock-version = "1.1" 728 | python-versions = "^3.8" 729 | content-hash = "f5c12e7b749dd73fab5b686759d7f0b7b260aafc58cb721b4e417835090dea32" 730 | 731 | [metadata.files] 732 | appdirs = [ 733 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 734 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 735 | ] 736 | atomicwrites = [ 737 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 738 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 739 | ] 740 | attrs = [ 741 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, 742 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, 743 | ] 744 | awscli-local = [ 745 | {file = "awscli-local-0.14.tar.gz", hash = "sha256:61a9182429211a91d057fdcdbcb0875f844fbcec1c66cecfec5770ae4fb3e8dc"}, 746 | ] 747 | black = [ 748 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 749 | ] 750 | boto3 = [ 751 | {file = "boto3-1.17.33-py2.py3-none-any.whl", hash = "sha256:3306dad87f993703b102a0a70ca19c549b7f41e7f70fa7b4c579735c9f79351d"}, 752 | {file = "boto3-1.17.33.tar.gz", hash = "sha256:0cac2fffc1ba915f7bb5ecee539318532db51f218c928a228fafe3e501e9472e"}, 753 | ] 754 | botocore = [ 755 | {file = "botocore-1.20.33-py2.py3-none-any.whl", hash = "sha256:a33e862685259fe22d9790d9c9f3567feda8b824d44d3c62a3617af1133543a4"}, 756 | {file = "botocore-1.20.33.tar.gz", hash = "sha256:e355305309699d3aca1e0050fc21d48595b40db046cb0d2491cd57ff5b26920b"}, 757 | ] 758 | certifi = [ 759 | {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, 760 | {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, 761 | ] 762 | cffi = [ 763 | {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, 764 | {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, 765 | {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, 766 | {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, 767 | {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, 768 | {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, 769 | {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, 770 | {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, 771 | {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, 772 | {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, 773 | {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, 774 | {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, 775 | {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, 776 | {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, 777 | {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, 778 | {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, 779 | {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, 780 | {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, 781 | {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, 782 | {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, 783 | {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, 784 | {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, 785 | {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, 786 | {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, 787 | {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, 788 | {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, 789 | {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, 790 | {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, 791 | {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, 792 | {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, 793 | {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, 794 | {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, 795 | {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, 796 | {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, 797 | {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, 798 | {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, 799 | {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, 800 | ] 801 | cfgv = [ 802 | {file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"}, 803 | {file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"}, 804 | ] 805 | chardet = [ 806 | {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, 807 | {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, 808 | ] 809 | click = [ 810 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 811 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 812 | ] 813 | colorama = [ 814 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 815 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 816 | ] 817 | cryptography = [ 818 | {file = "cryptography-3.4.6-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799"}, 819 | {file = "cryptography-3.4.6-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:4169a27b818de4a1860720108b55a2801f32b6ae79e7f99c00d79f2a2822eeb7"}, 820 | {file = "cryptography-3.4.6-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3"}, 821 | {file = "cryptography-3.4.6-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b"}, 822 | {file = "cryptography-3.4.6-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964"}, 823 | {file = "cryptography-3.4.6-cp36-abi3-win32.whl", hash = "sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2"}, 824 | {file = "cryptography-3.4.6-cp36-abi3-win_amd64.whl", hash = "sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0"}, 825 | {file = "cryptography-3.4.6-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:066bc53f052dfeda2f2d7c195cf16fb3e5ff13e1b6b7415b468514b40b381a5b"}, 826 | {file = "cryptography-3.4.6-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:600cf9bfe75e96d965509a4c0b2b183f74a4fa6f5331dcb40fb7b77b7c2484df"}, 827 | {file = "cryptography-3.4.6-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:0923ba600d00718d63a3976f23cab19aef10c1765038945628cd9be047ad0336"}, 828 | {file = "cryptography-3.4.6-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:9e98b452132963678e3ac6c73f7010fe53adf72209a32854d55690acac3f6724"}, 829 | {file = "cryptography-3.4.6.tar.gz", hash = "sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87"}, 830 | ] 831 | distlib = [ 832 | {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, 833 | {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, 834 | ] 835 | dnslib = [ 836 | {file = "dnslib-0.9.14.tar.gz", hash = "sha256:fbd20a7cd704836923be8e130f0da6c3d840f14ac04590fae420a41d1f1be6fb"}, 837 | ] 838 | dnspython = [ 839 | {file = "dnspython-1.16.0-py2.py3-none-any.whl", hash = "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"}, 840 | {file = "dnspython-1.16.0.zip", hash = "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01"}, 841 | ] 842 | docopt = [ 843 | {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, 844 | ] 845 | dulwich = [ 846 | {file = "dulwich-0.20.20-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da920a0d2ff736b89a403f8d5413c78fbac015fbc37775cd862820c868b31cad"}, 847 | {file = "dulwich-0.20.20-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cfb727c59bfa418d378934206e29857512f00608cbc5b27e544b2357d36f9713"}, 848 | {file = "dulwich-0.20.20-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1af700600ee04e0ddccc619431ea554e4af97ccbb37e5e61dfdf3ca08cea7b05"}, 849 | {file = "dulwich-0.20.20-cp36-cp36m-win_amd64.whl", hash = "sha256:61d91761a10ba90e2bb709297d7dab9c6a74b19bafb07c1bc644ee2a6f835c0b"}, 850 | {file = "dulwich-0.20.20-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:cfc48fa2ce877360f0a7aa0ec3aa5143d61617ec66f96bb11a30c7ae135ec450"}, 851 | {file = "dulwich-0.20.20-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c7bddd947b62c50cfe1bcc10da5ed51aecf38b4b2ea0e8b420232aaa7cf64ef"}, 852 | {file = "dulwich-0.20.20-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:22c3f90c9c85b09314c2775ad7454edada433cc0f4ba69334383c9b00b09ef3c"}, 853 | {file = "dulwich-0.20.20-cp37-cp37m-win_amd64.whl", hash = "sha256:2833e9cf32d653cb326ea2423eedb0439e21d95e7b72a9e4c3ca00e2526f4554"}, 854 | {file = "dulwich-0.20.20-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:1035720230d9defc2178ca79b8036442896a3391a451ed09d245f3fadf389452"}, 855 | {file = "dulwich-0.20.20-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ff11304aaa3f452257454d11d084226fb008f76977a4c90ae2ba3dee81017a74"}, 856 | {file = "dulwich-0.20.20-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:def6f826cae29d8d9ce0429fbec0c48ec7924ba37d596f2076b5f55bff93d793"}, 857 | {file = "dulwich-0.20.20-cp38-cp38-win_amd64.whl", hash = "sha256:a5507bdc13ac88eafc7d03e58288814bbf59648b484bd27b7692cc9fe3fd87a4"}, 858 | {file = "dulwich-0.20.20-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:03e7e75a8777fdb92efa66d25be378704d0b1c18af0f5970a3288c0a7c210e19"}, 859 | {file = "dulwich-0.20.20-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e0bbcb3e9a5889d3e9e21ea99ae239389da054208e31485c591f60c7b08345aa"}, 860 | {file = "dulwich-0.20.20-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:e24f7f665c7e615c7c6a51a1d8e24e89a9c1a9f5898b89bbeff4a845ad3c19dc"}, 861 | {file = "dulwich-0.20.20-cp39-cp39-win_amd64.whl", hash = "sha256:f94396c746df2678fc9a3c7abd6c135ea4ba10e1c27f5d1f1cdeebd163541232"}, 862 | {file = "dulwich-0.20.20.tar.gz", hash = "sha256:426959b9705fadcc6c820e5adf3291d71a48aba0afccf7411422e3308f115f87"}, 863 | ] 864 | filelock = [ 865 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 866 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 867 | ] 868 | flake8 = [ 869 | {file = "flake8-3.9.0-py2.py3-none-any.whl", hash = "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff"}, 870 | {file = "flake8-3.9.0.tar.gz", hash = "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"}, 871 | ] 872 | identify = [ 873 | {file = "identify-2.1.3-py2.py3-none-any.whl", hash = "sha256:46d1816c6a4fc2d1e8758f293a5dcc1ae6404ab344179d7c1e73637bf283beb1"}, 874 | {file = "identify-2.1.3.tar.gz", hash = "sha256:ed4a05fb80e3cbd12e83c959f9ff7f729ba6b66ab8d6178850fd5cb4c1cf6c5d"}, 875 | ] 876 | idna = [ 877 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 878 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 879 | ] 880 | iniconfig = [ 881 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 882 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 883 | ] 884 | jinja2 = [ 885 | {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, 886 | {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, 887 | ] 888 | jmespath = [ 889 | {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"}, 890 | {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"}, 891 | ] 892 | localstack = [ 893 | {file = "localstack-0.12.7.tar.gz", hash = "sha256:6ff6f10338d47cf06bb8ff540133f9d1026fc11c7ce2f595d2820c6c8f1d932c"}, 894 | ] 895 | localstack-client = [ 896 | {file = "localstack-client-1.14.tar.gz", hash = "sha256:dcfe0258d076d654c8b8ffd14a0783216ed7eb0567e66313928beab03966e549"}, 897 | ] 898 | localstack-ext = [ 899 | {file = "localstack-ext-0.12.5.34.tar.gz", hash = "sha256:8d44de4ad8c822421631771feb33573d6a10af63a0227aa5f2d0f88496ec23a4"}, 900 | ] 901 | markupsafe = [ 902 | {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, 903 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, 904 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, 905 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, 906 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, 907 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, 908 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, 909 | {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, 910 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, 911 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, 912 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, 913 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, 914 | {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, 915 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, 916 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, 917 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, 918 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, 919 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, 920 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, 921 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, 922 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, 923 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, 924 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, 925 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, 926 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, 927 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, 928 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, 929 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, 930 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, 931 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, 932 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, 933 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, 934 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, 935 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, 936 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, 937 | {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, 938 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, 939 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, 940 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, 941 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, 942 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, 943 | {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, 944 | {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, 945 | {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, 946 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, 947 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, 948 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, 949 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, 950 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, 951 | {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, 952 | {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, 953 | {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, 954 | ] 955 | mccabe = [ 956 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 957 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 958 | ] 959 | mock = [ 960 | {file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"}, 961 | {file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"}, 962 | ] 963 | more-itertools = [ 964 | {file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"}, 965 | {file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"}, 966 | ] 967 | moto = [ 968 | {file = "moto-2.0.2-py2.py3-none-any.whl", hash = "sha256:f5db62e50a5377da4457307675281198e9ffbe9425866a88f523bef0c6e8d463"}, 969 | {file = "moto-2.0.2.tar.gz", hash = "sha256:4610d27ead9124eaa84a78eca7dfa25a8ccb66cf6a7cb8a8889b5ca0c7796889"}, 970 | ] 971 | mypy-extensions = [ 972 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 973 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 974 | ] 975 | nodeenv = [ 976 | {file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"}, 977 | {file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"}, 978 | ] 979 | packaging = [ 980 | {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, 981 | {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, 982 | ] 983 | pathspec = [ 984 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 985 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 986 | ] 987 | pluggy = [ 988 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 989 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 990 | ] 991 | pre-commit = [ 992 | {file = "pre_commit-2.11.1-py2.py3-none-any.whl", hash = "sha256:94c82f1bf5899d56edb1d926732f4e75a7df29a0c8c092559c77420c9d62428b"}, 993 | {file = "pre_commit-2.11.1.tar.gz", hash = "sha256:de55c5c72ce80d79106e48beb1b54104d16495ce7f95b0c7b13d4784193a00af"}, 994 | ] 995 | py = [ 996 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 997 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 998 | ] 999 | pyaes = [ 1000 | {file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"}, 1001 | ] 1002 | pycodestyle = [ 1003 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 1004 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 1005 | ] 1006 | pycparser = [ 1007 | {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, 1008 | {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, 1009 | ] 1010 | pyflakes = [ 1011 | {file = "pyflakes-2.3.0-py2.py3-none-any.whl", hash = "sha256:910208209dcea632721cb58363d0f72913d9e8cf64dc6f8ae2e02a3609aba40d"}, 1012 | {file = "pyflakes-2.3.0.tar.gz", hash = "sha256:e59fd8e750e588358f1b8885e5a4751203a0516e0ee6d34811089ac294c8806f"}, 1013 | ] 1014 | pyparsing = [ 1015 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 1016 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 1017 | ] 1018 | pytest = [ 1019 | {file = "pytest-6.2.2-py3-none-any.whl", hash = "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"}, 1020 | {file = "pytest-6.2.2.tar.gz", hash = "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9"}, 1021 | ] 1022 | python-dateutil = [ 1023 | {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, 1024 | {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, 1025 | ] 1026 | pytz = [ 1027 | {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, 1028 | {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, 1029 | ] 1030 | pyyaml = [ 1031 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 1032 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 1033 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 1034 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 1035 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 1036 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 1037 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 1038 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 1039 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 1040 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 1041 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 1042 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 1043 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 1044 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 1045 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 1046 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 1047 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 1048 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 1049 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 1050 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 1051 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 1052 | ] 1053 | regex = [ 1054 | {file = "regex-2021.3.17-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b97ec5d299c10d96617cc851b2e0f81ba5d9d6248413cd374ef7f3a8871ee4a6"}, 1055 | {file = "regex-2021.3.17-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cb4ee827857a5ad9b8ae34d3c8cc51151cb4a3fe082c12ec20ec73e63cc7c6f0"}, 1056 | {file = "regex-2021.3.17-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:633497504e2a485a70a3268d4fc403fe3063a50a50eed1039083e9471ad0101c"}, 1057 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a59a2ee329b3de764b21495d78c92ab00b4ea79acef0f7ae8c1067f773570afa"}, 1058 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f85d6f41e34f6a2d1607e312820971872944f1661a73d33e1e82d35ea3305e14"}, 1059 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4651f839dbde0816798e698626af6a2469eee6d9964824bb5386091255a1694f"}, 1060 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:39c44532d0e4f1639a89e52355b949573e1e2c5116106a395642cbbae0ff9bcd"}, 1061 | {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3d9a7e215e02bd7646a91fb8bcba30bc55fd42a719d6b35cf80e5bae31d9134e"}, 1062 | {file = "regex-2021.3.17-cp36-cp36m-win32.whl", hash = "sha256:159fac1a4731409c830d32913f13f68346d6b8e39650ed5d704a9ce2f9ef9cb3"}, 1063 | {file = "regex-2021.3.17-cp36-cp36m-win_amd64.whl", hash = "sha256:13f50969028e81765ed2a1c5fcfdc246c245cf8d47986d5172e82ab1a0c42ee5"}, 1064 | {file = "regex-2021.3.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9d8d286c53fe0cbc6d20bf3d583cabcd1499d89034524e3b94c93a5ab85ca90"}, 1065 | {file = "regex-2021.3.17-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:201e2619a77b21a7780580ab7b5ce43835e242d3e20fef50f66a8df0542e437f"}, 1066 | {file = "regex-2021.3.17-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d47d359545b0ccad29d572ecd52c9da945de7cd6cf9c0cfcb0269f76d3555689"}, 1067 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ea2f41445852c660ba7c3ebf7d70b3779b20d9ca8ba54485a17740db49f46932"}, 1068 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:486a5f8e11e1f5bbfcad87f7c7745eb14796642323e7e1829a331f87a713daaa"}, 1069 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:18e25e0afe1cf0f62781a150c1454b2113785401ba285c745acf10c8ca8917df"}, 1070 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:a2ee026f4156789df8644d23ef423e6194fad0bc53575534101bb1de5d67e8ce"}, 1071 | {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:4c0788010a93ace8a174d73e7c6c9d3e6e3b7ad99a453c8ee8c975ddd9965643"}, 1072 | {file = "regex-2021.3.17-cp37-cp37m-win32.whl", hash = "sha256:575a832e09d237ae5fedb825a7a5bc6a116090dd57d6417d4f3b75121c73e3be"}, 1073 | {file = "regex-2021.3.17-cp37-cp37m-win_amd64.whl", hash = "sha256:8e65e3e4c6feadf6770e2ad89ad3deb524bcb03d8dc679f381d0568c024e0deb"}, 1074 | {file = "regex-2021.3.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a0df9a0ad2aad49ea3c7f65edd2ffb3d5c59589b85992a6006354f6fb109bb18"}, 1075 | {file = "regex-2021.3.17-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b98bc9db003f1079caf07b610377ed1ac2e2c11acc2bea4892e28cc5b509d8d5"}, 1076 | {file = "regex-2021.3.17-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:808404898e9a765e4058bf3d7607d0629000e0a14a6782ccbb089296b76fa8fe"}, 1077 | {file = "regex-2021.3.17-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:5770a51180d85ea468234bc7987f5597803a4c3d7463e7323322fe4a1b181578"}, 1078 | {file = "regex-2021.3.17-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:976a54d44fd043d958a69b18705a910a8376196c6b6ee5f2596ffc11bff4420d"}, 1079 | {file = "regex-2021.3.17-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:63f3ca8451e5ff7133ffbec9eda641aeab2001be1a01878990f6c87e3c44b9d5"}, 1080 | {file = "regex-2021.3.17-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bcd945175c29a672f13fce13a11893556cd440e37c1b643d6eeab1988c8b209c"}, 1081 | {file = "regex-2021.3.17-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:3d9356add82cff75413bec360c1eca3e58db4a9f5dafa1f19650958a81e3249d"}, 1082 | {file = "regex-2021.3.17-cp38-cp38-win32.whl", hash = "sha256:f5d0c921c99297354cecc5a416ee4280bd3f20fd81b9fb671ca6be71499c3fdf"}, 1083 | {file = "regex-2021.3.17-cp38-cp38-win_amd64.whl", hash = "sha256:14de88eda0976020528efc92d0a1f8830e2fb0de2ae6005a6fc4e062553031fa"}, 1084 | {file = "regex-2021.3.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4c2e364491406b7888c2ad4428245fc56c327e34a5dfe58fd40df272b3c3dab3"}, 1085 | {file = "regex-2021.3.17-cp39-cp39-manylinux1_i686.whl", hash = "sha256:8bd4f91f3fb1c9b1380d6894bd5b4a519409135bec14c0c80151e58394a4e88a"}, 1086 | {file = "regex-2021.3.17-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:882f53afe31ef0425b405a3f601c0009b44206ea7f55ee1c606aad3cc213a52c"}, 1087 | {file = "regex-2021.3.17-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:07ef35301b4484bce843831e7039a84e19d8d33b3f8b2f9aab86c376813d0139"}, 1088 | {file = "regex-2021.3.17-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:360a01b5fa2ad35b3113ae0c07fb544ad180603fa3b1f074f52d98c1096fa15e"}, 1089 | {file = "regex-2021.3.17-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:709f65bb2fa9825f09892617d01246002097f8f9b6dde8d1bb4083cf554701ba"}, 1090 | {file = "regex-2021.3.17-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:c66221e947d7207457f8b6f42b12f613b09efa9669f65a587a2a71f6a0e4d106"}, 1091 | {file = "regex-2021.3.17-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c782da0e45aff131f0bed6e66fbcfa589ff2862fc719b83a88640daa01a5aff7"}, 1092 | {file = "regex-2021.3.17-cp39-cp39-win32.whl", hash = "sha256:dc9963aacb7da5177e40874585d7407c0f93fb9d7518ec58b86e562f633f36cd"}, 1093 | {file = "regex-2021.3.17-cp39-cp39-win_amd64.whl", hash = "sha256:a0d04128e005142260de3733591ddf476e4902c0c23c1af237d9acf3c96e1b38"}, 1094 | {file = "regex-2021.3.17.tar.gz", hash = "sha256:4b8a1fb724904139149a43e172850f35aa6ea97fb0545244dc0b805e0154ed68"}, 1095 | ] 1096 | requests = [ 1097 | {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, 1098 | {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, 1099 | ] 1100 | responses = [ 1101 | {file = "responses-0.13.1-py2.py3-none-any.whl", hash = "sha256:3b1ea9cf026edaaf25e853abc4d3b2687d25467e9d8d41e77ee525cad0673f3e"}, 1102 | {file = "responses-0.13.1.tar.gz", hash = "sha256:cf62ab0f4119b81d485521b2c950d8aa55a885c90126488450b7acb8ee3f77ac"}, 1103 | ] 1104 | s3transfer = [ 1105 | {file = "s3transfer-0.3.6-py2.py3-none-any.whl", hash = "sha256:5d48b1fd2232141a9d5fb279709117aaba506cacea7f86f11bc392f06bfa8fc2"}, 1106 | {file = "s3transfer-0.3.6.tar.gz", hash = "sha256:c5dadf598762899d8cfaecf68eba649cd25b0ce93b6c954b156aaa3eed160547"}, 1107 | ] 1108 | six = [ 1109 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 1110 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 1111 | ] 1112 | toml = [ 1113 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1114 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1115 | ] 1116 | typed-ast = [ 1117 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, 1118 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, 1119 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, 1120 | {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, 1121 | {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, 1122 | {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, 1123 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, 1124 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, 1125 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, 1126 | {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, 1127 | {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, 1128 | {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, 1129 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, 1130 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, 1131 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, 1132 | {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, 1133 | {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, 1134 | {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, 1135 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, 1136 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, 1137 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, 1138 | {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, 1139 | {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, 1140 | {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, 1141 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, 1142 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, 1143 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, 1144 | {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, 1145 | {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, 1146 | {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, 1147 | ] 1148 | typing-extensions = [ 1149 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 1150 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 1151 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 1152 | ] 1153 | urllib3 = [ 1154 | {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, 1155 | {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, 1156 | ] 1157 | virtualenv = [ 1158 | {file = "virtualenv-20.4.3-py2.py3-none-any.whl", hash = "sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c"}, 1159 | {file = "virtualenv-20.4.3.tar.gz", hash = "sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107"}, 1160 | ] 1161 | werkzeug = [ 1162 | {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, 1163 | {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, 1164 | ] 1165 | xmltodict = [ 1166 | {file = "xmltodict-0.12.0-py2.py3-none-any.whl", hash = "sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"}, 1167 | {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"}, 1168 | ] 1169 | zipp = [ 1170 | {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, 1171 | {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, 1172 | ] 1173 | --------------------------------------------------------------------------------