├── containers ├── api │ ├── app │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_main.py │ │ ├── main.py │ │ ├── pyproject.toml │ │ ├── config │ │ │ └── settings.py │ │ └── poetry.lock │ └── Dockerfile └── frontend │ ├── Dockerfile │ └── app │ ├── package.json │ ├── server.js │ └── yarn.lock ├── terraform ├── infrastructure │ └── main │ │ ├── eu-west-1 │ │ ├── stage │ │ │ ├── .frontend.secrets.json │ │ │ ├── .api.environment.json │ │ │ ├── .api.secrets.json │ │ │ ├── .frontend.environment.json │ │ │ ├── services │ │ │ │ └── app │ │ │ │ │ └── terragrunt.hcl │ │ │ └── environment.terragrunt.hcl │ │ └── region.terragrunt.hcl │ │ └── account.terragrunt.hcl ├── common.terragrunt.hcl ├── modules │ └── services │ │ └── app │ │ ├── api_secrets.tf │ │ ├── variables.tf │ │ ├── frontend.tf │ │ ├── api.tf │ │ └── main.tf └── terragrunt.hcl ├── .vscode └── settings.json ├── scripts ├── configure_github_actions_runner.sh └── manual_push_images_to_ecr.sh ├── hooks └── pre-commit ├── .gitignore ├── .github └── workflows │ ├── pytest-api.yml │ └── terragrunt.yml ├── docker-compose.yml ├── Makefile └── README.md /containers/api/app/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /terraform/infrastructure/main/eu-west-1/stage/.frontend.secrets.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.extraPaths": ["./containers/api/app"], 3 | "python.venvPath": "~/.cache/pypoetry/virtualenvs/" 4 | } 5 | -------------------------------------------------------------------------------- /terraform/infrastructure/main/eu-west-1/stage/.api.environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEBUG": { 3 | "value": "true", 4 | "description": "API debug environment" 5 | } 6 | } -------------------------------------------------------------------------------- /terraform/infrastructure/main/eu-west-1/stage/.api.secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "SECRET_KEY": { 3 | "description": "Secret key required for API JWT authentication" 4 | } 5 | } -------------------------------------------------------------------------------- /terraform/infrastructure/main/eu-west-1/stage/.frontend.environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "NODE_ENV": { 3 | "value": "development", 4 | "description": "Frontend node environment" 5 | } 6 | } -------------------------------------------------------------------------------- /containers/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:15-alpine 2 | 3 | ARG YARN_RUN_BUILD=YARN_RUN_BUILD 4 | ARG YARN_INSTALL=YARN_INSTALL 5 | ARG NPM_RUN_START=NPM_RUN_START 6 | 7 | WORKDIR /usr/src/app 8 | 9 | COPY ./app ./ 10 | 11 | RUN yarn install 12 | 13 | CMD npm run start 14 | 15 | EXPOSE 3000 -------------------------------------------------------------------------------- /terraform/infrastructure/main/eu-west-1/region.terragrunt.hcl: -------------------------------------------------------------------------------- 1 | # Set common variables for the region. This is automatically pulled in in the root terragrunt.hcl configuration to 2 | # configure the remote state bucket and pass forward to the child modules as inputs. 3 | locals { 4 | aws_region = "eu-west-1" 5 | } -------------------------------------------------------------------------------- /terraform/infrastructure/main/account.terragrunt.hcl: -------------------------------------------------------------------------------- 1 | # Set account-wide variables. These are automatically pulled in to configure the remote state bucket in the root 2 | # terragrunt.hcl configuration. 3 | locals { 4 | account_name = "main" 5 | aws_account_id = "111111111111" 6 | aws_profile = "default" 7 | } -------------------------------------------------------------------------------- /containers/api/app/tests/test_main.py: -------------------------------------------------------------------------------- 1 | from main import app, API_ROOT 2 | from fastapi.testclient import TestClient 3 | 4 | 5 | client = TestClient(app) 6 | 7 | 8 | def test_main(): 9 | response = client.get(API_ROOT + "/") 10 | assert response.status_code == 200 11 | assert "status" in response.json() 12 | assert response.json()["status"] == "up" 13 | -------------------------------------------------------------------------------- /containers/frontend/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker_web_app", 3 | "version": "1.0.0", 4 | "description": "Node.js on Docker", 5 | "author": "First Last ", 6 | "main": "server.js", 7 | "scripts": { 8 | "start": "node server.js" 9 | }, 10 | "dependencies": { 11 | "express": "^4.16.1" 12 | } 13 | } -------------------------------------------------------------------------------- /containers/api/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from config.settings import settings 4 | 5 | # Since AWS ELB does not support path rewrite, we need to manually prefix routes 6 | API_ROOT = "/api" 7 | 8 | 9 | app = FastAPI(root_path="") 10 | 11 | 12 | @app.get(API_ROOT + "/") 13 | def api_index(): 14 | print("SECRET_KEY", settings.SECRET_KEY) 15 | return {"status": "up", "DEBUG": settings.DEBUG} 16 | -------------------------------------------------------------------------------- /terraform/common.terragrunt.hcl: -------------------------------------------------------------------------------- 1 | # Set common variables for the project. This is automatically pulled in in the root terragrunt.hcl configuration to 2 | # feed forward to the child modules. 3 | 4 | locals { 5 | app_name = "example-app" 6 | app_domain_name = "app.example.com" 7 | route53_hosted_zone_name = "example.com" 8 | use_existing_route53_hosted_zone = true 9 | github_sha = "will_be_automatically_set_by_github_actions_or_manual_script" 10 | } -------------------------------------------------------------------------------- /terraform/infrastructure/main/eu-west-1/stage/services/app/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | # Terragrunt will copy the Terraform configurations specified by the source parameter, along with any files in the 2 | # working directory, into a temporary folder, and execute your Terraform commands in that folder. 3 | terraform { 4 | source = "../../../../../../modules/services/app" 5 | } 6 | 7 | # Include all settings from the root terragrunt.hcl file 8 | include { 9 | path = find_in_parent_folders() 10 | } -------------------------------------------------------------------------------- /containers/api/app/pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | [tool.poetry] 3 | name = "app" 4 | version = "0.1.0" 5 | description = "" 6 | authors = ["Your Name "] 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.8" 10 | fastapi = "^0.62.0" 11 | click = "^7.1.2" 12 | 13 | [tool.poetry.dev-dependencies] 14 | black = "^20.8b1" 15 | flake8 = "^3.8.4" 16 | pytest = "^6.2.1" 17 | requests = "^2.25.1" 18 | 19 | [build-system] 20 | requires = ["poetry-core>=1.0.0"] 21 | build-backend = "poetry.core.masonry.api" 22 | -------------------------------------------------------------------------------- /terraform/modules/services/app/api_secrets.tf: -------------------------------------------------------------------------------- 1 | ###################################### 2 | # Parameters 3 | ###################################### 4 | 5 | resource "random_password" "SECRET_KEY" { 6 | length = 64 7 | special = true 8 | } 9 | 10 | resource "aws_ssm_parameter" "SECRET_KEY" { 11 | type = "SecureString" 12 | name = "/${var.app_name}/${var.environment}/api/secrets/SECRET_KEY" 13 | value = random_password.SECRET_KEY.result 14 | tags = { 15 | app_name = var.app_name 16 | environment = var.environment 17 | } 18 | } -------------------------------------------------------------------------------- /containers/api/app/config/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseSettings 2 | from functools import lru_cache 3 | 4 | 5 | class Settings(BaseSettings): 6 | 7 | # * SECRETS 8 | # --- 9 | SECRET_KEY: str 10 | 11 | # * ENVIRONMENT VARIABLES 12 | # --- 13 | ENVIRONMENT: str 14 | DEBUG: str 15 | 16 | class Config: 17 | case_sensitive = True 18 | # env_file = ".env" 19 | # env_file_encoding = "utf-8" 20 | 21 | 22 | @lru_cache() 23 | def cached_settings(): 24 | return Settings() 25 | 26 | 27 | settings = cached_settings() 28 | -------------------------------------------------------------------------------- /containers/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8 2 | 3 | WORKDIR /app/ 4 | 5 | # Install Poetry 6 | RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \ 7 | cd /usr/local/bin && \ 8 | ln -s /opt/poetry/bin/poetry && \ 9 | poetry config virtualenvs.create false 10 | 11 | # Copy poetry.lock* in case it doesn't exist in the repo 12 | COPY ./app/pyproject.toml ./app/poetry.lock* /app/ 13 | 14 | # Allow installing dev dependencies to run tests 15 | RUN poetry install --no-root --no-dev 16 | 17 | COPY ./app ./ 18 | 19 | ENV PYTHONPATH=/app 20 | -------------------------------------------------------------------------------- /scripts/configure_github_actions_runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script configures a local GitHub Actions runner via Docker 4 | 5 | read -p "Enter GitHub repo in format user/repo: " USER_REPO 6 | echo "get token from https://github.com/$USER_REPO > Settings > Actions > Add self-hosted runner" 7 | USER_REPO_CLEAN=$(echo $USER_REPO | sed -e 's/\//-/g') 8 | RUNNER_NAME="github_actions_runner_for_$USER_REPO" 9 | read -p "Enter token: " TOKEN 10 | docker run -d --restart always --name "github-runner-$USER_REPO_CLEAN" \ 11 | -e REPO_URL="https://github.com/$USER_REPO" \ 12 | -e RUNNER_NAME="$USER_REPO_CLEAN" \ 13 | -e RUNNER_TOKEN="$TOKEN" \ 14 | -e RUNNER_WORKDIR="/tmp/$USER_REPO_CLEAN" \ 15 | -v /var/run/docker.sock:/var/run/docker.sock \ 16 | -v /tmp/$USER_REPO_CLEAN:/tmp/$USER_REPO_CLEAN \ 17 | myoung34/github-runner:latest 18 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | declare -A lookup=( 4 | ['aws_account_id = "111111111111"']='./terraform/infrastructure/main/account.terragrunt.hcl' 5 | ['app_name = "example-app"']='./terraform/common.terragrunt.hcl' 6 | ['app_domain_name = "app.example.com"']='./terraform/common.terragrunt.hcl' 7 | ['route53_hosted_zone_name = "example.com"']='./terraform/common.terragrunt.hcl' 8 | ['traefik.http.routers.frontend.rule: Host(`dev.lan`)']="./docker-compose.yml" 9 | ['traefik.http.routers.api.rule: Host(`dev.lan`)']="./docker-compose.yml" 10 | ) 11 | 12 | echo "Checking for non-generic values in project files in pre-commit hook..." 13 | 14 | for i in "${!lookup[@]}"; do 15 | j=${lookup[$i]} 16 | if ! grep -q "$i" $j; then 17 | echo "Use generic ${i} in ${j} before comitting!" 18 | echo "Exiting..." 19 | exit 1 20 | fi 21 | done 22 | 23 | echo "All good! Proceeding with commit..." -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .terragrunt-cache 2 | .env 3 | 4 | node_modules 5 | __pycache__ 6 | .pytest_cache 7 | 8 | # Local .terraform directories 9 | **/.terraform/* 10 | 11 | # .tfstate files 12 | *.tfstate 13 | *.tfstate.* 14 | 15 | # Crash log files 16 | crash.log 17 | 18 | # Exclude all .tfvars files, which are likely to contain sentitive data, such as 19 | # password, private keys, and other secrets. These should not be part of version 20 | # control as they are data points which are potentially sensitive and subject 21 | # to change depending on the environment. 22 | # 23 | *.tfvars.json 24 | 25 | # Ignore override files as they are usually used to override resources locally and so 26 | # are not checked in 27 | override.tf 28 | override.tf.json 29 | *_override.tf 30 | *_override.tf.json 31 | 32 | # Include override files you do wish to add to version control using negated pattern 33 | # 34 | # !example_override.tf 35 | 36 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 37 | # example: *tfplan* 38 | 39 | # Ignore CLI configuration files 40 | .terraformrc 41 | terraform.rc 42 | -------------------------------------------------------------------------------- /.github/workflows/pytest-api.yml: -------------------------------------------------------------------------------- 1 | name: Pytest API 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | pytest: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: ["self-hosted"] 12 | runs-on: ${{ matrix.os }} 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: "3.8" 22 | 23 | - name: Update pip 24 | run: | 25 | python -m pip install --upgrade pip 26 | 27 | - name: Install poetry 28 | run: | 29 | # Install Poetry 30 | curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \ 31 | cd /usr/local/bin && \ 32 | poetry config virtualenvs.create false 33 | 34 | - name: Install dependencies via poetry 35 | run: | 36 | cd ./containers/api/app && poetry install 37 | 38 | - name: Test with pytest 39 | run: | 40 | cd ./containers/api/app && SECRET_KEY=pytest ENVIRONMENT=pytest DEBUG=1 poetry run pytest tests/ 41 | -------------------------------------------------------------------------------- /containers/frontend/app/server.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const express = require("express") 4 | 5 | // Constants 6 | const PORT = 3000 7 | const HOST = "0.0.0.0" 8 | 9 | // API get request 10 | var http = require("http") 11 | 12 | // Client-side requests to API: 13 | // (use https://example.com/api/*) 14 | 15 | // Server-side requests to API: 16 | // for docker-compose use "api" hostname (see docker-compose.yml) 17 | // for ecs use service discovery hostname ending in .local (see api.tf) 18 | 19 | let host = 20 | process.env.ENVIRONMENT === "dev" 21 | ? "api" 22 | : "api." + process.env.APP_DOMAIN_NAME + ".local" 23 | 24 | let options = { 25 | host: host, 26 | path: "/api/", 27 | } 28 | 29 | // Hello world 30 | const app = express() 31 | app.get("/", (req, res) => { 32 | var req = http.get(options, function (api_res) { 33 | var bodyChunks = [] 34 | api_res 35 | .on("data", function (chunk) { 36 | bodyChunks.push(chunk) 37 | }) 38 | .on("end", function () { 39 | var body = Buffer.concat(bodyChunks) 40 | var jsonObj = JSON.parse(body) 41 | res.send({ from_express: "hello world", from_api: jsonObj }) 42 | }) 43 | }) 44 | }) 45 | 46 | app.listen(PORT, HOST) 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | traefik: 4 | image: traefik:v2.3 5 | command: 6 | - --api.insecure=1 7 | - --entrypoints.web.address=:80 8 | - --providers.docker.exposedByDefault=false 9 | volumes: 10 | - /var/run/docker.sock:/var/run/docker.sock:ro 11 | ports: 12 | - "80:80" 13 | - "443:443" 14 | 15 | frontend: 16 | build: 17 | context: ./containers/frontend 18 | command: npm run start 19 | volumes: 20 | - ./containers/frontend/app:/usr/src/app 21 | labels: 22 | traefik.enable: "true" 23 | traefik.port: 3000 24 | traefik.http.services.frontend.loadbalancer.server.port: 3000 25 | traefik.http.routers.frontend.rule: Host(`dev.lan`) 26 | environment: 27 | NODE_ENV: "development" 28 | ENVIRONMENT: "dev" 29 | 30 | api: 31 | container_name: api 32 | build: 33 | context: ./containers/api 34 | command: /start-reload.sh 35 | volumes: 36 | - ./containers/api/app:/app 37 | labels: 38 | traefik.enable: "true" 39 | traefik.port: 80 40 | traefik.http.services.api.loadbalancer.server.port: 80 41 | traefik.http.routers.api.rule: Host(`dev.lan`) && PathPrefix(`/api`) 42 | ## Since AWS ELB does not support path rewrite, we need to manually prefix routes 43 | ## See app/main.py for details 44 | environment: 45 | SECRET_KEY: "dev_secret_key" 46 | DEBUG: "true" 47 | ENVIRONMENT: "dev" 48 | -------------------------------------------------------------------------------- /scripts/manual_push_images_to_ecr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | account=$1 4 | region=$2 5 | environment=$3 6 | services=$4 7 | 8 | # Note: Don't use implicit values from aws profile - always specify which account, which region etc. 9 | # aws_account_id=$(aws sts get-caller-identity --output json | jq -r '.Account') 10 | # aws_region=$(aws configure get region) 11 | 12 | tf="./terraform" 13 | tfi="$tf/infrastructure" 14 | 15 | common_terragrunt_file="$tf/common.terragrunt.hcl" 16 | account_terragrunt_file="$tfi/$account/account.terragrunt.hcl" 17 | region_terragrunt_file="$tfi/$account/$region/region.terragrunt.hcl" 18 | 19 | app_name=$(cat $common_terragrunt_file | hclq get ".locals.app_name" | tr -d '"') 20 | #github_sha=$(cat $common_terragrunt_file | hclq get ".locals.github_sha"| tr -d '"') 21 | github_sha=manual_$(openssl rand -hex 6)_$(date -u +%Y%m%dT%H%M%S) # Add random string and UTC date 22 | aws_account_id=$(cat $account_terragrunt_file | hclq get ".locals.aws_account_id" | tr -d '"') 23 | aws_region=$(cat $region_terragrunt_file | hclq get ".locals.aws_region" | tr -d '"') 24 | 25 | aws ecr get-login-password --region $aws_region | docker login --username AWS --password-stdin $aws_account_id.dkr.ecr.$aws_region.amazonaws.com 26 | 27 | 28 | for service in $(echo $services | sed "s/,/ /g"); do 29 | echo "Pushing image for $service" 30 | service_img=$app_name-$environment-$service:$github_sha 31 | cd ./containers/$service && docker build -t $service_img . 32 | docker tag $service_img $aws_account_id.dkr.ecr.$aws_region.amazonaws.com/$service_img 33 | docker push $aws_account_id.dkr.ecr.$aws_region.amazonaws.com/$service_img 34 | cd ../../ 35 | done -------------------------------------------------------------------------------- /terraform/modules/services/app/variables.tf: -------------------------------------------------------------------------------- 1 | variable "aws_region" { 2 | description = "The AWS region to use." 3 | } 4 | 5 | variable "aws_account_id" { 6 | description = "The AWS account ID to use." 7 | } 8 | 9 | variable "app_name" { 10 | description = "The name of the app." 11 | } 12 | 13 | variable "environment" { 14 | description = "The environment of the app deployment." 15 | } 16 | 17 | variable "app_domain_name" { 18 | description = "The domain name of the load balancer to create certificates for." 19 | } 20 | 21 | variable "route53_hosted_zone_name" { 22 | description = "The name of the Route 53 hosted zone." 23 | } 24 | 25 | variable "use_existing_route53_hosted_zone" { 26 | description = "Set to true if the hosted zone already exists in Route 53 (i.e., manually created)." 27 | } 28 | 29 | variable "github_sha" { 30 | description = "The GitHub commit SHA that triggered the workflow. Is set randomly for manual deployment via Makefile and helper script. In GitHub Actions workflows, this value is automatically set by 'github.sha'." 31 | } 32 | 33 | variable "service_configuration" { 34 | description = "An object of configuration options for all services." 35 | 36 | type = object({ 37 | frontend = object({ 38 | url_prefix = string, 39 | environment = list(object({ 40 | name = string 41 | value = string 42 | })), 43 | secrets = list(object({ 44 | name = string 45 | valueFrom = string 46 | })), 47 | }) 48 | api = object({ 49 | url_prefix = string, 50 | environment = list(object({ 51 | name = string 52 | value = string 53 | })), 54 | secrets = list(object({ 55 | name = string 56 | valueFrom = string 57 | })), 58 | }) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # https://www.thapaliya.com/en/writings/well-documented-makefiles/ 2 | 3 | .DEFAULT_GOAL:=help 4 | SHELL:=/bin/bash 5 | 6 | # ****************************************************************** 7 | ##@ Development 8 | 9 | dev: ## Run docker-compose 10 | docker-compose -f ./docker-compose.yml up --build 11 | 12 | test-api: 13 | cd ./containers/api/app && SECRET_KEY=pytest ENVIRONMENT=pytest DEBUG=1 poetry run pytest tests/ 14 | 15 | git-hooks: ## Set git hooks path to ./hooks 16 | git config core.hooksPath ./hooks 17 | 18 | # ****************************************************************** 19 | ##@ Manual Terragrunt Operations 20 | 21 | # Variables 22 | account := main 23 | aws_region := eu-west-1 24 | environment := stage 25 | app_services := frontend,api 26 | 27 | # Constants 28 | tfi := ./terraform/infrastructure 29 | 30 | init: ## Run terragrunt init 31 | cd ${tfi}/${account}/${aws_region}/${environment}/services/app && terragrunt init 32 | cd ${tfi}/${account}/${aws_region}/${environment}/services/app && for service in $(shell echo ${app_services} | sed "s/,/ /g"); do \ 33 | terragrunt apply -target=aws_ecr_repository.$$service -auto-approve ; \ 34 | done 35 | apply: ## Run terragrunt apply 36 | cd ${tfi}/${account}/${aws_region}/${environment}/services/app && terragrunt apply 37 | destroy: ## Run terragrunt destroy 38 | cd ${tfi}/${account}/${aws_region}/${environment}/services/app && terragrunt destroy 39 | push-images: ## Build and push images to ECR 40 | sh ./scripts/manual_push_images_to_ecr.sh ${account} ${aws_region} ${environment} ${app_services} 41 | 42 | # ****************************************************************** 43 | ##@ Helpers 44 | 45 | .PHONY: help 46 | 47 | help: ## Display this help 48 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 49 | -------------------------------------------------------------------------------- /terraform/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------------------------------------------------------- 2 | # TERRAGRUNT CONFIGURATION 3 | # Terragrunt is a thin wrapper for Terraform that provides extra tools for working with multiple Terraform modules, 4 | # remote state, and locking: https://github.com/gruntwork-io/terragrunt 5 | # --------------------------------------------------------------------------------------------------------------------- 6 | 7 | locals { 8 | common = read_terragrunt_config(find_in_parent_folders("common.terragrunt.hcl")) 9 | account = read_terragrunt_config(find_in_parent_folders("account.terragrunt.hcl")) 10 | region = read_terragrunt_config(find_in_parent_folders("region.terragrunt.hcl")) 11 | environment = read_terragrunt_config(find_in_parent_folders("environment.terragrunt.hcl")) 12 | } 13 | 14 | remote_state { 15 | backend = "s3" 16 | config = { 17 | encrypt = true 18 | region = local.region.locals.aws_region 19 | bucket = "terraform-state-${local.common.locals.app_name}-${local.account.locals.aws_account_id}" 20 | key = "${path_relative_to_include()}/terraform.tfstate" 21 | dynamodb_table = "terraform-locks-${local.common.locals.app_name}-${local.account.locals.aws_account_id}" 22 | } 23 | } 24 | 25 | # Generate an AWS provider block 26 | generate "provider" { 27 | path = "provider.tf" 28 | if_exists = "overwrite_terragrunt" 29 | contents = < { 21 | url_prefix = service == "frontend" ? "" : format("%s/",service) 22 | environment = concat([ 23 | for key, value in jsondecode(file(".${service}.environment.json")): 24 | { 25 | "name": key, 26 | "value": value["value"] 27 | } 28 | ], [ 29 | { 30 | "name": "ENVIRONMENT", 31 | "value": local.environment 32 | }, 33 | { 34 | "name": "APP_DOMAIN_NAME", 35 | "value": local.app_domain_name 36 | }, 37 | ]) 38 | secrets = concat([ 39 | for key, value in jsondecode(file(".${service}.secrets.json")): 40 | { 41 | "name": key, 42 | "valueFrom": "arn:aws:ssm:${local.aws_region}:${local.aws_account_id}:parameter/${local.parameter_group}/${service}/secrets/${key}" 43 | } 44 | if lookup(value, "alias", false) == false 45 | ]) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terragrunt-github-actions-aws-ecs 2 | 3 | This project leverages [Terragrunt](https://github.com/gruntwork-io/terragrunt), [Terraform](https://www.terraform.io/), and [GitHub Actions](https://github.com/features/actions) to deploy a basic web app (dockerized JS frontend and dockerized Python API) to [AWS ECS](https://aws.amazon.com/ecs/). 4 | 5 | See this article for more information: [https://camillovisini.com/article/terragrunt-github-actions-aws-ecs/](https://camillovisini.com/article/terragrunt-github-actions-aws-ecs/) 6 | 7 | ## GitHub Secrets 8 | 9 | Ensure the following secrets are provided in the repository settings: 10 | 11 | ```bash 12 | AWS_ACCESS_KEY_ID 13 | AWS_SECRET_ACCESS_KEY 14 | AWS_REGION 15 | ``` 16 | 17 | ## Makefile Targets 18 | 19 | ```bash 20 | ~/terragrunt-ecs$ make 21 | 22 | Usage: 23 | make 24 | 25 | Development 26 | dev Run docker-compose 27 | git-hooks Set git hooks path to ./hooks 28 | 29 | Manual Terragrunt Operations 30 | init Run terragrunt init 31 | apply Run terragrunt apply 32 | destroy Run terragrunt destroy 33 | push-images Build and push images to ECR 34 | 35 | Helpers 36 | help Display this help 37 | ``` 38 | 39 | ## Workflow 40 | 41 | ### Development 42 | 43 | ```bash 44 | # make git-hooks 45 | make dev 46 | ``` 47 | 48 | ### Initialization 49 | 50 | ```bash 51 | make init 52 | make push-images 53 | ``` 54 | 55 | Subsequent push to branch will trigger deployment via GitHub Actions: 56 | 57 | - Branch `dev` will deploy to `stage` environment 58 | - Branch `main` will deploy to `prod` environment 59 | 60 | ### Manual apply / destroy 61 | 62 | Besides GitHub Actions, deployments can be managed manually. Configure additional Makefile targets to manually manage deployments as your application scales across environments, regions, accounts, or includes additional services or data providers. For this repository, common targets are listed below. 63 | 64 | Change infrastructure: 65 | 66 | ```bash 67 | # after changes in ./terraform/* 68 | make apply 69 | ``` 70 | 71 | Change codebase and deploy to infrastructure: 72 | 73 | ```bash 74 | # after changes in ./containers/* 75 | make push-images 76 | make apply 77 | ``` 78 | 79 | Destroy infrastructure: 80 | 81 | ```bash 82 | # after infrastructure is no longer required 83 | make destroy 84 | ``` 85 | -------------------------------------------------------------------------------- /terraform/modules/services/app/frontend.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecr_repository" "frontend" { 2 | name = "${var.app_name}-${var.environment}-frontend" 3 | } 4 | 5 | resource "aws_cloudwatch_log_group" "frontend" { 6 | name = "${var.app_name}-${var.environment}-frontend" 7 | } 8 | 9 | resource "aws_lb_target_group" "frontend" { 10 | name = "${var.app_name}-${var.environment}-frontend" 11 | port = 80 12 | protocol = "HTTP" 13 | target_type = "ip" 14 | vpc_id = aws_default_vpc.default_vpc.id # Referencing the default VPC 15 | health_check { 16 | matcher = "200,301,302" 17 | path = "/${var.service_configuration.frontend.url_prefix}" 18 | } 19 | depends_on = [aws_lb.application_load_balancer] 20 | } 21 | 22 | resource "aws_lb_listener_rule" "frontend" { 23 | listener_arn = aws_lb_listener.listener_https.arn 24 | # important! frontend is catchall, and must not override service-specific rules 25 | # Note: priority 1 -> first/highest priority vs. priority 99 -> low priority 26 | priority = 99 27 | action { 28 | type = "forward" 29 | target_group_arn = aws_lb_target_group.frontend.arn 30 | } 31 | condition { 32 | path_pattern { 33 | values = ["/${var.service_configuration.frontend.url_prefix}*"] 34 | } 35 | } 36 | } 37 | 38 | resource "aws_ecs_service" "frontend" { 39 | name = "${var.app_name}-${var.environment}-frontend" # Naming our first service 40 | cluster = aws_ecs_cluster.app.id # Referencing our created Cluster 41 | task_definition = aws_ecs_task_definition.frontend.arn # Referencing the task our service will spin up 42 | launch_type = "FARGATE" 43 | desired_count = 2 # Setting the number of containers we want deployed to 2 44 | deployment_maximum_percent = 300 # Maximum overprovisioning 45 | deployment_minimum_healthy_percent = 0 # Minimum instance (set this to 100 to avoid downtime in prod) 46 | 47 | load_balancer { 48 | target_group_arn = aws_lb_target_group.frontend.arn # Referencing our target group 49 | container_name = aws_ecs_task_definition.frontend.family 50 | container_port = 3000 # Specifying the container port 51 | } 52 | 53 | network_configuration { 54 | subnets = [ 55 | aws_default_subnet.default_subnet_a.id, 56 | aws_default_subnet.default_subnet_b.id, 57 | aws_default_subnet.default_subnet_c.id 58 | ] 59 | assign_public_ip = true # Providing our containers with public IPs 60 | } 61 | 62 | depends_on = [aws_lb.application_load_balancer] 63 | 64 | } 65 | 66 | resource "aws_ecs_task_definition" "frontend" { 67 | family = "${var.app_name}-${var.environment}-frontend" # Naming our first task 68 | container_definitions = < first/highest priority vs. priority 99 -> low priority 26 | priority = 1 27 | action { 28 | type = "forward" 29 | target_group_arn = aws_lb_target_group.api.arn 30 | } 31 | condition { 32 | path_pattern { 33 | values = ["/${var.service_configuration.api.url_prefix}*"] 34 | } 35 | } 36 | } 37 | 38 | resource "aws_service_discovery_service" "api" { 39 | name = "api" 40 | 41 | dns_config { 42 | namespace_id = aws_service_discovery_private_dns_namespace.app.id 43 | 44 | dns_records { 45 | ttl = 10 46 | type = "A" 47 | } 48 | 49 | routing_policy = "MULTIVALUE" 50 | } 51 | 52 | health_check_custom_config { 53 | failure_threshold = 1 54 | } 55 | } 56 | 57 | resource "aws_ecs_service" "api" { 58 | name = "${var.app_name}-${var.environment}-api" # Naming our first service 59 | cluster = aws_ecs_cluster.app.id # Referencing our created Cluster 60 | task_definition = aws_ecs_task_definition.api.arn # Referencing the task our service will spin up 61 | launch_type = "FARGATE" 62 | desired_count = 2 # Setting the number of containers we want deployed to 2 63 | deployment_maximum_percent = 300 # Maximum overprovisioning 64 | deployment_minimum_healthy_percent = 0 # Minimum instance (set this to 100 to avoid downtime in prod) 65 | 66 | load_balancer { 67 | target_group_arn = aws_lb_target_group.api.arn # Referencing our target group 68 | container_name = aws_ecs_task_definition.api.family 69 | container_port = 80 # Specifying the container port 70 | } 71 | 72 | network_configuration { 73 | subnets = [ 74 | aws_default_subnet.default_subnet_a.id, 75 | aws_default_subnet.default_subnet_b.id, 76 | aws_default_subnet.default_subnet_c.id 77 | ] 78 | assign_public_ip = true # Providing our containers with public IPs 79 | } 80 | 81 | service_registries { 82 | registry_arn = aws_service_discovery_service.api.arn 83 | container_name = "api" 84 | } 85 | 86 | depends_on = [aws_lb.application_load_balancer] 87 | 88 | } 89 | 90 | resource "aws_ecs_task_definition" "api" { 91 | family = "${var.app_name}-${var.environment}-api" # Naming our first task 92 | container_definitions = <> $GITHUB_ENV 47 | 48 | - name: Set TF_ENV=prod for branch main 49 | if: | 50 | (github.event_name == 'push' && endsWith(github.ref, '/main')) || 51 | (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main') 52 | run: echo "TF_ENV=prod" >> $GITHUB_ENV 53 | 54 | - name: Set steps.tf_env.outputs.tf_env based on $TF_ENV 55 | id: tf_env 56 | run: echo "::set-output name=tf_env::$TF_ENV" 57 | 58 | outputs: 59 | target_push: ${{steps.target_push.outputs.target_push}} 60 | tf_env: ${{steps.tf_env.outputs.tf_env}} 61 | 62 | # -------------------------------------------------------------------------- 63 | # Push to ECR if target_push 64 | # -------------------------------------------------------------------------- 65 | ecr: 66 | needs: ["setup"] 67 | strategy: 68 | fail-fast: false 69 | matrix: 70 | os: ["self-hosted"] 71 | # which container(s) to push to ECR? e.g., ["frontend", "api"] 72 | container: ["frontend", "api"] 73 | runs-on: ${{ matrix.os }} 74 | if: needs.setup.outputs.target_push 75 | steps: 76 | - name: Configure AWS credentials 77 | uses: aws-actions/configure-aws-credentials@v1 78 | with: 79 | aws-access-key-id: ${{secrets.AWS_ACCESS_KEY_ID}} 80 | aws-secret-access-key: ${{secrets.AWS_SECRET_ACCESS_KEY}} 81 | aws-region: ${{secrets.AWS_REGION}} 82 | 83 | - name: Set variables from Terragrunt locals configuration 84 | id: terragrunt_locals 85 | run: | 86 | common_terragrunt_file="./terraform/common.terragrunt.hcl" 87 | #account_terragrunt_file="terraform/infrastructure/$account/account.terragrunt.hcl" 88 | #region_terragrunt_file="terraform/infrastructure/$account/$region/region.terragrunt.hcl" 89 | 90 | app_name=$(cat $common_terragrunt_file | hclq get ".locals.app_name" | tr -d '"') 91 | echo "::add-mask::$app_name" 92 | echo "::set-output name=app_name::$app_name" 93 | 94 | - name: Login to Amazon ECR 95 | id: login-ecr 96 | uses: aws-actions/amazon-ecr-login@v1 97 | 98 | - name: For each service (matrix) - Build, tag, and push image to Amazon ECR 99 | env: 100 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 101 | ECR_REPOSITORY: ${{ steps.terragrunt_locals.outputs.app_name }}-${{needs.setup.outputs.tf_env}}-${{matrix.container}} 102 | IMAGE_TAG: ${{github.sha}} 103 | run: | 104 | cd ./containers/${{ matrix.container }} && docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . 105 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG 106 | 107 | # -------------------------------------------------------------------------- 108 | # Terraform for deployment targets 109 | # Based on tf_env (pull request -> plan; push to branch -> apply) 110 | # -------------------------------------------------------------------------- 111 | terraform: 112 | needs: ["setup"] 113 | strategy: 114 | fail-fast: false 115 | matrix: 116 | os: ["self-hosted"] 117 | # which deployment target(s) should be applied? e.g., ["services/app"] 118 | target: ["services/app"] 119 | runs-on: ${{ matrix.os }} 120 | steps: 121 | - name: Configure AWS Credentials 122 | uses: aws-actions/configure-aws-credentials@v1 123 | with: 124 | aws-access-key-id: ${{secrets.AWS_ACCESS_KEY_ID}} 125 | aws-secret-access-key: ${{secrets.AWS_SECRET_ACCESS_KEY}} 126 | aws-region: ${{secrets.AWS_REGION}} 127 | 128 | - name: Override GitHub SHA in common.terragrunt.hcl 129 | run: | 130 | cat ./terraform/common.terragrunt.hcl | hclq set ".locals.github_sha" "${{github.sha}}" > ./terraform/common.terragrunt.tmp 131 | rm ./terraform/common.terragrunt.hcl 132 | mv ./terraform/common.terragrunt.tmp ./terraform/common.terragrunt.hcl 133 | 134 | # needed for self-hosted runner from myoung34/github-runner, since node is not installed 135 | # hashicorp/setup-terraform@v1 --> requires node! 136 | - uses: actions/setup-node@v2-beta 137 | with: 138 | node-version: "12" 139 | 140 | - name: Setup Terraform 0.13.5 141 | uses: hashicorp/setup-terraform@v1 142 | with: 143 | terraform_version: 0.13.5 144 | terraform_wrapper: true 145 | - name: Setup Terraform version 146 | run: terraform -v 147 | - name: Setup Terraform wrapper path 148 | run: which terraform 149 | - name: Install Terragrunt v0.26.4 150 | run: | 151 | sudo wget -q -O /bin/terragrunt "https://github.com/gruntwork-io/terragrunt/releases/download/v0.26.4/terragrunt_linux_amd64" 152 | sudo chmod +x /bin/terragrunt 153 | terragrunt -v 154 | 155 | - name: Terragrunt Format 156 | id: fmt 157 | run: cd ./terraform/infrastructure/main/${{secrets.AWS_REGION}}/${{needs.setup.outputs.tf_env}}/${{matrix.target}} && terragrunt validate 158 | 159 | - name: Terragrunt Init 160 | id: init 161 | run: cd ./terraform/infrastructure/main/${{secrets.AWS_REGION}}/${{needs.setup.outputs.tf_env}}/${{matrix.target}} && terragrunt init 162 | 163 | - name: Terragrunt Plan 164 | id: plan 165 | if: github.event_name == 'pull_request' 166 | run: cd ./terraform/infrastructure/main/${{secrets.AWS_REGION}}/${{needs.setup.outputs.tf_env}}/${{matrix.target}} && terragrunt plan -no-color 167 | continue-on-error: true 168 | 169 | - name: Create Comment with Terraform Plan Output 170 | uses: actions/github-script@0.9.0 171 | if: github.event_name == 'pull_request' 172 | env: 173 | PLAN: "terraform\n${{ steps.plan.outputs.stdout }}" 174 | with: 175 | github-token: ${{ secrets.GITHUB_TOKEN }} 176 | script: | 177 | const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\` 178 | #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` 179 | #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` 180 |
Show Plan 181 | 182 | \`\`\`${process.env.PLAN}\`\`\` 183 |
184 | 185 | *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`; 186 | 187 | github.issues.createComment({ 188 | issue_number: context.issue.number, 189 | owner: context.repo.owner, 190 | repo: context.repo.repo, 191 | body: output 192 | }) 193 | 194 | - name: Terraform Plan Status 195 | if: steps.plan.outcome == 'failure' 196 | run: exit 1 197 | 198 | - name: Terragrunt Apply 199 | if: needs.setup.outputs.target_push 200 | run: cd ./terraform/infrastructure/main/${{secrets.AWS_REGION}}/${{needs.setup.outputs.tf_env}}/${{matrix.target}} && terragrunt apply -auto-approve 201 | -------------------------------------------------------------------------------- /containers/frontend/app/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.7: 6 | version "1.3.7" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" 8 | integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== 9 | dependencies: 10 | mime-types "~2.1.24" 11 | negotiator "0.6.2" 12 | 13 | array-flatten@1.1.1: 14 | version "1.1.1" 15 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 16 | integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= 17 | 18 | body-parser@1.19.0: 19 | version "1.19.0" 20 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" 21 | integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== 22 | dependencies: 23 | bytes "3.1.0" 24 | content-type "~1.0.4" 25 | debug "2.6.9" 26 | depd "~1.1.2" 27 | http-errors "1.7.2" 28 | iconv-lite "0.4.24" 29 | on-finished "~2.3.0" 30 | qs "6.7.0" 31 | raw-body "2.4.0" 32 | type-is "~1.6.17" 33 | 34 | bytes@3.1.0: 35 | version "3.1.0" 36 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" 37 | integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== 38 | 39 | content-disposition@0.5.3: 40 | version "0.5.3" 41 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" 42 | integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== 43 | dependencies: 44 | safe-buffer "5.1.2" 45 | 46 | content-type@~1.0.4: 47 | version "1.0.4" 48 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 49 | integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== 50 | 51 | cookie-signature@1.0.6: 52 | version "1.0.6" 53 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 54 | integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= 55 | 56 | cookie@0.4.0: 57 | version "0.4.0" 58 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" 59 | integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== 60 | 61 | debug@2.6.9: 62 | version "2.6.9" 63 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 64 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 65 | dependencies: 66 | ms "2.0.0" 67 | 68 | depd@~1.1.2: 69 | version "1.1.2" 70 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 71 | integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= 72 | 73 | destroy@~1.0.4: 74 | version "1.0.4" 75 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 76 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= 77 | 78 | ee-first@1.1.1: 79 | version "1.1.1" 80 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 81 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= 82 | 83 | encodeurl@~1.0.2: 84 | version "1.0.2" 85 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 86 | integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= 87 | 88 | escape-html@~1.0.3: 89 | version "1.0.3" 90 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 91 | integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= 92 | 93 | etag@~1.8.1: 94 | version "1.8.1" 95 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 96 | integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= 97 | 98 | express@^4.16.1: 99 | version "4.17.1" 100 | resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" 101 | integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== 102 | dependencies: 103 | accepts "~1.3.7" 104 | array-flatten "1.1.1" 105 | body-parser "1.19.0" 106 | content-disposition "0.5.3" 107 | content-type "~1.0.4" 108 | cookie "0.4.0" 109 | cookie-signature "1.0.6" 110 | debug "2.6.9" 111 | depd "~1.1.2" 112 | encodeurl "~1.0.2" 113 | escape-html "~1.0.3" 114 | etag "~1.8.1" 115 | finalhandler "~1.1.2" 116 | fresh "0.5.2" 117 | merge-descriptors "1.0.1" 118 | methods "~1.1.2" 119 | on-finished "~2.3.0" 120 | parseurl "~1.3.3" 121 | path-to-regexp "0.1.7" 122 | proxy-addr "~2.0.5" 123 | qs "6.7.0" 124 | range-parser "~1.2.1" 125 | safe-buffer "5.1.2" 126 | send "0.17.1" 127 | serve-static "1.14.1" 128 | setprototypeof "1.1.1" 129 | statuses "~1.5.0" 130 | type-is "~1.6.18" 131 | utils-merge "1.0.1" 132 | vary "~1.1.2" 133 | 134 | finalhandler@~1.1.2: 135 | version "1.1.2" 136 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" 137 | integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== 138 | dependencies: 139 | debug "2.6.9" 140 | encodeurl "~1.0.2" 141 | escape-html "~1.0.3" 142 | on-finished "~2.3.0" 143 | parseurl "~1.3.3" 144 | statuses "~1.5.0" 145 | unpipe "~1.0.0" 146 | 147 | forwarded@~0.1.2: 148 | version "0.1.2" 149 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 150 | integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= 151 | 152 | fresh@0.5.2: 153 | version "0.5.2" 154 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 155 | integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= 156 | 157 | http-errors@1.7.2: 158 | version "1.7.2" 159 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" 160 | integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== 161 | dependencies: 162 | depd "~1.1.2" 163 | inherits "2.0.3" 164 | setprototypeof "1.1.1" 165 | statuses ">= 1.5.0 < 2" 166 | toidentifier "1.0.0" 167 | 168 | http-errors@~1.7.2: 169 | version "1.7.3" 170 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" 171 | integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== 172 | dependencies: 173 | depd "~1.1.2" 174 | inherits "2.0.4" 175 | setprototypeof "1.1.1" 176 | statuses ">= 1.5.0 < 2" 177 | toidentifier "1.0.0" 178 | 179 | iconv-lite@0.4.24: 180 | version "0.4.24" 181 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 182 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 183 | dependencies: 184 | safer-buffer ">= 2.1.2 < 3" 185 | 186 | inherits@2.0.3: 187 | version "2.0.3" 188 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 189 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 190 | 191 | inherits@2.0.4: 192 | version "2.0.4" 193 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 194 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 195 | 196 | ipaddr.js@1.9.1: 197 | version "1.9.1" 198 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 199 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 200 | 201 | media-typer@0.3.0: 202 | version "0.3.0" 203 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 204 | integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= 205 | 206 | merge-descriptors@1.0.1: 207 | version "1.0.1" 208 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 209 | integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= 210 | 211 | methods@~1.1.2: 212 | version "1.1.2" 213 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 214 | integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= 215 | 216 | mime-db@1.44.0: 217 | version "1.44.0" 218 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" 219 | integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== 220 | 221 | mime-types@~2.1.24: 222 | version "2.1.27" 223 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" 224 | integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== 225 | dependencies: 226 | mime-db "1.44.0" 227 | 228 | mime@1.6.0: 229 | version "1.6.0" 230 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 231 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 232 | 233 | ms@2.0.0: 234 | version "2.0.0" 235 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 236 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 237 | 238 | ms@2.1.1: 239 | version "2.1.1" 240 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 241 | integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== 242 | 243 | negotiator@0.6.2: 244 | version "0.6.2" 245 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" 246 | integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== 247 | 248 | on-finished@~2.3.0: 249 | version "2.3.0" 250 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 251 | integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= 252 | dependencies: 253 | ee-first "1.1.1" 254 | 255 | parseurl@~1.3.3: 256 | version "1.3.3" 257 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 258 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 259 | 260 | path-to-regexp@0.1.7: 261 | version "0.1.7" 262 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 263 | integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= 264 | 265 | proxy-addr@~2.0.5: 266 | version "2.0.6" 267 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" 268 | integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== 269 | dependencies: 270 | forwarded "~0.1.2" 271 | ipaddr.js "1.9.1" 272 | 273 | qs@6.7.0: 274 | version "6.7.0" 275 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" 276 | integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== 277 | 278 | range-parser@~1.2.1: 279 | version "1.2.1" 280 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 281 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 282 | 283 | raw-body@2.4.0: 284 | version "2.4.0" 285 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" 286 | integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== 287 | dependencies: 288 | bytes "3.1.0" 289 | http-errors "1.7.2" 290 | iconv-lite "0.4.24" 291 | unpipe "1.0.0" 292 | 293 | safe-buffer@5.1.2: 294 | version "5.1.2" 295 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 296 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 297 | 298 | "safer-buffer@>= 2.1.2 < 3": 299 | version "2.1.2" 300 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 301 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 302 | 303 | send@0.17.1: 304 | version "0.17.1" 305 | resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" 306 | integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== 307 | dependencies: 308 | debug "2.6.9" 309 | depd "~1.1.2" 310 | destroy "~1.0.4" 311 | encodeurl "~1.0.2" 312 | escape-html "~1.0.3" 313 | etag "~1.8.1" 314 | fresh "0.5.2" 315 | http-errors "~1.7.2" 316 | mime "1.6.0" 317 | ms "2.1.1" 318 | on-finished "~2.3.0" 319 | range-parser "~1.2.1" 320 | statuses "~1.5.0" 321 | 322 | serve-static@1.14.1: 323 | version "1.14.1" 324 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" 325 | integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== 326 | dependencies: 327 | encodeurl "~1.0.2" 328 | escape-html "~1.0.3" 329 | parseurl "~1.3.3" 330 | send "0.17.1" 331 | 332 | setprototypeof@1.1.1: 333 | version "1.1.1" 334 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" 335 | integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== 336 | 337 | "statuses@>= 1.5.0 < 2", statuses@~1.5.0: 338 | version "1.5.0" 339 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 340 | integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= 341 | 342 | toidentifier@1.0.0: 343 | version "1.0.0" 344 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" 345 | integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== 346 | 347 | type-is@~1.6.17, type-is@~1.6.18: 348 | version "1.6.18" 349 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 350 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 351 | dependencies: 352 | media-typer "0.3.0" 353 | mime-types "~2.1.24" 354 | 355 | unpipe@1.0.0, unpipe@~1.0.0: 356 | version "1.0.0" 357 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 358 | integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= 359 | 360 | utils-merge@1.0.1: 361 | version "1.0.1" 362 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 363 | integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= 364 | 365 | vary@~1.1.2: 366 | version "1.1.2" 367 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 368 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 369 | -------------------------------------------------------------------------------- /containers/api/app/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 = "main" 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 = "main" 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 = "black" 33 | version = "20.8b1" 34 | description = "The uncompromising code formatter." 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=3.6" 38 | 39 | [package.dependencies] 40 | appdirs = "*" 41 | click = ">=7.1.2" 42 | mypy-extensions = ">=0.4.3" 43 | pathspec = ">=0.6,<1" 44 | regex = ">=2020.1.8" 45 | toml = ">=0.10.1" 46 | typed-ast = ">=1.4.0" 47 | typing-extensions = ">=3.7.4" 48 | 49 | [package.extras] 50 | colorama = ["colorama (>=0.4.3)"] 51 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 52 | 53 | [[package]] 54 | name = "certifi" 55 | version = "2020.12.5" 56 | description = "Python package for providing Mozilla's CA Bundle." 57 | category = "main" 58 | optional = false 59 | python-versions = "*" 60 | 61 | [[package]] 62 | name = "chardet" 63 | version = "4.0.0" 64 | description = "Universal encoding detector for Python 2 and 3" 65 | category = "main" 66 | optional = false 67 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 68 | 69 | [[package]] 70 | name = "click" 71 | version = "7.1.2" 72 | description = "Composable command line interface toolkit" 73 | category = "main" 74 | optional = false 75 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 76 | 77 | [[package]] 78 | name = "colorama" 79 | version = "0.4.4" 80 | description = "Cross-platform colored terminal text." 81 | category = "main" 82 | optional = false 83 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 84 | 85 | [[package]] 86 | name = "fastapi" 87 | version = "0.62.0" 88 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 89 | category = "main" 90 | optional = false 91 | python-versions = ">=3.6" 92 | 93 | [package.dependencies] 94 | pydantic = ">=1.0.0,<2.0.0" 95 | starlette = "0.13.6" 96 | 97 | [package.extras] 98 | all = ["requests (>=2.24.0,<3.0.0)", "aiofiles (>=0.5.0,<0.6.0)", "jinja2 (>=2.11.2,<3.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<2.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "graphene (>=2.1.8,<3.0.0)", "ujson (>=3.0.0,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn (>=0.11.5,<0.12.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)"] 99 | dev = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn (>=0.11.5,<0.12.0)", "graphene (>=2.1.8,<3.0.0)"] 100 | doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=6.1.4,<7.0.0)", "markdown-include (>=0.5.1,<0.6.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.2.0)", "typer (>=0.3.0,<0.4.0)", "typer-cli (>=0.0.9,<0.0.10)", "pyyaml (>=5.3.1,<6.0.0)"] 101 | test = ["pytest (==5.4.3)", "pytest-cov (==2.10.0)", "pytest-asyncio (>=0.14.0,<0.15.0)", "mypy (==0.782)", "flake8 (>=3.8.3,<4.0.0)", "black (==19.10b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.15.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.4.0)", "orjson (>=3.2.1,<4.0.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "aiofiles (>=0.5.0,<0.6.0)", "flask (>=1.1.2,<2.0.0)"] 102 | 103 | [[package]] 104 | name = "flake8" 105 | version = "3.8.4" 106 | description = "the modular source code checker: pep8 pyflakes and co" 107 | category = "dev" 108 | optional = false 109 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 110 | 111 | [package.dependencies] 112 | mccabe = ">=0.6.0,<0.7.0" 113 | pycodestyle = ">=2.6.0a1,<2.7.0" 114 | pyflakes = ">=2.2.0,<2.3.0" 115 | 116 | [[package]] 117 | name = "idna" 118 | version = "2.10" 119 | description = "Internationalized Domain Names in Applications (IDNA)" 120 | category = "main" 121 | optional = false 122 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 123 | 124 | [[package]] 125 | name = "iniconfig" 126 | version = "1.1.1" 127 | description = "iniconfig: brain-dead simple config-ini parsing" 128 | category = "main" 129 | optional = false 130 | python-versions = "*" 131 | 132 | [[package]] 133 | name = "mccabe" 134 | version = "0.6.1" 135 | description = "McCabe checker, plugin for flake8" 136 | category = "dev" 137 | optional = false 138 | python-versions = "*" 139 | 140 | [[package]] 141 | name = "mypy-extensions" 142 | version = "0.4.3" 143 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 144 | category = "dev" 145 | optional = false 146 | python-versions = "*" 147 | 148 | [[package]] 149 | name = "packaging" 150 | version = "20.8" 151 | description = "Core utilities for Python packages" 152 | category = "main" 153 | optional = false 154 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 155 | 156 | [package.dependencies] 157 | pyparsing = ">=2.0.2" 158 | 159 | [[package]] 160 | name = "pathspec" 161 | version = "0.8.1" 162 | description = "Utility library for gitignore style pattern matching of file paths." 163 | category = "dev" 164 | optional = false 165 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 166 | 167 | [[package]] 168 | name = "pluggy" 169 | version = "0.13.1" 170 | description = "plugin and hook calling mechanisms for python" 171 | category = "main" 172 | optional = false 173 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 174 | 175 | [package.extras] 176 | dev = ["pre-commit", "tox"] 177 | 178 | [[package]] 179 | name = "py" 180 | version = "1.10.0" 181 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 182 | category = "main" 183 | optional = false 184 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 185 | 186 | [[package]] 187 | name = "pycodestyle" 188 | version = "2.6.0" 189 | description = "Python style guide checker" 190 | category = "dev" 191 | optional = false 192 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 193 | 194 | [[package]] 195 | name = "pydantic" 196 | version = "1.7.3" 197 | description = "Data validation and settings management using python 3.6 type hinting" 198 | category = "main" 199 | optional = false 200 | python-versions = ">=3.6" 201 | 202 | [package.extras] 203 | dotenv = ["python-dotenv (>=0.10.4)"] 204 | email = ["email-validator (>=1.0.3)"] 205 | typing_extensions = ["typing-extensions (>=3.7.2)"] 206 | 207 | [[package]] 208 | name = "pyflakes" 209 | version = "2.2.0" 210 | description = "passive checker of Python programs" 211 | category = "dev" 212 | optional = false 213 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 214 | 215 | [[package]] 216 | name = "pyparsing" 217 | version = "2.4.7" 218 | description = "Python parsing module" 219 | category = "main" 220 | optional = false 221 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 222 | 223 | [[package]] 224 | name = "pytest" 225 | version = "6.2.1" 226 | description = "pytest: simple powerful testing with Python" 227 | category = "main" 228 | optional = false 229 | python-versions = ">=3.6" 230 | 231 | [package.dependencies] 232 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 233 | attrs = ">=19.2.0" 234 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 235 | iniconfig = "*" 236 | packaging = "*" 237 | pluggy = ">=0.12,<1.0.0a1" 238 | py = ">=1.8.2" 239 | toml = "*" 240 | 241 | [package.extras] 242 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 243 | 244 | [[package]] 245 | name = "regex" 246 | version = "2020.11.13" 247 | description = "Alternative regular expression module, to replace re." 248 | category = "dev" 249 | optional = false 250 | python-versions = "*" 251 | 252 | [[package]] 253 | name = "requests" 254 | version = "2.25.1" 255 | description = "Python HTTP for Humans." 256 | category = "main" 257 | optional = false 258 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 259 | 260 | [package.dependencies] 261 | certifi = ">=2017.4.17" 262 | chardet = ">=3.0.2,<5" 263 | idna = ">=2.5,<3" 264 | urllib3 = ">=1.21.1,<1.27" 265 | 266 | [package.extras] 267 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 268 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 269 | 270 | [[package]] 271 | name = "starlette" 272 | version = "0.13.6" 273 | description = "The little ASGI library that shines." 274 | category = "main" 275 | optional = false 276 | python-versions = ">=3.6" 277 | 278 | [package.extras] 279 | full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"] 280 | 281 | [[package]] 282 | name = "toml" 283 | version = "0.10.2" 284 | description = "Python Library for Tom's Obvious, Minimal Language" 285 | category = "main" 286 | optional = false 287 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 288 | 289 | [[package]] 290 | name = "typed-ast" 291 | version = "1.4.1" 292 | description = "a fork of Python 2 and 3 ast modules with type comment support" 293 | category = "dev" 294 | optional = false 295 | python-versions = "*" 296 | 297 | [[package]] 298 | name = "typing-extensions" 299 | version = "3.7.4.3" 300 | description = "Backported and Experimental Type Hints for Python 3.5+" 301 | category = "dev" 302 | optional = false 303 | python-versions = "*" 304 | 305 | [[package]] 306 | name = "urllib3" 307 | version = "1.26.2" 308 | description = "HTTP library with thread-safe connection pooling, file post, and more." 309 | category = "main" 310 | optional = false 311 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 312 | 313 | [package.extras] 314 | brotli = ["brotlipy (>=0.6.0)"] 315 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 316 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 317 | 318 | [metadata] 319 | lock-version = "1.1" 320 | python-versions = "^3.8" 321 | content-hash = "a4346e9eeb9ec7e813c115fff4ee000780967c9219c7445b4bd24bb6eeff0c98" 322 | 323 | [metadata.files] 324 | appdirs = [ 325 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 326 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 327 | ] 328 | atomicwrites = [ 329 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 330 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 331 | ] 332 | attrs = [ 333 | {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, 334 | {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, 335 | ] 336 | black = [ 337 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 338 | ] 339 | certifi = [ 340 | {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, 341 | {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, 342 | ] 343 | chardet = [ 344 | {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, 345 | {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, 346 | ] 347 | click = [ 348 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 349 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 350 | ] 351 | colorama = [ 352 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 353 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 354 | ] 355 | fastapi = [ 356 | {file = "fastapi-0.62.0-py3-none-any.whl", hash = "sha256:62074dd38541d9d7245f3aacbbd0d44340c53d56186c9b249d261a18dad4874b"}, 357 | {file = "fastapi-0.62.0.tar.gz", hash = "sha256:8f4c64cd9cea67fb7dd175ca5015961efa572b9f43a8731014dac8929d86225f"}, 358 | ] 359 | flake8 = [ 360 | {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, 361 | {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, 362 | ] 363 | idna = [ 364 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 365 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 366 | ] 367 | iniconfig = [ 368 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 369 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 370 | ] 371 | mccabe = [ 372 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 373 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 374 | ] 375 | mypy-extensions = [ 376 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 377 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 378 | ] 379 | packaging = [ 380 | {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, 381 | {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, 382 | ] 383 | pathspec = [ 384 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 385 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 386 | ] 387 | pluggy = [ 388 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 389 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 390 | ] 391 | py = [ 392 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 393 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 394 | ] 395 | pycodestyle = [ 396 | {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, 397 | {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, 398 | ] 399 | pydantic = [ 400 | {file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"}, 401 | {file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"}, 402 | {file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127"}, 403 | {file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e"}, 404 | {file = "pydantic-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23"}, 405 | {file = "pydantic-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f"}, 406 | {file = "pydantic-1.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1"}, 407 | {file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c"}, 408 | {file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f"}, 409 | {file = "pydantic-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2"}, 410 | {file = "pydantic-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef"}, 411 | {file = "pydantic-1.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef"}, 412 | {file = "pydantic-1.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608"}, 413 | {file = "pydantic-1.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec"}, 414 | {file = "pydantic-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e"}, 415 | {file = "pydantic-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e"}, 416 | {file = "pydantic-1.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730"}, 417 | {file = "pydantic-1.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95"}, 418 | {file = "pydantic-1.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b"}, 419 | {file = "pydantic-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af"}, 420 | {file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"}, 421 | {file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"}, 422 | ] 423 | pyflakes = [ 424 | {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, 425 | {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, 426 | ] 427 | pyparsing = [ 428 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 429 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 430 | ] 431 | pytest = [ 432 | {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, 433 | {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, 434 | ] 435 | regex = [ 436 | {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, 437 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, 438 | {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, 439 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, 440 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, 441 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, 442 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, 443 | {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, 444 | {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, 445 | {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, 446 | {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, 447 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, 448 | {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, 449 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, 450 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, 451 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, 452 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, 453 | {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, 454 | {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, 455 | {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, 456 | {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, 457 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, 458 | {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, 459 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, 460 | {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, 461 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, 462 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, 463 | {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, 464 | {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, 465 | {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, 466 | {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, 467 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, 468 | {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, 469 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, 470 | {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, 471 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, 472 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, 473 | {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, 474 | {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, 475 | {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, 476 | {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, 477 | ] 478 | requests = [ 479 | {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, 480 | {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, 481 | ] 482 | starlette = [ 483 | {file = "starlette-0.13.6-py3-none-any.whl", hash = "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9"}, 484 | {file = "starlette-0.13.6.tar.gz", hash = "sha256:ebe8ee08d9be96a3c9f31b2cb2a24dbdf845247b745664bd8a3f9bd0c977fdbc"}, 485 | ] 486 | toml = [ 487 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 488 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 489 | ] 490 | typed-ast = [ 491 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 492 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 493 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 494 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 495 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 496 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 497 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 498 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, 499 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 500 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 501 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 502 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 503 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 504 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, 505 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 506 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 507 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 508 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 509 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 510 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, 511 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 512 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 513 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 514 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, 515 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, 516 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, 517 | {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, 518 | {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, 519 | {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, 520 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 521 | ] 522 | typing-extensions = [ 523 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 524 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 525 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 526 | ] 527 | urllib3 = [ 528 | {file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, 529 | {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, 530 | ] 531 | --------------------------------------------------------------------------------