diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml index c1e021f..3ab9b1f 100644 --- a/.github/workflows/publish-docker-image.yml +++ b/.github/workflows/publish-docker-image.yml @@ -8,24 +8,20 @@ jobs: docker: runs-on: ubuntu-latest steps: - - - name: Checkout + - name: Checkout uses: actions/checkout@v2 - - - name: Docker meta + - name: Docker meta id: meta uses: docker/metadata-action@v3 with: images: 20tab/talos-nextjs - - - name: Login to DockerHub + - name: Login to DockerHub if: github.event_name != 'pull_request' uses: docker/login-action@v1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push + - name: Build and push uses: docker/build-push-action@v2 with: context: . diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d4614ad..578eedf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,18 +14,17 @@ permissions: jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v3 - with: - python-version: "3.11" - - name: Install dependencies - run: | - python -m pip install -r requirements/test.txt - - name: Run Test - run: | - python3 -m unittest + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v3 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install -r requirements/test.txt + - name: Run Test + run: | + python3 -m unittest diff --git a/.gitignore b/.gitignore index 3c69372..98f5690 100644 --- a/.gitignore +++ b/.gitignore @@ -5,14 +5,107 @@ __pycache__/ *.py[cod] *$py.class -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy # Sphinx documentation docs/_build/ +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + # Environments .env .venv @@ -22,6 +115,12 @@ ENV/ env.bak/ venv.bak/ +# Terraform +.terraform.lock* +.terraform/ +*.tfstate* +terraform.tfvars + # Spyder project settings .spyderproject .spyproject @@ -29,36 +128,44 @@ venv.bak/ # Rope project settings .ropeproject +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + # Pyre type checker .pyre/ -# END https://github.com/github/gitignore/blob/master/Python.gitignore +# pytype static type analyzer +.pytype/ -# JetBrains -.idea/ +# Cython debug symbols +cython_debug/ -# Eclipse / Kate -*.swp +# END https://github.com/github/gitignore/blob/master/Python.gitignore -# SublimeText -*.sublime-project -*.sublime-workspace +# START local -# Terraform -.terraform.lock* -.terraform/ -*.log -*.tfstate* -terraform.tfvars +# JetBrains +.idea/ # Vim [._]*.un~ # VisualStudioCode +.devcontainer/ .vscode/ # macOS .DS_Store +# requirements +*/requirements/*.txt + # Ruff .ruff_cache + +# END local diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 89b9661..067a6e9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,40 +1,56 @@ default_language_version: - python: python3 + python: python3.11 +exclude: ^(\{\{cookiecutter\.project_dirname\}\}.*)$ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: "v4.4.0" hooks: - id: check-added-large-files + args: ["--maxkb=1024"] - id: check-case-conflict - id: check-docstring-first + - id: check-json - id: check-merge-conflict - - id: check-symlinks - id: check-toml - - id: check-xml + - id: check-yaml + args: ["--allow-multiple-documents"] - id: debug-statements - id: detect-private-key - id: end-of-file-fixer - id: file-contents-sorter - files: ^(requirements.in)$ + files: ^(requirements/\w*.in)$ args: ["--ignore-case", "--unique"] - id: fix-byte-order-marker - id: fix-encoding-pragma args: ["--remove"] - - id: forbid-new-submodules - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade rev: "v3.3.1" hooks: - id: pyupgrade - args: [--py310-plus] + args: [--py311-plus] - repo: https://github.com/psf/black rev: "22.12.0" hooks: - id: black + - repo: https://github.com/pre-commit/mirrors-prettier + rev: "v3.0.0-alpha.4" + hooks: + - id: prettier - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.218 + rev: v0.0.225 hooks: - id: ruff args: - --fix + - repo: https://github.com/pre-commit/mirrors-mypy + rev: "v0.991" + hooks: + - id: mypy + args: ["--no-site-packages"] + - repo: https://github.com/trailofbits/pip-audit + rev: v2.4.13 + hooks: + - id: pip-audit + args: ["--require-hashes", "--requirement", "requirements/local.txt"] diff --git a/Makefile b/Makefile index d4f8f1c..b81d992 100644 --- a/Makefile +++ b/Makefile @@ -20,13 +20,13 @@ outdated: ## Check outdated requirements and dependencies .PHONY: pip pip: pip_update ## Compile requirements - python3 -m piptools compile --no-header --quiet --resolver=backtracking --upgrade --output-file requirements/common.txt requirements/common.in - python3 -m piptools compile --no-header --quiet --resolver=backtracking --upgrade --output-file requirements/local.txt requirements/local.in - python3 -m piptools compile --no-header --quiet --resolver=backtracking --upgrade --output-file requirements/test.txt requirements/test.in + python3 -m piptools compile --generate-hashes --no-header --quiet --resolver=backtracking --upgrade --output-file requirements/common.txt requirements/common.in + python3 -m piptools compile --generate-hashes --no-header --quiet --resolver=backtracking --upgrade --output-file requirements/local.txt requirements/local.in + python3 -m piptools compile --generate-hashes --no-header --quiet --resolver=backtracking --upgrade --output-file requirements/test.txt requirements/test.in .PHONY: pip_update pip_update: ## Update requirements and dependencies - python3 -m pip install -q -U pip~=22.3.0 pip-tools~=6.12.0 setuptools~=65.6.0 wheel~=0.38.0 + python3 -m pip install -q -U pip~=22.3.0 pip-tools~=6.12.0 setuptools~=66.0.0 wheel~=0.38.0 .PHONY: precommit precommit: ## Fix code formatting, linting and sorting imports @@ -45,9 +45,11 @@ endif simpletest: python3 -m unittest $(simpletestargs) -.PHONY: shellplus -shellplus: ## Launch shell_plus - python3 -m manage shell_plus +.PHONY: test # Run full test and coverage +test: + python3 -m coverage run -m unittest + python3 -m coverage html + python3 -m coverage report .PHONY: update update: pip precommit_update ## Run update diff --git a/README.md b/README.md index 88b307b..55d65b8 100644 --- a/README.md +++ b/README.md @@ -1,188 +1,211 @@ -# Talos Submodule - NextJS - -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) - -> A [NextJS](https://nextjs.org/) project template ready for continuous delivery. - -## 🧩 Requirements - -The Talos script can be run either using Docker or as a local shell command. - -### 🐋 Docker - -In order to run Talos via Docker, a working [Docker installation](https://docs.docker.com/get-docker/) is the only requirement. - -### 👨‍💻 Shell command - -In order to run Talos as a shell command, first clone the repository in a local projects directory -```console -cd ~/projects -git clone git@github.com:20tab/nextjs-continuous-delivery.git talos-nextjs -``` -Then, install the following requirements -| Requirements | Instructions | -|--|--| -|🌎 Terraform | [Install Guide](https://learn.hashicorp.com/tutorials/terraform/install-cli) | -|🐍 Python Dependencies | `pip install -r talos/requirements/common.txt` | - -## 🔑 Credentials - -### 🦊 GitLab -If the GitLab integration is enabled, a Personal Access Token with _api_ permission is required.
-It can be generated in the GitLab User Settings panel. - -**Note:** the token can be generated in the Access Tokens section of the GitLab User Settings panel.
-⚠️ Beware that the token is shown only once after creation. - -## 🚀️ Quickstart - -Change to the projects directory, for example -```console -cd ~/projects -``` - -### 🐋 Docker - -```console -docker run --interactive --tty --rm --volume $PWD:/data 20tab/talos-nextjs:latest -``` - -### 👨‍💻 Shell command - -```console -./talos-nextjs/start.py -``` - -### Example -```console -Project name: My Project Name -Project slug [my-project-name]: -Service slug [frontend]: -Project dirname (frontend, myprojectname) [frontend]: myprojectname -Deploy type (digitalocean-k8s, other-k8s) [digitalocean-k8s]: -Terraform backend (gitlab, terraform-cloud) [terraform-cloud]: -Terraform host name [app.terraform.io]: -Terraform Cloud User token: -Terraform Organization: my-organization-name -Do you want to create Terraform Cloud Organization 'my-organization-name'? [y/N]: -Choose the environments distribution: - 1 - All environments share the same stack (Default) - 2 - Dev and Stage environments share the same stack, Prod has its own - 3 - Each environment has its own stack - (1, 2, 3) [1]: -Development environment complete URL [https://dev.my-project-name.com/]: -Staging environment complete URL [https://stage.my-project-name.com/]: -Production environment complete URL [https://www.my-project-name.com/]: -Do you want to configure Redis? [y/N]: -Do you want to use GitLab? [Y/n]: -GitLab group slug [my-project-name]: -Make sure the GitLab "my-project-name" group exists before proceeding. Continue? [y/N]: y -GitLab private token (with API scope enabled): -Sentry DSN (leave blank if unused) []: -Initializing the frontend service: -...cookiecutting the service -...generating the .env file -...creating the GitLab repository and associated resources -...creating the Terraform Cloud resources -``` - -## 🗒️ Arguments - -The following arguments can be appended to the Docker and shell commands - -#### User id -`--uid=$UID` - -#### Group id -`--gid=1000` - -#### Output directory -`--output-dir="~/projects"` - -#### Project name -`--project-name="My project name"` - -#### Project slug -`--project-slug="my-project-name"` - -#### Project dirname -`--project-dirname="myprojectname"` - -### 🎖️ Service - -#### Service slug -`--service-slug=frontend` - -#### Service port -`--internal-service-port=3000` - -#### Backend Internal Url -`--internal-backend-url=http://backend:8000` - -### 📐 Architecture - -#### Deploy type -Description | Argument -------------- | ------------- -DigitalOcean Kubernates | `--deployment-type=digitalocean-k8s` -Other Kubernetes | `--deployment-type=other-k8s` - -#### Terraform backend -Name | Argument -------------- | ------------- -Terraform Cloud | `--terraform-backend=terraform-cloud` -GitLab | `--terraform-backend=gitlab` - -##### Terraform Cloud required argument -`--terraform-cloud-hostname=app.terraform.io`
-`--terraform-cloud-token={{terraform-cloud-token}}`
-`--terraform-cloud-organization` - -##### Terraform Cloud create organization -`--terraform-cloud-organization-create`
-`--terraform-cloud-admin-email={{terraform-cloud-admin-email}}` - -Disabled args -`--terraform-cloud-organization-create-skip` - -#### Environment distribution -Choose the environments distribution: -Value | Description | Argument -------------- | ------------- | ------------- -1 | All environments share the same stack (Default) | `--environment-distribution=1` -2 | Dev and Stage environments share the same stack, Prod has its own | `--environment-distribution=2` -3 | Each environment has its own stack | `--environment-distribution=3` - -#### Project Domain -If you don't want DigitalOcean DNS configuration the following args are required - -`--project-url-dev=https://dev.project-domain.com`
-`--project-url-stage=https://stage.project-domain.com`
-`--project-url-prod=https://www.project-domain.com` - -#### Redis -For enabling redis integration the following arguments are needed: - -`--use-redis` - -Disabled args -`--no-redis` - -### 🦊 GitLab -> **⚠️ Important: Make sure the GitLab group exists before creating.** -> https://gitlab.com/gitlab-org/gitlab/-/issues/244345 - -For enabling gitlab integration the following arguments are needed: - -`--gitlab-private-token={{gitlab-private-token}}`
-`--gitlab-group-path={{gitlab-group-path}}` - -#### 🪖 Sentry -For enabling sentry integration the following arguments are needed: - -`--sentry-dsn={{frontend-sentry-dsn}}` - -#### 🔇 Quiet -No confirmations shown. - -`--quiet` +# Talos Submodule - NextJS + +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) + +> A [NextJS](https://nextjs.org/) project template ready for continuous delivery. + +## 🧩 Requirements + +The Talos script can be run either using Docker or as a local shell command. + +### 🐋 Docker + +In order to run Talos via Docker, a working [Docker installation](https://docs.docker.com/get-docker/) is the only requirement. + +### 👨‍💻 Shell command + +In order to run Talos as a shell command, first clone the repository in a local projects directory + +```console +cd ~/projects +git clone git@github.com:20tab/nextjs-continuous-delivery.git talos-nextjs +``` + +Then, install the following requirements +| Requirements | Instructions | +|--|--| +|🌎 Terraform | [Install Guide](https://learn.hashicorp.com/tutorials/terraform/install-cli) | +|🐍 Python Dependencies | `pip install -r talos/requirements/common.txt` | + +## 🔑 Credentials + +### 🦊 GitLab + +If the GitLab integration is enabled, a Personal Access Token with _api_ permission is required.
+It can be generated in the GitLab User Settings panel. + +**Note:** the token can be generated in the Access Tokens section of the GitLab User Settings panel.
+⚠️ Beware that the token is shown only once after creation. + +## 🚀️ Quickstart + +Change to the projects directory, for example + +```console +cd ~/projects +``` + +### 🐋 Docker + +```console +docker run --interactive --tty --rm --volume $PWD:/data 20tab/talos-nextjs:latest +``` + +### 👨‍💻 Shell command + +```console +./talos-nextjs/start.py +``` + +### Example + +```console +Project name: My Project Name +Project slug [my-project-name]: +Service slug [frontend]: +Project dirname (frontend, myprojectname) [frontend]: myprojectname +Deploy type (digitalocean-k8s, other-k8s) [digitalocean-k8s]: +Terraform backend (gitlab, terraform-cloud) [terraform-cloud]: +Terraform host name [app.terraform.io]: +Terraform Cloud User token: +Terraform Organization: my-organization-name +Do you want to create Terraform Cloud Organization 'my-organization-name'? [y/N]: +Choose the environments distribution: + 1 - All environments share the same stack (Default) + 2 - Dev and Stage environments share the same stack, Prod has its own + 3 - Each environment has its own stack + (1, 2, 3) [1]: +Development environment complete URL [https://dev.my-project-name.com/]: +Staging environment complete URL [https://stage.my-project-name.com/]: +Production environment complete URL [https://www.my-project-name.com/]: +Do you want to configure Redis? [y/N]: +Do you want to use GitLab? [Y/n]: +GitLab group slug [my-project-name]: +Make sure the GitLab "my-project-name" group exists before proceeding. Continue? [y/N]: y +GitLab private token (with API scope enabled): +Sentry DSN (leave blank if unused) []: +Initializing the frontend service: +...cookiecutting the service +...generating the .env file +...creating the GitLab repository and associated resources +...creating the Terraform Cloud resources +``` + +## 🗒️ Arguments + +The following arguments can be appended to the Docker and shell commands + +#### User id + +`--uid=$UID` + +#### Group id + +`--gid=1000` + +#### Output directory + +`--output-dir="~/projects"` + +#### Project name + +`--project-name="My project name"` + +#### Project slug + +`--project-slug="my-project-name"` + +#### Project dirname + +`--project-dirname="myprojectname"` + +### 🎖️ Service + +#### Service slug + +`--service-slug=frontend` + +#### Service port + +`--internal-service-port=3000` + +#### Backend Internal Url + +`--internal-backend-url=http://backend:8000` + +### 📐 Architecture + +#### Deploy type + +| Description | Argument | +| ----------------------- | ------------------------------------ | +| DigitalOcean Kubernates | `--deployment-type=digitalocean-k8s` | +| Other Kubernetes | `--deployment-type=other-k8s` | + +#### Terraform backend + +| Name | Argument | +| --------------- | ------------------------------------- | +| Terraform Cloud | `--terraform-backend=terraform-cloud` | +| GitLab | `--terraform-backend=gitlab` | + +##### Terraform Cloud required argument + +`--terraform-cloud-hostname=app.terraform.io`
+`--terraform-cloud-token={{terraform-cloud-token}}`
+`--terraform-cloud-organization` + +##### Terraform Cloud create organization + +`--terraform-cloud-organization-create`
+`--terraform-cloud-admin-email={{terraform-cloud-admin-email}}` + +Disabled args +`--terraform-cloud-organization-create-skip` + +#### Environment distribution + +Choose the environments distribution: +Value | Description | Argument +------------- | ------------- | ------------- +1 | All environments share the same stack (Default) | `--environment-distribution=1` +2 | Dev and Stage environments share the same stack, Prod has its own | `--environment-distribution=2` +3 | Each environment has its own stack | `--environment-distribution=3` + +#### Project Domain + +If you don't want DigitalOcean DNS configuration the following args are required + +`--project-url-dev=https://dev.project-domain.com`
+`--project-url-stage=https://stage.project-domain.com`
+`--project-url-prod=https://www.project-domain.com` + +#### Redis + +For enabling redis integration the following arguments are needed: + +`--use-redis` + +Disabled args +`--no-redis` + +### 🦊 GitLab + +> **⚠️ Important: Make sure the GitLab group exists before creating.** > https://gitlab.com/gitlab-org/gitlab/-/issues/244345 + +For enabling gitlab integration the following arguments are needed: + +`--gitlab-private-token={{gitlab-private-token}}`
+`--gitlab-group-path={{gitlab-group-path}}` + +#### 🪖 Sentry + +For enabling sentry integration the following arguments are needed: + +`--sentry-dsn={{frontend-sentry-dsn}}` + +#### 🔇 Quiet + +No confirmations shown. + +`--quiet` diff --git a/bootstrap/collector.py b/bootstrap/collector.py old mode 100755 new mode 100644 index e67d623..ddc4dc7 --- a/bootstrap/collector.py +++ b/bootstrap/collector.py @@ -1,457 +1,316 @@ -#!/usr/bin/env python """Initialize a web project Next.js service based on a template.""" -import re -from functools import partial +from dataclasses import dataclass +from pathlib import Path from shutil import rmtree import click -import validators +from pydantic import validate_arguments from slugify import slugify from bootstrap.constants import ( DEPLOYMENT_TYPE_CHOICES, DEPLOYMENT_TYPE_DIGITALOCEAN, DEPLOYMENT_TYPE_OTHER, - ENVIRONMENT_DISTRIBUTION_CHOICES, - ENVIRONMENT_DISTRIBUTION_DEFAULT, - ENVIRONMENT_DISTRIBUTION_PROMPT, + ENVIRONMENTS_DISTRIBUTION_CHOICES, + ENVIRONMENTS_DISTRIBUTION_DEFAULT, + ENVIRONMENTS_DISTRIBUTION_PROMPT, GITLAB_URL_DEFAULT, TERRAFORM_BACKEND_CHOICES, TERRAFORM_BACKEND_TFC, ) - -error = partial(click.style, fg="red") - -warning = partial(click.style, fg="yellow") - - -def collect( - uid, - gid, - output_dir, - project_name, - project_slug, - project_dirname, - service_slug, - internal_backend_url, - internal_service_port, - deployment_type, - terraform_backend, - terraform_cloud_hostname, - terraform_cloud_token, - terraform_cloud_organization, - terraform_cloud_organization_create, - terraform_cloud_admin_email, - vault_token, - vault_url, - environment_distribution, - project_url_dev, - project_url_stage, - project_url_prod, - sentry_dsn, - sentry_org, - sentry_url, - use_redis, - gitlab_url, - gitlab_private_token, - gitlab_group_path, - terraform_dir, - logs_dir, - quiet, -): - """Collect options and run the setup.""" - project_slug = clean_project_slug(project_name, project_slug) - service_slug = clean_service_slug(service_slug) - project_dirname = clean_project_dirname(project_dirname, project_slug, service_slug) - service_dir = clean_service_dir(output_dir, project_dirname) - deployment_type = clean_deployment_type(deployment_type) - ( - terraform_backend, - terraform_cloud_hostname, - terraform_cloud_token, - terraform_cloud_organization, - terraform_cloud_organization_create, - terraform_cloud_admin_email, - ) = clean_terraform_backend( - terraform_backend, - terraform_cloud_hostname, - terraform_cloud_token, - terraform_cloud_organization, - terraform_cloud_organization_create, - terraform_cloud_admin_email, - ) - vault_token, vault_url = clean_vault_data(vault_token, vault_url, quiet) - environment_distribution = clean_environment_distribution( - environment_distribution, deployment_type - ) - project_url_dev = validate_or_prompt_url( - "Development environment complete URL", - project_url_dev, - default=f"https://dev.{project_slug}.com/", - required=False, - ) - project_url_stage = validate_or_prompt_url( - "Staging environment complete URL", - project_url_stage, - default=f"https://stage.{project_slug}.com/", - required=False, - ) - project_url_prod = validate_or_prompt_url( - "Production environment complete URL", - project_url_prod, - default=f"https://www.{project_slug}.com/", - required=False, - ) - use_redis = clean_use_redis(use_redis) - gitlab_url, gitlab_private_token, gitlab_group_path = clean_gitlab_data( - gitlab_url, - gitlab_private_token, - gitlab_group_path, - quiet, - ) - if gitlab_group_path: - (sentry_org, sentry_url, sentry_dsn) = clean_sentry_data( - sentry_org, sentry_url, sentry_dsn +from bootstrap.helpers import ( + validate_or_prompt_domain, + validate_or_prompt_email, + validate_or_prompt_path, + validate_or_prompt_secret, + validate_or_prompt_url, + warning, +) +from bootstrap.runner import Runner + + +@validate_arguments +@dataclass(kw_only=True) +class Collector: + """The bootstrap CLI options collector.""" + + output_dir: Path = Path(".") + project_name: str | None = None + project_slug: str | None = None + project_dirname: str | None = None + service_slug: str | None = None + internal_backend_url: str | None = None + internal_service_port: int | None = None + deployment_type: str | None = None + terraform_backend: str | None = None + terraform_cloud_hostname: str | None = None + terraform_cloud_token: str | None = None + terraform_cloud_organization: str | None = None + terraform_cloud_organization_create: bool | None = None + terraform_cloud_admin_email: str | None = None + vault_token: str | None = None + vault_url: str | None = None + environments_distribution: str | None = None + project_url_dev: str | None = None + project_url_stage: str | None = None + project_url_prod: str | None = None + use_redis: bool | None = None + sentry_dsn: str | None = None + sentry_org: str | None = None + sentry_url: str | None = None + gitlab_url: str | None = None + gitlab_token: str | None = None + gitlab_namespace_path: str | None = None + uid: int | None = None + gid: int | None = None + terraform_dir: Path | None = None + logs_dir: Path | None = None + quiet: bool = False + + def __post_init__(self): + """Finalize initialization.""" + self._service_dir = None + + def collect(self): + """Collect options.""" + self.set_project_slug() + self.set_service_slug() + self.set_project_dirname() + self.set_service_dir() + self.set_use_redis() + self.set_terraform() + self.set_vault() + self.set_deployment_type() + self.set_environments_distribution() + self.set_project_urls() + self.set_sentry() + self.set_gitlab() + + def set_project_slug(self): + """Set the project slug option.""" + self.project_slug = slugify( + self.project_slug + or click.prompt("Project slug", default=slugify(self.project_name)) ) - return { - "uid": uid, - "gid": gid, - "output_dir": output_dir, - "project_name": project_name, - "project_slug": project_slug, - "project_dirname": project_dirname, - "service_dir": service_dir, - "service_slug": service_slug, - "internal_backend_url": internal_backend_url, - "internal_service_port": internal_service_port, - "deployment_type": deployment_type, - "terraform_backend": terraform_backend, - "terraform_cloud_hostname": terraform_cloud_hostname, - "terraform_cloud_token": terraform_cloud_token, - "terraform_cloud_organization": terraform_cloud_organization, - "terraform_cloud_organization_create": terraform_cloud_organization_create, - "terraform_cloud_admin_email": terraform_cloud_admin_email, - "vault_token": vault_token, - "vault_url": vault_url, - "environment_distribution": environment_distribution, - "project_url_dev": project_url_dev, - "project_url_stage": project_url_stage, - "project_url_prod": project_url_prod, - "sentry_dsn": sentry_dsn, - "sentry_org": sentry_org, - "sentry_url": sentry_url, - "use_redis": use_redis, - "gitlab_url": gitlab_url, - "gitlab_private_token": gitlab_private_token, - "gitlab_group_path": gitlab_group_path, - "terraform_dir": terraform_dir, - "logs_dir": logs_dir, - } - - -def validate_or_prompt_domain(message, value=None, default=None, required=True): - """Validate the given domain or prompt until a valid value is provided.""" - if value is None: - value = click.prompt(message, default=default) - try: - if not required and value == "" or validators.domain(value): - return value - except validators.ValidationFailure: - pass - click.echo(error("Please type a valid domain!")) - return validate_or_prompt_domain(message, None, default, required) - -def validate_or_prompt_email(message, value=None, default=None, required=True): - """Validate the given email address or prompt until a valid value is provided.""" - if value is None: - value = click.prompt(message, default=default) - try: - if not required and value == "" or validators.email(value): - return value - except validators.ValidationFailure: - pass - click.echo(error("Please type a valid email!")) - return validate_or_prompt_email(message, None, default, required) - - -def validate_or_prompt_secret(message, value=None, default=None, required=True): - """Validate the given secret or prompt until a valid value is provided.""" - if value is None: - value = click.prompt(message, default=default, hide_input=True) - try: - if not required and value == "" or validators.length(value, min=8): - return value - except validators.ValidationFailure: - pass - click.echo(error("Please type at least 8 chars!")) - return validate_or_prompt_secret(message, None, default, required) - - -def validate_or_prompt_path(message, value=None, default=None, required=True): - """Validate the given path or prompt until a valid path is provided.""" - if value is None: - value = click.prompt(message, default=default) - try: - if ( - not required - and value == "" - or re.match(r"^(?:[\w_\-]+)(?:\/[\w_\-]+)*\/?$", value) - ): - return value.rstrip("/") - except validators.ValidationFailure: - pass - click.echo( - error( - "Please type a valid slash-separated path containing letters, digits, " - "dashes and underscores!" + def set_service_slug(self): + """Set the service slug option.""" + self.service_slug = slugify( + self.service_slug or click.prompt("Service slug", default="frontend") ) - ) - return validate_or_prompt_path(message, None, default, required) - - -def validate_or_prompt_url(message, value=None, default=None, required=True): - """Validate the given URL or prompt until a valid value is provided.""" - if value is None: - value = click.prompt(message, default=default) - try: - if not required and value == "" or validators.url(value): - return value.strip("/") - except validators.ValidationFailure: - pass - click.echo(error("Please type a valid URL!")) - return validate_or_prompt_url(message, None, default, required) - -def clean_project_slug(project_name, project_slug): - """Return the project slug.""" - return slugify( - project_slug or click.prompt("Project slug", default=slugify(project_name)) - ) - - -def clean_service_slug(service_slug): - """Return the service slug.""" - return slugify( - service_slug or click.prompt("Service slug", default="frontend"), - separator="", - ) - - -def clean_project_dirname(project_dirname, project_slug, service_slug): - """Return the project directory name.""" - project_dirname_choices = [service_slug, slugify(project_slug, separator="")] - return project_dirname or click.prompt( - "Project dirname", - default=project_dirname_choices[0], - type=click.Choice(project_dirname_choices), - ) - - -def clean_service_dir(output_dir, project_dirname): - """Return the service directory.""" - service_dir = output_dir / project_dirname - if service_dir.is_dir() and click.confirm( - warning( - f'A directory "{service_dir}" already exists and ' - "must be deleted. Continue?", - ), - abort=True, - ): - rmtree(service_dir) - return service_dir - - -def clean_deployment_type(deployment_type): - """Return the deployment type.""" - return ( - deployment_type - if deployment_type in DEPLOYMENT_TYPE_CHOICES - else click.prompt( - "Deploy type", - default=DEPLOYMENT_TYPE_DIGITALOCEAN, - type=click.Choice(DEPLOYMENT_TYPE_CHOICES, case_sensitive=False), + def set_project_dirname(self): + """Set the project dirname option.""" + self.project_dirname = self.project_dirname or click.prompt( + "Project dirname", + default=self.service_slug, + type=click.Choice( + [self.service_slug, slugify(self.project_slug, separator="")] + ), ) - ).lower() + def set_service_dir(self): + """Set the service dir option.""" + service_dir = self.output_dir / self.project_dirname + if service_dir.is_dir() and click.confirm( + warning( + f'A directory "{service_dir.resolve()}" already exists and ' + "must be deleted. Continue?", + ), + abort=True, + ): + rmtree(service_dir) + self._service_dir = service_dir + + def set_use_redis(self): + """Set the use Redis option.""" + if self.use_redis is None: + self.use_redis = click.confirm( + warning("Do you want to use Redis?"), default=False + ) -def clean_terraform_backend( - terraform_backend, - terraform_cloud_hostname, - terraform_cloud_token, - terraform_cloud_organization, - terraform_cloud_organization_create, - terraform_cloud_admin_email, -): - """Return the terraform backend and the Terraform Cloud data, if applicable.""" - terraform_backend = ( - terraform_backend - if terraform_backend in TERRAFORM_BACKEND_CHOICES - else click.prompt( - "Terraform backend", - default=TERRAFORM_BACKEND_TFC, - type=click.Choice(TERRAFORM_BACKEND_CHOICES, case_sensitive=False), + def set_terraform(self): + """Set the Terraform options.""" + if self.terraform_backend not in TERRAFORM_BACKEND_CHOICES: + self.terraform_backend = click.prompt( + "Terraform backend", + default=TERRAFORM_BACKEND_TFC, + type=click.Choice(TERRAFORM_BACKEND_CHOICES, case_sensitive=False), + ).lower() + if self.terraform_backend == TERRAFORM_BACKEND_TFC: + self.set_terraform_cloud() + + def set_terraform_cloud(self): + """Set the Terraform Cloud options.""" + self.terraform_cloud_hostname = validate_or_prompt_domain( + "Terraform host name", + self.terraform_cloud_hostname, + default="app.terraform.io", ) - ).lower() - if terraform_backend == TERRAFORM_BACKEND_TFC: - terraform_cloud_hostname = validate_or_prompt_domain( - "Terraform host name", terraform_cloud_hostname, default="app.terraform.io" + self.terraform_cloud_token = validate_or_prompt_secret( + "Terraform Cloud User token", self.terraform_cloud_token ) - terraform_cloud_token = validate_or_prompt_secret( - "Terraform Cloud User token", terraform_cloud_token + self.terraform_cloud_organization = ( + self.terraform_cloud_organization or click.prompt("Terraform Organization") ) - terraform_cloud_organization = terraform_cloud_organization or click.prompt( - "Terraform Organization" - ) - terraform_cloud_organization_create = ( - terraform_cloud_organization_create - if terraform_cloud_organization_create is not None - else click.confirm( + if self.terraform_cloud_organization_create is None: + self.terraform_cloud_organization_create = click.confirm( "Do you want to create Terraform Cloud Organization " - f"'{terraform_cloud_organization}'?", + f"'{self.terraform_cloud_organization}'?", ) - ) - if terraform_cloud_organization_create: - terraform_cloud_admin_email = validate_or_prompt_email( + if self.terraform_cloud_organization_create: + self.terraform_cloud_admin_email = validate_or_prompt_email( "Terraform Cloud Organization admin email (e.g. tech@20tab.com)", - terraform_cloud_admin_email, + self.terraform_cloud_admin_email, ) else: - terraform_cloud_admin_email = None - else: - terraform_cloud_organization = None - terraform_cloud_hostname = None - terraform_cloud_token = None - terraform_cloud_organization_create = None - terraform_cloud_admin_email = None - return ( - terraform_backend, - terraform_cloud_hostname, - terraform_cloud_token, - terraform_cloud_organization, - terraform_cloud_organization_create, - terraform_cloud_admin_email, - ) + self.terraform_cloud_admin_email = "" + def set_vault(self): + """Set the Vault options.""" + if self.vault_url or ( + self.vault_url is None + and click.confirm("Do you want to use Vault for secrets management?") + ): + self.vault_token = validate_or_prompt_secret( + "Vault token " + "(leave blank to perform a browser-based OIDC authentication)", + self.vault_token, + default="", + required=False, + ) + self.quiet or click.confirm( + warning( + "Make sure your Vault permissions allow to enable the " + "project secrets backends and manage the project secrets. Continue?" + ), + abort=True, + ) + self.vault_url = validate_or_prompt_url("Vault address", self.vault_url) + + def set_deployment_type(self): + """Set the deployment type option.""" + if self.deployment_type not in DEPLOYMENT_TYPE_CHOICES: + self.deployment_type = click.prompt( + "Deploy type", + default=DEPLOYMENT_TYPE_DIGITALOCEAN, + type=click.Choice(DEPLOYMENT_TYPE_CHOICES, case_sensitive=False), + ).lower() + + def set_environments_distribution(self): + """Set the environments distribution option.""" + # TODO: forcing a single stack when deployment is `k8s-other` should be removed, + # and `set_deployment_type` merged with `set_deployment` + if self.deployment_type == DEPLOYMENT_TYPE_OTHER: + self.environments_distribution = "1" + elif self.environments_distribution not in ENVIRONMENTS_DISTRIBUTION_CHOICES: + self.environments_distribution = click.prompt( + ENVIRONMENTS_DISTRIBUTION_PROMPT, + default=ENVIRONMENTS_DISTRIBUTION_DEFAULT, + type=click.Choice(ENVIRONMENTS_DISTRIBUTION_CHOICES), + ) -def clean_vault_data(vault_token, vault_url, quiet=False): - """Return the Vault data, if applicable.""" - if vault_url or ( - vault_url is None - and click.confirm( - "Do you want to use Vault for secrets management?", + def set_project_urls(self): + """Set the project urls options.""" + self.project_url_dev = validate_or_prompt_url( + "Development environment complete URL", + self.project_url_dev or None, + default=f"https://dev.{self.project_slug}.com", ) - ): - vault_token = validate_or_prompt_secret( - "Vault token (leave blank to perform a browser-based OIDC authentication)", - vault_token, - default="", - required=False, + self.project_url_stage = validate_or_prompt_url( + "Staging environment complete URL", + self.project_url_stage or None, + default=f"https://stage.{self.project_slug}.com", ) - quiet or click.confirm( - warning( - "Make sure your Vault permissions allow to enable the " - "project secrets backends and manage the project secrets. Continue?" - ), - abort=True, + self.project_url_prod = validate_or_prompt_url( + "Production environment complete URL", + self.project_url_prod or None, + default=f"https://www.{self.project_slug}.com", ) - vault_url = validate_or_prompt_url("Vault address", vault_url) - else: - vault_token = None - vault_url = None - return vault_token, vault_url - -def clean_environment_distribution(environment_distribution, deployment_type): - """Return the environment distribution.""" - if deployment_type == DEPLOYMENT_TYPE_OTHER: - return "1" - return ( - environment_distribution - if environment_distribution in ENVIRONMENT_DISTRIBUTION_CHOICES - else click.prompt( - ENVIRONMENT_DISTRIBUTION_PROMPT, - default=ENVIRONMENT_DISTRIBUTION_DEFAULT, - type=click.Choice(ENVIRONMENT_DISTRIBUTION_CHOICES), - ) - ) + def set_sentry(self): + """Set the Sentry options.""" + if self.sentry_org or ( + self.sentry_org is None + and click.confirm(warning("Do you want to use Sentry?"), default=False) + ): + self.sentry_org = self.sentry_org or click.prompt("Sentry organization") + self.sentry_url = validate_or_prompt_url( + "Sentry URL", self.sentry_url, default="https://sentry.io/" + ) + self.sentry_dsn = validate_or_prompt_url( + "Sentry DSN (leave blank if unused)", + self.sentry_dsn, + default="", + required=False, + ) + def set_gitlab(self): + """Set the GitLab options.""" + if self.gitlab_url or ( + self.gitlab_url is None + and click.confirm(warning("Do you want to use GitLab?"), default=True) + ): + self.gitlab_url = validate_or_prompt_url( + "GitLab URL", self.gitlab_url, default=GITLAB_URL_DEFAULT + ) + self.gitlab_token = self.gitlab_token or click.prompt( + "GitLab access token (with API scope enabled)", hide_input=True + ) + # TODO: extend support for root level projects (empty namespace) + self.gitlab_namespace_path = validate_or_prompt_path( + "GitLab parent group path", self.gitlab_namespace_path + ) + self.quiet or ( + self.gitlab_namespace_path == "" + and self.gitlab_url == GITLAB_URL_DEFAULT + and click.confirm( + warning( + f'Make sure the GitLab "{self.gitlab_namespace_path}" group ' + "exists before proceeding. Continue?" + ), + abort=True, + ) + ) -def clean_sentry_data( - sentry_org, - sentry_url, - sentry_dsn, -): - """Return the Sentry configuration data.""" - if sentry_org or ( - sentry_org is None - and click.confirm(warning("Do you want to use Sentry?"), default=False) - ): - sentry_org = clean_sentry_org(sentry_org) - sentry_url = validate_or_prompt_url( - "Sentry URL", sentry_url, default="https://sentry.io/" + def get_runner(self): + """Get the bootstrap runner instance.""" + return Runner( + uid=self.uid, + gid=self.gid, + output_dir=self.output_dir, + project_name=self.project_name, + project_slug=self.project_slug, + project_dirname=self.project_dirname, + service_dir=self._service_dir, + service_slug=self.service_slug, + internal_backend_url=self.internal_backend_url, + internal_service_port=self.internal_service_port, + deployment_type=self.deployment_type, + terraform_backend=self.terraform_backend, + terraform_cloud_hostname=self.terraform_cloud_hostname, + terraform_cloud_token=self.terraform_cloud_token, + terraform_cloud_organization=self.terraform_cloud_organization, + terraform_cloud_organization_create=self.terraform_cloud_organization_create, + terraform_cloud_admin_email=self.terraform_cloud_admin_email, + vault_token=self.vault_token, + vault_url=self.vault_url, + environments_distribution=self.environments_distribution, + project_url_dev=self.project_url_dev, + project_url_stage=self.project_url_stage, + project_url_prod=self.project_url_prod, + sentry_dsn=self.sentry_dsn, + sentry_org=self.sentry_org, + sentry_url=self.sentry_url, + use_redis=self.use_redis, + gitlab_url=self.gitlab_url, + gitlab_token=self.gitlab_token, + gitlab_namespace_path=self.gitlab_namespace_path, + terraform_dir=self.terraform_dir, + logs_dir=self.logs_dir, ) - sentry_dsn = clean_sentry_dsn(sentry_dsn) - else: - sentry_org = None - sentry_url = None - sentry_dsn = None - return ( - sentry_org, - sentry_url, - sentry_dsn, - ) - - -def clean_sentry_org(sentry_org): - """Return the Sentry organization.""" - return sentry_org if sentry_org is not None else click.prompt("Sentry organization") - - -def clean_sentry_dsn(sentry_dsn): - """Return the Sentry DSN.""" - return validate_or_prompt_url( - "Sentry DSN (leave blank if unused)", sentry_dsn, default="", required=False - ) - - -def clean_use_redis(use_redis): - """Tell whether Redis should be used.""" - if use_redis is None: - return click.confirm(warning("Do you want to configure Redis?"), default=False) - return use_redis - -def clean_gitlab_data( - gitlab_url, - gitlab_private_token, - gitlab_group_path, - quiet=False, -): - """Return GitLab group data.""" - if gitlab_group_path or ( - gitlab_group_path is None - and click.confirm(warning("Do you want to use GitLab?"), default=True) - ): - gitlab_url = validate_or_prompt_url( - "GitLab URL", gitlab_url, default=GITLAB_URL_DEFAULT - ) - gitlab_private_token = gitlab_private_token or click.prompt( - "GitLab private token (with API scope enabled)", hide_input=True - ) - gitlab_group_path = validate_or_prompt_path( - "GitLab group full path", gitlab_group_path - ) - quiet or click.confirm( - warning( - f'Make sure the GitLab "{gitlab_group_path}" group exists ' - "before proceeding. Continue?" - ), - abort=True, - ) - else: - gitlab_url = None - gitlab_private_token = None - gitlab_group_path = None - return (gitlab_url, gitlab_private_token, gitlab_group_path) + def launch_runner(self): + """Launch a bootstrap runner with the collected options.""" + self.get_runner().run() diff --git a/bootstrap/constants.py b/bootstrap/constants.py old mode 100755 new mode 100644 index a2632aa..85403b9 --- a/bootstrap/constants.py +++ b/bootstrap/constants.py @@ -73,11 +73,11 @@ # Environments distribution -ENVIRONMENT_DISTRIBUTION_DEFAULT = "1" +ENVIRONMENTS_DISTRIBUTION_DEFAULT = "1" -ENVIRONMENT_DISTRIBUTION_CHOICES = [ENVIRONMENT_DISTRIBUTION_DEFAULT, "2", "3"] +ENVIRONMENTS_DISTRIBUTION_CHOICES = [ENVIRONMENTS_DISTRIBUTION_DEFAULT, "2", "3"] -ENVIRONMENT_DISTRIBUTION_PROMPT = """Choose the environments distribution: +ENVIRONMENTS_DISTRIBUTION_PROMPT = """Choose the environments distribution: 1 - All environments share the same stack (Default) 2 - Dev and Stage environments share the same stack, Prod has its own 3 - Each environment has its own stack diff --git a/bootstrap/exceptions.py b/bootstrap/exceptions.py old mode 100755 new mode 100644 diff --git a/bootstrap/helpers.py b/bootstrap/helpers.py old mode 100755 new mode 100644 index 2437559..18bd5a3 --- a/bootstrap/helpers.py +++ b/bootstrap/helpers.py @@ -1,15 +1,24 @@ """Web project initialization helpers.""" +import re +from functools import partial + +import click +import validators from slugify import slugify +error = partial(click.style, fg="red") + +warning = partial(click.style, fg="yellow") + def format_gitlab_variable(value, masked=False, protected=True): - """Format the given value to be used as a Terraform variable.""" + """Format the given value to be used as a GitLab variable.""" return ( f'{{ value = "{value}"' + (masked and ", masked = true" or "") + (not protected and ", protected = false" or "") - + "}" + + " }" ) @@ -28,3 +37,63 @@ def format_tfvar(value, value_type=None): def slugify_option(ctx, param, value): """Slugify an option value.""" return value and slugify(value) + + +def validate_or_prompt_domain(message, value=None, default=None, required=True): + """Validate the given domain or prompt until a valid value is provided.""" + if value is None: + value = click.prompt(message, default=default) + if not required and value == "" or validators.domain(value): + return value + click.echo(error("Please type a valid domain!")) + return validate_or_prompt_domain(message, None, default, required) + + +def validate_or_prompt_email(message, value=None, default=None, required=True): + """Validate the given email address or prompt until a valid value is provided.""" + if value is None: + value = click.prompt(message, default=default) + if not required and value == "" or validators.email(value): + return value + click.echo(error("Please type a valid email!")) + return validate_or_prompt_email(message, None, default, required) + + +def validate_or_prompt_secret(message, value=None, default=None, required=True): + """Validate the given secret or prompt until a valid value is provided.""" + if value is None: + value = click.prompt(message, default=default, hide_input=True) + if not required and value == "" or validators.length(value, min=8): + return value + click.echo(error("Please type at least 8 chars!")) + return validate_or_prompt_secret(message, None, default, required) + + +def validate_or_prompt_path(message, value=None, default=None, required=True): + """Validate the given path or prompt until a valid path is provided.""" + if value is None: + value = click.prompt(message, default=default) + if ( + not required + and value == "" + or re.match(r"^(?:/?[\w_\-]+)(?:\/[\w_\-]+)*\/?$", value) + ): + return value + click.echo( + error( + "Please type a valid slash-separated path containing letters, digits, " + "dashes and underscores!" + ) + ) + return validate_or_prompt_path(message, None, default, required) + + +def validate_or_prompt_url(message, value=None, default=None, required=True): + """Validate the given URL or prompt until a valid value is provided.""" + if value is None: + value = click.prompt(message, default=default) + if not required and value == "" or validators.url(value): + return value.rstrip("/") + + click.echo(error("Please type a valid URL!")) + return validate_or_prompt_url(message, None, default, required) diff --git a/bootstrap/runner.py b/bootstrap/runner.py old mode 100755 new mode 100644 index 3b8d7b3..6a757cc --- a/bootstrap/runner.py +++ b/bootstrap/runner.py @@ -55,7 +55,7 @@ class Runner: internal_backend_url: str | None internal_service_port: int deployment_type: str - environment_distribution: str + environments_distribution: str project_url_dev: str = "" project_url_stage: str = "" project_url_prod: str = "" @@ -72,8 +72,8 @@ class Runner: sentry_url: str | None = None use_redis: bool = False gitlab_url: str | None = None - gitlab_group_path: str | None = None - gitlab_private_token: str | None = None + gitlab_namespace_path: str | None = None + gitlab_token: str | None = None uid: int | None = None gid: int | None = None terraform_dir: Path | None = None @@ -99,7 +99,7 @@ def __post_init__(self): def set_stacks(self): """Set the stacks.""" - self.stacks = STACKS_CHOICES[self.environment_distribution] + self.stacks = STACKS_CHOICES[self.environments_distribution] def set_envs(self): """Set the envs.""" @@ -109,7 +109,7 @@ def set_envs(self): "name": DEV_ENV_NAME, "slug": DEV_ENV_SLUG, "stack_slug": DEV_ENV_STACK_CHOICES.get( - self.environment_distribution, DEV_STACK_SLUG + self.environments_distribution, DEV_STACK_SLUG ), "url": self.project_url_dev, }, @@ -118,7 +118,7 @@ def set_envs(self): "name": STAGE_ENV_NAME, "slug": STAGE_ENV_SLUG, "stack_slug": STAGE_ENV_STACK_CHOICES.get( - self.environment_distribution, STAGE_STACK_SLUG + self.environments_distribution, STAGE_STACK_SLUG ), "url": self.project_url_stage, }, @@ -127,7 +127,7 @@ def set_envs(self): "name": PROD_ENV_NAME, "slug": PROD_ENV_SLUG, "stack_slug": PROD_ENV_STACK_CHOICES.get( - self.environment_distribution, MAIN_STACK_SLUG + self.environments_distribution, MAIN_STACK_SLUG ), "url": self.project_url_prod, }, @@ -281,10 +281,10 @@ def init_gitlab(self): """Initialize the GitLab repository and associated resources.""" click.echo(info("...creating the GitLab repository and associated resources")) env = { - "TF_VAR_gitlab_token": self.gitlab_private_token, + "TF_VAR_gitlab_token": self.gitlab_token, "TF_VAR_gitlab_url": self.gitlab_url, - "TF_VAR_group_path": self.gitlab_group_path, "TF_VAR_group_variables": self.render_gitlab_variables_to_string("group"), + "TF_VAR_namespace_path": self.gitlab_namespace_path, "TF_VAR_project_name": self.project_name, "TF_VAR_project_slug": self.project_slug, "TF_VAR_project_variables": self.render_gitlab_variables_to_string( @@ -470,7 +470,7 @@ def run(self): self.create_env_file() if self.terraform_backend == TERRAFORM_BACKEND_TFC: self.init_terraform_cloud() - if self.gitlab_group_path: + if self.gitlab_namespace_path: self.init_gitlab() if self.vault_url: self.init_vault() diff --git a/cookiecutter.json b/cookiecutter.json index 9f906a5..d5d2027 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -1,48 +1,60 @@ { - "project_name": null, - "project_slug": "{{ cookiecutter.project_name | slugify() }}", - "service_slug": "frontend", - "project_description": "", - "project_dirname": "frontend", - "internal_service_port": "3000", - "deployment_type": [ - "digitalocean-k8s", - "other-k8s" + "project_name": null, + "project_slug": "{{ cookiecutter.project_name | slugify() }}", + "service_slug": "frontend", + "project_description": "", + "project_dirname": "frontend", + "internal_service_port": "3000", + "deployment_type": ["digitalocean-k8s", "other-k8s"], + "terraform_backend": "gitlab", + "terraform_cloud_organization": null, + "use_redis": "false", + "use_vault": "false", + "environment_distribution": "1", + "resources": { + "stacks": [ + [ + { + "name": "main", + "slug": "main" + } + ] ], "terraform_backend": "gitlab", "terraform_cloud_organization": null, "use_redis": "false", "use_vault": "false", - "environment_distribution": "1", + "environments_distribution": "1", "resources": { - "stacks": [ - [ - { - "name": "main", - "slug": "main" - } - ] - ], - "envs": [ - { - "name": "development", - "slug": "dev", - "stack_slug": "main" - }, - { - "name": "staging", - "slug": "stage", - "stack_slug": "main" - }, - { - "name": "production", - "slug": "prod", - "stack_slug": "main" - } + "stacks": [ + [ + { + "name": "main", + "slug": "main" + } ] + ], + "envs": [ + { + "name": "development", + "slug": "dev", + "stack_slug": "main" + }, + { + "name": "staging", + "slug": "stage", + "stack_slug": "main" + }, + { + "name": "production", + "slug": "prod", + "stack_slug": "main" + } + ] }, "tfvars": {}, - "_extensions": [ - "cookiecutter.extensions.SlugifyExtension" - ] + "_extensions": ["cookiecutter.extensions.SlugifyExtension"] + }, + "tfvars": {}, + "_extensions": ["cookiecutter.extensions.SlugifyExtension"] } diff --git a/pyproject.toml b/pyproject.toml index 3301cf5..d922bfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,22 @@ [tool.black] target-version = ["py311"] +[tool.coverage.html] +title = "Talos - Coverage" +show_contexts = true + +[tool.coverage.report] +show_missing = true + +[tool.coverage.run] +branch = true +dynamic_context = "test_function" +omit = [ + ".venv/*", + "venv/*", +] +source = ["."] + [tool.mypy] python_version = "3.11" ignore_missing_imports = true diff --git a/requirements/common.txt b/requirements/common.txt index c63ae26..f6a2068 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -1,74 +1,354 @@ -arrow==1.2.3 +arrow==1.2.3 \ + --hash=sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1 \ + --hash=sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2 # via jinja2-time -binaryornot==0.4.4 +binaryornot==0.4.4 \ + --hash=sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061 \ + --hash=sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4 # via cookiecutter -black==22.12.0 +black==22.12.0 \ + --hash=sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320 \ + --hash=sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351 \ + --hash=sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350 \ + --hash=sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f \ + --hash=sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf \ + --hash=sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148 \ + --hash=sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4 \ + --hash=sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d \ + --hash=sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc \ + --hash=sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d \ + --hash=sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2 \ + --hash=sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f # via -r requirements/common.in -build==0.9.0 +build==0.10.0 \ + --hash=sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171 \ + --hash=sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269 # via pip-tools -certifi==2022.12.7 +certifi==2022.12.7 \ + --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \ + --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18 # via requests -chardet==5.1.0 +chardet==5.1.0 \ + --hash=sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5 \ + --hash=sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9 # via binaryornot -charset-normalizer==2.1.1 +charset-normalizer==3.0.1 \ + --hash=sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b \ + --hash=sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42 \ + --hash=sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d \ + --hash=sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b \ + --hash=sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a \ + --hash=sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59 \ + --hash=sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154 \ + --hash=sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1 \ + --hash=sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c \ + --hash=sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a \ + --hash=sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d \ + --hash=sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6 \ + --hash=sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b \ + --hash=sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b \ + --hash=sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783 \ + --hash=sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5 \ + --hash=sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918 \ + --hash=sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555 \ + --hash=sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639 \ + --hash=sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786 \ + --hash=sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e \ + --hash=sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed \ + --hash=sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820 \ + --hash=sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8 \ + --hash=sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3 \ + --hash=sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541 \ + --hash=sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14 \ + --hash=sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be \ + --hash=sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e \ + --hash=sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76 \ + --hash=sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b \ + --hash=sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c \ + --hash=sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b \ + --hash=sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3 \ + --hash=sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc \ + --hash=sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6 \ + --hash=sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59 \ + --hash=sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4 \ + --hash=sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d \ + --hash=sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d \ + --hash=sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3 \ + --hash=sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a \ + --hash=sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea \ + --hash=sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6 \ + --hash=sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e \ + --hash=sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603 \ + --hash=sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24 \ + --hash=sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a \ + --hash=sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58 \ + --hash=sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678 \ + --hash=sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a \ + --hash=sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c \ + --hash=sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6 \ + --hash=sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18 \ + --hash=sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174 \ + --hash=sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317 \ + --hash=sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f \ + --hash=sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc \ + --hash=sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837 \ + --hash=sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41 \ + --hash=sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c \ + --hash=sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579 \ + --hash=sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753 \ + --hash=sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8 \ + --hash=sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291 \ + --hash=sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087 \ + --hash=sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866 \ + --hash=sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3 \ + --hash=sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d \ + --hash=sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1 \ + --hash=sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca \ + --hash=sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e \ + --hash=sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db \ + --hash=sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72 \ + --hash=sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d \ + --hash=sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc \ + --hash=sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539 \ + --hash=sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d \ + --hash=sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af \ + --hash=sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b \ + --hash=sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602 \ + --hash=sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f \ + --hash=sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478 \ + --hash=sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c \ + --hash=sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e \ + --hash=sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479 \ + --hash=sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7 \ + --hash=sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8 # via requests -click==8.1.3 +click==8.1.3 \ + --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ + --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 # via # -r requirements/common.in # black # cookiecutter # pip-tools -cookiecutter==2.1.1 +cookiecutter==2.1.1 \ + --hash=sha256:9f3ab027cec4f70916e28f03470bdb41e637a3ad354b4d65c765d93aad160022 \ + --hash=sha256:f3982be8d9c53dac1261864013fdec7f83afd2e42ede6f6dd069c5e149c540d5 # via -r requirements/common.in -decorator==5.1.1 +decorator==5.1.1 \ + --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ + --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 # via validators -idna==3.4 +idna==3.4 \ + --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ + --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 # via requests -jinja2==3.1.2 +jinja2==3.1.2 \ + --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ + --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 # via # cookiecutter # jinja2-time -jinja2-time==0.2.0 +jinja2-time==0.2.0 \ + --hash=sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40 \ + --hash=sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa # via cookiecutter -markupsafe==2.1.1 +markupsafe==2.1.2 \ + --hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \ + --hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \ + --hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2 \ + --hash=sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460 \ + --hash=sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7 \ + --hash=sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0 \ + --hash=sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1 \ + --hash=sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa \ + --hash=sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03 \ + --hash=sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323 \ + --hash=sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65 \ + --hash=sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013 \ + --hash=sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036 \ + --hash=sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f \ + --hash=sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4 \ + --hash=sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419 \ + --hash=sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2 \ + --hash=sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619 \ + --hash=sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a \ + --hash=sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a \ + --hash=sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd \ + --hash=sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7 \ + --hash=sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666 \ + --hash=sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65 \ + --hash=sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859 \ + --hash=sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625 \ + --hash=sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff \ + --hash=sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156 \ + --hash=sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd \ + --hash=sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba \ + --hash=sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f \ + --hash=sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1 \ + --hash=sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094 \ + --hash=sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a \ + --hash=sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513 \ + --hash=sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed \ + --hash=sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d \ + --hash=sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3 \ + --hash=sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147 \ + --hash=sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c \ + --hash=sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603 \ + --hash=sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601 \ + --hash=sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a \ + --hash=sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1 \ + --hash=sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d \ + --hash=sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3 \ + --hash=sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54 \ + --hash=sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2 \ + --hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6 \ + --hash=sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58 # via jinja2 -mypy-extensions==0.4.3 +mypy-extensions==0.4.3 \ + --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ + --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 # via black -packaging==23.0 +packaging==23.0 \ + --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \ + --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97 # via build -pathspec==0.10.3 +pathspec==0.10.3 \ + --hash=sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6 \ + --hash=sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6 # via black -pep517==0.13.0 - # via build -pip-tools==6.12.1 +pip-tools==6.12.1 \ + --hash=sha256:88efb7b29a923ffeac0713e6f23ef8529cc6175527d42b93f73756cc94387293 \ + --hash=sha256:f0c0c0ec57b58250afce458e2e6058b1f30a4263db895b7d72fd6311bf1dc6f7 # via -r requirements/common.in -platformdirs==2.6.2 +platformdirs==2.6.2 \ + --hash=sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490 \ + --hash=sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2 # via black -pydantic==1.10.4 +pydantic==1.10.4 \ + --hash=sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72 \ + --hash=sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423 \ + --hash=sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f \ + --hash=sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c \ + --hash=sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06 \ + --hash=sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53 \ + --hash=sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774 \ + --hash=sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6 \ + --hash=sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c \ + --hash=sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f \ + --hash=sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6 \ + --hash=sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3 \ + --hash=sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817 \ + --hash=sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903 \ + --hash=sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a \ + --hash=sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e \ + --hash=sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d \ + --hash=sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85 \ + --hash=sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00 \ + --hash=sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28 \ + --hash=sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3 \ + --hash=sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024 \ + --hash=sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4 \ + --hash=sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e \ + --hash=sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d \ + --hash=sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa \ + --hash=sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854 \ + --hash=sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15 \ + --hash=sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648 \ + --hash=sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8 \ + --hash=sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c \ + --hash=sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857 \ + --hash=sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f \ + --hash=sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416 \ + --hash=sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978 \ + --hash=sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d # via -r requirements/common.in -python-dateutil==2.8.2 +pyproject-hooks==1.0.0 \ + --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \ + --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5 + # via build +python-dateutil==2.8.2 \ + --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ + --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 # via arrow -python-slugify==7.0.0 +python-slugify==7.0.0 \ + --hash=sha256:003aee64f9fd955d111549f96c4b58a3f40b9319383c70fad6277a4974bbf570 \ + --hash=sha256:7a0f21a39fa6c1c4bf2e5984c9b9ae944483fd10b54804cb0e23a3ccd4954f0b # via cookiecutter -pyyaml==6.0 +pyyaml==6.0 \ + --hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \ + --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \ + --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \ + --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \ + --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \ + --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \ + --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \ + --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \ + --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \ + --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \ + --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \ + --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \ + --hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \ + --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \ + --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \ + --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \ + --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \ + --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \ + --hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \ + --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \ + --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \ + --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \ + --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \ + --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \ + --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \ + --hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \ + --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \ + --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \ + --hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \ + --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \ + --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \ + --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \ + --hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \ + --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \ + --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \ + --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \ + --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \ + --hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \ + --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \ + --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5 # via cookiecutter -requests==2.28.1 +requests==2.28.2 \ + --hash=sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa \ + --hash=sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf # via cookiecutter -six==1.16.0 +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 # via python-dateutil -text-unidecode==1.3 +text-unidecode==1.3 \ + --hash=sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8 \ + --hash=sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93 # via python-slugify -types-python-slugify==7.0.0.1 +types-python-slugify==7.0.0.1 \ + --hash=sha256:3644bb44a25847ba5c8f9d15757f1d4b76064857c30e93517723618bd1152a19 \ + --hash=sha256:6b2d16e1465896df0e3685bd38eab7b8f945a3a7c18c4a2c2ef391b0e48dfce8 # via -r requirements/common.in -typing-extensions==4.4.0 +typing-extensions==4.4.0 \ + --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ + --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e # via pydantic -urllib3==1.26.13 +urllib3==1.26.14 \ + --hash=sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72 \ + --hash=sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1 # via requests -validators==0.20.0 +validators==0.20.0 \ + --hash=sha256:24148ce4e64100a2d5e267233e23e7afeb55316b47d30faae7eb6e7292bc226a # via -r requirements/common.in -wheel==0.38.4 +wheel==0.38.4 \ + --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \ + --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8 # via pip-tools -# The following packages are considered to be unsafe in a requirements file: +# WARNING: The following packages were not pinned, but pip requires them to be +# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. # pip # setuptools diff --git a/requirements/local.txt b/requirements/local.txt index cd1c45e..44b4728 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -1,140 +1,563 @@ -arrow==1.2.3 +arrow==1.2.3 \ + --hash=sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1 \ + --hash=sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2 # via jinja2-time -asttokens==2.2.1 +asttokens==2.2.1 \ + --hash=sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3 \ + --hash=sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c # via stack-data -backcall==0.2.0 +backcall==0.2.0 \ + --hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e \ + --hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255 # via ipython -binaryornot==0.4.4 +binaryornot==0.4.4 \ + --hash=sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061 \ + --hash=sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4 # via cookiecutter -black==22.12.0 +black==22.12.0 \ + --hash=sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320 \ + --hash=sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351 \ + --hash=sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350 \ + --hash=sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f \ + --hash=sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf \ + --hash=sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148 \ + --hash=sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4 \ + --hash=sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d \ + --hash=sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc \ + --hash=sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d \ + --hash=sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2 \ + --hash=sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f # via -r requirements/common.in -build==0.9.0 +build==0.10.0 \ + --hash=sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171 \ + --hash=sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269 # via pip-tools -certifi==2022.12.7 +certifi==2022.12.7 \ + --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \ + --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18 # via requests -cfgv==3.3.1 +cfgv==3.3.1 \ + --hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \ + --hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736 # via pre-commit -chardet==5.1.0 +chardet==5.1.0 \ + --hash=sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5 \ + --hash=sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9 # via binaryornot -charset-normalizer==2.1.1 +charset-normalizer==3.0.1 \ + --hash=sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b \ + --hash=sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42 \ + --hash=sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d \ + --hash=sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b \ + --hash=sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a \ + --hash=sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59 \ + --hash=sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154 \ + --hash=sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1 \ + --hash=sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c \ + --hash=sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a \ + --hash=sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d \ + --hash=sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6 \ + --hash=sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b \ + --hash=sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b \ + --hash=sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783 \ + --hash=sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5 \ + --hash=sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918 \ + --hash=sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555 \ + --hash=sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639 \ + --hash=sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786 \ + --hash=sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e \ + --hash=sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed \ + --hash=sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820 \ + --hash=sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8 \ + --hash=sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3 \ + --hash=sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541 \ + --hash=sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14 \ + --hash=sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be \ + --hash=sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e \ + --hash=sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76 \ + --hash=sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b \ + --hash=sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c \ + --hash=sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b \ + --hash=sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3 \ + --hash=sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc \ + --hash=sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6 \ + --hash=sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59 \ + --hash=sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4 \ + --hash=sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d \ + --hash=sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d \ + --hash=sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3 \ + --hash=sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a \ + --hash=sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea \ + --hash=sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6 \ + --hash=sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e \ + --hash=sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603 \ + --hash=sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24 \ + --hash=sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a \ + --hash=sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58 \ + --hash=sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678 \ + --hash=sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a \ + --hash=sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c \ + --hash=sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6 \ + --hash=sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18 \ + --hash=sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174 \ + --hash=sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317 \ + --hash=sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f \ + --hash=sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc \ + --hash=sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837 \ + --hash=sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41 \ + --hash=sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c \ + --hash=sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579 \ + --hash=sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753 \ + --hash=sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8 \ + --hash=sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291 \ + --hash=sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087 \ + --hash=sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866 \ + --hash=sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3 \ + --hash=sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d \ + --hash=sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1 \ + --hash=sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca \ + --hash=sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e \ + --hash=sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db \ + --hash=sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72 \ + --hash=sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d \ + --hash=sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc \ + --hash=sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539 \ + --hash=sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d \ + --hash=sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af \ + --hash=sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b \ + --hash=sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602 \ + --hash=sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f \ + --hash=sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478 \ + --hash=sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c \ + --hash=sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e \ + --hash=sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479 \ + --hash=sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7 \ + --hash=sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8 # via requests -click==8.1.3 +click==8.1.3 \ + --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ + --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 # via # -r requirements/common.in # black # cookiecutter # pip-tools -cookiecutter==2.1.1 +cookiecutter==2.1.1 \ + --hash=sha256:9f3ab027cec4f70916e28f03470bdb41e637a3ad354b4d65c765d93aad160022 \ + --hash=sha256:f3982be8d9c53dac1261864013fdec7f83afd2e42ede6f6dd069c5e149c540d5 # via -r requirements/common.in -coverage[toml]==7.0.5 +coverage[toml]==7.0.5 \ + --hash=sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45 \ + --hash=sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809 \ + --hash=sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4 \ + --hash=sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b \ + --hash=sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7 \ + --hash=sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0 \ + --hash=sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0 \ + --hash=sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea \ + --hash=sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2 \ + --hash=sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a \ + --hash=sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45 \ + --hash=sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b \ + --hash=sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209 \ + --hash=sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca \ + --hash=sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab \ + --hash=sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095 \ + --hash=sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7 \ + --hash=sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6 \ + --hash=sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af \ + --hash=sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499 \ + --hash=sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831 \ + --hash=sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637 \ + --hash=sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2 \ + --hash=sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb \ + --hash=sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029 \ + --hash=sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc \ + --hash=sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8 \ + --hash=sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f \ + --hash=sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2 \ + --hash=sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d \ + --hash=sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289 \ + --hash=sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c \ + --hash=sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded \ + --hash=sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96 \ + --hash=sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0 \ + --hash=sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904 \ + --hash=sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21 \ + --hash=sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89 \ + --hash=sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78 \ + --hash=sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad \ + --hash=sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196 \ + --hash=sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd \ + --hash=sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0 \ + --hash=sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882 \ + --hash=sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757 \ + --hash=sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16 \ + --hash=sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0 \ + --hash=sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47 \ + --hash=sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40 \ + --hash=sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1 \ + --hash=sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3 # via -r requirements/test.in -decorator==5.1.1 +decorator==5.1.1 \ + --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ + --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 # via # ipython # validators -distlib==0.3.6 +distlib==0.3.6 \ + --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \ + --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e # via virtualenv -executing==1.2.0 +executing==1.2.0 \ + --hash=sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc \ + --hash=sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107 # via stack-data -filelock==3.9.0 +filelock==3.9.0 \ + --hash=sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de \ + --hash=sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d # via virtualenv -identify==2.5.12 +identify==2.5.13 \ + --hash=sha256:8aa48ce56e38c28b6faa9f261075dea0a942dfbb42b341b4e711896cbb40f3f7 \ + --hash=sha256:abb546bca6f470228785338a01b539de8a85bbf46491250ae03363956d8ebb10 # via pre-commit -idna==3.4 +idna==3.4 \ + --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ + --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 # via requests -ipython==8.8.0 +ipython==8.8.0 \ + --hash=sha256:da01e6df1501e6e7c32b5084212ddadd4ee2471602e2cf3e0190f4de6b0ea481 \ + --hash=sha256:f3bf2c08505ad2c3f4ed5c46ae0331a8547d36bf4b21a451e8ae80c0791db95b # via -r requirements/local.in -jedi==0.18.2 +jedi==0.18.2 \ + --hash=sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e \ + --hash=sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612 # via ipython -jinja2==3.1.2 +jinja2==3.1.2 \ + --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ + --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 # via # cookiecutter # jinja2-time -jinja2-time==0.2.0 +jinja2-time==0.2.0 \ + --hash=sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40 \ + --hash=sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa # via cookiecutter -markupsafe==2.1.1 +markupsafe==2.1.2 \ + --hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \ + --hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \ + --hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2 \ + --hash=sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460 \ + --hash=sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7 \ + --hash=sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0 \ + --hash=sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1 \ + --hash=sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa \ + --hash=sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03 \ + --hash=sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323 \ + --hash=sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65 \ + --hash=sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013 \ + --hash=sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036 \ + --hash=sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f \ + --hash=sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4 \ + --hash=sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419 \ + --hash=sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2 \ + --hash=sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619 \ + --hash=sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a \ + --hash=sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a \ + --hash=sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd \ + --hash=sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7 \ + --hash=sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666 \ + --hash=sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65 \ + --hash=sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859 \ + --hash=sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625 \ + --hash=sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff \ + --hash=sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156 \ + --hash=sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd \ + --hash=sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba \ + --hash=sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f \ + --hash=sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1 \ + --hash=sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094 \ + --hash=sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a \ + --hash=sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513 \ + --hash=sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed \ + --hash=sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d \ + --hash=sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3 \ + --hash=sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147 \ + --hash=sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c \ + --hash=sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603 \ + --hash=sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601 \ + --hash=sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a \ + --hash=sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1 \ + --hash=sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d \ + --hash=sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3 \ + --hash=sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54 \ + --hash=sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2 \ + --hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6 \ + --hash=sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58 # via jinja2 -matplotlib-inline==0.1.6 +matplotlib-inline==0.1.6 \ + --hash=sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311 \ + --hash=sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304 # via ipython -mypy==0.991 +mypy==0.991 \ + --hash=sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d \ + --hash=sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6 \ + --hash=sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf \ + --hash=sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f \ + --hash=sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813 \ + --hash=sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33 \ + --hash=sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad \ + --hash=sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05 \ + --hash=sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297 \ + --hash=sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06 \ + --hash=sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd \ + --hash=sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243 \ + --hash=sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305 \ + --hash=sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476 \ + --hash=sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711 \ + --hash=sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70 \ + --hash=sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5 \ + --hash=sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461 \ + --hash=sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab \ + --hash=sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c \ + --hash=sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d \ + --hash=sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135 \ + --hash=sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93 \ + --hash=sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648 \ + --hash=sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a \ + --hash=sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb \ + --hash=sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3 \ + --hash=sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372 \ + --hash=sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb \ + --hash=sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef # via -r requirements/test.in -mypy-extensions==0.4.3 +mypy-extensions==0.4.3 \ + --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ + --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 # via # black # mypy -nodeenv==1.7.0 +nodeenv==1.7.0 \ + --hash=sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e \ + --hash=sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b # via pre-commit -packaging==23.0 +packaging==23.0 \ + --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \ + --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97 # via build -parso==0.8.3 +parso==0.8.3 \ + --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ + --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 # via jedi -pathspec==0.10.3 +pathspec==0.10.3 \ + --hash=sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6 \ + --hash=sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6 # via black -pep517==0.13.0 - # via build -pexpect==4.8.0 +pexpect==4.8.0 \ + --hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \ + --hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c # via ipython -pickleshare==0.7.5 +pickleshare==0.7.5 \ + --hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca \ + --hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56 # via ipython -pip-tools==6.12.1 +pip-tools==6.12.1 \ + --hash=sha256:88efb7b29a923ffeac0713e6f23ef8529cc6175527d42b93f73756cc94387293 \ + --hash=sha256:f0c0c0ec57b58250afce458e2e6058b1f30a4263db895b7d72fd6311bf1dc6f7 # via -r requirements/common.in -platformdirs==2.6.2 +platformdirs==2.6.2 \ + --hash=sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490 \ + --hash=sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2 # via # black # virtualenv -pre-commit==2.21.0 +pre-commit==2.21.0 \ + --hash=sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658 \ + --hash=sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad # via -r requirements/local.in -prompt-toolkit==3.0.36 +prompt-toolkit==3.0.36 \ + --hash=sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63 \ + --hash=sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305 # via ipython -ptyprocess==0.7.0 +ptyprocess==0.7.0 \ + --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \ + --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220 # via pexpect -pure-eval==0.2.2 +pure-eval==0.2.2 \ + --hash=sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350 \ + --hash=sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3 # via stack-data -pydantic==1.10.4 +pydantic==1.10.4 \ + --hash=sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72 \ + --hash=sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423 \ + --hash=sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f \ + --hash=sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c \ + --hash=sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06 \ + --hash=sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53 \ + --hash=sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774 \ + --hash=sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6 \ + --hash=sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c \ + --hash=sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f \ + --hash=sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6 \ + --hash=sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3 \ + --hash=sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817 \ + --hash=sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903 \ + --hash=sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a \ + --hash=sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e \ + --hash=sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d \ + --hash=sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85 \ + --hash=sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00 \ + --hash=sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28 \ + --hash=sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3 \ + --hash=sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024 \ + --hash=sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4 \ + --hash=sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e \ + --hash=sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d \ + --hash=sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa \ + --hash=sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854 \ + --hash=sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15 \ + --hash=sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648 \ + --hash=sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8 \ + --hash=sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c \ + --hash=sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857 \ + --hash=sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f \ + --hash=sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416 \ + --hash=sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978 \ + --hash=sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d # via -r requirements/common.in -pygments==2.14.0 +pygments==2.14.0 \ + --hash=sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297 \ + --hash=sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717 # via ipython -python-dateutil==2.8.2 +pyproject-hooks==1.0.0 \ + --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \ + --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5 + # via build +python-dateutil==2.8.2 \ + --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ + --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 # via arrow -python-slugify==7.0.0 +python-slugify==7.0.0 \ + --hash=sha256:003aee64f9fd955d111549f96c4b58a3f40b9319383c70fad6277a4974bbf570 \ + --hash=sha256:7a0f21a39fa6c1c4bf2e5984c9b9ae944483fd10b54804cb0e23a3ccd4954f0b # via cookiecutter -pyyaml==6.0 +pyyaml==6.0 \ + --hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \ + --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \ + --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \ + --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \ + --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \ + --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \ + --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \ + --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \ + --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \ + --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \ + --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \ + --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \ + --hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \ + --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \ + --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \ + --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \ + --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \ + --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \ + --hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \ + --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \ + --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \ + --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \ + --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \ + --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \ + --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \ + --hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \ + --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \ + --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \ + --hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \ + --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \ + --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \ + --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \ + --hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \ + --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \ + --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \ + --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \ + --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \ + --hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \ + --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \ + --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5 # via # cookiecutter # pre-commit -requests==2.28.1 +requests==2.28.2 \ + --hash=sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa \ + --hash=sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf # via cookiecutter -ruff==0.0.218 +ruff==0.0.225 \ + --hash=sha256:16b2a8a1171f7a1a6126ea76e6f6a4e622352ee4518156f989c6b8563bcd86b6 \ + --hash=sha256:1b6b50b0d2db8e000c24d501c112d33cced64cb0bc0c3a6ed06621c83974ad37 \ + --hash=sha256:287734a8a9f94517e1aab345a6850225f190aadfa326a6947ed5013c2f44d8b2 \ + --hash=sha256:2fee118f65100ada956ce6faeb0028635dd2ff9e2e00ce41bfe81edfd58993b8 \ + --hash=sha256:31a82d4a0bb0163d967d2fc67d1dd50f244199fbf01b9034f4bc471a9f8b7a62 \ + --hash=sha256:434d7efbfca2de88d8173922cd86dda757924e1213e9b758153f850fe9d675be \ + --hash=sha256:53cd192e856c30131b44fc2e7d2b43db12244f373660fe451529b8e197a16f12 \ + --hash=sha256:61bd0ea623e04b57f7fbba74f49e5dc2bc6ebe66cc66594b6dc142a4ff5f0c37 \ + --hash=sha256:6cfedd4f4df8b4a9352386e75714d86772d765f8eea16dc435633565e7513c33 \ + --hash=sha256:8f2d0f45bbd38aafabdc14777c74bf74dc5ea0aef10f329f52422f068d979a99 \ + --hash=sha256:a48c298b78ab84634fa21c82bea9d355fcbebc6119980af4d88230acd53ba58c \ + --hash=sha256:a51928de667795b88541a81b064d900d947ff7be9daaf70f7f63d5d144e2129c \ + --hash=sha256:bf92628dc1caf2dd747c4dc1124f744b7fe4714a26bb4f868493f92f4b4a9ebf \ + --hash=sha256:c2b67c55cb810756847ec0d01145f50470ff6f20f729b0a57216637463e58226 \ + --hash=sha256:df0b37c2a5830087f3b915c353602d2c370e4778470e4509b2332dbf7a48bd59 \ + --hash=sha256:fda14be1b8ea0a2bf4998cb12580fbd64f335f41cad1896b06a29beb6167bd61 # via -r requirements/test.in -six==1.16.0 +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 # via # asttokens # python-dateutil -stack-data==0.6.2 +stack-data==0.6.2 \ + --hash=sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815 \ + --hash=sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8 # via ipython -text-unidecode==1.3 +text-unidecode==1.3 \ + --hash=sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8 \ + --hash=sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93 # via python-slugify -traitlets==5.8.1 +traitlets==5.8.1 \ + --hash=sha256:32500888f5ff7bbf3b9267ea31748fa657aaf34d56d85e60f91dda7dc7f5785b \ + --hash=sha256:a1ca5df6414f8b5760f7c5f256e326ee21b581742114545b462b35ffe3f04861 # via # ipython # matplotlib-inline -types-python-slugify==7.0.0.1 +types-python-slugify==7.0.0.1 \ + --hash=sha256:3644bb44a25847ba5c8f9d15757f1d4b76064857c30e93517723618bd1152a19 \ + --hash=sha256:6b2d16e1465896df0e3685bd38eab7b8f945a3a7c18c4a2c2ef391b0e48dfce8 # via -r requirements/common.in -typing-extensions==4.4.0 +typing-extensions==4.4.0 \ + --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ + --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e # via # mypy # pydantic -urllib3==1.26.13 +urllib3==1.26.14 \ + --hash=sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72 \ + --hash=sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1 # via requests -validators==0.20.0 +validators==0.20.0 \ + --hash=sha256:24148ce4e64100a2d5e267233e23e7afeb55316b47d30faae7eb6e7292bc226a # via -r requirements/common.in -virtualenv==20.17.1 +virtualenv==20.17.1 \ + --hash=sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4 \ + --hash=sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058 # via pre-commit -wcwidth==0.2.5 +wcwidth==0.2.6 \ + --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \ + --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0 # via prompt-toolkit -wheel==0.38.4 +wheel==0.38.4 \ + --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \ + --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8 # via pip-tools -# The following packages are considered to be unsafe in a requirements file: +# WARNING: The following packages were not pinned, but pip requires them to be +# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. # pip # setuptools diff --git a/requirements/test.in b/requirements/test.in index 89cce9b..d08ce3d 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -1,4 +1,4 @@ -r common.in coverage[toml]~=7.0.0 -ruff~=0.0.0 mypy~=0.991 +ruff~=0.0.0 diff --git a/requirements/test.txt b/requirements/test.txt index 52a44c6..7242478 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,84 +1,461 @@ -arrow==1.2.3 +arrow==1.2.3 \ + --hash=sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1 \ + --hash=sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2 # via jinja2-time -binaryornot==0.4.4 +binaryornot==0.4.4 \ + --hash=sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061 \ + --hash=sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4 # via cookiecutter -black==22.12.0 +black==22.12.0 \ + --hash=sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320 \ + --hash=sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351 \ + --hash=sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350 \ + --hash=sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f \ + --hash=sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf \ + --hash=sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148 \ + --hash=sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4 \ + --hash=sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d \ + --hash=sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc \ + --hash=sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d \ + --hash=sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2 \ + --hash=sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f # via -r requirements/common.in -build==0.9.0 +build==0.10.0 \ + --hash=sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171 \ + --hash=sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269 # via pip-tools -certifi==2022.12.7 +certifi==2022.12.7 \ + --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \ + --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18 # via requests -chardet==5.1.0 +chardet==5.1.0 \ + --hash=sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5 \ + --hash=sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9 # via binaryornot -charset-normalizer==2.1.1 +charset-normalizer==3.0.1 \ + --hash=sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b \ + --hash=sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42 \ + --hash=sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d \ + --hash=sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b \ + --hash=sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a \ + --hash=sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59 \ + --hash=sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154 \ + --hash=sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1 \ + --hash=sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c \ + --hash=sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a \ + --hash=sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d \ + --hash=sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6 \ + --hash=sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b \ + --hash=sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b \ + --hash=sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783 \ + --hash=sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5 \ + --hash=sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918 \ + --hash=sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555 \ + --hash=sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639 \ + --hash=sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786 \ + --hash=sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e \ + --hash=sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed \ + --hash=sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820 \ + --hash=sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8 \ + --hash=sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3 \ + --hash=sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541 \ + --hash=sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14 \ + --hash=sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be \ + --hash=sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e \ + --hash=sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76 \ + --hash=sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b \ + --hash=sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c \ + --hash=sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b \ + --hash=sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3 \ + --hash=sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc \ + --hash=sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6 \ + --hash=sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59 \ + --hash=sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4 \ + --hash=sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d \ + --hash=sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d \ + --hash=sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3 \ + --hash=sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a \ + --hash=sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea \ + --hash=sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6 \ + --hash=sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e \ + --hash=sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603 \ + --hash=sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24 \ + --hash=sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a \ + --hash=sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58 \ + --hash=sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678 \ + --hash=sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a \ + --hash=sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c \ + --hash=sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6 \ + --hash=sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18 \ + --hash=sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174 \ + --hash=sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317 \ + --hash=sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f \ + --hash=sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc \ + --hash=sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837 \ + --hash=sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41 \ + --hash=sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c \ + --hash=sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579 \ + --hash=sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753 \ + --hash=sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8 \ + --hash=sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291 \ + --hash=sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087 \ + --hash=sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866 \ + --hash=sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3 \ + --hash=sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d \ + --hash=sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1 \ + --hash=sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca \ + --hash=sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e \ + --hash=sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db \ + --hash=sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72 \ + --hash=sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d \ + --hash=sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc \ + --hash=sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539 \ + --hash=sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d \ + --hash=sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af \ + --hash=sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b \ + --hash=sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602 \ + --hash=sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f \ + --hash=sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478 \ + --hash=sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c \ + --hash=sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e \ + --hash=sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479 \ + --hash=sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7 \ + --hash=sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8 # via requests -click==8.1.3 +click==8.1.3 \ + --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ + --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 # via # -r requirements/common.in # black # cookiecutter # pip-tools -cookiecutter==2.1.1 +cookiecutter==2.1.1 \ + --hash=sha256:9f3ab027cec4f70916e28f03470bdb41e637a3ad354b4d65c765d93aad160022 \ + --hash=sha256:f3982be8d9c53dac1261864013fdec7f83afd2e42ede6f6dd069c5e149c540d5 # via -r requirements/common.in -coverage[toml]==7.0.5 +coverage[toml]==7.0.5 \ + --hash=sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45 \ + --hash=sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809 \ + --hash=sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4 \ + --hash=sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b \ + --hash=sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7 \ + --hash=sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0 \ + --hash=sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0 \ + --hash=sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea \ + --hash=sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2 \ + --hash=sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a \ + --hash=sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45 \ + --hash=sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b \ + --hash=sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209 \ + --hash=sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca \ + --hash=sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab \ + --hash=sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095 \ + --hash=sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7 \ + --hash=sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6 \ + --hash=sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af \ + --hash=sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499 \ + --hash=sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831 \ + --hash=sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637 \ + --hash=sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2 \ + --hash=sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb \ + --hash=sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029 \ + --hash=sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc \ + --hash=sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8 \ + --hash=sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f \ + --hash=sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2 \ + --hash=sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d \ + --hash=sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289 \ + --hash=sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c \ + --hash=sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded \ + --hash=sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96 \ + --hash=sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0 \ + --hash=sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904 \ + --hash=sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21 \ + --hash=sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89 \ + --hash=sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78 \ + --hash=sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad \ + --hash=sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196 \ + --hash=sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd \ + --hash=sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0 \ + --hash=sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882 \ + --hash=sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757 \ + --hash=sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16 \ + --hash=sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0 \ + --hash=sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47 \ + --hash=sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40 \ + --hash=sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1 \ + --hash=sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3 # via -r requirements/test.in -decorator==5.1.1 +decorator==5.1.1 \ + --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ + --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 # via validators -idna==3.4 +idna==3.4 \ + --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ + --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 # via requests -jinja2==3.1.2 +jinja2==3.1.2 \ + --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ + --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 # via # cookiecutter # jinja2-time -jinja2-time==0.2.0 +jinja2-time==0.2.0 \ + --hash=sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40 \ + --hash=sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa # via cookiecutter -markupsafe==2.1.1 +markupsafe==2.1.2 \ + --hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \ + --hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \ + --hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2 \ + --hash=sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460 \ + --hash=sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7 \ + --hash=sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0 \ + --hash=sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1 \ + --hash=sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa \ + --hash=sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03 \ + --hash=sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323 \ + --hash=sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65 \ + --hash=sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013 \ + --hash=sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036 \ + --hash=sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f \ + --hash=sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4 \ + --hash=sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419 \ + --hash=sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2 \ + --hash=sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619 \ + --hash=sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a \ + --hash=sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a \ + --hash=sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd \ + --hash=sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7 \ + --hash=sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666 \ + --hash=sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65 \ + --hash=sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859 \ + --hash=sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625 \ + --hash=sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff \ + --hash=sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156 \ + --hash=sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd \ + --hash=sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba \ + --hash=sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f \ + --hash=sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1 \ + --hash=sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094 \ + --hash=sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a \ + --hash=sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513 \ + --hash=sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed \ + --hash=sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d \ + --hash=sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3 \ + --hash=sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147 \ + --hash=sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c \ + --hash=sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603 \ + --hash=sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601 \ + --hash=sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a \ + --hash=sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1 \ + --hash=sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d \ + --hash=sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3 \ + --hash=sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54 \ + --hash=sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2 \ + --hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6 \ + --hash=sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58 # via jinja2 -mypy==0.991 +mypy==0.991 \ + --hash=sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d \ + --hash=sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6 \ + --hash=sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf \ + --hash=sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f \ + --hash=sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813 \ + --hash=sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33 \ + --hash=sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad \ + --hash=sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05 \ + --hash=sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297 \ + --hash=sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06 \ + --hash=sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd \ + --hash=sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243 \ + --hash=sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305 \ + --hash=sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476 \ + --hash=sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711 \ + --hash=sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70 \ + --hash=sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5 \ + --hash=sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461 \ + --hash=sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab \ + --hash=sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c \ + --hash=sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d \ + --hash=sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135 \ + --hash=sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93 \ + --hash=sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648 \ + --hash=sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a \ + --hash=sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb \ + --hash=sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3 \ + --hash=sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372 \ + --hash=sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb \ + --hash=sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef # via -r requirements/test.in -mypy-extensions==0.4.3 +mypy-extensions==0.4.3 \ + --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ + --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 # via # black # mypy -packaging==23.0 +packaging==23.0 \ + --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \ + --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97 # via build -pathspec==0.10.3 +pathspec==0.10.3 \ + --hash=sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6 \ + --hash=sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6 # via black -pep517==0.13.0 - # via build -pip-tools==6.12.1 +pip-tools==6.12.1 \ + --hash=sha256:88efb7b29a923ffeac0713e6f23ef8529cc6175527d42b93f73756cc94387293 \ + --hash=sha256:f0c0c0ec57b58250afce458e2e6058b1f30a4263db895b7d72fd6311bf1dc6f7 # via -r requirements/common.in -platformdirs==2.6.2 +platformdirs==2.6.2 \ + --hash=sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490 \ + --hash=sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2 # via black -pydantic==1.10.4 +pydantic==1.10.4 \ + --hash=sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72 \ + --hash=sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423 \ + --hash=sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f \ + --hash=sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c \ + --hash=sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06 \ + --hash=sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53 \ + --hash=sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774 \ + --hash=sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6 \ + --hash=sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c \ + --hash=sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f \ + --hash=sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6 \ + --hash=sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3 \ + --hash=sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817 \ + --hash=sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903 \ + --hash=sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a \ + --hash=sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e \ + --hash=sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d \ + --hash=sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85 \ + --hash=sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00 \ + --hash=sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28 \ + --hash=sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3 \ + --hash=sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024 \ + --hash=sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4 \ + --hash=sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e \ + --hash=sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d \ + --hash=sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa \ + --hash=sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854 \ + --hash=sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15 \ + --hash=sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648 \ + --hash=sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8 \ + --hash=sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c \ + --hash=sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857 \ + --hash=sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f \ + --hash=sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416 \ + --hash=sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978 \ + --hash=sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d # via -r requirements/common.in -python-dateutil==2.8.2 +pyproject-hooks==1.0.0 \ + --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \ + --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5 + # via build +python-dateutil==2.8.2 \ + --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ + --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 # via arrow -python-slugify==7.0.0 +python-slugify==7.0.0 \ + --hash=sha256:003aee64f9fd955d111549f96c4b58a3f40b9319383c70fad6277a4974bbf570 \ + --hash=sha256:7a0f21a39fa6c1c4bf2e5984c9b9ae944483fd10b54804cb0e23a3ccd4954f0b # via cookiecutter -pyyaml==6.0 +pyyaml==6.0 \ + --hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \ + --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \ + --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \ + --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \ + --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \ + --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \ + --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \ + --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \ + --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \ + --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \ + --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \ + --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \ + --hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \ + --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \ + --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \ + --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \ + --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \ + --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \ + --hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \ + --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \ + --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \ + --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \ + --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \ + --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \ + --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \ + --hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \ + --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \ + --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \ + --hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \ + --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \ + --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \ + --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \ + --hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \ + --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \ + --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \ + --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \ + --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \ + --hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \ + --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \ + --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5 # via cookiecutter -requests==2.28.1 +requests==2.28.2 \ + --hash=sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa \ + --hash=sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf # via cookiecutter -ruff==0.0.218 +ruff==0.0.225 \ + --hash=sha256:16b2a8a1171f7a1a6126ea76e6f6a4e622352ee4518156f989c6b8563bcd86b6 \ + --hash=sha256:1b6b50b0d2db8e000c24d501c112d33cced64cb0bc0c3a6ed06621c83974ad37 \ + --hash=sha256:287734a8a9f94517e1aab345a6850225f190aadfa326a6947ed5013c2f44d8b2 \ + --hash=sha256:2fee118f65100ada956ce6faeb0028635dd2ff9e2e00ce41bfe81edfd58993b8 \ + --hash=sha256:31a82d4a0bb0163d967d2fc67d1dd50f244199fbf01b9034f4bc471a9f8b7a62 \ + --hash=sha256:434d7efbfca2de88d8173922cd86dda757924e1213e9b758153f850fe9d675be \ + --hash=sha256:53cd192e856c30131b44fc2e7d2b43db12244f373660fe451529b8e197a16f12 \ + --hash=sha256:61bd0ea623e04b57f7fbba74f49e5dc2bc6ebe66cc66594b6dc142a4ff5f0c37 \ + --hash=sha256:6cfedd4f4df8b4a9352386e75714d86772d765f8eea16dc435633565e7513c33 \ + --hash=sha256:8f2d0f45bbd38aafabdc14777c74bf74dc5ea0aef10f329f52422f068d979a99 \ + --hash=sha256:a48c298b78ab84634fa21c82bea9d355fcbebc6119980af4d88230acd53ba58c \ + --hash=sha256:a51928de667795b88541a81b064d900d947ff7be9daaf70f7f63d5d144e2129c \ + --hash=sha256:bf92628dc1caf2dd747c4dc1124f744b7fe4714a26bb4f868493f92f4b4a9ebf \ + --hash=sha256:c2b67c55cb810756847ec0d01145f50470ff6f20f729b0a57216637463e58226 \ + --hash=sha256:df0b37c2a5830087f3b915c353602d2c370e4778470e4509b2332dbf7a48bd59 \ + --hash=sha256:fda14be1b8ea0a2bf4998cb12580fbd64f335f41cad1896b06a29beb6167bd61 # via -r requirements/test.in -six==1.16.0 +six==1.16.0 \ + --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ + --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 # via python-dateutil -text-unidecode==1.3 +text-unidecode==1.3 \ + --hash=sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8 \ + --hash=sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93 # via python-slugify -types-python-slugify==7.0.0.1 +types-python-slugify==7.0.0.1 \ + --hash=sha256:3644bb44a25847ba5c8f9d15757f1d4b76064857c30e93517723618bd1152a19 \ + --hash=sha256:6b2d16e1465896df0e3685bd38eab7b8f945a3a7c18c4a2c2ef391b0e48dfce8 # via -r requirements/common.in -typing-extensions==4.4.0 +typing-extensions==4.4.0 \ + --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ + --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e # via # mypy # pydantic -urllib3==1.26.13 +urllib3==1.26.14 \ + --hash=sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72 \ + --hash=sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1 # via requests -validators==0.20.0 +validators==0.20.0 \ + --hash=sha256:24148ce4e64100a2d5e267233e23e7afeb55316b47d30faae7eb6e7292bc226a # via -r requirements/common.in -wheel==0.38.4 +wheel==0.38.4 \ + --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \ + --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8 # via pip-tools -# The following packages are considered to be unsafe in a requirements file: +# WARNING: The following packages were not pinned, but pip requires them to be +# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. # pip # setuptools diff --git a/start.py b/start.py index 6c67431..4460b3b 100755 --- a/start.py +++ b/start.py @@ -1,23 +1,19 @@ #!/usr/bin/env python """Initialize a web project Next.js service based on a template.""" -import os from pathlib import Path import click -from bootstrap.collector import collect +from bootstrap.collector import Collector from bootstrap.constants import ( DEPLOYMENT_TYPE_CHOICES, - ENVIRONMENT_DISTRIBUTION_CHOICES, + ENVIRONMENTS_DISTRIBUTION_CHOICES, GITLAB_TOKEN_ENV_VAR, VAULT_TOKEN_ENV_VAR, ) from bootstrap.exceptions import BootstrapError from bootstrap.helpers import slugify_option -from bootstrap.runner import Runner - -OUTPUT_DIR = os.getenv("OUTPUT_BASE_DIR") or "." @click.command() @@ -25,7 +21,8 @@ @click.option("--gid", type=int) @click.option( "--output-dir", - default=OUTPUT_DIR, + default=".", + envvar="OUTPUT_BASE_DIR", type=click.Path( exists=True, path_type=Path, file_okay=False, readable=True, writable=True ), @@ -53,7 +50,7 @@ @click.option("--vault-token", envvar=VAULT_TOKEN_ENV_VAR) @click.option("--vault-url") @click.option( - "--environment-distribution", type=click.Choice(ENVIRONMENT_DISTRIBUTION_CHOICES) + "--environments-distribution", type=click.Choice(ENVIRONMENTS_DISTRIBUTION_CHOICES) ) @click.option("--project-url-dev") @click.option("--project-url-stage") @@ -63,15 +60,17 @@ @click.option("--sentry-url") @click.option("--use-redis/--no-redis", is_flag=True, default=None) @click.option("--gitlab-url") -@click.option("--gitlab-private-token", envvar=GITLAB_TOKEN_ENV_VAR) -@click.option("--gitlab-group-path") +@click.option("--gitlab-token", envvar=GITLAB_TOKEN_ENV_VAR) +@click.option("--gitlab-namespace-path") @click.option("--terraform-dir") @click.option("--logs-dir") @click.option("--quiet", is_flag=True) def main(**options): """Run the setup.""" try: - Runner(**collect(**options)).run() + collector = Collector(**options) + collector.collect() + collector.launch_runner() except BootstrapError as e: raise click.Abort() from e diff --git a/terraform/gitlab/main.tf b/terraform/gitlab/main.tf index 284d015..219f078 100644 --- a/terraform/gitlab/main.tf +++ b/terraform/gitlab/main.tf @@ -40,7 +40,7 @@ data "http" "user_info" { /* Group */ data "gitlab_group" "main" { - full_path = var.group_path + full_path = var.namespace_path } data "gitlab_group" "main_parent" { diff --git a/terraform/gitlab/variables.tf b/terraform/gitlab/variables.tf index 400f9e6..1dee5ea 100644 --- a/terraform/gitlab/variables.tf +++ b/terraform/gitlab/variables.tf @@ -9,17 +9,17 @@ variable "gitlab_url" { type = string } -variable "group_path" { - description = "The GitLab group full path." - type = string -} - variable "group_variables" { description = "A map of GitLab group variables to create." type = map(map(any)) default = {} } +variable "namespace_path" { + description = "The GitLab namespace path." + type = string +} + variable "project_name" { description = "The project name." type = string diff --git a/tests/test_collector.py b/tests/test_collector.py index b106884..d2f315d 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -1,35 +1,12 @@ -"""Project bootstrap tests.""" +"""Bootstrap collector tests.""" -from contextlib import contextmanager -from io import StringIO +import os from pathlib import Path +from shutil import rmtree from unittest import TestCase, mock -from bootstrap.collector import ( - clean_deployment_type, - clean_environment_distribution, - clean_gitlab_data, - clean_project_dirname, - clean_project_slug, - clean_sentry_dsn, - clean_sentry_org, - clean_service_dir, - clean_service_slug, - clean_terraform_backend, - clean_use_redis, - clean_vault_data, -) - - -@contextmanager -def input(*cmds): - """Mock the input.""" - visible_cmds = "\n".join([c for c in cmds if isinstance(c, str)]) - hidden_cmds = [c.get("hidden") for c in cmds if isinstance(c, dict)] - with mock.patch("sys.stdin", StringIO(f"{visible_cmds}\n")), mock.patch( - "getpass.getpass", side_effect=hidden_cmds - ): - yield +from bootstrap.collector import Collector +from tests.utils import mock_input class TestBootstrapCollector(TestCase): @@ -37,190 +14,582 @@ class TestBootstrapCollector(TestCase): maxDiff = None - def test_clean_deployment_type(self): - """Test cleaning the deployment type.""" - with input(""): - self.assertEqual(clean_deployment_type(None), "digitalocean-k8s") - with input("non-existing", ""): - self.assertEqual(clean_deployment_type(None), "digitalocean-k8s") - - def test_clean_environment_distribution(self): - """Test cleaning the environment distribution.""" - self.assertEqual(clean_environment_distribution(None, "other-k8s"), "1") - with input("1", ""): - self.assertEqual( - clean_environment_distribution(None, "digitalocean-k8s"), "1" - ) - with input("999", "3"): - self.assertEqual( - clean_environment_distribution(None, "digitalocean-k8s"), "3" - ) - - def test_clean_gitlab_data(self): - """Test cleaning the GitLab group data.""" - with input("Y"): - self.assertEqual( - clean_gitlab_data( - "https://gitlab.com/", - "mYV4l1DT0k3N", - "my-gitlab-group", - ), - ("https://gitlab.com", "mYV4l1DT0k3N", "my-gitlab-group"), - ) - with input( - "Y", "https://gitlab.com", "/wrong/", "parent-group/my-project-group", "Y" - ): - self.assertEqual( - clean_gitlab_data( - None, - "mYV4l1DT0k3N", - None, - ), - ( - "https://gitlab.com", - "mYV4l1DT0k3N", - "parent-group/my-project-group", - ), - ) - with input( - "Y", - "https://gitlab.com", - {"hidden": "mYV4l1DT0k3N"}, - "my-project-group", - "Y", + def setUp(self): + """Setup the test data.""" + self.output_dir = Path("./tests/test_files") + rmtree(self.output_dir, ignore_errors=True) + return super().setUp() + + def tearDown(self): + """Cleanup after each test.""" + rmtree(self.output_dir, ignore_errors=True) + return super().tearDown() + + def test_project_slug_from_default(self): + """Test collecting the project slug from its default value.""" + collector = Collector(project_name="My Project") + self.assertIsNone(collector.project_slug) + with mock_input(""): + collector.set_project_slug() + self.assertEqual(collector.project_slug, "my-project") + + def test_project_slug_from_input(self): + """Test collecting the project slug from user input.""" + collector = Collector(project_name="Test Project") + self.assertIsNone(collector.project_slug) + with mock_input("My New Project Slug"): + collector.set_project_slug() + self.assertEqual(collector.project_slug, "my-new-project-slug") + + def test_project_slug_from_options(self): + """Test collecting the project slug from the collected options.""" + collector = Collector( + project_name="My Project", + project_slug="my-new-project", + ) + self.assertEqual(collector.project_slug, "my-new-project") + with mock.patch("bootstrap.collector.click.prompt") as mocked_prompt: + collector.set_project_slug() + self.assertEqual(collector.project_slug, "my-new-project") + mocked_prompt.assert_not_called() + + def test_project_dirname(self): + """Test collecting the project dirname.""" + collector = Collector(project_slug="project-slug", service_slug="service-slug") + self.assertIsNone(collector.project_dirname) + with mock_input("service-slug"): + collector.set_project_dirname() + self.assertEqual(collector.project_dirname, "service-slug") + + def test_use_redis_from_input(self): + """Test setting the `use_redis` flag from user input.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.use_redis) + with mock_input("y"): + collector.set_use_redis() + self.assertTrue(collector.use_redis) + + def test_use_redis_from_options(self): + """Test setting the `use_redis` flag from user input.""" + collector = Collector(project_name="project_name", use_redis=False) + self.assertFalse(collector.use_redis) + with mock.patch("bootstrap.collector.click.confirm") as mocked_confirm: + collector.set_use_redis() + self.assertFalse(collector.use_redis) + mocked_confirm.assert_not_called() + + def test_terraform_backend_from_default(self): + """Test setting the Terraform backend from its default value.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.terraform_backend) + collector.set_terraform_cloud = mock.MagicMock() + with mock_input(""): + collector.set_terraform() + self.assertEqual(collector.terraform_backend, "terraform-cloud") + collector.set_terraform_cloud.assert_called_once() + + def test_terraform_backend_from_input(self): + """Test setting the Terraform backend from user input.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.terraform_backend) + collector.set_terraform_cloud = mock.MagicMock() + with mock_input("bad-tf-backend", "another-bad-tf-backend", "gitlab"): + collector.set_terraform() + self.assertEqual(collector.terraform_backend, "gitlab") + collector.set_terraform_cloud.assert_not_called() + + def test_terraform_backend_from_options(self): + """Test setting the Terraform backend from the collected options.""" + collector = Collector( + project_name="project_name", terraform_backend="terraform-cloud" + ) + self.assertEqual(collector.terraform_backend, "terraform-cloud") + collector.set_terraform_cloud = mock.MagicMock() + with mock.patch("bootstrap.collector.click") as mocked_click: + collector.set_terraform() + self.assertEqual(collector.terraform_backend, "terraform-cloud") + mocked_click.prompt.assert_not_called() + collector.set_terraform_cloud.assert_called_once() + + def test_terraform_cloud_from_input(self): + """Test setting up Terraform Cloud from user input.""" + collector = Collector( + project_name="project_name", terraform_backend="terraform-cloud" + ) + self.assertIsNone(collector.terraform_cloud_hostname) + self.assertIsNone(collector.terraform_cloud_token) + self.assertIsNone(collector.terraform_cloud_organization) + self.assertIsNone(collector.terraform_cloud_organization_create) + self.assertIsNone(collector.terraform_cloud_admin_email) + with mock_input( + "", + {"hidden": "mytfcT0k3N"}, + "myTFCOrg", + "y", + "bad-email", + "admin@test.com", ): - self.assertEqual( - clean_gitlab_data(None, None, None), - ("https://gitlab.com", "mYV4l1DT0k3N", "my-project-group"), - ) - self.assertEqual( - clean_gitlab_data("", "", ""), - (None, None, None), + collector.set_terraform_cloud() + self.assertEqual(collector.terraform_cloud_hostname, "app.terraform.io") + self.assertEqual(collector.terraform_cloud_token, "mytfcT0k3N") + self.assertEqual(collector.terraform_cloud_organization, "myTFCOrg") + self.assertTrue(collector.terraform_cloud_organization_create) + self.assertEqual(collector.terraform_cloud_admin_email, "admin@test.com") + + def test_terraform_cloud_from_options(self): + """Test setting up Terraform Cloud from the collected options.""" + collector = Collector( + project_name="project_name", + terraform_backend="terraform-cloud", + terraform_cloud_hostname="app.terraform.io", + terraform_cloud_token="mytfcT0k3N", + terraform_cloud_organization="myTFCOrg", + terraform_cloud_organization_create=True, + terraform_cloud_admin_email="admin@test.com", ) + self.assertEqual(collector.terraform_cloud_hostname, "app.terraform.io") + self.assertEqual(collector.terraform_cloud_token, "mytfcT0k3N") + self.assertEqual(collector.terraform_cloud_organization, "myTFCOrg") + self.assertTrue(collector.terraform_cloud_organization_create) + self.assertEqual(collector.terraform_cloud_admin_email, "admin@test.com") + with mock.patch("bootstrap.collector.click") as mocked_click: + collector.set_terraform_cloud() + self.assertEqual(collector.terraform_cloud_hostname, "app.terraform.io") + self.assertEqual(collector.terraform_cloud_token, "mytfcT0k3N") + self.assertEqual(collector.terraform_cloud_organization, "myTFCOrg") + self.assertTrue(collector.terraform_cloud_organization_create) + self.assertEqual(collector.terraform_cloud_admin_email, "admin@test.com") + mocked_click.prompt.assert_not_called() - def test_clean_project_dirname(self): - """Test cleaning the project directory.""" - self.assertEqual( - clean_project_dirname("tests", "my_project", "frontend"), "tests" - ) - with input("frontend"): - self.assertEqual( - clean_project_dirname(None, "my_project", "frontend"), "frontend" - ) - - def test_clean_project_slug(self): - """Test cleaning the project slug.""" - with input("My Project"): - self.assertEqual(clean_project_slug("My Project", None), "my-project") - project_slug = "my-new-project" + def test_terraform_cloud_from_input_and_options(self): + """Test setting up Terraform Cloud from options and user input.""" + collector = Collector( + project_name="project_name", + terraform_backend="terraform-cloud", + terraform_cloud_token="mytfcT0k3N", + ) + self.assertIsNone(collector.terraform_cloud_hostname) + self.assertEqual(collector.terraform_cloud_token, "mytfcT0k3N") + self.assertIsNone(collector.terraform_cloud_organization) + self.assertIsNone(collector.terraform_cloud_organization_create) + self.assertIsNone(collector.terraform_cloud_admin_email) + with mock_input("tfc.my-company.com", "myTFCOrg", "n"): + collector.set_terraform_cloud() + self.assertEqual(collector.terraform_cloud_hostname, "tfc.my-company.com") + self.assertEqual(collector.terraform_cloud_token, "mytfcT0k3N") + self.assertEqual(collector.terraform_cloud_organization, "myTFCOrg") + self.assertFalse(collector.terraform_cloud_organization_create) + self.assertEqual(collector.terraform_cloud_admin_email, "") + + def test_vault_no(self): + """Test not setting vault.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.vault_token) + self.assertIsNone(collector.vault_url) + with mock_input("n"): + collector.set_vault() + self.assertIsNone(collector.vault_token) + + def test_vault_from_input(self): + """Test setting up Vault from user input.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.vault_token) + self.assertIsNone(collector.vault_url) + with mock_input( + {"hidden": "v4UlTtok3N"}, + "https://vault.test.com", + ), mock.patch( + "bootstrap.collector.click.confirm", return_value=True + ) as mocked_confirm: + collector.set_vault() + self.assertEqual(collector.vault_token, "v4UlTtok3N") + self.assertEqual(collector.vault_url, "https://vault.test.com") + self.assertEqual(len(mocked_confirm.mock_calls), 2) + + def test_vault_from_options(self): + """Test setting up Vault from the collected options.""" + collector = Collector( + project_name="project_name", + vault_token="v4UlTtok3N", + vault_url="https://vault.test.com", + ) + self.assertEqual(collector.vault_token, "v4UlTtok3N") + self.assertEqual(collector.vault_url, "https://vault.test.com") + with mock.patch( + "bootstrap.collector.click.confirm", return_value=True + ) as mocked_confirm: + collector.set_vault() + self.assertEqual(collector.vault_token, "v4UlTtok3N") + self.assertEqual(collector.vault_url, "https://vault.test.com") + self.assertEqual(len(mocked_confirm.mock_calls), 1) + + def test_vault_from_input_and_options(self): + """Test setting up Vault from options and user input.""" + collector = Collector( + project_name="project_name", vault_url="https://vault.test.com" + ) + self.assertIsNone(collector.vault_token) + self.assertEqual(collector.vault_url, "https://vault.test.com") + with mock_input({"hidden": "v4UlTtok3N"}), mock.patch( + "bootstrap.collector.click.confirm", return_value=True + ) as mocked_confirm: + collector.set_vault() + self.assertEqual(collector.vault_token, "v4UlTtok3N") + self.assertEqual(collector.vault_url, "https://vault.test.com") + self.assertEqual(len(mocked_confirm.mock_calls), 1) + + def test_deployment_type_from_default(self): + """Test collecting the deployment type from its default value.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.deployment_type) + with mock_input(""): + collector.set_deployment_type() + self.assertEqual(collector.deployment_type, "digitalocean-k8s") + + def test_deployment_type_from_input(self): + """Test collecting the deployment type from user input.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.deployment_type) + with mock_input("bad-deployment-type", "yet-another-bad-value", "other-k8s"): + collector.set_deployment_type() + self.assertEqual(collector.deployment_type, "other-k8s") + + def test_deployment_type_from_options(self): + """Test collecting the deployment type from the collected options.""" + collector = Collector(project_name="project_name", deployment_type="other-k8s") + self.assertEqual(collector.deployment_type, "other-k8s") + with mock.patch("bootstrap.collector.click.prompt") as mocked_prompt: + collector.set_deployment_type() + self.assertEqual(collector.deployment_type, "other-k8s") + mocked_prompt.assert_not_called() + + def test_environments_distribution_for_other_k8s_deployment(self): + """Test collecting the environments distribution for other-k8s deployment.""" + collector = Collector(project_name="project_name", deployment_type="other-k8s") + self.assertIsNone(collector.environments_distribution) + with mock.patch("bootstrap.collector.click.prompt") as mocked_prompt: + collector.set_environments_distribution() + self.assertEqual(collector.environments_distribution, "1") + mocked_prompt.assert_not_called() + + def test_environments_distribution_from_default(self): + """Test collecting the environments distribution from its default value.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.environments_distribution) + with mock_input(""): + collector.set_environments_distribution() + self.assertEqual(collector.environments_distribution, "1") + + def test_environments_distribution_from_input(self): + """Test collecting the environments distribution from user input.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.environments_distribution) + with mock_input("one", "yet-another-bad-value", "3"): + collector.set_environments_distribution() + self.assertEqual(collector.environments_distribution, "3") + + def test_environments_distribution_from_options(self): + """Test collecting the environments distribution from the collected options.""" + collector = Collector( + project_name="project_name", environments_distribution="2" + ) + self.assertEqual(collector.environments_distribution, "2") + with mock.patch("bootstrap.collector.click.prompt") as mocked_prompt: + collector.set_environments_distribution() + self.assertEqual(collector.environments_distribution, "2") + mocked_prompt.assert_not_called() + + def test_set_project_urls_from_default(self): + """Test collecting the domain and urls options from default.""" + collector = Collector(project_name="project_name", project_slug="test-project") + self.assertIsNone(collector.project_url_dev) + self.assertIsNone(collector.project_url_stage) + self.assertIsNone(collector.project_url_prod) + with mock_input("", "", ""): + collector.set_project_urls() + self.assertEqual(collector.project_url_dev, "https://dev.test-project.com") + self.assertEqual(collector.project_url_stage, "https://stage.test-project.com") + self.assertEqual(collector.project_url_prod, "https://www.test-project.com") + + def test_set_project_urls_from_input(self): + """Test collecting the domain and urls options from input.""" + collector = Collector(project_name="project_name", project_slug="test-project") + self.assertIsNone(collector.project_url_dev) + self.assertIsNone(collector.project_url_stage) + self.assertIsNone(collector.project_url_prod) + with mock_input( + "bad domain.com", + "https://dev-from-input.domain.com", + "dev from input", + "https://dev-from-input.domain.com", + "prod-from-input ", + "https://prod-from-input.domain.com", + ): + collector.set_project_urls() + self.assertEqual(collector.project_url_dev, "https://dev-from-input.domain.com") self.assertEqual( - clean_project_slug("My Project", "my-new-project"), project_slug - ) - - def test_clean_sentry_dsn(self): - """Test cleaning the Sentry DSN.""" - with input("https://public@sentry.example.com/1"): - self.assertEqual( - clean_sentry_dsn("https://public@sentry.example.com/1"), - "https://public@sentry.example.com/1", - ) - - def test_clean_sentry_org(self): - """Test cleaning the Sentry organization.""" - self.assertEqual(clean_sentry_org("MyOrganization"), "MyOrganization") - with input("MyOrganization"): - self.assertEqual(clean_sentry_org(None), "MyOrganization") - - def test_clean_service_dir(self): - """Test cleaning the service directory.""" - MockedPath = mock.MagicMock(spec=Path) - output_dir = MockedPath("mocked-tests") - output_dir.is_absolute.return_value = True - service_dir = MockedPath("mocked-tests/my_project") - service_dir.is_dir.return_value = False - output_dir.__truediv__.return_value = service_dir - self.assertEqual(clean_service_dir(output_dir, "my_project"), service_dir) - service_dir.is_dir.return_value = True - output_dir.__truediv__.return_value = service_dir - with mock.patch("bootstrap.collector.rmtree", return_value=None), input("Y"): - self.assertEqual(clean_service_dir(output_dir, "my_project"), service_dir) - - def test_clean_service_slug(self): - """Test cleaning the back end service slug.""" - with input(""): - self.assertEqual(clean_service_slug(""), "frontend") - with input("my frontend"): - self.assertEqual(clean_service_slug(""), "myfrontend") - - def test_clean_terraform_backend(self): - """Test cleaning the Terraform .""" + collector.project_url_stage, "https://dev-from-input.domain.com" + ) self.assertEqual( - clean_terraform_backend("gitlab", None, None, None, None, None), - ("gitlab", None, None, None, None, None), - ) - with input("gitlab"): - self.assertEqual( - clean_terraform_backend("wrong-backend", None, None, None, None, None), - ("gitlab", None, None, None, None, None), - ) - with input("terraform-cloud", "", "myOrg", "y", "bad-email", "admin@test.com"): - self.assertEqual( - clean_terraform_backend( - "wrong-backend", None, "mytfcT0k3N", None, None, None - ), - ( - "terraform-cloud", - "app.terraform.io", - "mytfcT0k3N", - "myOrg", - True, - "admin@test.com", - ), - ) - with input( - "terraform-cloud", - "tfc.mydomain.com", - {"hidden": "mytfcT0k3N"}, - "myOrg", - "n", - None, + collector.project_url_prod, "https://prod-from-input.domain.com" + ) + + def test_set_project_urls_from_options(self): + """Test collecting the domain and urls options from input.""" + collector = Collector( + project_url_dev="https://dev.domain.com", + project_url_stage="https://stage.domain.com", + project_url_prod="https://www.domain.com", + ) + self.assertEqual(collector.project_url_dev, "https://dev.domain.com") + self.assertEqual(collector.project_url_stage, "https://stage.domain.com") + self.assertEqual(collector.project_url_prod, "https://www.domain.com") + with mock.patch("bootstrap.collector.click.prompt") as mocked_prompt: + collector.set_project_urls() + self.assertEqual(collector.project_url_dev, "https://dev.domain.com") + self.assertEqual(collector.project_url_stage, "https://stage.domain.com") + self.assertEqual(collector.project_url_prod, "https://www.domain.com") + mocked_prompt.assert_not_called() + + def test_sentry_no(self): + """Test not setting Sentry.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.sentry_org) + with mock_input("n"): + collector.set_sentry() + self.assertIsNone(collector.sentry_org) + + def test_sentry_default(self): + """Test setting Sentry options from default.""" + collector = Collector(project_name="project_name") + self.assertIsNone(collector.sentry_org) + self.assertIsNone(collector.sentry_url) + with mock_input( + "y", + "sentry-input-organization", + "", + "https://backend.sentry.dsn", ): - self.assertEqual( - clean_terraform_backend("wrong-backend", None, None, None, None, None), - ( - "terraform-cloud", - "tfc.mydomain.com", - "mytfcT0k3N", - "myOrg", - False, - None, - ), - ) - - def test_clean_use_redis(self): - """Test cleaning the use Redis.""" - self.assertEqual(clean_use_redis("Y"), "Y") - - def test_clean_vault_data(self): - """Test cleaning the Vault data .""" - self.assertEqual( - clean_vault_data("v4UlTtok3N", "https://vault.test.com", True), - ("v4UlTtok3N", "https://vault.test.com"), - ) - with input("y", {"hidden": "v4UlTtok3N"}, "https://vault.test.com"): - self.assertEqual( - clean_vault_data(None, None, True), - ("v4UlTtok3N", "https://vault.test.com"), - ) - with input("y", {"hidden": "v4UlTtok3N"}, "y", "https://vault.test.com"): - self.assertEqual( - clean_vault_data(None, None, False), - ("v4UlTtok3N", "https://vault.test.com"), - ) - with input( - "y", {"hidden": "v4UlTtok3N"}, "y", "bad_address", "https://vault.test.com" + collector.set_sentry() + self.assertEqual(collector.sentry_org, "sentry-input-organization") + self.assertEqual(collector.sentry_url, "https://sentry.io") + self.assertEqual(collector.sentry_dsn, "https://backend.sentry.dsn") + + def test_sentry_options(self): + """Test setting Sentry options from options.""" + collector = Collector( + project_name="project_name", + sentry_org="sentry-options-organization", + sentry_url="https://other-sentry-url.com", + sentry_dsn="https://backend.sentry.dsn", + ) + self.assertEqual(collector.sentry_org, "sentry-options-organization") + self.assertEqual(collector.sentry_url, "https://other-sentry-url.com") + collector.set_sentry() + self.assertEqual(collector.sentry_org, "sentry-options-organization") + self.assertEqual(collector.sentry_url, "https://other-sentry-url.com") + + def test_gitlab_no(self): + """Test not setting Gitlab.""" + collector = Collector(project_name="project_name", gitlab_url="") + with mock_input("n"): + collector.set_gitlab() + self.assertEqual(collector.gitlab_url, "") + + def test_gitlab_default(self): + """Test setting Gitlab options from default.""" + collector = Collector( + project_name="project_name", project_slug="gitlab-project" + ) + self.assertIsNone(collector.gitlab_url) + self.assertIsNone(collector.gitlab_token) + self.assertIsNone(collector.gitlab_namespace_path) + with mock_input("y", "", {"hidden": "G1tl4b_Tok3n!"}, "namespacepath"): + collector.set_gitlab() + self.assertEqual(collector.gitlab_url, "https://gitlab.com") + self.assertEqual(collector.gitlab_token, "G1tl4b_Tok3n!") + self.assertEqual(collector.gitlab_namespace_path, "namespacepath") + + def test_gitlab_input(self): + """Test setting Gitlab options from input.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.gitlab_url) + self.assertIsNone(collector.gitlab_token) + self.assertIsNone(collector.gitlab_namespace_path) + with mock_input( + "y", + "https://gitlab.custom-domain.com", + {"hidden": "input-G1tl4b_Tok3n!"}, + "inputnamespacepath", ): - self.assertEqual( - clean_vault_data(None, None, False), - ("v4UlTtok3N", "https://vault.test.com"), - ) + collector.set_gitlab() + self.assertEqual(collector.gitlab_url, "https://gitlab.custom-domain.com") + self.assertEqual(collector.gitlab_token, "input-G1tl4b_Tok3n!") + self.assertEqual(collector.gitlab_namespace_path, "inputnamespacepath") + + def test_gitlab_options(self): + """Test setting Gitlab options from options.""" + collector = Collector( + project_name="project_name", + gitlab_url="https://gitlab.custom-domain.com", + gitlab_token="input-G1tl4b_Tok3n!", + gitlab_namespace_path="inputnamespacepath", + ) + self.assertEqual(collector.gitlab_url, "https://gitlab.custom-domain.com") + self.assertEqual(collector.gitlab_token, "input-G1tl4b_Tok3n!") + self.assertEqual(collector.gitlab_namespace_path, "inputnamespacepath") + collector.set_gitlab() + self.assertEqual(collector.gitlab_url, "https://gitlab.custom-domain.com") + self.assertEqual(collector.gitlab_token, "input-G1tl4b_Tok3n!") + self.assertEqual(collector.gitlab_namespace_path, "inputnamespacepath") + + def test_launch_runner(self): + """Test launching the runner.""" + collector = Collector( + project_name="project_name", + ) + runner = mock.MagicMock() + collector.get_runner = mock.MagicMock(return_value=runner) + collector.launch_runner() + runner.run.assert_called_once() + + def test_get_runner(self): + """Test getting the runner.""" + collector = Collector( + deployment_type="digitalocean-k8s", + environments_distribution="1", + internal_service_port=8000, + project_dirname="project_dirname", + project_name="Test Project", + project_slug="test-project", + project_url_dev="https://dev.test.com", + project_url_prod="https://www.test.com", + project_url_stage="https://stage.test.com", + service_slug="django", + terraform_backend="terraform-cloud", + use_redis=False, + ) + collector._service_dir = Path(".") + runner = collector.get_runner() + self.assertEqual(runner.deployment_type, "digitalocean-k8s") + self.assertEqual(runner.environments_distribution, "1") + self.assertEqual(runner.internal_service_port, 8000) + self.assertEqual(runner.project_dirname, "project_dirname") + self.assertEqual(runner.project_name, "Test Project") + self.assertEqual(runner.project_slug, "test-project") + self.assertEqual(runner.project_url_dev, "https://dev.test.com") + self.assertEqual(runner.project_url_prod, "https://www.test.com") + self.assertEqual(runner.project_url_stage, "https://stage.test.com") + self.assertEqual(runner.service_slug, "django") + self.assertEqual(runner.terraform_backend, "terraform-cloud") + self.assertEqual(runner.use_redis, False) + + def test_service_slug_default(self): + """Test setting the service slug from default.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.service_slug) + with mock_input(""): + collector.set_service_slug() + self.assertEqual(collector.service_slug, "frontend") + + def test_service_slug_input(self): + """Test setting the service slug from input.""" + collector = Collector( + project_name="project_name", + ) + self.assertIsNone(collector.service_slug) + with mock_input("input-slug"): + collector.set_service_slug() + self.assertEqual(collector.service_slug, "input-slug") + + def test_service_slug_options(self): + """Test setting the service slug from options.""" + collector = Collector( + project_name="project_name", service_slug="service-slug-options" + ) + self.assertEqual(collector.service_slug, "service-slug-options") + with mock_input("asdadsa"): + collector.set_service_slug() + self.assertEqual(collector.service_slug, "service-slug-options") + + def test_service_dir_new(self): + """Test service dir with a new folder.""" + collector = Collector( + project_name="project_name", + output_dir=str(self.output_dir.resolve()), + project_dirname="test_project", + ) + service_dir = self.output_dir / "test_project" + collector.set_service_dir() + self.assertFalse(os.path.exists(service_dir)) + self.assertEqual(collector._service_dir, service_dir.resolve()) + + def test_service_dir_already_exists(self): + """Test service dir with a new folder.""" + collector = Collector( + project_name="project_name", + output_dir=str(self.output_dir.resolve()), + project_dirname="test_project", + ) + service_dir = self.output_dir / "test_project" + os.makedirs(service_dir, exist_ok=True) + with mock_input("y"): + collector.set_service_dir() + self.assertFalse(os.path.exists(service_dir)) + self.assertEqual(collector._service_dir, service_dir.resolve()) + + def test_collect(self): + """Test collect options.""" + collector = Collector( + project_name="project_name", + ) + collector.set_project_dirname = mock.MagicMock() + collector.set_project_slug = mock.MagicMock() + collector.set_service_slug = mock.MagicMock() + collector.set_project_urls = mock.MagicMock() + collector.set_project_dirname = mock.MagicMock() + collector.set_service_dir = mock.MagicMock() + collector.set_use_redis = mock.MagicMock() + collector.set_terraform = mock.MagicMock() + collector.set_vault = mock.MagicMock() + collector.set_deployment_type = mock.MagicMock() + collector.set_environments_distribution = mock.MagicMock() + collector.set_project_urls = mock.MagicMock() + collector.set_sentry = mock.MagicMock() + collector.set_gitlab = mock.MagicMock() + collector.collect() + collector.set_project_slug.assert_called_once() + collector.set_project_dirname.assert_called_once() + collector.set_service_dir.assert_called_once() + collector.set_use_redis.assert_called_once() + collector.set_terraform.assert_called_once() + collector.set_vault.assert_called_once() + collector.set_deployment_type.assert_called_once() + collector.set_environments_distribution.assert_called_once() + collector.set_project_urls.assert_called_once() + collector.set_sentry.assert_called_once() + collector.set_gitlab.assert_called_once() diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..25c66ac --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,171 @@ +"""Bootstrap helpers tests.""" +from unittest import TestCase + +from bootstrap.helpers import ( + format_gitlab_variable, + format_tfvar, + slugify_option, + validate_or_prompt_domain, + validate_or_prompt_path, + validate_or_prompt_secret, + validate_or_prompt_url, +) +from tests.utils import mock_input + + +class GitlabVariableTestcase(TestCase): + """Test the 'format_gitlab_varialbe' function.""" + + def test_gitlab_variable_unmasked_unprotected(self): + """Test the formatting of a gitlab unmasked, unprotected variable.""" + self.assertEqual( + format_gitlab_variable("value", False, False), + '{ value = "value", protected = false }', + ) + + def test_gitlab_variable_masked_unprotected(self): + """Test the formatting of a gitlab masked, unprotected variable.""" + self.assertEqual( + format_gitlab_variable("value", True, False), + '{ value = "value", masked = true, protected = false }', + ) + + def test_gitlab_variable_unmasked_protected(self): + """Test the formatting of a gitlab unmasked, protected variable.""" + self.assertEqual( + format_gitlab_variable("value", False, True), '{ value = "value" }' + ) + + def test_gitlab_variable_masked_protected(self): + """Test the formatting of a gitlab masked, unprotected variable.""" + self.assertEqual( + format_gitlab_variable("value", True, True), + '{ value = "value", masked = true }', + ) + + +class FormatTFVarTestCase(TestCase): + """Test the 'format_tfvar' function.""" + + def test_format_list(self): + """Test the function formats a list properly.""" + self.assertEqual(format_tfvar([1, 2, 3], "list"), '["1", "2", "3"]') + + def test_format_bool(self): + """Test the function formats a boolean properly.""" + self.assertEqual(format_tfvar(True, "bool"), "true") + + def test_format_number(self): + """Test the function formats a number properly.""" + self.assertEqual(format_tfvar(6, "num"), "6") + + def test_format_default(self): + """Test the function formats a boolean properly.""" + self.assertEqual(format_tfvar("something else", "default"), '"something else"') + + +class OptionSlugifyTestCase(TestCase): + """Test the 'option_slugify' function.""" + + def test_slugify_with_value(self): + """Test slugifying with a value.""" + self.assertEqual( + slugify_option(None, None, "Text to slugify"), "text-to-slugify" + ) + + def test_slugify_no_value(self): + """Test slugifying without a value.""" + self.assertEqual(slugify_option(None, None, None), None) + + +class ValidatePromptDomain(TestCase): + """Test the 'validate_or_prompt_domain' function.""" + + def test_validate_good(self): + """Test validation of a good domain.""" + self.assertEqual( + validate_or_prompt_domain("message", "www.google.com"), "www.google.com" + ) + + def test_validate_bad(self): + """Test validation of a bad domain.""" + with mock_input("www.test.com"): + self.assertEqual( + validate_or_prompt_domain("message", "www. google .com"), + "www.test.com", + ) + + def test_validate_no_value(self): + """Test validation without a domain.""" + with mock_input("www google com", "www.google.com"): + self.assertEqual( + validate_or_prompt_domain("message", None), "www.google.com" + ) + + +class ValidatePromptUrl(TestCase): + """Test the 'validate_or_prompt_url' function.""" + + def test_validate_good(self): + """Test validation of a good URL.""" + self.assertEqual( + validate_or_prompt_url("message", "https://www.google.com"), + "https://www.google.com", + ) + + def test_validate_bad(self): + """Test validation of a bad URL.""" + with mock_input("https://www.google.com"): + self.assertEqual( + validate_or_prompt_url("message", "https://www. google .com"), + "https://www.google.com", + ) + + def test_validate_no_value(self): + """Test validation with no starting value.""" + with mock_input("www google com", "www.google.com", "https://www.google.com"): + self.assertEqual( + validate_or_prompt_url("message", None), "https://www.google.com" + ) + + +class ValidatePromptPath(TestCase): + """Test the 'validate_or_prompt_path' function.""" + + def test_validate_good(self): + """Test validation of a good path.""" + self.assertEqual(validate_or_prompt_path("message", "/app"), "/app") + + def test_validate_bad(self): + """Test validation of a bad path.""" + with mock_input("app"): + self.assertEqual(validate_or_prompt_path("message", "// app / "), "app") + + def test_validate_no_value(self): + """Test validation with no starting value.""" + with mock_input("app"): + self.assertEqual(validate_or_prompt_path("message", None), "app") + + +class ValidatePromptSecret(TestCase): + """Test the 'validate_or_prompt_secret' function.""" + + def test_validate_good(self): + """Test validation of a good secret.""" + self.assertEqual( + validate_or_prompt_secret("message", "P4ssWord!"), + "P4ssWord!", + ) + + def test_validate_bad(self): + """Test validation of a bad secret.""" + with mock_input({"hidden": "P4ssWord!"}): + self.assertEqual( + validate_or_prompt_secret("message", "pw"), + "P4ssWord!", + ) + + def test_validate_no_value(self): + """Test validation with no starting value.""" + with mock_input({"hidden": "P4ssWord!"}): + self.assertEqual(validate_or_prompt_secret("message", None), "P4ssWord!") diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..d46f305 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,17 @@ +"""Test utils for the project.""" + + +from contextlib import contextmanager +from io import StringIO +from unittest import mock + + +@contextmanager +def mock_input(*cmds): + """Mock the user input.""" + visible_cmds = "\n".join(c for c in cmds if isinstance(c, str)) + hidden_cmds = [c["hidden"] for c in cmds if isinstance(c, dict) and "hidden" in c] + with mock.patch("sys.stdin", StringIO(f"{visible_cmds}\n")), mock.patch( + "getpass.getpass", side_effect=hidden_cmds + ): + yield