From b12413ed673cbda058cb9aa3d6f9e10c924efd90 Mon Sep 17 00:00:00 2001 From: Nikolai R Kristiansen Date: Sun, 7 Aug 2022 15:44:40 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Modernize=20bit=20bit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bumps - poetry - linting --- .gitignore | 1 + .gitlab-ci.yml | 53 +- .pre-commit-config.yaml | 36 + README.md | 20 +- bin/deploy | 18 + bin/ensure-python-deps.sh | 6 + bin/format | 9 + bin/lint | 4 + bin/run | 3 + bin/test | 3 + dpdapi/admin.py | 6 +- dpdapi/filters.py | 5 +- dpdapi/management/commands/import_valiases.py | 38 +- dpdapi/migrations/0001_initial.py | 58 +- dpdapi/migrations/0002_auto_20150118_1638.py | 11 +- dpdapi/migrations/0003_auto_20200909_2342.py | 6 +- dpdapi/models.py | 17 +- dpdapi/serializers.py | 20 +- dpdapi/settings.py | 106 ++ dpdapi/tests.py | 32 +- dpdapi/urls.py | 19 +- dpdapi/views.py | 34 +- dpdapi/wsgi.py | 7 + dpdapisite/__init__.py | 0 dpdapisite/settings.py | 150 --- dpdapisite/urls.py | 14 - dpdapisite/wsgi.py | 15 - manage.py | 2 +- poetry.lock | 991 ++++++++++++++++++ pyproject.toml | 47 + requirements.txt | 9 - 31 files changed, 1409 insertions(+), 331 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100755 bin/deploy create mode 100755 bin/ensure-python-deps.sh create mode 100755 bin/format create mode 100755 bin/lint create mode 100755 bin/run create mode 100755 bin/test create mode 100644 dpdapi/settings.py create mode 100644 dpdapi/wsgi.py delete mode 100644 dpdapisite/__init__.py delete mode 100644 dpdapisite/settings.py delete mode 100644 dpdapisite/urls.py delete mode 100644 dpdapisite/wsgi.py create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 420f626..08c0b26 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ *.pyc .idea venv +dpdapi/local.py local_settings.py db.sqlite3 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ac99977..ff58afd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,19 +1,44 @@ image: python:latest -variables: - PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache" - -cache: - paths: - - "$CI_PROJECT_DIR/pip-cache" - key: "$CI_PROJECT_ID" - -before_script: - - pip install -r requirements.txt - - pip install coverage +default: + image: python:3.8-slim + cache: + paths: + - .cache/pip + - .venv test: + before_script: + - poetry install script: - - coverage run --source=dpdapi --omit=dpdapi/migrations/* manage.py test -v2 - - coverage report - coverage: '/TOTAL.+ ([0-9]{1,3}%)/' + - bin/ensure-python-deps.sh + - poetry run python manage.py migrate + - poetry run python manage.py check + - poetry run coverage run --source=dpdapi --omit=dpdapi/migrations/* -m pytest -v + - poetry run coverage report + - poetry run coverage xml + - poetry run bin/lint + coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml + +deploy: + stage: deploy + before_script: + # Install ssh if not already + - 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )' + - eval $(ssh-agent -s) + # Make SSH key available + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + # Add known host keys + - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + script: + - ssh gitdeploy@lynx.neuf.no "$(< bin/deploy)" + only: + - main diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..324b1db --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: local + hooks: + - id: black + name: black + entry: black + args: [--check] + language: system + require_serial: true + types: [python] + - id: isort + name: isort + entry: isort + args: [--check] + language: system + types: [python] + - id: pycln + name: pycln + entry: pycln + args: [--check] + language: system + types: [python] + - id: pyupgrade + name: pyupgrade + entry: pyupgrade + args: [--py38-plus] + language: system + types: [python] + - id: django-upgrade + name: django-upgrade + entry: django-upgrade + language: system + types: [python] + args: [--target-version, "3.2"] \ No newline at end of file diff --git a/README.md b/README.md index 7c79b0c..98482c8 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,20 @@ -# Install - apt install python-dev libmysqlclient-dev python-virtualenv - python -m venv venv - . venv/bin/activate - pip install -r requirements.txt +# django-postfix-dovecot-api + +## Getting started + +```bash +sudo apt install libmysqlclient-dev +poetry install +poetry run bin/run +``` + +## Deploy -# Deploy First you need to have your public key in `/home/gitdeploy/.ssh/authorized_keys` on the server and then you can do: fab deploy -# Inspiration: +## Inspiration: +- - http://modoboa.readthedocs.org/ - https://github.com/lgunsch/django-vmail diff --git a/bin/deploy b/bin/deploy new file mode 100755 index 0000000..b6e8b8f --- /dev/null +++ b/bin/deploy @@ -0,0 +1,18 @@ +#!/bin/bash +set -eu + +export DJANGO_SETTINGS_MODULE=dusken.settings.prod +PROJECT_PATH=/var/www/neuf.no/django-postfix-dovecot-api +PROCESS_NAME="mxapi.neuf.no" + +echo "Deploying mxapi to ${PROJECT_PATH}" +cd "$PROJECT_PATH" || exit 1 +git pull + +poetry install +umask 022; poetry run python manage.py collectstatic --noinput -c # Collect static +poetry run python manage.py migrate # Run DB migrations + +# Reload backend +echo "Reloading supervisor process ${PROCESS_NAME}" +sudo /usr/bin/supervisorctl pid "$PROCESS_NAME" | xargs kill -HUP diff --git a/bin/ensure-python-deps.sh b/bin/ensure-python-deps.sh new file mode 100755 index 0000000..63080bb --- /dev/null +++ b/bin/ensure-python-deps.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -eu + +poetry config virtualenvs.in-project true +poetry run pip install -U pip wheel +poetry install \ No newline at end of file diff --git a/bin/format b/bin/format new file mode 100755 index 0000000..13bb01b --- /dev/null +++ b/bin/format @@ -0,0 +1,9 @@ +#!/bin/bash + +SRC_DIR="dpdapi" + +black $SRC_DIR +isort $SRC_DIR +pycln $SRC_DIR +pyupgrade --py38-plus $(find $SRC_DIR -name '*.py') +django-upgrade --target-version 3.2 $(find $SRC_DIR -name '*.py') diff --git a/bin/lint b/bin/lint new file mode 100755 index 0000000..bf7a20a --- /dev/null +++ b/bin/lint @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -eu + +pre-commit run -a diff --git a/bin/run b/bin/run new file mode 100755 index 0000000..4a65373 --- /dev/null +++ b/bin/run @@ -0,0 +1,3 @@ +#!/bin/bash + +python manage.py runserver \ No newline at end of file diff --git a/bin/test b/bin/test new file mode 100755 index 0000000..cd58336 --- /dev/null +++ b/bin/test @@ -0,0 +1,3 @@ +#!/bin/bash + +pytest -v \ No newline at end of file diff --git a/dpdapi/admin.py b/dpdapi/admin.py index fe98ee3..76b97b0 100644 --- a/dpdapi/admin.py +++ b/dpdapi/admin.py @@ -5,9 +5,9 @@ from dpdapi.models import Alias, Domain, User @admin.register(Alias) class AliasAdmin(admin.ModelAdmin): - list_display = ['source', 'destination', 'active', 'domain'] - list_filter = ['domain'] - search_fields = ['source', 'destination'] + list_display = ["source", "destination", "active", "domain"] + list_filter = ["domain"] + search_fields = ["source", "destination"] @admin.register(Domain) diff --git a/dpdapi/filters.py b/dpdapi/filters.py index ec63073..eaec423 100644 --- a/dpdapi/filters.py +++ b/dpdapi/filters.py @@ -7,10 +7,7 @@ from dpdapi.models import Alias class AliasRegexFilter(django_filters.FilterSet): class Meta: model = Alias - fields = { - 'source': ['iregex', 'regex'], - 'destination': ['iregex', 'regex'] - } + fields = {"source": ["iregex", "regex"], "destination": ["iregex", "regex"]} class AliasRegexFilterBackend(BaseFilterBackend): diff --git a/dpdapi/management/commands/import_valiases.py b/dpdapi/management/commands/import_valiases.py index f5a3630..3e7c34b 100644 --- a/dpdapi/management/commands/import_valiases.py +++ b/dpdapi/management/commands/import_valiases.py @@ -1,60 +1,62 @@ +import os + from django.core.management.base import BaseCommand from django.db.utils import IntegrityError -import os from dpdapi.models import Alias, Domain class Command(BaseCommand): - help = 'Import aliases for a single domain at a time, which is the domain of the first source address.' + help = "Import aliases for a single domain at a time, which is the domain of the first source address." def handle(self, *args, **options): if len(args) == 1: if os.path.exists(args[0]): self.import_file(args[0]) else: - self.stdout.write("{} does not exist".format(args[0])) + self.stdout.write(f"{args[0]} does not exist") else: self.stdout.write("Missing argument") def import_file(self, filename): new_aliases = [] - counts = {'imported': 0, 'existing': 0} + counts = {"imported": 0, "existing": 0} # Open file with open(filename) as f: # Parse for l in f: - if l.startswith('*: '): + if l.startswith("*: "): continue # Skip catch all - source, destinations = l.strip().split(': ') - for destination in destinations.split(','): - if destination.startswith('"|') or '-admin@' in destination: + source, destinations = l.strip().split(": ") + for destination in destinations.split(","): + if destination.startswith('"|') or "-admin@" in destination: continue # Skip mailman-aliases new_aliases.append(Alias(source=source, destination=destination)) if len(new_aliases) == 0: - self.stdout.write('No aliases found') + self.stdout.write("No aliases found") return # Lookup domain - domain_name = new_aliases[0].source.split('@')[1] # Assumes that every alias is in the same domain + domain_name = new_aliases[0].source.split("@")[1] # Assumes that every alias is in the same domain d, created = Domain.objects.get_or_create(name=domain_name) if created: - self.stdout.write('Created domain {}.'.format(d.name)) + self.stdout.write(f"Created domain {d.name}.") # Try to create, log if exists. Slow but works for a in new_aliases: a.domain = d try: a.save() - counts['imported'] += 1 # Woop! + counts["imported"] += 1 # Woop! except IntegrityError: - counts['existing'] += 1 # Exists - self.stdout.write('Could not import \'{}={}\', already exists.'.format(a.source, a.destination)) + counts["existing"] += 1 # Exists + self.stdout.write(f"Could not import '{a.source}={a.destination}', already exists.") # Print num imported, existing, total - self.stdout.write('Imported {}, skipped {} existing, total {}'.format( - counts['imported'], - counts['existing'], - len(new_aliases))) + self.stdout.write( + "Imported {}, skipped {} existing, total {}".format( + counts["imported"], counts["existing"], len(new_aliases) + ) + ) diff --git a/dpdapi/migrations/0001_initial.py b/dpdapi/migrations/0001_initial.py index 6cce1d7..5b55eb6 100644 --- a/dpdapi/migrations/0001_initial.py +++ b/dpdapi/migrations/0001_initial.py @@ -1,62 +1,58 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Alias', + name="Alias", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('updated', models.DateTimeField(auto_now=True)), - ('source', models.EmailField(max_length=254)), - ('destination', models.EmailField(max_length=254)), - ('active', models.BooleanField(default=True)), + ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ("source", models.EmailField(max_length=254)), + ("destination", models.EmailField(max_length=254)), + ("active", models.BooleanField(default=True)), ], options={ - 'verbose_name_plural': 'aliases', + "verbose_name_plural": "aliases", }, bases=(models.Model,), ), migrations.CreateModel( - name='Domain', + name="Domain", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('updated', models.DateTimeField(auto_now=True)), - ('name', models.CharField(unique=True, max_length=254)), + ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ("name", models.CharField(unique=True, max_length=254)), ], options={ - 'abstract': False, + "abstract": False, }, bases=(models.Model,), ), migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('updated', models.DateTimeField(auto_now=True)), - ('email', models.EmailField(unique=True, max_length=254)), - ('password', models.CharField(max_length=109)), - ('domain', models.ForeignKey(to='dpdapi.Domain', on_delete=models.CASCADE)), + ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ("email", models.EmailField(unique=True, max_length=254)), + ("password", models.CharField(max_length=109)), + ("domain", models.ForeignKey(to="dpdapi.Domain", on_delete=models.CASCADE)), ], options={ - 'abstract': False, + "abstract": False, }, bases=(models.Model,), ), migrations.AddField( - model_name='alias', - name='domain', - field=models.ForeignKey(to='dpdapi.Domain', on_delete=models.CASCADE), + model_name="alias", + name="domain", + field=models.ForeignKey(to="dpdapi.Domain", on_delete=models.CASCADE), preserve_default=True, ), ] diff --git a/dpdapi/migrations/0002_auto_20150118_1638.py b/dpdapi/migrations/0002_auto_20150118_1638.py index ddeaf07..4b1c347 100644 --- a/dpdapi/migrations/0002_auto_20150118_1638.py +++ b/dpdapi/migrations/0002_auto_20150118_1638.py @@ -1,18 +1,15 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('dpdapi', '0001_initial'), + ("dpdapi", "0001_initial"), ] operations = [ migrations.AlterUniqueTogether( - name='alias', - unique_together=set([('source', 'destination', 'domain')]), + name="alias", + unique_together={("source", "destination", "domain")}, ), ] diff --git a/dpdapi/migrations/0003_auto_20200909_2342.py b/dpdapi/migrations/0003_auto_20200909_2342.py index c74d499..93f0a4a 100644 --- a/dpdapi/migrations/0003_auto_20200909_2342.py +++ b/dpdapi/migrations/0003_auto_20200909_2342.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('dpdapi', '0002_auto_20150118_1638'), + ("dpdapi", "0002_auto_20150118_1638"), ] operations = [ migrations.AlterField( - model_name='user', - name='password', + model_name="user", + name="password", field=models.CharField(max_length=512), ), ] diff --git a/dpdapi/models.py b/dpdapi/models.py index aa8d721..ce7a6b3 100644 --- a/dpdapi/models.py +++ b/dpdapi/models.py @@ -12,21 +12,21 @@ class BaseModel(models.Model): class Alias(BaseModel): source = models.EmailField(max_length=254) destination = models.EmailField(max_length=254) - domain = models.ForeignKey('dpdapi.Domain', models.CASCADE) + domain = models.ForeignKey("dpdapi.Domain", models.CASCADE) active = models.BooleanField(default=True) def __str__(self): - return "{}={}".format(self.source, self.destination) + return f"{self.source}={self.destination}" def save(self, *args, **kwargs): self.source = self.source.lower() self.destination = self.destination.lower() - super(Alias, self).save(*args, **kwargs) + super().save(*args, **kwargs) class Meta: - verbose_name_plural = 'aliases' - unique_together = ('source', 'destination', 'domain') + verbose_name_plural = "aliases" + unique_together = ("source", "destination", "domain") class Domain(BaseModel): @@ -38,13 +38,13 @@ class Domain(BaseModel): def save(self, *args, **kwargs): self.name = self.name.lower() - super(Domain, self).save(*args, **kwargs) + super().save(*args, **kwargs) class User(BaseModel): email = models.EmailField(max_length=254, unique=True) password = models.CharField(max_length=512) - domain = models.ForeignKey('dpdapi.Domain', models.CASCADE) + domain = models.ForeignKey("dpdapi.Domain", models.CASCADE) def __str__(self): return self.email @@ -52,5 +52,4 @@ class User(BaseModel): def save(self, *args, **kwargs): self.email = self.email.lower() - super(User, self).save(*args, **kwargs) - + super().save(*args, **kwargs) diff --git a/dpdapi/serializers.py b/dpdapi/serializers.py index 58f8e4d..d4a5828 100644 --- a/dpdapi/serializers.py +++ b/dpdapi/serializers.py @@ -1,10 +1,10 @@ -from rest_framework import serializers from passlib.hash import sha512_crypt +from rest_framework import serializers + from .models import Alias, Domain, User class AliasSerializer(serializers.ModelSerializer): - def create(self, validated_data): # Workaround to accept duplicates # FIXME, will fail with existing alias with validated_data['active']=False @@ -14,36 +14,36 @@ class AliasSerializer(serializers.ModelSerializer): class Meta: model = Alias validators = [] # Disable unique togheter check - fields = '__all__' + fields = "__all__" class AliasDeleteSerializer(serializers.ModelSerializer): class Meta: model = Alias validators = [] # No Unique togheter check on deletion - fields = '__all__' + fields = "__all__" class DomainSerializer(serializers.ModelSerializer): class Meta: model = Domain - fields = '__all__' + fields = "__all__" class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = '__all__' - extra_kwargs = {'password': {'write_only': True}} + fields = "__all__" + extra_kwargs = {"password": {"write_only": True}} def _hash_password(self, raw_password): return sha512_crypt.encrypt(raw_password) def create(self, validated_data): user = User( - email=validated_data['email'], - domain=validated_data['domain'], - password=self._hash_password(validated_data['password']) + email=validated_data["email"], + domain=validated_data["domain"], + password=self._hash_password(validated_data["password"]), ) user.save() return user diff --git a/dpdapi/settings.py b/dpdapi/settings.py new file mode 100644 index 0000000..9445460 --- /dev/null +++ b/dpdapi/settings.py @@ -0,0 +1,106 @@ +""" Ref: https://docs.djangoproject.com/en/3.2/topics/settings/ """ + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os + +import sentry_sdk +from sentry_sdk.integrations.django import DjangoIntegration + +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +SECRET_KEY = os.getenv("SECRET_KEY", "hlaf#yma(8s!cy-bf5e6kc&_5n_l1h!34c(j32-b3v8o7pg$5n") +DEBUG = bool(os.getenv("DEBUG", True)) +ALLOWED_HOSTS = [] + +# Application definition +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.messages", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.staticfiles", + "django_extensions", + "rest_framework", + "django_filters", +] +INSTALLED_APPS += [ + "dpdapi", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", +] +ROOT_URLCONF = "dpdapi.urls" +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + # insert your TEMPLATE_DIRS here + ], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] +WSGI_APPLICATION = "dpdapi.wsgi.application" +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + } +} +LANGUAGE_CODE = "en-us" +TIME_ZONE = "Europe/Oslo" +USE_I18N = True +USE_L10N = True +USE_TZ = True +STATIC_URL = "/static/" +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework.authentication.BasicAuthentication", + "rest_framework.authentication.SessionAuthentication", + ) +} +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "root": { + "level": "INFO", + "handlers": ["console"], + }, + "formatters": { + "verbose": {"format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"}, + }, + "handlers": {"console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose"}}, + "loggers": { + "django.db.backends": { + "level": "ERROR", + "handlers": ["console"], + "propagate": False, + }, + }, +} + +SENTRY_DSN = os.getenv("SENTRY_DSN", "https://c7e6a47844484c0a81a98e10e48c1b71@sentry.neuf.no/8") +if SENTRY_DSN: + SENTRY_ENVIRONMENT = os.getenv("SENTRY_ENVIRONMENT", "production") + sentry_sdk.init(dsn=SENTRY_DSN, environment=SENTRY_ENVIRONMENT, integrations=[DjangoIntegration()]) + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + +try: + from .local import * # noqa +except ImportError: + pass diff --git a/dpdapi/tests.py b/dpdapi/tests.py index 219acfc..5b0334e 100644 --- a/dpdapi/tests.py +++ b/dpdapi/tests.py @@ -1,43 +1,41 @@ -import json - from django.contrib.auth.models import User from django.urls import reverse from rest_framework.test import APITestCase -from dpdapi.models import Domain, Alias +from dpdapi.models import Alias, Domain class RegexTestCase(APITestCase): - aliases = ['ops-alert@example.com', 'yolo-announce@example.com'] + aliases = ["ops-alert@example.com", "yolo-announce@example.com"] def setUp(self): - user = User.objects.create(username='yolo') - self.domain = Domain.objects.create(name='example.com') + user = User.objects.create(username="yolo") + self.domain = Domain.objects.create(name="example.com") for a in self.aliases: - Alias.objects.create(source=a, destination='ninja@example.com', domain=self.domain) + Alias.objects.create(source=a, destination="ninja@example.com", domain=self.domain) self.client.force_login(user) def test_get_all(self): - res = self.client.get(reverse('alias-list')) + res = self.client.get(reverse("alias-list")) self.assertEqual(len(res.data), len(self.aliases)) def test_domain_filter(self): - params = {'domain__name': 'example.com'} - res = self.client.get(reverse('alias-list'), data=params) + params = {"domain__name": "example.com"} + res = self.client.get(reverse("alias-list"), data=params) self.assertEqual(len(res.data), len(self.aliases)) def test_domain_filter_no_hits(self): - params = {'domain__name': 'zexample.com'} - res = self.client.get(reverse('alias-list'), data=params) + params = {"domain__name": "zexample.com"} + res = self.client.get(reverse("alias-list"), data=params) self.assertEqual(len(res.data), 0) def test_regex_filter(self): - params = {'source__regex': '^ops-|^ops@'} - res = self.client.get(reverse('alias-list'), data=params) + params = {"source__regex": "^ops-|^ops@"} + res = self.client.get(reverse("alias-list"), data=params) self.assertEqual(len(res.data), 1) def test_delete_bulk_single(self): - data = {'destination': 'ninja@example.com', 'source': 'ding@example.com', 'domain': self.domain.pk} - Alias.objects.create(source=data['source'], destination=data['destination'], domain=self.domain) - res = self.client.delete(reverse('alias-delete-bulk'), data=[data], format='json') + data = {"destination": "ninja@example.com", "source": "ding@example.com", "domain": self.domain.pk} + Alias.objects.create(source=data["source"], destination=data["destination"], domain=self.domain) + res = self.client.delete(reverse("alias-delete-bulk"), data=[data], format="json") self.assertEqual(res.status_code, 204, res.data) diff --git a/dpdapi/urls.py b/dpdapi/urls.py index 4da37cb..465571a 100644 --- a/dpdapi/urls.py +++ b/dpdapi/urls.py @@ -1,10 +1,19 @@ +from django.contrib import admin +from django.urls import include, path, re_path +from django.views.generic import RedirectView +from rest_framework import urls as rest_framework_urls from rest_framework.routers import DefaultRouter from .views import AliasViewSet, DomainViewSet, UserViewSet +api_router = DefaultRouter() +api_router.register(r"aliases", AliasViewSet) +api_router.register(r"domains", DomainViewSet) +api_router.register(r"users", UserViewSet) -router = DefaultRouter() -router.register(r'aliases', AliasViewSet) -router.register(r'domains', DomainViewSet) -router.register(r'users', UserViewSet) -urlpatterns = router.urls +urlpatterns = [ + path("", RedirectView.as_view(pattern_name="api-root")), + re_path(r"^api/", include(api_router.urls)), + re_path(r"^admin/", admin.site.urls), + re_path(r"^api-auth/", include(rest_framework_urls, namespace="rest_framework")), +] diff --git a/dpdapi/views.py b/dpdapi/views.py index 082e9b5..42e459f 100644 --- a/dpdapi/views.py +++ b/dpdapi/views.py @@ -1,32 +1,36 @@ import logging import operator - from functools import reduce + from django.db.models import Q from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.mixins import ListModelMixin from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from rest_framework import status, viewsets from rest_framework.viewsets import GenericViewSet from dpdapi.filters import AliasRegexFilterBackend from dpdapi.models import Alias, Domain, User -from dpdapi.serializers import AliasSerializer, AliasDeleteSerializer, DomainSerializer, UserSerializer +from dpdapi.serializers import AliasDeleteSerializer, AliasSerializer, DomainSerializer, UserSerializer logger = logging.getLogger(__name__) class AliasViewSet(ListModelMixin, GenericViewSet): - """ Aliases """ + """Aliases""" + queryset = Alias.objects.all() serializer_class = AliasSerializer permission_classes = [IsAuthenticated] filter_backends = (DjangoFilterBackend, AliasRegexFilterBackend) - filterset_fields = ('domain', 'domain__name',) + filterset_fields = ( + "domain", + "domain__name", + ) - @action(detail=False, methods=['post']) + @action(detail=False, methods=["post"]) def create_bulk(self, request): serializer = AliasSerializer(data=request.data, many=True) @@ -35,7 +39,7 @@ class AliasViewSet(ListModelMixin, GenericViewSet): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) - @action(detail=False, methods=['delete']) + @action(detail=False, methods=["delete"]) def delete_bulk(self, request): serializer = AliasDeleteSerializer(data=request.data, many=True) @@ -45,14 +49,14 @@ class AliasViewSet(ListModelMixin, GenericViewSet): q_list = map(lambda x: Q(**x), serializer.initial_data) aliases = Alias.objects.filter(reduce(operator.or_, q_list)) - initial_set = set(map(lambda x: (x['source'], x['destination'], x['domain']), serializer.initial_data)) - deleted_set = set(aliases.values_list('source', 'destination', 'domain')) + initial_set = set(map(lambda x: (x["source"], x["destination"], x["domain"]), serializer.initial_data)) + deleted_set = set(aliases.values_list("source", "destination", "domain")) diff = initial_set.difference(deleted_set) - logger.debug('Deleted: {}'.format(','.join(map(lambda x: '{0[0]}={0[1]}'.format(x), deleted_set)))) + logger.debug("Deleted: {}".format(",".join(map(lambda x: "{0[0]}={0[1]}".format(x), deleted_set)))) aliases.delete() if diff: - diff_formatted = map(lambda x: '{0[0]}={0[1]}'.format(x), diff) + diff_formatted = map(lambda x: "{0[0]}={0[1]}".format(x), diff) res = "Aliases '{}' could not be found and were not deleted.".format(", ".join(diff_formatted)) logger.info(res) return Response({"result": res}, status=status.HTTP_200_OK) @@ -61,16 +65,18 @@ class AliasViewSet(ListModelMixin, GenericViewSet): class DomainViewSet(viewsets.ModelViewSet): - """ Domains """ + """Domains""" + queryset = Domain.objects.all() serializer_class = DomainSerializer permission_classes = [IsAuthenticated] filter_backends = (DjangoFilterBackend,) - filterset_fields = ('name',) + filterset_fields = ("name",) class UserViewSet(viewsets.ModelViewSet): - """ Users """ + """Users""" + queryset = User.objects.all() serializer_class = UserSerializer permission_classes = [IsAuthenticated] diff --git a/dpdapi/wsgi.py b/dpdapi/wsgi.py new file mode 100644 index 0000000..9a3eba2 --- /dev/null +++ b/dpdapi/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dpdapi.settings") + +application = get_wsgi_application() diff --git a/dpdapisite/__init__.py b/dpdapisite/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/dpdapisite/settings.py b/dpdapisite/settings.py deleted file mode 100644 index ec3146d..0000000 --- a/dpdapisite/settings.py +++ /dev/null @@ -1,150 +0,0 @@ -""" -For more information on this file, see -https://docs.djangoproject.com/en/1.7/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.7/ref/settings/ -""" - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -import os - -import sentry_sdk -from sentry_sdk.integrations.django import DjangoIntegration - -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'hlaf#yma(8s!cy-bf5e6kc&_5n_l1h!34c(j32-b3v8o7pg$5n' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - -# Application definition -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.messages', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.staticfiles', - - 'django_extensions', - 'rest_framework', - 'django_filters', -] - -INSTALLED_APPS += [ - 'dpdapi', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', -] - -ROOT_URLCONF = 'dpdapisite.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - # insert your TEMPLATE_DIRS here - ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'dpdapisite.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/1.7/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} - -# Internationalization -# https://docs.djangoproject.com/en/1.7/topics/i18n/ -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'Europe/Oslo' -USE_I18N = True -USE_L10N = True -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.7/howto/static-files/ -STATIC_URL = '/static/' - -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.BasicAuthentication', - 'rest_framework.authentication.SessionAuthentication', - ) -} - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'root': { - 'level': 'INFO', - 'handlers': ['console'], - }, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' - }, - }, - 'handlers': { - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'verbose' - } - }, - 'loggers': { - 'django.db.backends': { - 'level': 'ERROR', - 'handlers': ['console'], - 'propagate': False, - }, - }, -} - -SENTRY_DSN = os.getenv('SENTRY_DSN', 'https://c7e6a47844484c0a81a98e10e48c1b71@sentry.neuf.no/8') -if SENTRY_DSN: - SENTRY_ENVIRONMENT = os.getenv('SENTRY_ENVIRONMENT', 'production') - sentry_sdk.init( - dsn=SENTRY_DSN, - environment=SENTRY_ENVIRONMENT, - integrations=[DjangoIntegration()] - ) - - -try: - from .local_settings import * -except ImportError: - pass diff --git a/dpdapisite/urls.py b/dpdapisite/urls.py deleted file mode 100644 index f8e8e65..0000000 --- a/dpdapisite/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.conf.urls import include, url -from django.contrib import admin -from django.views.generic import RedirectView - -from rest_framework import urls as rest_framework_urls -from dpdapi import urls as dpdapi_urls - - -urlpatterns = [ - url(r'^$', RedirectView.as_view(pattern_name='api-root')), - url(r'^api/', include(dpdapi_urls)), - url(r'^admin/', admin.site.urls), - url(r'^api-auth/', include(rest_framework_urls, namespace='rest_framework')) -] diff --git a/dpdapisite/wsgi.py b/dpdapisite/wsgi.py deleted file mode 100644 index 002f71e..0000000 --- a/dpdapisite/wsgi.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -WSGI config for pfrsite project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ -""" - -import os -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dpdapisite.settings") - -application = get_wsgi_application() diff --git a/manage.py b/manage.py index 8f5160c..17d19ba 100755 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import os import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dpdapisite.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dpdapi.settings") from django.core.management import execute_from_command_line diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..1a004c3 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,991 @@ +[[package]] +name = "asgiref" +version = "3.5.2" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "bcrypt" +version = "3.2.2" +description = "Modern password hashing for your software and your servers" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "black" +version = "22.6.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2022.6.15" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "charset-normalizer" +version = "2.1.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "codecov" +version = "2.1.12" +description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +coverage = "*" +requests = ">=2.7.9" + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "6.4.3" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "37.0.4" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools_rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + +[[package]] +name = "distlib" +version = "0.3.5" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "django" +version = "3.2.15" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +asgiref = ">=3.3.2,<4" +pytz = "*" +sqlparse = ">=0.2.2" + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-extensions" +version = "3.2.0" +description = "Extensions for Django" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Django = ">=3.2" + +[[package]] +name = "django-filter" +version = "22.1" +description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +Django = ">=3.2" + +[[package]] +name = "django-upgrade" +version = "1.7.0" +description = "Automatically upgrade your Django projects." +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +tokenize-rt = ">=4.1.0" + +[[package]] +name = "djangorestframework" +version = "3.13.1" +description = "Web APIs for Django, made easy." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +django = ">=2.2" +pytz = "*" + +[[package]] +name = "fabric" +version = "2.7.1" +description = "High level SSH command execution" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +invoke = ">=1.3,<2.0" +paramiko = ">=2.4" +pathlib2 = "*" + +[package.extras] +pytest = ["mock (>=2.0.0,<3.0)", "pytest (>=3.2.5,<4.0)"] +testing = ["mock (>=2.0.0,<3.0)"] + +[[package]] +name = "filelock" +version = "3.7.1" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] + +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "identify" +version = "2.5.3" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "invoke" +version = "1.7.1" +description = "Pythonic task execution" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "libcst" +version = "0.4.7" +description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 programs." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pyyaml = ">=5.2" +typing-extensions = ">=3.7.4.2" +typing-inspect = ">=0.4.0" + +[package.extras] +dev = ["black (==22.3.0)", "coverage (>=4.5.4)", "fixit (==0.1.1)", "flake8 (>=3.7.8)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jupyter (>=1.0.0)", "maturin (>=0.8.3,<0.9)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "setuptools-scm (>=6.0.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==1.3)", "usort (==1.0.0rc1)", "setuptools-rust (>=0.12.1)", "slotscheck (>=0.7.1)", "jinja2 (==3.0.3)", "pyre-check (==0.9.9)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mysqlclient" +version = "2.1.1" +description = "Python interface to MySQL" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "paramiko" +version = "2.11.0" +description = "SSH2 protocol library" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +bcrypt = ">=3.1.3" +cryptography = ">=2.5" +pynacl = ">=1.0.1" +six = "*" + +[package.extras] +all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] +gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=1.3)"] + +[[package]] +name = "passlib" +version = "1.7.4" +description = "comprehensive password hashing framework supporting over 30 schemes" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"] +totp = ["cryptography"] + +[[package]] +name = "pathlib2" +version = "2.3.7.post1" +description = "Object-oriented filesystem paths" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.20.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycln" +version = "2.1.1" +description = "A formatter for finding and removing unused import statements." +category = "dev" +optional = false +python-versions = ">=3.6.2,<4" + +[package.dependencies] +libcst = ">=0.3.10,<0.5.0" +pathspec = ">=0.9.0,<0.10.0" +pyyaml = ">=5.3.1,<7.0.0" +tomlkit = ">=0.11.1,<0.12.0" +typer = ">=0.4.1,<0.7.0" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "dev" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-django" +version = "4.5.2" +description = "A Django plugin for pytest." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["django", "django-configurations (>=2.0)"] + +[[package]] +name = "pytz" +version = "2022.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyupgrade" +version = "2.37.3" +description = "A tool to automatically upgrade syntax for newer versions." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tokenize-rt = ">=3.2.0" + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "requests" +version = "2.28.1" +description = "Python HTTP for Humans." +category = "dev" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "sentry-sdk" +version = "1.9.2" +description = "Python client for Sentry (https://sentry.io)" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +certifi = "*" +urllib3 = [ + {version = ">=1.26.9", markers = "python_version >= \"3.5\""}, + {version = ">=1.26.11", markers = "python_version >= \"3.6\""}, +] + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +chalice = ["chalice (>=1.16.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["flask (>=0.11)", "blinker (>=1.1)"] +httpx = ["httpx (>=0.16.0)"] +pure_eval = ["pure-eval", "executing", "asttokens"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["quart (>=0.16.1)", "blinker (>=1.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +tornado = ["tornado (>=5)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "sqlparse" +version = "0.4.2" +description = "A non-validating SQL parser." +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "tokenize-rt" +version = "4.2.1" +description = "A wrapper around the stdlib `tokenize` which roundtrips." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tomlkit" +version = "0.11.1" +description = "Style preserving TOML library" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[[package]] +name = "typer" +version = "0.6.1" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +click = ">=7.1.1,<9.0.0" + +[package.extras] +all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)", "rich (>=10.11.0,<13.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)"] +test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=22.3.0,<23.0.0)", "isort (>=5.0.6,<6.0.0)", "rich (>=10.11.0,<13.0.0)"] + +[[package]] +name = "typing-extensions" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typing-inspect" +version = "0.7.1" +description = "Runtime inspection utilities for typing module." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "urllib3" +version = "1.26.11" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "virtualenv" +version = "20.16.3" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +distlib = ">=0.3.5,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<3" + +[package.extras] +docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "c0829f4c7197c9f5658a2b3ea22927240b0e53c0948a27d109314d338b808c1d" + +[metadata.files] +asgiref = [ + {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, + {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, +] +atomicwrites = [] +attrs = [] +bcrypt = [] +black = [] +certifi = [ + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, +] +cffi = [] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] +charset-normalizer = [] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +codecov = [ + {file = "codecov-2.1.12-py2.py3-none-any.whl", hash = "sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47"}, + {file = "codecov-2.1.12-py3.8.egg", hash = "sha256:782a8e5352f22593cbc5427a35320b99490eb24d9dcfa2155fd99d2b75cfb635"}, + {file = "codecov-2.1.12.tar.gz", hash = "sha256:a0da46bb5025426da895af90938def8ee12d37fcbcbbbc15b6dc64cf7ebc51c1"}, +] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +coverage = [] +cryptography = [] +distlib = [] +django = [] +django-extensions = [] +django-filter = [] +django-upgrade = [] +djangorestframework = [ + {file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"}, + {file = "djangorestframework-3.13.1.tar.gz", hash = "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee"}, +] +fabric = [] +filelock = [ + {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, + {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, +] +gunicorn = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] +identify = [] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +invoke = [] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +libcst = [] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +mysqlclient = [] +nodeenv = [] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +paramiko = [] +passlib = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] +pathlib2 = [ + {file = "pathlib2-2.3.7.post1-py2.py3-none-any.whl", hash = "sha256:5266a0fd000452f1b3467d782f079a4343c63aaa119221fbdc4e39577489ca5b"}, + {file = "pathlib2-2.3.7.post1.tar.gz", hash = "sha256:9fe0edad898b83c0c3e199c842b27ed216645d2e177757b2dd67384d4113c641"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +pre-commit = [] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pycln = [] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] +pynacl = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [ + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] +pytest-cov = [] +pytest-django = [ + {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"}, + {file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"}, +] +pytz = [ + {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, + {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, +] +pyupgrade = [] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +requests = [] +sentry-sdk = [] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +sqlparse = [ + {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, + {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, +] +tokenize-rt = [ + {file = "tokenize_rt-4.2.1-py2.py3-none-any.whl", hash = "sha256:08a27fa032a81cf45e8858d0ac706004fcd523e8463415ddf1442be38e204ea8"}, + {file = "tokenize_rt-4.2.1.tar.gz", hash = "sha256:0d4f69026fed520f8a1e0103aa36c406ef4661417f20ca643f913e33531b3b94"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +tomlkit = [] +typer = [] +typing-extensions = [] +typing-inspect = [ + {file = "typing_inspect-0.7.1-py2-none-any.whl", hash = "sha256:b1f56c0783ef0f25fb064a01be6e5407e54cf4a4bf4f3ba3fe51e0bd6dcea9e5"}, + {file = "typing_inspect-0.7.1-py3-none-any.whl", hash = "sha256:3cd7d4563e997719a710a3bfe7ffb544c6b72069b6812a02e9b414a8fa3aaa6b"}, + {file = "typing_inspect-0.7.1.tar.gz", hash = "sha256:047d4097d9b17f46531bf6f014356111a1b6fb821a24fe7ac909853ca2a782aa"}, +] +urllib3 = [] +virtualenv = [] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..535ca9f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,47 @@ +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "dpdapi.settings" +# FIXME: https://github.com/django-extensions/django-extensions/pull/1741 +filterwarnings = "ignore::PendingDeprecationWarning:django.apps.registry.*" + +[tool.black] +line-length = 120 +target_version = ['py38'] + +[tool.isort] +profile = "black" +line_length = 120 + +[tool.poetry] +name = "django-postfix-dovecot-api" +version = "0.1.0" +description = "" +authors = ["EDB "] + +[tool.poetry.dependencies] +python = "^3.8" + +Django="3.2.*" +django-extensions="3.2.*" +django-filter="22.1" +djangorestframework="3.13.*" +gunicorn="20.1.*" +mysqlclient="2.1.*" +passlib="1.7.*" +sentry-sdk="1.9.*" + +[tool.poetry.dev-dependencies] +black = "22.6.*" +codecov = "2.1.*" +django-upgrade = "^1.7.0" +fabric="2.7.*" +isort = "*" +pre-commit = "*" +pycln = "2.1.*" +pytest = "*" +pytest-cov = "^3.0.0" +pytest-django = "*" +pyupgrade = "2.37.*" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 4de9445..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -Django==2.2.* -fabric==2.5.* -django-extensions==2.2.* -django-filter==2.2.* -djangorestframework==3.11.* -gunicorn==20.0.* -mysqlclient==1.4.6 -passlib==1.7.* -sentry-sdk==0.13.*