2 Commits
Author SHA1 Message Date
nikolark 419857f195 oops 2022-08-07 15:48:13 +02:00
nikolark b12413ed67 ✨ Modernize bit bit
- bumps
- poetry
- linting
2022-08-07 15:44:40 +02:00
31 changed files with 1404 additions and 328 deletions

No files matched your search

+1
View File
@@ -2,5 +2,6 @@
*.pyc *.pyc
.idea .idea
venv venv
dpdapi/local.py
local_settings.py local_settings.py
db.sqlite3 db.sqlite3
+37 -14
View File
@@ -1,19 +1,42 @@
image: python:latest default:
image: python:3.8-slim
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
cache: cache:
paths: paths:
- "$CI_PROJECT_DIR/pip-cache" - .cache/pip
key: "$CI_PROJECT_ID" - .venv
before_script:
- pip install -r requirements.txt
- pip install coverage
test: test:
before_script:
- poetry install
script: script:
- coverage run --source=dpdapi --omit=dpdapi/migrations/* manage.py test -v2 - bin/ensure-python-deps.sh
- coverage report - poetry run python manage.py migrate
coverage: '/TOTAL.+ ([0-9]{1,3}%)/' - 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
+36
View File
@@ -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"]
+13 -7
View File
@@ -1,14 +1,20 @@
# Install # django-postfix-dovecot-api
apt install python-dev libmysqlclient-dev python-virtualenv
python -m venv venv ## Getting started
. venv/bin/activate
pip install -r requirements.txt ```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: First you need to have your public key in `/home/gitdeploy/.ssh/authorized_keys` on the server and then you can do:
fab deploy fab deploy
# Inspiration: ## Inspiration:
-
- http://modoboa.readthedocs.org/ - http://modoboa.readthedocs.org/
- https://github.com/lgunsch/django-vmail - https://github.com/lgunsch/django-vmail
Executable
+18
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -eu
poetry config virtualenvs.in-project true
poetry run pip install -U pip wheel
poetry install
Executable
+9
View File
@@ -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')
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -eu
pre-commit run -a
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
python manage.py runserver
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
pytest -v
+3 -3
View File
@@ -5,9 +5,9 @@ from dpdapi.models import Alias, Domain, User
@admin.register(Alias) @admin.register(Alias)
class AliasAdmin(admin.ModelAdmin): class AliasAdmin(admin.ModelAdmin):
list_display = ['source', 'destination', 'active', 'domain'] list_display = ["source", "destination", "active", "domain"]
list_filter = ['domain'] list_filter = ["domain"]
search_fields = ['source', 'destination'] search_fields = ["source", "destination"]
@admin.register(Domain) @admin.register(Domain)
+1 -4
View File
@@ -7,10 +7,7 @@ from dpdapi.models import Alias
class AliasRegexFilter(django_filters.FilterSet): class AliasRegexFilter(django_filters.FilterSet):
class Meta: class Meta:
model = Alias model = Alias
fields = { fields = {"source": ["iregex", "regex"], "destination": ["iregex", "regex"]}
'source': ['iregex', 'regex'],
'destination': ['iregex', 'regex']
}
class AliasRegexFilterBackend(BaseFilterBackend): class AliasRegexFilterBackend(BaseFilterBackend):
+20 -18
View File
@@ -1,60 +1,62 @@
import os
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
import os
from dpdapi.models import Alias, Domain from dpdapi.models import Alias, Domain
class Command(BaseCommand): 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): def handle(self, *args, **options):
if len(args) == 1: if len(args) == 1:
if os.path.exists(args[0]): if os.path.exists(args[0]):
self.import_file(args[0]) self.import_file(args[0])
else: else:
self.stdout.write("{} does not exist".format(args[0])) self.stdout.write(f"{args[0]} does not exist")
else: else:
self.stdout.write("Missing argument") self.stdout.write("Missing argument")
def import_file(self, filename): def import_file(self, filename):
new_aliases = [] new_aliases = []
counts = {'imported': 0, 'existing': 0} counts = {"imported": 0, "existing": 0}
# Open file # Open file
with open(filename) as f: with open(filename) as f:
# Parse # Parse
for l in f: for l in f:
if l.startswith('*: '): if l.startswith("*: "):
continue # Skip catch all continue # Skip catch all
source, destinations = l.strip().split(': ') source, destinations = l.strip().split(": ")
for destination in destinations.split(','): for destination in destinations.split(","):
if destination.startswith('"|') or '-admin@' in destination: if destination.startswith('"|') or "-admin@" in destination:
continue # Skip mailman-aliases continue # Skip mailman-aliases
new_aliases.append(Alias(source=source, destination=destination)) new_aliases.append(Alias(source=source, destination=destination))
if len(new_aliases) == 0: if len(new_aliases) == 0:
self.stdout.write('No aliases found') self.stdout.write("No aliases found")
return return
# Lookup domain # 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) d, created = Domain.objects.get_or_create(name=domain_name)
if created: 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 # Try to create, log if exists. Slow but works
for a in new_aliases: for a in new_aliases:
a.domain = d a.domain = d
try: try:
a.save() a.save()
counts['imported'] += 1 # Woop! counts["imported"] += 1 # Woop!
except IntegrityError: except IntegrityError:
counts['existing'] += 1 # Exists counts["existing"] += 1 # Exists
self.stdout.write('Could not import \'{}={}\', already exists.'.format(a.source, a.destination)) self.stdout.write(f"Could not import '{a.source}={a.destination}', already exists.")
# Print num imported, existing, total # Print num imported, existing, total
self.stdout.write('Imported {}, skipped {} existing, total {}'.format( self.stdout.write(
counts['imported'], "Imported {}, skipped {} existing, total {}".format(
counts['existing'], counts["imported"], counts["existing"], len(new_aliases)
len(new_aliases))) )
)
+27 -31
View File
@@ -1,62 +1,58 @@
# -*- coding: utf-8 -*- from django.db import migrations, models
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Alias', name="Alias",
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(auto_now_add=True)), ("created", models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)), ("updated", models.DateTimeField(auto_now=True)),
('source', models.EmailField(max_length=254)), ("source", models.EmailField(max_length=254)),
('destination', models.EmailField(max_length=254)), ("destination", models.EmailField(max_length=254)),
('active', models.BooleanField(default=True)), ("active", models.BooleanField(default=True)),
], ],
options={ options={
'verbose_name_plural': 'aliases', "verbose_name_plural": "aliases",
}, },
bases=(models.Model,), bases=(models.Model,),
), ),
migrations.CreateModel( migrations.CreateModel(
name='Domain', name="Domain",
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(auto_now_add=True)), ("created", models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)), ("updated", models.DateTimeField(auto_now=True)),
('name', models.CharField(unique=True, max_length=254)), ("name", models.CharField(unique=True, max_length=254)),
], ],
options={ options={
'abstract': False, "abstract": False,
}, },
bases=(models.Model,), bases=(models.Model,),
), ),
migrations.CreateModel( migrations.CreateModel(
name='User', name="User",
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(auto_now_add=True)), ("created", models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)), ("updated", models.DateTimeField(auto_now=True)),
('email', models.EmailField(unique=True, max_length=254)), ("email", models.EmailField(unique=True, max_length=254)),
('password', models.CharField(max_length=109)), ("password", models.CharField(max_length=109)),
('domain', models.ForeignKey(to='dpdapi.Domain', on_delete=models.CASCADE)), ("domain", models.ForeignKey(to="dpdapi.Domain", on_delete=models.CASCADE)),
], ],
options={ options={
'abstract': False, "abstract": False,
}, },
bases=(models.Model,), bases=(models.Model,),
), ),
migrations.AddField( migrations.AddField(
model_name='alias', model_name="alias",
name='domain', name="domain",
field=models.ForeignKey(to='dpdapi.Domain', on_delete=models.CASCADE), field=models.ForeignKey(to="dpdapi.Domain", on_delete=models.CASCADE),
preserve_default=True, preserve_default=True,
), ),
] ]
+4 -7
View File
@@ -1,18 +1,15 @@
# -*- coding: utf-8 -*- from django.db import migrations, models
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('dpdapi', '0001_initial'), ("dpdapi", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='alias', name="alias",
unique_together=set([('source', 'destination', 'domain')]), unique_together={("source", "destination", "domain")},
), ),
] ]
+3 -3
View File
@@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('dpdapi', '0002_auto_20150118_1638'), ("dpdapi", "0002_auto_20150118_1638"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='user', model_name="user",
name='password', name="password",
field=models.CharField(max_length=512), field=models.CharField(max_length=512),
), ),
] ]
+8 -9
View File
@@ -12,21 +12,21 @@ class BaseModel(models.Model):
class Alias(BaseModel): class Alias(BaseModel):
source = models.EmailField(max_length=254) source = models.EmailField(max_length=254)
destination = 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) active = models.BooleanField(default=True)
def __str__(self): def __str__(self):
return "{}={}".format(self.source, self.destination) return f"{self.source}={self.destination}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.source = self.source.lower() self.source = self.source.lower()
self.destination = self.destination.lower() self.destination = self.destination.lower()
super(Alias, self).save(*args, **kwargs) super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name_plural = 'aliases' verbose_name_plural = "aliases"
unique_together = ('source', 'destination', 'domain') unique_together = ("source", "destination", "domain")
class Domain(BaseModel): class Domain(BaseModel):
@@ -38,13 +38,13 @@ class Domain(BaseModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.name = self.name.lower() self.name = self.name.lower()
super(Domain, self).save(*args, **kwargs) super().save(*args, **kwargs)
class User(BaseModel): class User(BaseModel):
email = models.EmailField(max_length=254, unique=True) email = models.EmailField(max_length=254, unique=True)
password = models.CharField(max_length=512) password = models.CharField(max_length=512)
domain = models.ForeignKey('dpdapi.Domain', models.CASCADE) domain = models.ForeignKey("dpdapi.Domain", models.CASCADE)
def __str__(self): def __str__(self):
return self.email return self.email
@@ -52,5 +52,4 @@ class User(BaseModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.email = self.email.lower() self.email = self.email.lower()
super(User, self).save(*args, **kwargs) super().save(*args, **kwargs)
+10 -10
View File
@@ -1,10 +1,10 @@
from rest_framework import serializers
from passlib.hash import sha512_crypt from passlib.hash import sha512_crypt
from rest_framework import serializers
from .models import Alias, Domain, User from .models import Alias, Domain, User
class AliasSerializer(serializers.ModelSerializer): class AliasSerializer(serializers.ModelSerializer):
def create(self, validated_data): def create(self, validated_data):
# Workaround to accept duplicates # Workaround to accept duplicates
# FIXME, will fail with existing alias with validated_data['active']=False # FIXME, will fail with existing alias with validated_data['active']=False
@@ -14,36 +14,36 @@ class AliasSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Alias model = Alias
validators = [] # Disable unique togheter check validators = [] # Disable unique togheter check
fields = '__all__' fields = "__all__"
class AliasDeleteSerializer(serializers.ModelSerializer): class AliasDeleteSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Alias model = Alias
validators = [] # No Unique togheter check on deletion validators = [] # No Unique togheter check on deletion
fields = '__all__' fields = "__all__"
class DomainSerializer(serializers.ModelSerializer): class DomainSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Domain model = Domain
fields = '__all__' fields = "__all__"
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = '__all__' fields = "__all__"
extra_kwargs = {'password': {'write_only': True}} extra_kwargs = {"password": {"write_only": True}}
def _hash_password(self, raw_password): def _hash_password(self, raw_password):
return sha512_crypt.encrypt(raw_password) return sha512_crypt.encrypt(raw_password)
def create(self, validated_data): def create(self, validated_data):
user = User( user = User(
email=validated_data['email'], email=validated_data["email"],
domain=validated_data['domain'], domain=validated_data["domain"],
password=self._hash_password(validated_data['password']) password=self._hash_password(validated_data["password"]),
) )
user.save() user.save()
return user return user
+106
View File
@@ -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
+15 -17
View File
@@ -1,43 +1,41 @@
import json
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from dpdapi.models import Domain, Alias from dpdapi.models import Alias, Domain
class RegexTestCase(APITestCase): class RegexTestCase(APITestCase):
aliases = ['ops-alert@example.com', 'yolo-announce@example.com'] aliases = ["ops-alert@example.com", "yolo-announce@example.com"]
def setUp(self): def setUp(self):
user = User.objects.create(username='yolo') user = User.objects.create(username="yolo")
self.domain = Domain.objects.create(name='example.com') self.domain = Domain.objects.create(name="example.com")
for a in self.aliases: 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) self.client.force_login(user)
def test_get_all(self): 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)) self.assertEqual(len(res.data), len(self.aliases))
def test_domain_filter(self): def test_domain_filter(self):
params = {'domain__name': 'example.com'} params = {"domain__name": "example.com"}
res = self.client.get(reverse('alias-list'), data=params) res = self.client.get(reverse("alias-list"), data=params)
self.assertEqual(len(res.data), len(self.aliases)) self.assertEqual(len(res.data), len(self.aliases))
def test_domain_filter_no_hits(self): def test_domain_filter_no_hits(self):
params = {'domain__name': 'zexample.com'} params = {"domain__name": "zexample.com"}
res = self.client.get(reverse('alias-list'), data=params) res = self.client.get(reverse("alias-list"), data=params)
self.assertEqual(len(res.data), 0) self.assertEqual(len(res.data), 0)
def test_regex_filter(self): def test_regex_filter(self):
params = {'source__regex': '^ops-|^ops@'} params = {"source__regex": "^ops-|^ops@"}
res = self.client.get(reverse('alias-list'), data=params) res = self.client.get(reverse("alias-list"), data=params)
self.assertEqual(len(res.data), 1) self.assertEqual(len(res.data), 1)
def test_delete_bulk_single(self): def test_delete_bulk_single(self):
data = {'destination': 'ninja@example.com', 'source': 'ding@example.com', 'domain': self.domain.pk} 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) Alias.objects.create(source=data["source"], destination=data["destination"], domain=self.domain)
res = self.client.delete(reverse('alias-delete-bulk'), data=[data], format='json') res = self.client.delete(reverse("alias-delete-bulk"), data=[data], format="json")
self.assertEqual(res.status_code, 204, res.data) self.assertEqual(res.status_code, 204, res.data)
+14 -5
View File
@@ -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 rest_framework.routers import DefaultRouter
from .views import AliasViewSet, DomainViewSet, UserViewSet 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() urlpatterns = [
router.register(r'aliases', AliasViewSet) path("", RedirectView.as_view(pattern_name="api-root")),
router.register(r'domains', DomainViewSet) re_path(r"^api/", include(api_router.urls)),
router.register(r'users', UserViewSet) re_path(r"^admin/", admin.site.urls),
urlpatterns = router.urls re_path(r"^api-auth/", include(rest_framework_urls, namespace="rest_framework")),
]
+17 -11
View File
@@ -1,32 +1,36 @@
import logging import logging
import operator import operator
from functools import reduce from functools import reduce
from django.db.models import Q from django.db.models import Q
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status, viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.mixins import ListModelMixin from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status, viewsets
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from dpdapi.filters import AliasRegexFilterBackend from dpdapi.filters import AliasRegexFilterBackend
from dpdapi.models import Alias, Domain, User 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__) logger = logging.getLogger(__name__)
class AliasViewSet(ListModelMixin, GenericViewSet): class AliasViewSet(ListModelMixin, GenericViewSet):
"""Aliases""" """Aliases"""
queryset = Alias.objects.all() queryset = Alias.objects.all()
serializer_class = AliasSerializer serializer_class = AliasSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
filter_backends = (DjangoFilterBackend, AliasRegexFilterBackend) 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): def create_bulk(self, request):
serializer = AliasSerializer(data=request.data, many=True) serializer = AliasSerializer(data=request.data, many=True)
@@ -35,7 +39,7 @@ class AliasViewSet(ListModelMixin, GenericViewSet):
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['delete']) @action(detail=False, methods=["delete"])
def delete_bulk(self, request): def delete_bulk(self, request):
serializer = AliasDeleteSerializer(data=request.data, many=True) 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) q_list = map(lambda x: Q(**x), serializer.initial_data)
aliases = Alias.objects.filter(reduce(operator.or_, q_list)) aliases = Alias.objects.filter(reduce(operator.or_, q_list))
initial_set = set(map(lambda x: (x['source'], x['destination'], x['domain']), serializer.initial_data)) initial_set = set(map(lambda x: (x["source"], x["destination"], x["domain"]), serializer.initial_data))
deleted_set = set(aliases.values_list('source', 'destination', 'domain')) deleted_set = set(aliases.values_list("source", "destination", "domain"))
diff = initial_set.difference(deleted_set) 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() aliases.delete()
if diff: 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)) res = "Aliases '{}' could not be found and were not deleted.".format(", ".join(diff_formatted))
logger.info(res) logger.info(res)
return Response({"result": res}, status=status.HTTP_200_OK) return Response({"result": res}, status=status.HTTP_200_OK)
@@ -62,15 +66,17 @@ class AliasViewSet(ListModelMixin, GenericViewSet):
class DomainViewSet(viewsets.ModelViewSet): class DomainViewSet(viewsets.ModelViewSet):
"""Domains""" """Domains"""
queryset = Domain.objects.all() queryset = Domain.objects.all()
serializer_class = DomainSerializer serializer_class = DomainSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filterset_fields = ('name',) filterset_fields = ("name",)
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(viewsets.ModelViewSet):
"""Users""" """Users"""
queryset = User.objects.all() queryset = User.objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
+7
View File
@@ -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()
View File
Whitespace-only changes.
-150
View File
@@ -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
-14
View File
@@ -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'))
]
-15
View File
@@ -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()
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import sys import sys
if __name__ == "__main__": 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 from django.core.management import execute_from_command_line
Generated
+991
View File
@@ -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 = []
+47
View File
@@ -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 <edb@neuf.no>"]
[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"
-9
View File
@@ -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.*