├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── cookiecutter.json └── {{cookiecutter.project_name}} ├── .dockerignore ├── .editorconfig ├── .gitignore ├── .gitlab-ci.yml ├── .pre-commit-config.yaml ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── Makefile ├── README.md ├── docker-compose.override.yml ├── docker-compose.yml ├── manage.py ├── poetry.lock ├── pyproject.toml ├── setup.py ├── uwsgi.ini └── {{cookiecutter.project_name}} ├── __init__.py ├── conftest.py ├── models.py ├── settings.py ├── urls.py ├── wsgi.py └── {{cookiecutter.django_app}} ├── __init__.py ├── apps.py ├── authentication.py ├── factories.py ├── filters.py ├── migrations └── __init__.py ├── models.py ├── serializers.py ├── tests ├── __init__.py ├── test_authentication.py └── test_user.py ├── urls.py └── views.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | 9 | [*.sql] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.py] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.json] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.yml] 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [Makefile] 26 | indent_style = tab 27 | indent_size = 4 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/{{cookiecutter.project_name}}" 5 | schedule: 6 | interval: weekly 7 | day: friday 8 | time: "12:00" 9 | timezone: "Europe/Zurich" 10 | ignore: 11 | - dependency-name: django 12 | versions: 13 | - ">=4.3" 14 | commit-message: 15 | prefix: chore 16 | include: scope 17 | - package-ecosystem: docker 18 | directory: "/{{cookiecutter.project_name}}" 19 | schedule: 20 | interval: weekly 21 | day: friday 22 | time: "12:00" 23 | timezone: "Europe/Zurich" 24 | ignore: 25 | - dependency-name: python 26 | versions: 27 | - ">=3.13" 28 | commit-message: 29 | prefix: chore 30 | include: scope 31 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | standard-tests: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Install cookiecutter 13 | run: pip install cookiecutter 14 | - name: Test project 15 | run: make test 16 | commit-lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Setup Python 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: '3.9' 26 | - name: Install gitlint 27 | run: pip install gitlint 28 | - name: Run gitlint 29 | run: gitlint --contrib contrib-title-conventional-commits --ignore B1,B5,B6 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | .pytest_cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | #Ipython Notebook 63 | .ipynb_checkpoints 64 | 65 | # Pyenv 66 | .python-version 67 | 68 | # Dotenv 69 | .env 70 | 71 | # Editor swap files 72 | *.swp 73 | 74 | # Cookiecutter test project 75 | ci_project 76 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ### [Unreleased][unreleased] 5 | 6 | #### Added 7 | - Initial release 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Adfinis SyGroup AG 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or 5 | without modification, are permitted provided that the following 6 | conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of border nor the names of its contributors 17 | may be used to endorse or promote products derived from this 18 | software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 21 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 22 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 23 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 25 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF 28 | USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 29 | AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | SHELL:=/bin/sh 4 | USER_ID=$(shell id --user) 5 | 6 | .PHONY: help 7 | help: 8 | @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort -k 1,1 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 9 | 10 | .PHONY: uid 11 | uid: 12 | @echo "UID=$(USER_ID)" > ci_project/.env 13 | 14 | .PHONY: clean 15 | clean: ## stop project and remove local build 16 | @if [ -d "ci_project" ]; then \ 17 | cd ci_project; docker compose down -v; cd ..; \ 18 | rm -rf ci_project; \ 19 | fi 20 | 21 | .PHONY: build 22 | build: clean ## build the project 23 | @cookiecutter --no-input --overwrite-if-exists . project_name=ci_project django_app=api organization_slug=ci-project 24 | @echo "UID=$(USER_ID)" > ci_project/.env 25 | @cd ci_project; docker compose build --pull; cd ..; 26 | @cd ci_project; docker compose run --rm backend poetry run ./manage.py makemigrations; cd ..; 27 | 28 | .PHONY: start 29 | start: build clean ## build and start the project 30 | @make -C ci_project start 31 | 32 | .PHONY: lint-output ## Lint the built project 33 | lint-output: build uid 34 | @cd ci_project; docker compose exec -T backend /bin/sh -c "poetry run ruff format --diff . && poetry run ruff check --diff ."; cd ..; 35 | 36 | .PHONY: start 37 | start: build uid ## start the project 38 | @make -C ci_project start 39 | 40 | .PHONY: test 41 | test: start lint-output ## test the project 42 | @cd ci_project; docker compose exec -T backend /bin/sh -c "poetry run pytest -vv --no-cov-on-fail --cov --create-db"; cd ..; 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django JSON API CookieCutter template 2 | ===================================== 3 | 4 | [![Build Status](https://github.com/adfinis/cookiecutter-django-json-api/workflows/Tests/badge.svg)](https://github.com/adfinis/cookiecutter-django-json-api/actions?query=workflow%3ATests) 5 | [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/adfinis/cookiecutter-django-json-api/blob/master/{{cookiecutter.project_name}}/pyproject.toml#L155) 6 | [![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://docs.astral.sh/ruff/) 7 | [![License: MIT](https://img.shields.io/badge/License-BSD-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) 8 | 9 | This cookie cutter provides a Django project with JSON API support. It combines Adfinis best practices in terms of setup, structure and configuration. 10 | 11 | Requirements 12 | ------------ 13 | - Python ^3.9 14 | - Latest [CookieCutter](http://cookiecutter.readthedocs.org/en/latest/) 15 | - [Docker](https://docs.docker.com/) 16 | 17 | Usage 18 | ----- 19 | 20 | To use, simply run 21 | `cookiecutter https://github.com/adfinis/cookiecutter-django-json-api` 22 | 23 | Included in this template 24 | ------------------------- 25 | 26 | Django specific: 27 | 28 | - [Django](https://www.djangoproject.com/) - usually last LTS release 29 | - [Django REST Framework](http://www.django-rest-framework.org/) 30 | - [Django REST Framework JSON API](https://github.com/django-json-api/django-rest-framework-json-api) 31 | - [Django Filter](https://django-filter.readthedocs.org/en/latest/) 32 | - [mozilla_django_oidc](https://github.com/mozilla/mozilla-django-oidc) 33 | - [Django Environ](https://github.com/joke2k/django-environ) 34 | 35 | 36 | Code quality and formatting tools: 37 | 38 | - [ruff](https://docs.astral.sh/ruff/) 39 | - [Pytest](https://docs.pytest.org/en/latest/) 40 | - [Pytest Coverage Plugin](https://github.com/pytest-dev/pytest-cov) - coverage set to 100% 41 | - [Pytest Django Plugin](https://pytest-django.readthedocs.io/en/latest/) 42 | 43 | 44 | Per default postgres is configured and a docker compose file provided. To support other database only 45 | `DATABASE_ENGINE` environment variable needs to be changed. 46 | 47 | License 48 | ------- 49 | 50 | Code released under the [BSD-3 Clause](LICENSE). 51 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "Project name", 3 | "django_app": "Django app name (e.g. api)", 4 | "description": "A short description of the project.", 5 | "url": "https://example.com", 6 | "organization_slug": "Organization slug (e.g. used on GitHub)" 7 | } 8 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.dockerignore: -------------------------------------------------------------------------------- 1 | .cache 2 | CONTRIBUTING.md 3 | README.md 4 | .coverage 5 | .coverage.* 6 | docker-compose.* 7 | Dockerfile 8 | .dockerignore 9 | .env 10 | .git 11 | *.pyc 12 | __pycache__ 13 | *.pyd 14 | *.pyo 15 | .pytest_cache 16 | .Python 17 | .python-version 18 | *.swp 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | 9 | [*.sql] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.py] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.json] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.yml] 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [Makefile] 26 | indent_style = tab 27 | indent_size = 4 28 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | .pytest_cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | #Ipython Notebook 63 | .ipynb_checkpoints 64 | 65 | # Pyenv 66 | .python-version 67 | 68 | # Dotenv 69 | .env 70 | 71 | # Editor swap files 72 | *.swp 73 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | 4 | test: 5 | stage: test 6 | image: python:3.6 7 | variables: 8 | PIP_CACHE_DIR: "pip-cache" 9 | POSTGRES_DB: {{cookiecutter.project_name}} 10 | POSTGRES_USER: {{cookiecutter.project_name}} 11 | POSTGRES_PASSWORD: {{cookiecutter.project_name}} 12 | DATABASE_HOST: postgres 13 | script: 14 | - echo "ENV=ci" > .env 15 | - make install-dev 16 | - make test 17 | services: 18 | - postgres 19 | cache: 20 | key: "$CI_PROJECT_NAME" 21 | paths: 22 | - pip-cache 23 | 24 | 25 | # Pyup example: Configure USER_TOKEN as secret environment variables 26 | # and potentially adjust gitlab.com with your custom instance 27 | # pyup: 28 | # stage: test 29 | # image: python:3.6 30 | # variables: 31 | # PIP_CACHE_DIR: "pip-cache" 32 | # cache: 33 | # key: "$CI_PROJECT_NAME" 34 | # paths: 35 | # - pip-cache 36 | # script: 37 | # - pip install -U pyupio 38 | # - pyup --provider gitlab --repo=$CI_PROJECT_ID --user-token=$USER_TOKEN@https://gitlab.com 39 | # only: 40 | # - master 41 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: ruff-format 5 | stages: [commit] 6 | name: ruff-format 7 | language: system 8 | entry: ruff format . 9 | types: [python] 10 | - id: ruff-check 11 | stages: [commit] 12 | name: ruff-check 13 | language: system 14 | entry: ruff check . --show-source --fix 15 | types: [python] 16 | - id: gitlint 17 | stages: [commit-msg] 18 | name: gitlint 19 | description: Validate commit lint 20 | entry: gitlint --msg-filename 21 | language: system 22 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | env: 4 | - DOCKER_COMPOSE_VERSION=1.21.0 5 | 6 | python: 7 | - "3.6" 8 | 9 | services: 10 | - docker 11 | 12 | before_install: 13 | # install newer compose version 14 | - sudo rm /usr/local/bin/docker-compose 15 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose 16 | - chmod +x docker-compose 17 | - sudo mv docker-compose /usr/local/bin 18 | 19 | # Workaround for https://github.com/travis-ci/travis-ci/issues/4842 20 | # Let's stop postgresql 21 | - sudo service postgresql stop 22 | # wait for postgresql to shutdown 23 | - while sudo lsof -Pi :5432 -sTCP:LISTEN -t; do sleep 1; done 24 | 25 | # set UID to run docker service with 26 | - echo "UID=$(id --user)" > .env 27 | 28 | install: make start 29 | 30 | script: make test 31 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions to {{cookiecutter.project_name}} are very welcome! Best have a look at the open [issues](https://github.com/{{cookiecutter.organization_slug}}/{{cookiecutter.project_name}}) 4 | and open a [GitHub pull request](https://github.com/{{cookiecutter.organization_slug}}/{{cookiecutter.project_name}}/compare). See instructions below how to setup development 5 | environment. Before writing any code, best discuss your proposed change in a GitHub issue to see if the proposed change makes sense for the project. 6 | 7 | ## Setup development environment 8 | 9 | ### Clone 10 | 11 | To work on {{cookiecutter.project_name}} you first need to clone 12 | 13 | ```bash 14 | git clone https://github.com/{{cookiecutter.organization_slug}}/{{cookiecutter.project_name}}.git 15 | cd {{cookiecutter.project_name}} 16 | ``` 17 | 18 | ### Open Shell 19 | 20 | Once it is cloned you can easily open a shell in the docker container to 21 | open an development environment. 22 | 23 | ```bash 24 | # needed for permission handling 25 | # only needs to be run once 26 | echo UID=$UID > .env 27 | # open shell 28 | docker compose run --rm {{cookiecutter.project_name}} bash 29 | ``` 30 | 31 | ### Testing 32 | 33 | Once you have shelled in docker container as described above 34 | you can use common python tooling for formatting, linting, testing 35 | etc. 36 | 37 | ```bash 38 | # linting 39 | ruff check . 40 | # format code 41 | ruff format . 42 | # running tests 43 | pytest 44 | # create migrations 45 | ./manage.py makemigrations 46 | # install debugger or other temporary dependencies 47 | pip install --user pdbpp 48 | ``` 49 | 50 | Writing of code can still happen outside the docker container of course. 51 | 52 | ### Install new requirements 53 | 54 | In case you're adding new requirements you simply need to build the docker container 55 | again for those to be installed and re-open shell. 56 | 57 | ```bash 58 | docker compose build --pull 59 | ``` 60 | 61 | ### Setup pre commit 62 | 63 | Pre commit hooks is an additional option instead of executing checks in your editor of choice. 64 | 65 | ```bash 66 | pre-commit install --hook=pre-commit 67 | pre-commit install --hook=commit-msg 68 | ``` 69 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12 2 | 3 | # needs to be set for users with manually set UID 4 | ENV HOME=/home/{{cookiecutter.project_name}} 5 | 6 | ENV PYTHONUNBUFFERED=1 7 | ENV DJANGO_SETTINGS_MODULE {{cookiecutter.project_name}}.settings 8 | ENV APP_HOME=/app 9 | 10 | RUN mkdir -p /app \ 11 | && useradd -u 901 -r {{cookiecutter.project_name}} --create-home \ 12 | # all project specific folders need to be accessible by newly created user but also for unknown users (when UID is set manually). Such users are in group root. 13 | && chown -R {{cookiecutter.project_name}}:root /home/{{cookiecutter.project_name}} \ 14 | && chmod -R 770 /home/{{cookiecutter.project_name}} \ 15 | && apt-get update && apt-get install -y --no-install-recommends \ 16 | wait-for-it \ 17 | # needed for psycopg2 18 | libpq-dev \ 19 | && pip install -U poetry 20 | 21 | USER {{cookiecutter.project_name}} 22 | 23 | WORKDIR $APP_HOME 24 | 25 | ARG INSTALL_DEV_DEPENDENCIES=false 26 | COPY pyproject.toml poetry.lock $APP_HOME/ 27 | RUN if [ "$INSTALL_DEV_DEPENDENCIES" = "true" ]; then poetry install --with dev; else poetry install; fi 28 | 29 | COPY . $APP_HOME 30 | 31 | EXPOSE 8000 32 | 33 | CMD [\ 34 | "/bin/sh", "-c", \ 35 | "wait-for-it $DATABASE_HOST:${DATABASE_PORT:-5432} -- \ 36 | poetry run ./manage.py migrate && \ 37 | exec poetry run gunicorn --workers 10 --access-logfile - --limit-request-line 16384 --bind 0.0.0.0:8000 {{cookiecutter.project_name}}.wsgi" \ 38 | ] 39 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | .PHONY: help 4 | help: 5 | @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort -k 1,1 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 6 | 7 | .PHONY: start 8 | start: ## Start the development server 9 | @docker compose up -d --build 10 | 11 | .PHONY: test 12 | test: ## Test the backend 13 | @docker compose exec backend sh -c "black --check . && flake8 && pytest --no-cov-on-fail --cov --create-db" 14 | 15 | .PHONY: shell 16 | shell: ## Shell into the backend 17 | @docker compose exec backend bash 18 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/README.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_name}} 2 | 3 | [![Build Status]({{cookiecutter.url}}/workflows/Tests/badge.svg)]({{cookiecutter.url}}/actions?query=workflow%3ATests) 4 | [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)]({{cookiecutter.url}}/blob/main/{{cookiecutter.project_name}}/pyproject.toml#L115) 5 | [![Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://docs.astral.sh/ruff/) 6 | 7 | {{cookiecutter.description}} 8 | 9 | ## Getting started 10 | 11 | ### Installation 12 | 13 | **Requirements** 14 | * docker 15 | 16 | After installing and configuring those, download [docker-compose.yml](https://raw.githubusercontent.com/{{cookiecutter.organization_slug}}/{{cookiecutter.project_name}}/master/docker-compose.yml) and run the following command: 17 | 18 | ```bash 19 | docker compose build --pull 20 | docker compose run --rm backend poetry run ./manage.py makemigrations 21 | docker compose up -d 22 | ``` 23 | 24 | You can now access the api at [http://localhost:8000/api/v1/](http://localhost:8000/api/v1/). 25 | 26 | ### Configuration 27 | 28 | {{cookiecutter.project_name}} is a [12factor app](https://12factor.net/) which means that configuration is stored in environment variables. 29 | Different environment variable types are explained at [django-environ](https://github.com/joke2k/django-environ#supported-types). 30 | 31 | #### Common 32 | 33 | A list of configuration options which you need 34 | 35 | * `SECRET_KEY`: A secret key used for cryptography. This needs to be a random string of a certain length. See [more](https://docs.djangoproject.com/en/2.1/ref/settings/#std:setting-SECRET_KEY). 36 | * `ALLOWED_HOSTS`: A list of hosts/domains your service will be served from. See [more](https://docs.djangoproject.com/en/2.1/ref/settings/#allowed-hosts). 37 | * `DATABASE_ENGINE`: Database backend to use. See [more](https://docs.djangoproject.com/en/2.1/ref/settings/#std:setting-DATABASE-ENGINE). (default: django.db.backends.postgresql) 38 | * `DATABASE_HOST`: Host to use when connecting to database (default: localhost) 39 | * `DATABASE_PORT`: Port to use when connecting to database (default: 5432) 40 | * `DATABASE_NAME`: Name of database to use (default: {{cookiecutter.project_name}}) 41 | * `DATABASE_USER`: Username to use when connecting to the database (default: {{cookiecutter.project_name}}) 42 | * `DATABASE_PASSWORD`: Password to use when connecting to database 43 | 44 | ## Contributing 45 | 46 | Look at our [contributing guidelines](CONTRIBUTING.md) to start with your first contribution. 47 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | db: 4 | environment: 5 | - POSTGRES_PASSWORD={{cookiecutter.project_name}} 6 | ports: 7 | - "5432:5432" 8 | 9 | backend: 10 | build: 11 | context: . 12 | args: 13 | INSTALL_DEV_DEPENDENCIES: "true" 14 | user: "${UID:?Set UID env variable to your user id}" 15 | volumes: 16 | - ./:/app 17 | command: 18 | [ 19 | "/bin/sh", 20 | "-c", 21 | "wait-for-it db:5432 -- poetry run ./manage.py migrate && poetry run ./manage.py runserver 0.0.0.0:8000", 22 | ] 23 | environment: 24 | - ENV=dev 25 | # - OIDC_VERIFY_SSL=False 26 | - OIDC_OP_USER_ENDPOINT=https://{{cookiecutter.project_name}}.local/auth/realms/{{cookiecutter.project_name}}/protocol/openid-connect/userinfo 27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | db: 4 | image: postgres:alpine 5 | environment: 6 | - POSTGRES_USER={{cookiecutter.project_name}} 7 | # following option is a must to configure on production system: 8 | # https://hub.docker.com/_/postgres 9 | # - POSTGRES_PASSWORD= 10 | volumes: 11 | - dbdata:/var/lib/postgresql/data 12 | 13 | backend: 14 | image: {{cookiecutter.organization_slug}}/{{cookiecutter.project_name}} 15 | ports: 16 | - "8000:8000" 17 | depends_on: 18 | - db 19 | environment: 20 | - DATABASE_HOST=db 21 | # following options are a must to configure on production system: 22 | # https://docs.djangoproject.com/en/2.1/ref/settings/#std:setting-SECRET_KEY 23 | # - SECRET_KEY= 24 | # https://docs.djangoproject.com/en/2.1/ref/settings/#allowed-hosts 25 | # - ALLOWED_HOSTS= 26 | # https://docs.djangoproject.com/en/2.1/ref/settings/#password 27 | # same as postgres password above 28 | # - DATABASE_PASSWORD= 29 | 30 | volumes: 31 | dbdata: 32 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault( 8 | "DJANGO_SETTINGS_MODULE", "{{cookiecutter.project_name}}.settings" 9 | ) 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "arrow" 5 | version = "1.2.3" 6 | description = "Better dates & times for Python" 7 | optional = false 8 | python-versions = ">=3.6" 9 | files = [ 10 | {file = "arrow-1.2.3-py3-none-any.whl", hash = "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"}, 11 | {file = "arrow-1.2.3.tar.gz", hash = "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1"}, 12 | ] 13 | 14 | [package.dependencies] 15 | python-dateutil = ">=2.7.0" 16 | 17 | [[package]] 18 | name = "asgiref" 19 | version = "3.8.1" 20 | description = "ASGI specs, helper code, and adapters" 21 | optional = false 22 | python-versions = ">=3.8" 23 | files = [ 24 | {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, 25 | {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, 26 | ] 27 | 28 | [package.extras] 29 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 30 | 31 | [[package]] 32 | name = "attrs" 33 | version = "23.2.0" 34 | description = "Classes Without Boilerplate" 35 | optional = false 36 | python-versions = ">=3.7" 37 | files = [ 38 | {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, 39 | {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, 40 | ] 41 | 42 | [package.extras] 43 | cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] 44 | dev = ["attrs[tests]", "pre-commit"] 45 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] 46 | tests = ["attrs[tests-no-zope]", "zope-interface"] 47 | tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] 48 | tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] 49 | 50 | [[package]] 51 | name = "certifi" 52 | version = "2024.7.4" 53 | description = "Python package for providing Mozilla's CA Bundle." 54 | optional = false 55 | python-versions = ">=3.6" 56 | files = [ 57 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 58 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 59 | ] 60 | 61 | [[package]] 62 | name = "cffi" 63 | version = "1.16.0" 64 | description = "Foreign Function Interface for Python calling C code." 65 | optional = false 66 | python-versions = ">=3.8" 67 | files = [ 68 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 69 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 70 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 71 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 72 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 73 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 74 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 75 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 76 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 77 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 78 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 79 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 80 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 81 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 82 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 83 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 84 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 85 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 86 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 87 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 88 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 89 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 90 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 91 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 92 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 93 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 94 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 95 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 96 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 97 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 98 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 99 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 100 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 101 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 102 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 103 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 104 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 105 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 106 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 107 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 108 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 109 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 110 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 111 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 112 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 113 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 114 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 115 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 116 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 117 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 118 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 119 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 120 | ] 121 | 122 | [package.dependencies] 123 | pycparser = "*" 124 | 125 | [[package]] 126 | name = "cfgv" 127 | version = "3.4.0" 128 | description = "Validate configuration and produce human readable error messages." 129 | optional = false 130 | python-versions = ">=3.8" 131 | files = [ 132 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 133 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 134 | ] 135 | 136 | [[package]] 137 | name = "charset-normalizer" 138 | version = "3.3.2" 139 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 140 | optional = false 141 | python-versions = ">=3.7.0" 142 | files = [ 143 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 144 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 145 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 146 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 147 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 148 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 149 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 150 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 151 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 152 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 153 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 154 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 155 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 156 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 157 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 158 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 159 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 160 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 161 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 162 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 163 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 164 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 165 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 166 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 167 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 168 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 169 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 170 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 171 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 172 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 173 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 174 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 175 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 176 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 177 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 178 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 179 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 180 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 181 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 182 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 183 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 184 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 185 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 186 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 187 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 188 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 189 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 190 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 191 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 192 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 193 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 194 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 195 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 196 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 197 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 198 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 199 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 200 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 201 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 202 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 203 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 204 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 205 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 206 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 207 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 208 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 209 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 210 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 211 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 212 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 213 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 214 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 215 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 216 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 217 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 218 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 219 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 220 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 221 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 222 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 223 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 224 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 225 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 226 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 227 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 228 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 229 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 230 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 231 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 232 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 233 | ] 234 | 235 | [[package]] 236 | name = "click" 237 | version = "8.1.3" 238 | description = "Composable command line interface toolkit" 239 | optional = false 240 | python-versions = ">=3.7" 241 | files = [ 242 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 243 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 244 | ] 245 | 246 | [package.dependencies] 247 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 248 | 249 | [[package]] 250 | name = "colorama" 251 | version = "0.4.6" 252 | description = "Cross-platform colored terminal text." 253 | optional = false 254 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 255 | files = [ 256 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 257 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 258 | ] 259 | 260 | [[package]] 261 | name = "coverage" 262 | version = "7.6.0" 263 | description = "Code coverage measurement for Python" 264 | optional = false 265 | python-versions = ">=3.8" 266 | files = [ 267 | {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, 268 | {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, 269 | {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, 270 | {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, 271 | {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, 272 | {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, 273 | {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, 274 | {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, 275 | {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, 276 | {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, 277 | {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, 278 | {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, 279 | {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, 280 | {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, 281 | {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, 282 | {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, 283 | {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, 284 | {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, 285 | {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, 286 | {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, 287 | {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, 288 | {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, 289 | {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, 290 | {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, 291 | {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, 292 | {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, 293 | {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, 294 | {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, 295 | {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, 296 | {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, 297 | {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, 298 | {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, 299 | {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, 300 | {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, 301 | {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, 302 | {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, 303 | {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, 304 | {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, 305 | {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, 306 | {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, 307 | {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, 308 | {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, 309 | {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, 310 | {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, 311 | {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, 312 | {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, 313 | {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, 314 | {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, 315 | {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, 316 | {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, 317 | {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, 318 | {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, 319 | ] 320 | 321 | [package.extras] 322 | toml = ["tomli"] 323 | 324 | [[package]] 325 | name = "cryptography" 326 | version = "43.0.0" 327 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 328 | optional = false 329 | python-versions = ">=3.7" 330 | files = [ 331 | {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, 332 | {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, 333 | {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, 334 | {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, 335 | {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, 336 | {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, 337 | {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, 338 | {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, 339 | {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, 340 | {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, 341 | {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, 342 | {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, 343 | {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, 344 | {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, 345 | {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, 346 | {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, 347 | {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, 348 | {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, 349 | {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, 350 | {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, 351 | {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, 352 | {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, 353 | {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, 354 | {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, 355 | {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, 356 | {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, 357 | {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, 358 | ] 359 | 360 | [package.dependencies] 361 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 362 | 363 | [package.extras] 364 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 365 | docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] 366 | nox = ["nox"] 367 | pep8test = ["check-sdist", "click", "mypy", "ruff"] 368 | sdist = ["build"] 369 | ssh = ["bcrypt (>=3.1.5)"] 370 | test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 371 | test-randomorder = ["pytest-randomly"] 372 | 373 | [[package]] 374 | name = "distlib" 375 | version = "0.3.8" 376 | description = "Distribution utilities" 377 | optional = false 378 | python-versions = "*" 379 | files = [ 380 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 381 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 382 | ] 383 | 384 | [[package]] 385 | name = "django" 386 | version = "4.2.14" 387 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 388 | optional = false 389 | python-versions = ">=3.8" 390 | files = [ 391 | {file = "Django-4.2.14-py3-none-any.whl", hash = "sha256:3ec32bc2c616ab02834b9cac93143a7dc1cdcd5b822d78ac95fc20a38c534240"}, 392 | {file = "Django-4.2.14.tar.gz", hash = "sha256:fc6919875a6226c7ffcae1a7d51e0f2ceaf6f160393180818f6c95f51b1e7b96"}, 393 | ] 394 | 395 | [package.dependencies] 396 | asgiref = ">=3.6.0,<4" 397 | sqlparse = ">=0.3.1" 398 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 399 | 400 | [package.extras] 401 | argon2 = ["argon2-cffi (>=19.1.0)"] 402 | bcrypt = ["bcrypt"] 403 | 404 | [[package]] 405 | name = "django-environ" 406 | version = "0.11.2" 407 | description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application." 408 | optional = false 409 | python-versions = ">=3.6,<4" 410 | files = [ 411 | {file = "django-environ-0.11.2.tar.gz", hash = "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be"}, 412 | {file = "django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05"}, 413 | ] 414 | 415 | [package.extras] 416 | develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] 417 | docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] 418 | testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] 419 | 420 | [[package]] 421 | name = "django-filter" 422 | version = "24.2" 423 | description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." 424 | optional = false 425 | python-versions = ">=3.8" 426 | files = [ 427 | {file = "django-filter-24.2.tar.gz", hash = "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e"}, 428 | {file = "django_filter-24.2-py3-none-any.whl", hash = "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"}, 429 | ] 430 | 431 | [package.dependencies] 432 | Django = ">=4.2" 433 | 434 | [[package]] 435 | name = "djangorestframework" 436 | version = "3.15.2" 437 | description = "Web APIs for Django, made easy." 438 | optional = false 439 | python-versions = ">=3.8" 440 | files = [ 441 | {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, 442 | {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, 443 | ] 444 | 445 | [package.dependencies] 446 | django = ">=4.2" 447 | 448 | [[package]] 449 | name = "djangorestframework-jsonapi" 450 | version = "7.0.2" 451 | description = "A Django REST framework API adapter for the JSON:API spec." 452 | optional = false 453 | python-versions = ">=3.8" 454 | files = [ 455 | {file = "djangorestframework-jsonapi-7.0.2.tar.gz", hash = "sha256:d6c72a2bee539f1093dd86620e862af2d1a0e60408e38a710146286dbde71d75"}, 456 | {file = "djangorestframework_jsonapi-7.0.2-py2.py3-none-any.whl", hash = "sha256:be457adb50aac77eec8893048bf46ad6926dcd26204aa10965a1430610828d50"}, 457 | ] 458 | 459 | [package.dependencies] 460 | django = ">=4.2" 461 | djangorestframework = ">=3.14" 462 | inflection = ">=0.5.0" 463 | 464 | [package.extras] 465 | django-filter = ["django-filter (>=2.4)"] 466 | django-polymorphic = ["django-polymorphic (>=3.0)"] 467 | openapi = ["pyyaml (>=5.4)", "uritemplate (>=3.0.1)"] 468 | 469 | [[package]] 470 | name = "factory-boy" 471 | version = "3.3.0" 472 | description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." 473 | optional = false 474 | python-versions = ">=3.7" 475 | files = [ 476 | {file = "factory_boy-3.3.0-py2.py3-none-any.whl", hash = "sha256:a2cdbdb63228177aa4f1c52f4b6d83fab2b8623bf602c7dedd7eb83c0f69c04c"}, 477 | {file = "factory_boy-3.3.0.tar.gz", hash = "sha256:bc76d97d1a65bbd9842a6d722882098eb549ec8ee1081f9fb2e8ff29f0c300f1"}, 478 | ] 479 | 480 | [package.dependencies] 481 | Faker = ">=0.7.0" 482 | 483 | [package.extras] 484 | dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "sqlalchemy-utils", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"] 485 | doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] 486 | 487 | [[package]] 488 | name = "faker" 489 | version = "26.0.0" 490 | description = "Faker is a Python package that generates fake data for you." 491 | optional = false 492 | python-versions = ">=3.8" 493 | files = [ 494 | {file = "Faker-26.0.0-py3-none-any.whl", hash = "sha256:886ee28219be96949cd21ecc96c4c742ee1680e77f687b095202c8def1a08f06"}, 495 | {file = "Faker-26.0.0.tar.gz", hash = "sha256:0f60978314973de02c00474c2ae899785a42b2cf4f41b7987e93c132a2b8a4a9"}, 496 | ] 497 | 498 | [package.dependencies] 499 | python-dateutil = ">=2.4" 500 | 501 | [[package]] 502 | name = "fancycompleter" 503 | version = "0.9.1" 504 | description = "colorful TAB completion for Python prompt" 505 | optional = false 506 | python-versions = "*" 507 | files = [ 508 | {file = "fancycompleter-0.9.1-py3-none-any.whl", hash = "sha256:dd076bca7d9d524cc7f25ec8f35ef95388ffef9ef46def4d3d25e9b044ad7080"}, 509 | {file = "fancycompleter-0.9.1.tar.gz", hash = "sha256:09e0feb8ae242abdfd7ef2ba55069a46f011814a80fe5476be48f51b00247272"}, 510 | ] 511 | 512 | [package.dependencies] 513 | pyreadline = {version = "*", markers = "platform_system == \"Windows\""} 514 | pyrepl = ">=0.8.2" 515 | 516 | [[package]] 517 | name = "filelock" 518 | version = "3.15.4" 519 | description = "A platform independent file lock." 520 | optional = false 521 | python-versions = ">=3.8" 522 | files = [ 523 | {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, 524 | {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, 525 | ] 526 | 527 | [package.extras] 528 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 529 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] 530 | typing = ["typing-extensions (>=4.8)"] 531 | 532 | [[package]] 533 | name = "freezegun" 534 | version = "1.5.1" 535 | description = "Let your Python tests travel through time" 536 | optional = false 537 | python-versions = ">=3.7" 538 | files = [ 539 | {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, 540 | {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, 541 | ] 542 | 543 | [package.dependencies] 544 | python-dateutil = ">=2.7" 545 | 546 | [[package]] 547 | name = "gitlint" 548 | version = "0.19.1" 549 | description = "Git commit message linter written in python, checks your commit messages for style." 550 | optional = false 551 | python-versions = ">=3.7" 552 | files = [ 553 | {file = "gitlint-0.19.1-py3-none-any.whl", hash = "sha256:26bb085959148d99fbbc178b4e56fda6c3edd7646b7c2a24d8ee1f8e036ed85d"}, 554 | {file = "gitlint-0.19.1.tar.gz", hash = "sha256:b5b70fb894e80849b69abbb65ee7dbb3520fc3511f202a6e6b6ddf1a71ee8f61"}, 555 | ] 556 | 557 | [package.dependencies] 558 | gitlint-core = {version = "0.19.1", extras = ["trusted-deps"]} 559 | 560 | [[package]] 561 | name = "gitlint-core" 562 | version = "0.19.1" 563 | description = "Git commit message linter written in python, checks your commit messages for style." 564 | optional = false 565 | python-versions = ">=3.7" 566 | files = [ 567 | {file = "gitlint_core-0.19.1-py3-none-any.whl", hash = "sha256:f41effd1dcbc06ffbfc56b6888cce72241796f517b46bd9fd4ab1b145056988c"}, 568 | {file = "gitlint_core-0.19.1.tar.gz", hash = "sha256:7bf977b03ff581624a9e03f65ebb8502cc12dfaa3e92d23e8b2b54bbdaa29992"}, 569 | ] 570 | 571 | [package.dependencies] 572 | arrow = [ 573 | {version = ">=1"}, 574 | {version = "1.2.3", optional = true, markers = "extra == \"trusted-deps\""}, 575 | ] 576 | click = [ 577 | {version = ">=8"}, 578 | {version = "8.1.3", optional = true, markers = "extra == \"trusted-deps\""}, 579 | ] 580 | sh = [ 581 | {version = ">=1.13.0", markers = "sys_platform != \"win32\""}, 582 | {version = "1.14.3", optional = true, markers = "sys_platform != \"win32\" and extra == \"trusted-deps\""}, 583 | ] 584 | 585 | [package.extras] 586 | trusted-deps = ["arrow (==1.2.3)", "click (==8.1.3)", "sh (==1.14.3)"] 587 | 588 | [[package]] 589 | name = "gunicorn" 590 | version = "22.0.0" 591 | description = "WSGI HTTP Server for UNIX" 592 | optional = false 593 | python-versions = ">=3.7" 594 | files = [ 595 | {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, 596 | {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, 597 | ] 598 | 599 | [package.dependencies] 600 | packaging = "*" 601 | 602 | [package.extras] 603 | eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] 604 | gevent = ["gevent (>=1.4.0)"] 605 | setproctitle = ["setproctitle"] 606 | testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] 607 | tornado = ["tornado (>=0.2)"] 608 | 609 | [[package]] 610 | name = "identify" 611 | version = "2.6.0" 612 | description = "File identification library for Python" 613 | optional = false 614 | python-versions = ">=3.8" 615 | files = [ 616 | {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, 617 | {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, 618 | ] 619 | 620 | [package.extras] 621 | license = ["ukkonen"] 622 | 623 | [[package]] 624 | name = "idna" 625 | version = "3.7" 626 | description = "Internationalized Domain Names in Applications (IDNA)" 627 | optional = false 628 | python-versions = ">=3.5" 629 | files = [ 630 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 631 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 632 | ] 633 | 634 | [[package]] 635 | name = "inflection" 636 | version = "0.5.1" 637 | description = "A port of Ruby on Rails inflector to Python" 638 | optional = false 639 | python-versions = ">=3.5" 640 | files = [ 641 | {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, 642 | {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, 643 | ] 644 | 645 | [[package]] 646 | name = "iniconfig" 647 | version = "2.0.0" 648 | description = "brain-dead simple config-ini parsing" 649 | optional = false 650 | python-versions = ">=3.7" 651 | files = [ 652 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 653 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 654 | ] 655 | 656 | [[package]] 657 | name = "josepy" 658 | version = "1.14.0" 659 | description = "JOSE protocol implementation in Python" 660 | optional = false 661 | python-versions = ">=3.7,<4.0" 662 | files = [ 663 | {file = "josepy-1.14.0-py3-none-any.whl", hash = "sha256:d2b36a30f316269f3242f4c2e45e15890784178af5ec54fa3e49cf9234ee22e0"}, 664 | {file = "josepy-1.14.0.tar.gz", hash = "sha256:308b3bf9ce825ad4d4bba76372cf19b5dc1c2ce96a9d298f9642975e64bd13dd"}, 665 | ] 666 | 667 | [package.dependencies] 668 | cryptography = ">=1.5" 669 | pyopenssl = ">=0.13" 670 | 671 | [package.extras] 672 | docs = ["sphinx (>=4.3.0)", "sphinx-rtd-theme (>=1.0)"] 673 | 674 | [[package]] 675 | name = "mozilla-django-oidc" 676 | version = "4.0.1" 677 | description = "A lightweight authentication and access management library for integration with OpenID Connect enabled authentication services." 678 | optional = false 679 | python-versions = "*" 680 | files = [ 681 | {file = "mozilla-django-oidc-4.0.1.tar.gz", hash = "sha256:4ff8c64069e3e05c539cecf9345e73225a99641a25e13b7a5f933ec897b58918"}, 682 | {file = "mozilla_django_oidc-4.0.1-py2.py3-none-any.whl", hash = "sha256:04ef58759be69f22cdc402d082480aaebf193466cad385dc9e4f8df2a0b187ca"}, 683 | ] 684 | 685 | [package.dependencies] 686 | cryptography = "*" 687 | Django = ">=3.2" 688 | josepy = "*" 689 | requests = "*" 690 | 691 | [[package]] 692 | name = "nodeenv" 693 | version = "1.9.1" 694 | description = "Node.js virtual environment builder" 695 | optional = false 696 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 697 | files = [ 698 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 699 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 700 | ] 701 | 702 | [[package]] 703 | name = "packaging" 704 | version = "24.1" 705 | description = "Core utilities for Python packages" 706 | optional = false 707 | python-versions = ">=3.8" 708 | files = [ 709 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 710 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 711 | ] 712 | 713 | [[package]] 714 | name = "pdbpp" 715 | version = "0.10.3" 716 | description = "pdb++, a drop-in replacement for pdb" 717 | optional = false 718 | python-versions = "*" 719 | files = [ 720 | {file = "pdbpp-0.10.3-py2.py3-none-any.whl", hash = "sha256:79580568e33eb3d6f6b462b1187f53e10cd8e4538f7d31495c9181e2cf9665d1"}, 721 | {file = "pdbpp-0.10.3.tar.gz", hash = "sha256:d9e43f4fda388eeb365f2887f4e7b66ac09dce9b6236b76f63616530e2f669f5"}, 722 | ] 723 | 724 | [package.dependencies] 725 | fancycompleter = ">=0.8" 726 | pygments = "*" 727 | wmctrl = "*" 728 | 729 | [package.extras] 730 | funcsigs = ["funcsigs"] 731 | testing = ["funcsigs", "pytest"] 732 | 733 | [[package]] 734 | name = "platformdirs" 735 | version = "4.2.2" 736 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 737 | optional = false 738 | python-versions = ">=3.8" 739 | files = [ 740 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 741 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 742 | ] 743 | 744 | [package.extras] 745 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 746 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 747 | type = ["mypy (>=1.8)"] 748 | 749 | [[package]] 750 | name = "pluggy" 751 | version = "1.5.0" 752 | description = "plugin and hook calling mechanisms for python" 753 | optional = false 754 | python-versions = ">=3.8" 755 | files = [ 756 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 757 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 758 | ] 759 | 760 | [package.extras] 761 | dev = ["pre-commit", "tox"] 762 | testing = ["pytest", "pytest-benchmark"] 763 | 764 | [[package]] 765 | name = "pre-commit" 766 | version = "3.7.1" 767 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 768 | optional = false 769 | python-versions = ">=3.9" 770 | files = [ 771 | {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, 772 | {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, 773 | ] 774 | 775 | [package.dependencies] 776 | cfgv = ">=2.0.0" 777 | identify = ">=1.0.0" 778 | nodeenv = ">=0.11.1" 779 | pyyaml = ">=5.1" 780 | virtualenv = ">=20.10.0" 781 | 782 | [[package]] 783 | name = "psycopg2" 784 | version = "2.9.9" 785 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 786 | optional = false 787 | python-versions = ">=3.7" 788 | files = [ 789 | {file = "psycopg2-2.9.9-cp310-cp310-win32.whl", hash = "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516"}, 790 | {file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"}, 791 | {file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"}, 792 | {file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"}, 793 | {file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"}, 794 | {file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"}, 795 | {file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"}, 796 | {file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"}, 797 | {file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"}, 798 | {file = "psycopg2-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e"}, 799 | {file = "psycopg2-2.9.9-cp39-cp39-win32.whl", hash = "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59"}, 800 | {file = "psycopg2-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913"}, 801 | {file = "psycopg2-2.9.9.tar.gz", hash = "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156"}, 802 | ] 803 | 804 | [[package]] 805 | name = "pycparser" 806 | version = "2.22" 807 | description = "C parser in Python" 808 | optional = false 809 | python-versions = ">=3.8" 810 | files = [ 811 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 812 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 813 | ] 814 | 815 | [[package]] 816 | name = "pygments" 817 | version = "2.18.0" 818 | description = "Pygments is a syntax highlighting package written in Python." 819 | optional = false 820 | python-versions = ">=3.8" 821 | files = [ 822 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 823 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 824 | ] 825 | 826 | [package.extras] 827 | windows-terminal = ["colorama (>=0.4.6)"] 828 | 829 | [[package]] 830 | name = "pyopenssl" 831 | version = "24.2.1" 832 | description = "Python wrapper module around the OpenSSL library" 833 | optional = false 834 | python-versions = ">=3.7" 835 | files = [ 836 | {file = "pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"}, 837 | {file = "pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95"}, 838 | ] 839 | 840 | [package.dependencies] 841 | cryptography = ">=41.0.5,<44" 842 | 843 | [package.extras] 844 | docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"] 845 | test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] 846 | 847 | [[package]] 848 | name = "pyreadline" 849 | version = "2.1" 850 | description = "A python implmementation of GNU readline." 851 | optional = false 852 | python-versions = "*" 853 | files = [ 854 | {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, 855 | ] 856 | 857 | [[package]] 858 | name = "pyrepl" 859 | version = "0.9.0" 860 | description = "A library for building flexible command line interfaces" 861 | optional = false 862 | python-versions = "*" 863 | files = [ 864 | {file = "pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775"}, 865 | ] 866 | 867 | [[package]] 868 | name = "pytest" 869 | version = "8.2.2" 870 | description = "pytest: simple powerful testing with Python" 871 | optional = false 872 | python-versions = ">=3.8" 873 | files = [ 874 | {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, 875 | {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, 876 | ] 877 | 878 | [package.dependencies] 879 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 880 | iniconfig = "*" 881 | packaging = "*" 882 | pluggy = ">=1.5,<2.0" 883 | 884 | [package.extras] 885 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 886 | 887 | [[package]] 888 | name = "pytest-cov" 889 | version = "5.0.0" 890 | description = "Pytest plugin for measuring coverage." 891 | optional = false 892 | python-versions = ">=3.8" 893 | files = [ 894 | {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, 895 | {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, 896 | ] 897 | 898 | [package.dependencies] 899 | coverage = {version = ">=5.2.1", extras = ["toml"]} 900 | pytest = ">=4.6" 901 | 902 | [package.extras] 903 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 904 | 905 | [[package]] 906 | name = "pytest-django" 907 | version = "4.8.0" 908 | description = "A Django plugin for pytest." 909 | optional = false 910 | python-versions = ">=3.8" 911 | files = [ 912 | {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, 913 | {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, 914 | ] 915 | 916 | [package.dependencies] 917 | pytest = ">=7.0.0" 918 | 919 | [package.extras] 920 | docs = ["sphinx", "sphinx-rtd-theme"] 921 | testing = ["Django", "django-configurations (>=2.0)"] 922 | 923 | [[package]] 924 | name = "pytest-env" 925 | version = "1.1.3" 926 | description = "pytest plugin that allows you to add environment variables." 927 | optional = false 928 | python-versions = ">=3.8" 929 | files = [ 930 | {file = "pytest_env-1.1.3-py3-none-any.whl", hash = "sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc"}, 931 | {file = "pytest_env-1.1.3.tar.gz", hash = "sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b"}, 932 | ] 933 | 934 | [package.dependencies] 935 | pytest = ">=7.4.3" 936 | 937 | [package.extras] 938 | test = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "pytest-mock (>=3.12)"] 939 | 940 | [[package]] 941 | name = "pytest-factoryboy" 942 | version = "2.7.0" 943 | description = "Factory Boy support for pytest." 944 | optional = false 945 | python-versions = ">=3.8" 946 | files = [ 947 | {file = "pytest_factoryboy-2.7.0-py3-none-any.whl", hash = "sha256:bf3222db22d954fbf46f4bff902a0a8d82f3fc3594a47c04bbdc0546ff4c59a6"}, 948 | {file = "pytest_factoryboy-2.7.0.tar.gz", hash = "sha256:67fc54ec8669a3feb8ac60094dd57cd71eb0b20b2c319d2957873674c776a77b"}, 949 | ] 950 | 951 | [package.dependencies] 952 | factory_boy = ">=2.10.0" 953 | inflection = "*" 954 | packaging = "*" 955 | pytest = ">=6.2" 956 | typing_extensions = "*" 957 | 958 | [[package]] 959 | name = "pytest-freezer" 960 | version = "0.4.8" 961 | description = "Pytest plugin providing a fixture interface for spulec/freezegun" 962 | optional = false 963 | python-versions = ">= 3.6" 964 | files = [ 965 | {file = "pytest_freezer-0.4.8-py3-none-any.whl", hash = "sha256:644ce7ddb8ba52b92a1df0a80a699bad2b93514c55cf92e9f2517b68ebe74814"}, 966 | {file = "pytest_freezer-0.4.8.tar.gz", hash = "sha256:8ee2f724b3ff3540523fa355958a22e6f4c1c819928b78a7a183ae4248ce6ee6"}, 967 | ] 968 | 969 | [package.dependencies] 970 | freezegun = ">=1.0" 971 | pytest = ">=3.6" 972 | 973 | [[package]] 974 | name = "pytest-mock" 975 | version = "3.14.0" 976 | description = "Thin-wrapper around the mock package for easier use with pytest" 977 | optional = false 978 | python-versions = ">=3.8" 979 | files = [ 980 | {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, 981 | {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, 982 | ] 983 | 984 | [package.dependencies] 985 | pytest = ">=6.2.5" 986 | 987 | [package.extras] 988 | dev = ["pre-commit", "pytest-asyncio", "tox"] 989 | 990 | [[package]] 991 | name = "pytest-randomly" 992 | version = "3.15.0" 993 | description = "Pytest plugin to randomly order tests and control random.seed." 994 | optional = false 995 | python-versions = ">=3.8" 996 | files = [ 997 | {file = "pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6"}, 998 | {file = "pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047"}, 999 | ] 1000 | 1001 | [package.dependencies] 1002 | pytest = "*" 1003 | 1004 | [[package]] 1005 | name = "python-dateutil" 1006 | version = "2.9.0.post0" 1007 | description = "Extensions to the standard Python datetime module" 1008 | optional = false 1009 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 1010 | files = [ 1011 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 1012 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 1013 | ] 1014 | 1015 | [package.dependencies] 1016 | six = ">=1.5" 1017 | 1018 | [[package]] 1019 | name = "pyyaml" 1020 | version = "6.0.1" 1021 | description = "YAML parser and emitter for Python" 1022 | optional = false 1023 | python-versions = ">=3.6" 1024 | files = [ 1025 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 1026 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 1027 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 1028 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 1029 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 1030 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 1031 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 1032 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 1033 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 1034 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 1035 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 1036 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 1037 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 1038 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 1039 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 1040 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 1041 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 1042 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 1043 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, 1044 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 1045 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 1046 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 1047 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 1048 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 1049 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 1050 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 1051 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 1052 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 1053 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 1054 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 1055 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 1056 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 1057 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 1058 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 1059 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 1060 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 1061 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 1062 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 1063 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 1064 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 1065 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 1066 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 1067 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 1068 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 1069 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 1070 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 1071 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 1072 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 1073 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 1074 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 1075 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "requests" 1080 | version = "2.32.3" 1081 | description = "Python HTTP for Humans." 1082 | optional = false 1083 | python-versions = ">=3.8" 1084 | files = [ 1085 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 1086 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 1087 | ] 1088 | 1089 | [package.dependencies] 1090 | certifi = ">=2017.4.17" 1091 | charset-normalizer = ">=2,<4" 1092 | idna = ">=2.5,<4" 1093 | urllib3 = ">=1.21.1,<3" 1094 | 1095 | [package.extras] 1096 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 1097 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 1098 | 1099 | [[package]] 1100 | name = "requests-mock" 1101 | version = "1.12.1" 1102 | description = "Mock out responses from the requests package" 1103 | optional = false 1104 | python-versions = ">=3.5" 1105 | files = [ 1106 | {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, 1107 | {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, 1108 | ] 1109 | 1110 | [package.dependencies] 1111 | requests = ">=2.22,<3" 1112 | 1113 | [package.extras] 1114 | fixture = ["fixtures"] 1115 | 1116 | [[package]] 1117 | name = "ruff" 1118 | version = "0.3.7" 1119 | description = "An extremely fast Python linter and code formatter, written in Rust." 1120 | optional = false 1121 | python-versions = ">=3.7" 1122 | files = [ 1123 | {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, 1124 | {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, 1125 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, 1126 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, 1127 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, 1128 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, 1129 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, 1130 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, 1131 | {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, 1132 | {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, 1133 | {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, 1134 | {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, 1135 | {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, 1136 | {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, 1137 | {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, 1138 | {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, 1139 | {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, 1140 | ] 1141 | 1142 | [[package]] 1143 | name = "sh" 1144 | version = "1.14.3" 1145 | description = "Python subprocess replacement" 1146 | optional = false 1147 | python-versions = "*" 1148 | files = [ 1149 | {file = "sh-1.14.3.tar.gz", hash = "sha256:e4045b6c732d9ce75d571c79f5ac2234edd9ae4f5fa9d59b09705082bdca18c7"}, 1150 | ] 1151 | 1152 | [[package]] 1153 | name = "sh" 1154 | version = "2.0.7" 1155 | description = "Python subprocess replacement" 1156 | optional = false 1157 | python-versions = "<4.0,>=3.8.1" 1158 | files = [ 1159 | {file = "sh-2.0.7-py3-none-any.whl", hash = "sha256:2f2f79a65abd00696cf2e9ad26508cf8abb6dba5745f40255f1c0ded2876926d"}, 1160 | {file = "sh-2.0.7.tar.gz", hash = "sha256:029d45198902bfb967391eccfd13a88d92f7cebd200411e93f99ebacc6afbb35"}, 1161 | ] 1162 | 1163 | [[package]] 1164 | name = "six" 1165 | version = "1.16.0" 1166 | description = "Python 2 and 3 compatibility utilities" 1167 | optional = false 1168 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 1169 | files = [ 1170 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1171 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "sqlparse" 1176 | version = "0.5.1" 1177 | description = "A non-validating SQL parser." 1178 | optional = false 1179 | python-versions = ">=3.8" 1180 | files = [ 1181 | {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, 1182 | {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, 1183 | ] 1184 | 1185 | [package.extras] 1186 | dev = ["build", "hatch"] 1187 | doc = ["sphinx"] 1188 | 1189 | [[package]] 1190 | name = "typing-extensions" 1191 | version = "4.12.2" 1192 | description = "Backported and Experimental Type Hints for Python 3.8+" 1193 | optional = false 1194 | python-versions = ">=3.8" 1195 | files = [ 1196 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 1197 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 1198 | ] 1199 | 1200 | [[package]] 1201 | name = "tzdata" 1202 | version = "2024.1" 1203 | description = "Provider of IANA time zone data" 1204 | optional = false 1205 | python-versions = ">=2" 1206 | files = [ 1207 | {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, 1208 | {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, 1209 | ] 1210 | 1211 | [[package]] 1212 | name = "urllib3" 1213 | version = "2.2.2" 1214 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1215 | optional = false 1216 | python-versions = ">=3.8" 1217 | files = [ 1218 | {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, 1219 | {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, 1220 | ] 1221 | 1222 | [package.extras] 1223 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 1224 | h2 = ["h2 (>=4,<5)"] 1225 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1226 | zstd = ["zstandard (>=0.18.0)"] 1227 | 1228 | [[package]] 1229 | name = "virtualenv" 1230 | version = "20.26.3" 1231 | description = "Virtual Python Environment builder" 1232 | optional = false 1233 | python-versions = ">=3.7" 1234 | files = [ 1235 | {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, 1236 | {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, 1237 | ] 1238 | 1239 | [package.dependencies] 1240 | distlib = ">=0.3.7,<1" 1241 | filelock = ">=3.12.2,<4" 1242 | platformdirs = ">=3.9.1,<5" 1243 | 1244 | [package.extras] 1245 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1246 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 1247 | 1248 | [[package]] 1249 | name = "wmctrl" 1250 | version = "0.5" 1251 | description = "A tool to programmatically control windows inside X" 1252 | optional = false 1253 | python-versions = ">=2.7" 1254 | files = [ 1255 | {file = "wmctrl-0.5-py2.py3-none-any.whl", hash = "sha256:ae695c1863a314c899e7cf113f07c0da02a394b968c4772e1936219d9234ddd7"}, 1256 | {file = "wmctrl-0.5.tar.gz", hash = "sha256:7839a36b6fe9e2d6fd22304e5dc372dbced2116ba41283ea938b2da57f53e962"}, 1257 | ] 1258 | 1259 | [package.dependencies] 1260 | attrs = "*" 1261 | 1262 | [package.extras] 1263 | test = ["pytest"] 1264 | 1265 | [metadata] 1266 | lock-version = "2.0" 1267 | python-versions = "^3.12" 1268 | content-hash = "15906d3c88739d1d4522b74a0324905dc0389ed3147c046786fde57bb177d2cb" 1269 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "{{cookiecutter.project_name}}" 3 | version = "0.0.0" 4 | description = "{{cookiecutter.description}}" 5 | repository = "{{cookiecutter.url}}" 6 | authors = ["{{cookiecutter.organization_slug}}"] 7 | license = "GPL-3.0-or-later" 8 | readme = "../README.md" 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.12" 12 | django = "^4.2.11" 13 | django-environ = "^0.11.2" 14 | django-filter = "^24.2" 15 | djangorestframework = "^3.15.2" 16 | djangorestframework-jsonapi = "^7.0.2" 17 | gunicorn = "^22.0.0" 18 | mozilla-django-oidc = "^4.0.1" 19 | psycopg2 = "^2.9.9" 20 | 21 | [tool.poetry.group.dev.dependencies] 22 | ruff = "^0.3.3" 23 | factory-boy = "^3.3.0" 24 | gitlint = "^0.19.1" 25 | pdbpp = "^0.10.3" 26 | pre-commit = "^3.7.1" 27 | pytest = "^8.2.2" 28 | pytest-cov = "^5.0.0" 29 | pytest-django = "^4.8.0" 30 | pytest-env = "^1.1.3" 31 | pytest-factoryboy = "^2.7.0" 32 | pytest-freezer = "^0.4.8" 33 | pytest-mock = "^3.14.0" 34 | pytest-randomly = "^3.15.0" 35 | requests-mock = "^1.12.1" 36 | 37 | [tool.ruff] 38 | exclude = [ 39 | "migrations", 40 | ] 41 | line-length = 88 42 | 43 | [tool.ruff.lint] 44 | select = ["ALL"] 45 | ignore = [ 46 | "A003", # `flake8-builtins` - Class attribute {name} is shadowing a Python builtin 47 | "ANN", # `flake8-annotations` 48 | "ARG", # `flake8-unused-arguments` 49 | "COM812", # handled by ruff format 50 | "D100", # Missing docstring in public module 51 | "D101", # Missing docstring in public class 52 | "D102", # Missing docstring in public method 53 | "D103", # Missing docstring in public function 54 | "D104", # Missing docstring in public package 55 | "D105", # Missing docstring in magic method 56 | "D106", # Missing docstring in public nested class 57 | "D107", # Missing docstring in __init__ 58 | "D202", # No blank lines allowed after function docstring (found {num_lines}) 59 | "D203", # 1 blank line required before class docstring 60 | "D212", # Multi-line docstring summary should start at the first line 61 | "DJ001", # flake8-django` - Avoid using null=True on string-based fields 62 | "E501", # Line too long ({width} > {limit} characters) - managed by ruff format 63 | "ERA001", # eradicate - Found commented-out code 64 | "FA100", # `future-rewritable-type-annotation` - obsolete in python >= 3.9 65 | "FBT002", # `flake8-boolean-trap` - Boolean default positional argument in function definition 66 | "FBT003", # Boolean positional value in function call 67 | "FIX", # `flake8-fixme` 68 | "ISC001", # handled by ruff format 69 | "N818", # Exception name {name} should be named with an Error suffix - https://github.com/astral-sh/ruff/issues/5367 70 | "PERF203", # `try-except-in-loop` - obsolete in python >= 3.11 71 | "PGH005", # doesn't work correctly with `requests-mock` 72 | "PLR0913", # Too many arguments to function call 73 | "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable 74 | "PT006", # Wrong name(s) type in @pytest.mark.parametrize, expected {expected} 75 | "PT017", # Found assertion on exception {name} in except block, use pytest.raises() instead 76 | "PLW2901", # `for` loop variable `search_term` overwritten by assignment target 77 | "RET502", # Do not implicitly return None in function able to return non-None value 78 | "RET503", # Missing explicit return at the end of function able to return non-None value 79 | "RUF001", # ambiguous-unicode-character-string 80 | "RUF012", # Mutable class attributes should be annotated with typing.ClassVar 81 | "S101", # Use of assert detected 82 | "TD002", # missing-todo-author 83 | "TD003", # missing-todo-link 84 | "TID252", # banned-module-level-imports 85 | ] 86 | 87 | [tool.ruff.lint.mccabe] 88 | max-complexity = 10 89 | 90 | [tool.ruff.lint.isort] 91 | known-first-party = ["{{cookiecutter.project_name}}"] 92 | combine-as-imports = true 93 | 94 | [tool.ruff.lint.flake8-pytest-style] 95 | fixture-parentheses = false 96 | 97 | [tool.ruff.lint.per-file-ignores] 98 | "manage.py" = ["INP001"] 99 | 100 | [tool.pytest.ini_options] 101 | addopts = "--reuse-db --randomly-seed=1521188766 --randomly-dont-reorganize" 102 | DJANGO_SETTINGS_MODULE = "{{cookiecutter.project_name}}.settings" 103 | env = [ 104 | "ADMINS=Test Example ,Test2 ", 105 | ] 106 | filterwarnings = [ 107 | "error::DeprecationWarning", 108 | "error::PendingDeprecationWarning", 109 | "ignore:distutils Version classes are deprecated. Use packaging.version instead.:DeprecationWarning", # issue in pytest-freezer 110 | "ignore:CSR support in pyOpenSSL is deprecated. You should use the APIs in cryptography.:DeprecationWarning" 111 | ] 112 | 113 | [tool.coverage.run] 114 | source = ["."] 115 | 116 | [tool.coverage.report] 117 | fail_under = 100 118 | exclude_lines = [ 119 | "pragma: no cover", 120 | "pragma: todo cover", 121 | "def __str__", 122 | "def __unicode__", 123 | "def __repr__", 124 | ] 125 | omit = [ 126 | "*/migrations/*", 127 | "*/apps.py", 128 | "manage.py", 129 | "setup.py", 130 | "{{cookiecutter.project_name}}/settings_*.py", 131 | "{{cookiecutter.project_name}}/wsgi.py", 132 | ] 133 | show_missing = true 134 | 135 | [build-system] 136 | requires = ["poetry-core>=1.0.0"] 137 | build-backend = "poetry.core.masonry.api" 138 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/setup.py: -------------------------------------------------------------------------------- 1 | """Setuptools package definition.""" 2 | 3 | from setuptools import find_packages, setup 4 | 5 | setup( 6 | name="{{cookiecutter.project_name}}", 7 | version="0.0.0", 8 | author="{{cookiecutter.organization_slug}}", 9 | description="{{cookiecutter.description}}", 10 | url="{{cookiecutter.url}}", 11 | packages=find_packages(), 12 | ) 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http = 0.0.0.0:8000 3 | wsgi-file = /app/{{cookiecutter.project_name}}/wsgi.py 4 | max-requests = 2000 5 | harakiri = 5 6 | processes = 4 7 | master = True 8 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/cookiecutter-django-json-api/791714d13046e7f000eb3bac20c5dfb20193522f/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/conftest.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | import pytest 4 | from django.core.cache import cache 5 | from pytest_factoryboy import register 6 | from rest_framework.test import APIClient 7 | 8 | from .{{cookiecutter.django_app}} import factories 9 | from .{{cookiecutter.django_app}}.models import OIDCUser 10 | 11 | register(factories.UserProfileFactory) 12 | 13 | 14 | def _get_claims( 15 | settings, 16 | id_claim="00000000-0000-0000-0000-000000000000", 17 | groups_claim=None, 18 | email_claim="test@example.com", 19 | first_name_claim=None, 20 | last_name_claim=None, 21 | ): 22 | groups_claim = groups_claim if groups_claim else [] 23 | return { 24 | settings.OIDC_ID_CLAIM: id_claim, 25 | settings.OIDC_GROUPS_CLAIM: groups_claim, 26 | settings.OIDC_EMAIL_CLAIM: email_claim, 27 | settings.OIDC_FIRST_NAME_CLAIM: first_name_claim, 28 | settings.OIDC_LAST_NAME_CLAIM: last_name_claim, 29 | } 30 | 31 | 32 | @pytest.fixture 33 | def get_claims(settings): 34 | return partial(_get_claims, settings) 35 | 36 | 37 | @pytest.fixture 38 | def claims(settings): 39 | return _get_claims(settings) 40 | 41 | 42 | @pytest.fixture 43 | def admin_user(settings, get_claims): 44 | return OIDCUser( 45 | "sometoken", 46 | get_claims( 47 | id_claim="admin", 48 | groups_claim=[settings.ADMIN_GROUP], 49 | email_claim="admin@example.com", 50 | ), 51 | ) 52 | 53 | 54 | @pytest.fixture 55 | def user(get_claims): 56 | return OIDCUser( 57 | "sometoken", 58 | get_claims(id_claim="user", groups_claim=[], email_claim="user@example.com"), 59 | ) 60 | 61 | 62 | @pytest.fixture(params=["admin"]) 63 | def client(db, user, admin_user, request): 64 | usermap = {"user": user, "admin": admin_user} 65 | client = APIClient() 66 | user = usermap[request.param] 67 | client.force_authenticate(user=user) 68 | client.user = user 69 | return client 70 | 71 | 72 | @pytest.fixture(autouse=True) 73 | def _autoclear_cache(): 74 | cache.clear() 75 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.db import models 4 | 5 | 6 | class UUIDModel(models.Model): 7 | """ 8 | Models which use uuid as primary key. 9 | 10 | Defined as {{cookiecutter.project_name}} default 11 | """ 12 | 13 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 14 | 15 | class Meta: 16 | abstract = True 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | import environ 5 | 6 | env = environ.Env() 7 | django_root = environ.Path(__file__) - 2 8 | 9 | ENV_FILE = env.str("ENV_FILE", default=django_root(".env")) 10 | if os.path.exists(ENV_FILE): # noqa: PTH110 11 | environ.Env.read_env(ENV_FILE) 12 | 13 | # per default production is enabled for security reasons 14 | # for development create .env file with ENV=development 15 | ENV = env.str("ENV", "production") 16 | 17 | 18 | def default(default_dev=env.NOTSET, default_prod=env.NOTSET): 19 | """Environment aware default.""" 20 | return default_prod if ENV == "production" else default_dev 21 | 22 | 23 | SECRET_KEY = env.str("SECRET_KEY", default=default("uuuuuuuuuu")) 24 | DEBUG = env.bool("DEBUG", default=default(True, False)) 25 | ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=default(["*"])) 26 | 27 | 28 | # Application definition 29 | 30 | INSTALLED_APPS = [ 31 | "django.contrib.auth", 32 | "django.contrib.contenttypes", 33 | "django.contrib.postgres", 34 | "{{cookiecutter.project_name}}.{{cookiecutter.django_app}}.apps.DefaultConfig", 35 | ] 36 | 37 | MIDDLEWARE = [ 38 | "django.middleware.security.SecurityMiddleware", 39 | "django.middleware.common.CommonMiddleware", 40 | "django.middleware.locale.LocaleMiddleware", 41 | ] 42 | 43 | ROOT_URLCONF = "{{cookiecutter.project_name}}.urls" 44 | WSGI_APPLICATION = "{{cookiecutter.project_name}}.wsgi.application" 45 | 46 | 47 | # Database 48 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 49 | 50 | DATABASES = { 51 | "default": { 52 | "ENGINE": env.str( 53 | "DATABASE_ENGINE", default="django.db.backends.postgresql_psycopg2" 54 | ), 55 | "NAME": env.str("DATABASE_NAME", default="{{cookiecutter.project_name}}"), 56 | "USER": env.str("DATABASE_USER", default="{{cookiecutter.project_name}}"), 57 | "PASSWORD": env.str( 58 | "DATABASE_PASSWORD", default=default("{{cookiecutter.project_name}}") 59 | ), 60 | "HOST": env.str("DATABASE_HOST", default="localhost"), 61 | "PORT": env.str("DATABASE_PORT", default=""), 62 | "OPTIONS": env.dict("DATABASE_OPTIONS", default={}), 63 | } 64 | } 65 | 66 | # Password validation 67 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 68 | 69 | AUTH_PASSWORD_VALIDATORS = [ 70 | { 71 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 72 | }, 73 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 74 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 75 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 76 | ] 77 | 78 | 79 | # Internationalization 80 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 81 | 82 | LANGUAGE_CODE = env.str("LANGUAGE_CODE", "en-us") 83 | TIME_ZONE = env.str("TIME_ZONE", "UTC") 84 | USE_I18N = True 85 | USE_TZ = True 86 | 87 | REST_FRAMEWORK = { 88 | "EXCEPTION_HANDLER": "rest_framework_json_api.exceptions.exception_handler", 89 | "DEFAULT_PAGINATION_CLASS": "rest_framework_json_api.pagination.JsonApiPageNumberPagination", 90 | "DEFAULT_PARSER_CLASSES": ( 91 | "rest_framework_json_api.parsers.JSONParser", 92 | "rest_framework.parsers.JSONParser", 93 | "rest_framework.parsers.FormParser", 94 | "rest_framework.parsers.MultiPartParser", 95 | ), 96 | "DEFAULT_RENDERER_CLASSES": ( 97 | "rest_framework_json_api.renderers.JSONRenderer", 98 | "rest_framework.renderers.JSONRenderer", 99 | ), 100 | "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), 101 | "DEFAULT_AUTHENTICATION_CLASSES": ( 102 | "mozilla_django_oidc.contrib.drf.OIDCAuthentication", 103 | ), 104 | "DEFAULT_METADATA_CLASS": "rest_framework_json_api.metadata.JSONAPIMetadata", 105 | "DEFAULT_FILTER_BACKENDS": ( 106 | "rest_framework_json_api.filters.QueryParameterValidationFilter", 107 | "rest_framework_json_api.filters.OrderingFilter", 108 | "rest_framework_json_api.django_filters.DjangoFilterBackend", 109 | "rest_framework.filters.SearchFilter", 110 | ), 111 | "ORDERING_PARAM": "sort", 112 | "TEST_REQUEST_RENDERER_CLASSES": ( 113 | "rest_framework_json_api.renderers.JSONRenderer", 114 | "rest_framework.renderers.JSONRenderer", 115 | ), 116 | "TEST_REQUEST_DEFAULT_FORMAT": "vnd.api+json", 117 | } 118 | 119 | JSON_API_FORMAT_FIELD_NAMES = "dasherize" 120 | JSON_API_FORMAT_TYPES = "dasherize" 121 | JSON_API_PLURALIZE_TYPES = True 122 | 123 | # Authentication 124 | OIDC_OP_USER_ENDPOINT = env.str("OIDC_OP_USER_ENDPOINT", default=None) 125 | OIDC_OP_TOKEN_ENDPOINT = "not supported in {{cookiecutter.project_name}}, but a value is needed" # noqa: S105 126 | OIDC_VERIFY_SSL = env.bool("OIDC_VERIFY_SSL", default=True) 127 | OIDC_ID_CLAIM = env.str("OIDC_ID_CLAIM", default="sub") 128 | OIDC_EMAIL_CLAIM = env.str("OIDC_EMAIL_CLAIM", default="email") 129 | OIDC_FIRST_NAME_CLAIM = env.str("OIDC_FIRST_NAME_CLAIM", default="given_name") 130 | OIDC_LAST_NAME_CLAIM = env.str("OIDC_LAST_NAME_CLAIM", default="family_name") 131 | OIDC_GROUPS_CLAIM = env.str("OIDC_GROUPS_CLAIM", default="{{cookiecutter.project_name}}_groups") 132 | OIDC_CLIENT_GRANT_USERNAME_CLAIM = env.str( 133 | "OIDC_CLIENT_GRANT_USERNAME_CLAIM", 134 | default="preferred_username", 135 | ) 136 | OIDC_BEARER_TOKEN_REVALIDATION_TIME = env.int( 137 | "OIDC_BEARER_TOKEN_REVALIDATION_TIME", 138 | default=300, 139 | ) 140 | OIDC_DRF_AUTH_BACKEND = "{{cookiecutter.project_name}}.{{cookiecutter.django_app}}.authentication.OIDCAuthenticationBackend" 141 | 142 | 143 | # Needed to instantiate `mozilla_django_oidc.auth.OIDCAuthenticationBackend` 144 | OIDC_RP_CLIENT_ID = None 145 | OIDC_RP_CLIENT_SECRET = None 146 | 147 | ADMIN_GROUP = env.str("ADMIN_GROUP", default="admin") 148 | 149 | 150 | def parse_admins(admins): 151 | """ 152 | Parse env admins to django admins. 153 | 154 | Example of ADMINS environment variable: 155 | Test Example ,Test2 156 | """ 157 | result = [] 158 | for admin in admins: 159 | match = re.search(r"(.+) \<(.+@.+)\>", admin) 160 | if not match: # pragma: no cover 161 | msg = ( 162 | f'In ADMINS admin "{admin}" is not in correct ' 163 | '"Firstname Lastname " format' 164 | ) 165 | raise environ.ImproperlyConfigured(msg) 166 | result.append((match.group(1), match.group(2))) 167 | return result 168 | 169 | 170 | ADMINS = parse_admins(env.list("ADMINS", default=[])) 171 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include 2 | from django.urls import path 3 | 4 | urlpatterns = [ 5 | path( 6 | "api/v1/", 7 | include("{{cookiecutter.project_name}}.{{cookiecutter.django_app}}.urls"), 8 | ), 9 | ] 10 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for {{cookiecutter.django_app}} project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault( 15 | "DJANGO_SETTINGS_MODULE", "{{cookiecutter.project_name}}.settings" 16 | ) 17 | 18 | application = get_wsgi_application() 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/cookiecutter-django-json-api/791714d13046e7f000eb3bac20c5dfb20193522f/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DefaultConfig(AppConfig): 5 | name = "{{cookiecutter.project_name}}.{{cookiecutter.django_app}}" 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/authentication.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import hashlib 3 | import warnings 4 | 5 | from django.conf import settings 6 | from django.core.cache import cache 7 | from django.core.exceptions import SuspiciousOperation 8 | from django.utils.encoding import force_bytes 9 | from mozilla_django_oidc.auth import OIDCAuthenticationBackend 10 | from urllib3.exceptions import InsecureRequestWarning 11 | 12 | from .models import OIDCUser 13 | 14 | 15 | class OIDCAuthenticationBackend(OIDCAuthenticationBackend): 16 | def verify_claims(self, claims): 17 | # claims for human users 18 | claims_to_verify = [ 19 | settings.OIDC_ID_CLAIM, 20 | settings.OIDC_EMAIL_CLAIM, 21 | ] 22 | 23 | for claim in claims_to_verify: 24 | if claim not in claims: 25 | msg = f'Couldn\'t find "{claim}" claim' 26 | raise SuspiciousOperation(msg) 27 | 28 | def get_or_create_user(self, access_token, id_token, payload): 29 | """Verify claims and return user, otherwise raise an Exception.""" 30 | 31 | claims = self.cached_request(access_token, id_token, payload) 32 | 33 | self.verify_claims(claims) 34 | 35 | return OIDCUser(access_token, claims) 36 | 37 | def cached_request(self, access_token, id_token, payload): 38 | token_hash = hashlib.sha256(force_bytes(access_token)).hexdigest() 39 | 40 | func = functools.partial(self.get_userinfo, access_token, id_token, payload) 41 | 42 | with warnings.catch_warnings(): 43 | if settings.DEBUG: # pragma: no cover 44 | warnings.simplefilter("ignore", InsecureRequestWarning) 45 | return cache.get_or_set( 46 | f"auth.userinfo.{token_hash}", 47 | func, 48 | timeout=settings.OIDC_BEARER_TOKEN_REVALIDATION_TIME, 49 | ) 50 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/factories.py: -------------------------------------------------------------------------------- 1 | from factory import Faker 2 | from factory.django import DjangoModelFactory 3 | 4 | from . import models 5 | 6 | 7 | class UserProfileFactory(DjangoModelFactory): 8 | idp_id = Faker("uuid4") 9 | email = Faker("email") 10 | first_name = Faker("first_name") 11 | last_name = Faker("last_name") 12 | 13 | class Meta: 14 | model = models.UserProfile 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/filters.py: -------------------------------------------------------------------------------- 1 | # from django_filters import Filter 2 | 3 | # Add your filters here 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/cookiecutter-django-json-api/791714d13046e7f000eb3bac20c5dfb20193522f/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/migrations/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | from django.db import IntegrityError, models, transaction 5 | from django.db.models import Q 6 | 7 | from ..models import UUIDModel 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class UserProfile(UUIDModel): 13 | idp_id = models.CharField( 14 | max_length=255, unique=True, null=True, blank=False, db_index=True 15 | ) 16 | email = models.EmailField(unique=True, null=True, blank=True, db_index=True) 17 | first_name = models.CharField(max_length=255, null=True, blank=True) 18 | last_name = models.CharField(max_length=255, null=True, blank=True) 19 | 20 | class Meta: 21 | ordering = ("last_name", "first_name", "email") 22 | 23 | 24 | class BaseOIDCUser: # pragma: no cover 25 | def __init__(self): 26 | self.email = None 27 | self.groups = [] 28 | self.group = None 29 | self.token = None 30 | self.claims = {} 31 | self.is_authenticated = False 32 | 33 | def __str__(self): 34 | raise NotImplementedError 35 | 36 | @property 37 | def is_admin(self): 38 | return settings.ADMIN_GROUP in self.groups 39 | 40 | 41 | class OIDCUser(BaseOIDCUser): 42 | def __init__(self, token: str, claims: dict): 43 | super().__init__() 44 | 45 | self.claims = claims 46 | self.id = self.claims[settings.OIDC_ID_CLAIM] 47 | self.email = self.claims.get(settings.OIDC_EMAIL_CLAIM) 48 | self.first_name = self.claims.get(settings.OIDC_FIRST_NAME_CLAIM) 49 | self.last_name = self.claims.get(settings.OIDC_LAST_NAME_CLAIM) 50 | 51 | self.groups = self.claims.get(settings.OIDC_GROUPS_CLAIM, []) 52 | self.group = self.groups[0] if self.groups else None 53 | self.token = token 54 | self.is_authenticated = True 55 | self.profile = self._update_or_create_profile() 56 | 57 | def _update_or_create_profile(self): 58 | """ 59 | Update or create UserProfile. 60 | 61 | Analogous to QuerySet.get_or_create(), in order to handle race conditions as 62 | gracefully as possible. 63 | """ 64 | try: 65 | profile = UserProfile.objects.get( 66 | Q(idp_id=self.id) | Q(email__iexact=self.email), 67 | ) 68 | # we only want to save if necessary 69 | if profile.idp_id != self.id or profile.email != self.email: 70 | profile.idp_id = self.id 71 | profile.email = self.email 72 | profile.save() 73 | except UserProfile.MultipleObjectsReturned: 74 | # TODO: trigger notification for staff members or admins 75 | logger.warning( 76 | "Found one UserProfile with same idp_id and one with same email. " 77 | "Matching on idp_id.", 78 | ) 79 | return UserProfile.objects.get(idp_id=self.id) 80 | except UserProfile.DoesNotExist: 81 | try: 82 | with transaction.atomic(using=UserProfile.objects.db): 83 | return UserProfile.objects.create( 84 | idp_id=self.id, 85 | email=self.email, 86 | first_name=self.first_name, 87 | last_name=self.last_name, 88 | ) 89 | except IntegrityError: # pragma: no cover 90 | # race condition happened 91 | try: 92 | return UserProfile.objects.get(idp_id=self.id) 93 | except UserProfile.DoesNotExist: 94 | pass 95 | raise 96 | else: 97 | return profile 98 | 99 | def __str__(self): 100 | return f"{self.email} - {self.id}" 101 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework_json_api import serializers 2 | 3 | from . import models 4 | 5 | 6 | class UserSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = models.UserProfile 9 | fields = ( 10 | "idp_id", 11 | "first_name", 12 | "last_name", 13 | "email", 14 | ) 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/cookiecutter-django-json-api/791714d13046e7f000eb3bac20c5dfb20193522f/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/tests/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/tests/test_authentication.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | from uuid import uuid4 4 | 5 | import pytest 6 | from django.core.cache import cache 7 | from mozilla_django_oidc.contrib.drf import OIDCAuthentication 8 | from requests.exceptions import HTTPError 9 | from rest_framework import exceptions, status 10 | from rest_framework.exceptions import AuthenticationFailed 11 | 12 | from {{cookiecutter.project_name}}.{{cookiecutter.django_app}}.models import UserProfile 13 | 14 | 15 | @pytest.mark.parametrize( 16 | "authentication_header,authenticated,error", 17 | [ 18 | ("", False, False), 19 | ("Bearer", False, True), 20 | ("Bearer Too many params", False, True), 21 | ("Basic Auth", False, True), 22 | ("Bearer Token", True, False), 23 | ], 24 | ) 25 | def test_authentication( 26 | db, 27 | rf, 28 | authentication_header, 29 | authenticated, 30 | error, 31 | requests_mock, 32 | settings, 33 | claims, 34 | ): 35 | assert UserProfile.objects.count() == 0 36 | 37 | requests_mock.get(settings.OIDC_OP_USER_ENDPOINT, text=json.dumps(claims)) 38 | 39 | request = rf.get("/openid", HTTP_AUTHORIZATION=authentication_header) 40 | 41 | try: 42 | result = OIDCAuthentication().authenticate(request) 43 | except exceptions.AuthenticationFailed: 44 | assert error 45 | else: 46 | if authenticated: 47 | user, auth = result 48 | assert user.is_authenticated 49 | assert auth == authentication_header.split(" ")[1] 50 | assert ( 51 | cache.get(f"auth.userinfo.{hashlib.sha256(b'Token').hexdigest()}") 52 | == claims 53 | ) 54 | assert UserProfile.objects.count() == 1 55 | else: 56 | assert result is None 57 | 58 | 59 | @pytest.mark.parametrize("email_claim", ["foo@example.com", "bar@example.com"]) 60 | @pytest.mark.parametrize("first_name_claim", ["Winston", "Hagbard"]) 61 | @pytest.mark.parametrize("last_name_claim", ["Smith", "Celine"]) 62 | def test_authentication_profile_create( 63 | db, 64 | rf, 65 | requests_mock, 66 | settings, 67 | get_claims, 68 | email_claim, 69 | first_name_claim, 70 | last_name_claim, 71 | ): 72 | idp_id = str(uuid4()) 73 | claims = get_claims( 74 | id_claim=idp_id, 75 | email_claim=email_claim, 76 | first_name_claim=first_name_claim, 77 | last_name_claim=last_name_claim, 78 | ) 79 | assert UserProfile.objects.count() == 0 80 | 81 | requests_mock.get(settings.OIDC_OP_USER_ENDPOINT, text=json.dumps(claims)) 82 | 83 | request = rf.get("/openid", HTTP_AUTHORIZATION="Bearer Token") 84 | 85 | result = OIDCAuthentication().authenticate(request) 86 | user, auth = result 87 | assert user.is_authenticated 88 | assert cache.get(f"auth.userinfo.{hashlib.sha256(b'Token').hexdigest()}") == claims 89 | assert UserProfile.objects.count() == 1 90 | 91 | profile = UserProfile.objects.get(idp_id=idp_id) 92 | 93 | assert [ 94 | profile.email, 95 | profile.first_name, 96 | profile.last_name, 97 | ] == [ 98 | email_claim, 99 | first_name_claim, 100 | last_name_claim, 101 | ] 102 | 103 | 104 | @pytest.mark.parametrize( 105 | "user_profile__email,user_profile__first_name,user_profile__last_name", 106 | [ 107 | ( 108 | "foo@example.com", 109 | "Winston", 110 | "Smith", 111 | ) 112 | ], 113 | ) 114 | @pytest.mark.parametrize("email_claim", ["bar@example.com"]) 115 | @pytest.mark.parametrize("first_name_claim", ["Hagbard"]) 116 | @pytest.mark.parametrize("last_name_claim", ["Celine"]) 117 | def test_authentication_profile_update_existing_profile( 118 | db, 119 | rf, 120 | requests_mock, 121 | settings, 122 | get_claims, 123 | user_profile, 124 | email_claim, 125 | first_name_claim, 126 | last_name_claim, 127 | ): 128 | claims = get_claims( 129 | id_claim=str(user_profile.idp_id), 130 | email_claim=email_claim, 131 | first_name_claim=first_name_claim, 132 | last_name_claim=last_name_claim, 133 | ) 134 | assert UserProfile.objects.count() == 1 135 | 136 | requests_mock.get(settings.OIDC_OP_USER_ENDPOINT, text=json.dumps(claims)) 137 | 138 | request = rf.get("/openid", HTTP_AUTHORIZATION="Bearer Token") 139 | 140 | result = OIDCAuthentication().authenticate(request) 141 | assert result[0].is_authenticated 142 | assert UserProfile.objects.count() == 1 143 | 144 | user_profile.refresh_from_db() 145 | 146 | # only the email should be updated 147 | assert user_profile.email == email_claim 148 | assert user_profile.first_name == "Winston" 149 | assert user_profile.last_name == "Smith" 150 | 151 | 152 | def test_authentication_multiple_existing_profile( 153 | db, 154 | rf, 155 | requests_mock, 156 | settings, 157 | user_profile_factory, 158 | get_claims, 159 | caplog, 160 | ): 161 | user_profile = user_profile_factory(idp_id="matching_id") 162 | user_profile_factory(email="match@example.com") 163 | claims = get_claims( 164 | id_claim="matching_id", 165 | groups_claim=[], 166 | email_claim="match@example.com", 167 | ) 168 | 169 | requests_mock.get(settings.OIDC_OP_USER_ENDPOINT, text=json.dumps(claims)) 170 | 171 | assert UserProfile.objects.count() == 2 172 | 173 | request = rf.get("/openid", HTTP_AUTHORIZATION="Bearer Token") 174 | 175 | result = OIDCAuthentication().authenticate(request) 176 | 177 | user, auth = result 178 | assert user.is_authenticated 179 | assert user.profile == user_profile 180 | assert UserProfile.objects.count() == 2 181 | assert caplog.records[0].msg == ( 182 | "Found one UserProfile with same idp_id and one with same email. " 183 | "Matching on idp_id." 184 | ) 185 | 186 | 187 | def test_authentication_idp_502( 188 | db, 189 | rf, 190 | requests_mock, 191 | settings, 192 | ): 193 | requests_mock.get( 194 | settings.OIDC_OP_USER_ENDPOINT, 195 | status_code=status.HTTP_502_BAD_GATEWAY, 196 | ) 197 | 198 | request = rf.get("/openid", HTTP_AUTHORIZATION="Bearer Token") 199 | with pytest.raises(HTTPError): 200 | OIDCAuthentication().authenticate(request) 201 | 202 | 203 | def test_authentication_idp_missing_claim( 204 | db, 205 | rf, 206 | requests_mock, 207 | settings, 208 | claims, 209 | ): 210 | settings.OIDC_ID_CLAIM = "missing" 211 | requests_mock.get(settings.OIDC_OP_USER_ENDPOINT, text=json.dumps(claims)) 212 | 213 | request = rf.get("/openid", HTTP_AUTHORIZATION="Bearer Token") 214 | with pytest.raises(AuthenticationFailed): 215 | OIDCAuthentication().authenticate(request) 216 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/tests/test_user.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from rest_framework import status 3 | 4 | 5 | def test_user_detail(client): 6 | url = reverse("userprofile-detail", args=[client.user.profile.id]) 7 | 8 | response = client.get(url) 9 | 10 | assert response.status_code == status.HTTP_200_OK 11 | 12 | json = response.json() 13 | assert json["data"]["id"] == str(client.user.profile.id) 14 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework.routers import SimpleRouter 2 | 3 | from . import views 4 | 5 | r = SimpleRouter(trailing_slash=False) 6 | 7 | r.register(r"users", views.UserViewSet) 8 | 9 | urlpatterns = r.urls 10 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/{{cookiecutter.django_app}}/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework_json_api import views 2 | 3 | from . import models, serializers 4 | 5 | 6 | class UserViewSet(views.ModelViewSet): 7 | serializer_class = serializers.UserSerializer 8 | queryset = models.UserProfile.objects 9 | 10 | def get_queryset(self): 11 | queryset = super().get_queryset() 12 | user = self.request.user 13 | return queryset.filter(idp_id=user.id) 14 | --------------------------------------------------------------------------------