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