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
Nikolai R Kristiansen 3b9a43fc7a 🐛Allow longer password hashes 2020-09-09 23:42:58 +02:00
nikolark 1104c64c2a Correct Sentry DSN 2019-12-31 00:27:10 +01:00
nikolark 946af247c6 ⬆ Bump all the things 2019-12-31 00:21:23 +01:00
Nikolai R Kristiansen 35929d755a ⬆️ bump deps 2018-08-12 22:00:38 +02:00
ponas e3567ed06d Add django MessageMiddleware 2018-04-04 19:51:08 +02:00
Nikolai R Kristiansen 861f890567 Settings fixes
`MIDDLEWARE_CLASSES` is no more
2018-03-17 13:36:28 +01:00
Nikolai R Kristiansen e3fcb4b543 Remove deprecated middleware, add sentry 2018-03-17 13:30:29 +01:00
Nikolai R Kristiansen 8c0c84eb49 ⬆️Django 2.x 2018-03-17 13:09:43 +01:00
Nikolai R Kristiansen 70be41df96 ⬆️ Bump deps 2018-03-17 11:42:19 +01:00
nikolark 3daa708ea2 Update .gitlab-ci.yml 2017-11-20 14:42:52 +01:00
Nikolai R Kristiansen 7ad5191361 chore: run ci 2017-11-20 14:33:14 +01:00
Nikolai R Kristiansen 39f4ee654a Fix delete bulk
Add test
2017-11-20 14:28:42 +01:00
nikolark 053c02d8bc Merge branch 'bump' into 'master'
Bump

See merge request edb/django-postfix-dovecot-api!2
2017-11-01 23:36:21 +01:00
Nikolai R Kristiansen f0a7858811 Fix regex lookup
Note: Renames filter fields:
 - source_regex to source__iregex
 - destination_regex to destination__iregex
2017-11-01 23:29:57 +01:00
Nikolai R Kristiansen 2a037fbe8f Try fixing regex lookup
Adds tests
2017-11-01 21:44:15 +01:00
Nikolai R Kristiansen 4e41af0bd8 Bump to latest django, drf
TODO: Fix regex filter (write a test)
2017-10-25 21:36:20 +02:00
nikolark ad025f17d8 Add gunicorn dep, deploy with supervisor 2016-02-08 23:33:53 +01:00
nikolark 2a2baef6d2 Merge branch 'python3' into 'master'
python3 compat and deps++



See merge request !1
2016-02-08 23:15:16 +01:00
nikolark 5efea835d0 python3 compat and deps++ 2016-02-08 23:07:01 +01:00
nikolark 88eeabe746 Allow filtering aliases on destination 2015-03-28 18:10:45 +01:00
nikolark c5654d7993 Adds comment 2015-03-24 17:31:44 +01:00
nikolark 243f480ce2 Allow duplicate emails in API, but dont save them 2015-03-24 17:30:56 +01:00
32 changed files with 1487 additions and 311 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
+42
View File
@@ -0,0 +1,42 @@
default:
image: python:3.8-slim
cache:
paths:
- .cache/pip
- .venv
test:
before_script:
- poetry install
script:
- 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
+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"]
+11 -14
View File
@@ -1,23 +1,20 @@
# Install # django-postfix-dovecot-api
apt install python-dev libmysqlclient-dev python-virtualenv
virtualenv venv
. venv/bin/activate
pip install -r requirements
# Deploy ## Getting started
First you need to have your public key in /home/gitdeploy/.ssh/authorized_keys on the server.
Then, to deploy via login.neuf.no, add the following to your personal ~/.ssh/config ```bash
sudo apt install libmysqlclient-dev
poetry install
poetry run bin/run
```
Host lynx.neuf.no ## Deploy
User gitdeploy
ForwardAgent yes
ProxyCommand ssh -W %h:%p login.neuf.no -l USERNAME
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
+4 -4
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)
@@ -17,4 +17,4 @@ class DomainAdmin(admin.ModelAdmin):
@admin.register(User) @admin.register(User)
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
pass pass
+2 -4
View File
@@ -1,15 +1,13 @@
import django_filters import django_filters
from rest_framework.filters import BaseFilterBackend
from dpdapi.models import Alias from dpdapi.models import Alias
from rest_framework.filters import BaseFilterBackend
class AliasRegexFilter(django_filters.FilterSet): class AliasRegexFilter(django_filters.FilterSet):
# Ref: https://docs.djangoproject.com/en/1.7/ref/models/querysets/#iregex
source_regex = django_filters.CharFilter(name="source", lookup_type='iregex')
class Meta: class Meta:
model = Alias model = Alias
fields = {"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')), ("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'), 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")},
), ),
] ]
@@ -0,0 +1,18 @@
# Generated by Django 2.2.16 on 2020-09-09 21:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("dpdapi", "0002_auto_20150118_1638"),
]
operations = [
migrations.AlterField(
model_name="user",
name="password",
field=models.CharField(max_length=512),
),
]
+12 -14
View File
@@ -1,4 +1,3 @@
from __future__ import unicode_literals
from django.db import models from django.db import models
@@ -13,45 +12,44 @@ 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') domain = models.ForeignKey("dpdapi.Domain", models.CASCADE)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)
def __unicode__(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):
name = models.CharField(max_length=254, unique=True) name = models.CharField(max_length=254, unique=True)
def __unicode__(self): def __str__(self):
return self.name return self.name
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=109) password = models.CharField(max_length=512)
domain = models.ForeignKey('dpdapi.Domain') domain = models.ForeignKey("dpdapi.Domain", models.CASCADE)
def __unicode__(self): def __str__(self):
return self.email return self.email
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)
+17 -5
View File
@@ -1,37 +1,49 @@
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):
# Workaround to accept duplicates
# FIXME, will fail with existing alias with validated_data['active']=False
alias, created = Alias.objects.get_or_create(**validated_data)
return alias
class Meta: class Meta:
model = Alias model = Alias
validators = [] # Disable unique togheter check
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__"
class DomainSerializer(serializers.ModelSerializer): class DomainSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Domain model = Domain
fields = "__all__"
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
extra_kwargs = {'password': {'write_only': True}} fields = "__all__"
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
+40 -2
View File
@@ -1,3 +1,41 @@
from django.test import TestCase from django.contrib.auth.models import User
from django.urls import reverse
from rest_framework.test import APITestCase
# Create your tests here. from dpdapi.models import Alias, Domain
class RegexTestCase(APITestCase):
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")
for a in self.aliases:
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"))
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)
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)
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)
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")
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")),
]
+28 -19
View File
@@ -1,29 +1,36 @@
from __future__ import unicode_literals
import logging import logging
import operator import operator
from functools import reduce
from django.db.models import Q from django.db.models import Q
from rest_framework.decorators import list_route 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.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status, filters, viewsets 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(viewsets.ModelViewSet): 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 = (filters.DjangoFilterBackend, AliasRegexFilterBackend) filter_backends = (DjangoFilterBackend, AliasRegexFilterBackend)
filter_fields = ('domain', 'domain__name',) filterset_fields = (
"domain",
"domain__name",
)
@list_route(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)
@@ -32,7 +39,7 @@ class AliasViewSet(viewsets.ModelViewSet):
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@list_route(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)
@@ -42,14 +49,14 @@ class AliasViewSet(viewsets.ModelViewSet):
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)
@@ -58,16 +65,18 @@ class AliasViewSet(viewsets.ModelViewSet):
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 = (filters.DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filter_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.
-98
View File
@@ -1,98 +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
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
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
)
INSTALLED_APPS += (
'rest_framework',
'dpdapi',
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
ROOT_URLCONF = 'dpdapisite.urls'
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',
)
}
try:
from local_settings import *
except ImportError:
pass
-11
View File
@@ -1,11 +0,0 @@
from django.conf.urls import patterns, include, url
from django.contrib import admin
from django.views.generic import RedirectView
urlpatterns = patterns(
'',
url(r'^$', RedirectView.as_view(pattern_name='api-root')),
url(r'^api/', include('dpdapi.urls')),
url(r'^admin/', include(admin.site.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
)
-14
View File
@@ -1,14 +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
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dpdapisite.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
Vendored
+16 -57
View File
@@ -1,63 +1,22 @@
import logging import os
import sys
from contextlib import contextmanager as _contextmanager from fabric import Connection
from fabric.api import run, sudo, env, cd, prefix, lcd from invoke import task
from fabric.api import local as lrun
env.use_ssh_config = True
env.hosts = ['lynx.neuf.no']
env.project_path = '/var/www/neuf.no/django-postfix-dovecot-api'
env.user = 'gitdeploy'
env.activate = 'source {}/venv/bin/activate'.format(env.project_path)
@_contextmanager @task()
def virtualenv(): def deploy(c):
with cd(env.project_path): """Make sure proxy_user is set to your neuf username."""
with prefix(env.activate): project_path = '/var/www/neuf.no/django-postfix-dovecot-api'
yield proxy_user = os.getenv('DEPLOY_USER', os.getenv('USER'))
c = Connection(host='gitdeploy@lynx.neuf.no', gateway=Connection('login.neuf.no', user=proxy_user))
def __git_validate_clean(): with c.cd(project_path), c.prefix('source {}/venv/bin/activate'.format(project_path)):
with lcd(env.project_path): c.run('git pull') # Get source
if lrun('git diff-files', capture=True) != '': c.run('pip install -r requirements.txt') # install deps in virtualenv
logging.error("Your git-tree isn't clean! Commit your changes before deploying.") c.run('umask 022; python manage.py collectstatic --noinput') # Collect static
sys.exit(1) c.run('python manage.py migrate') # Run DB migrations
# Reload mxapi.neuf.no
def test(): c.sudo('/usr/bin/supervisorctl pid mxapi.neuf.no | xargs kill -HUP', shell=False)
lrun("python manage.py test dpdapi")
def commit():
lrun("git add -p && git commit")
def push():
lrun("git push")
def prepare_deploy():
test()
commit()
push()
def gitstatus():
# __git_validate_clean()
with virtualenv():
# Get source
run('git branch -v')
def deploy():
# __git_validate_clean()
with virtualenv():
run('git pull') # Get source
run('pip install -r requirements.txt') # install deps in virtualenv
run('umask 022; python manage.py collectstatic --noinput') # Collect static
run('python manage.py migrate') # Run DB migrations
# Reload gunicorn
sudo('/etc/init.d/gunicorn reload', shell=False)
+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"
-7
View File
@@ -1,7 +0,0 @@
Django>=1.7,<1.8
django-extensions==1.4.9
django-filter==0.9.1
djangorestframework>=3.0,<3.1
passlib==1.6.2
mysqlclient==1.3.4
Fabric==1.10.1