chore: ruff fixes and formatting

This commit is contained in:
nikolark committed 2025-05-30 13:04:13 +02:00
1 parent 9a796bdc24
commit 930c6a3059
27 files changed
+444 -313

No files matched your search

+1
View File
@@ -8,3 +8,4 @@ node_modules/
.sass-cache/
staticfiles/
mailadmin/static/mailadmin/dist/
__pycache__/
+6 -6
View File
@@ -1,12 +1,12 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
# - repo: https://github.com/astral-sh/ruff-pre-commit
# rev: 'v0.11.7'
# hooks:
# - id: ruff
# args: [--fix, --exit-non-zero-on-fix]
# - id: ruff-format
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.11.7'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/adamchainz/django-upgrade
rev: "1.24.0"
hooks:
Vendored
+22 -16
View File
@@ -7,27 +7,33 @@ from invoke import task
@task()
def deploy(c):
"""Make sure proxy_user is set to your neuf username."""
project_path = '/var/www/neuf.no/mailadmin'
proxy_user = os.getenv('DEPLOY_USER', os.getenv('USER'))
project_path = "/var/www/neuf.no/mailadmin"
proxy_user = os.getenv("DEPLOY_USER", os.getenv("USER"))
c = Connection(host='gitdeploy@dreamcast.neuf.no', gateway=Connection('login.neuf.no', user=proxy_user))
c = Connection(
host="gitdeploy@dreamcast.neuf.no",
gateway=Connection("login.neuf.no", user=proxy_user),
)
with c.cd(project_path), c.prefix('source {}/venv/bin/activate'.format(project_path)):
c.run('git pull') # Get source
c.run('pip install -U pip')
c.run('pip install -r requirements.txt') # install deps in virtualenv
with c.cd('mailadmin/static/mailadmin'): # install and compile frontend deps
c.run('npm i')
c.run('npm run build')
c.run('python manage.py collectstatic --noinput -i node_modules') # Collect static
c.run('python manage.py migrate') # Run DB migrations
with (
c.cd(project_path),
c.prefix(f"source {project_path}/venv/bin/activate"),
):
c.run("git pull") # Get source
c.run("pip install -U pip")
c.run("pip install -r requirements.txt") # install deps in virtualenv
with c.cd("mailadmin/static/mailadmin"): # install and compile frontend deps
c.run("npm i")
c.run("npm run build")
c.run("python manage.py collectstatic --noinput -i node_modules") # Collect static
c.run("python manage.py migrate") # Run DB migrations
# Reload gunicorn
c.sudo('/usr/bin/supervisorctl pid lister.neuf.no | xargs kill -HUP', shell=False)
c.sudo("/usr/bin/supervisorctl pid lister.neuf.no | xargs kill -HUP", shell=False)
@task
def build(c):
with c.cd('mailadmin/static/mailadmin'):
c.run('npm i')
c.run('npm run build')
with c.cd("mailadmin/static/mailadmin"):
c.run("npm i")
c.run("npm run build")
+4 -3
View File
@@ -10,9 +10,10 @@ class PrefixInline(admin.TabularInline):
@admin.register(OrgUnit)
class OrgUnitAdmin(admin.ModelAdmin):
list_display = ('name', 'is_active')
list_filter = ('is_active',)
filter_horizontal = ('admin_groups', 'member_groups')
list_display = ("name", "is_active")
list_filter = ("is_active",)
filter_horizontal = ("admin_groups", "member_groups")
inlines = [PrefixInline]
admin.site.register(Prefix)
+30 -31
View File
@@ -1,38 +1,44 @@
import logging
import requests
from django.conf import settings
import logging
from requests.exceptions import ConnectionError
from requests.auth import HTTPBasicAuth
from rest_framework import exceptions
from rest_framework import status
from requests.exceptions import ConnectionError
from rest_framework import exceptions, status
logger = logging.getLogger(__name__)
class DPDAPIException(exceptions.APIException):
status_code = 503
detail = 'Unknown error.'
detail = "Unknown error."
def __init__(self, status_code, detail):
self.status_code = status_code
self.detail = detail
class DjangoPostfixDovecotAPI(object):
class DjangoPostfixDovecotAPI:
"""
django-postfix-dovecot-api
https://git.neuf.no/edb/django-postfix-dovecot-api#tab-readme
"""
def __init__(self, base_url=settings.DPD_API_URL, username=settings.DPD_API_USERNAME,
password=settings.DPD_API_PASSWORD):
def __init__(
self,
base_url=settings.DPD_API_URL,
username=settings.DPD_API_USERNAME,
password=settings.DPD_API_PASSWORD,
):
self.base_url = base_url
self.username = username
self.password = password
def _request(self, method, path, params=None, json=None):
headers = {'content-type': 'application/json'}
url = "{}{}".format(self.base_url, path)
headers = {"content-type": "application/json"}
url = f"{self.base_url}{path}"
try:
r = requests.request(
@@ -41,20 +47,21 @@ class DjangoPostfixDovecotAPI(object):
params=params,
auth=HTTPBasicAuth(self.username, self.password),
json=json,
headers=headers)
headers=headers,
)
except ConnectionError as e:
msg = 'mxapi at {} unavailable {}'.format(url, str(e))
msg = f"mxapi at {url} unavailable {e!s}"
logger.warning(msg)
raise DPDAPIException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail={'error': msg})
raise DPDAPIException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail={"error": msg})
try:
r.raise_for_status()
except requests.exceptions.RequestException as e:
logger.warning('mxapi errored {}'.format(e.response))
logger.warning(f"mxapi errored {e.response}")
try:
raise DPDAPIException(status_code=e.response.status_code, detail=e.response.json())
except ValueError:
raise DPDAPIException(status_code=e.response.status_code, detail='mxapi fatal error')
raise DPDAPIException(status_code=e.response.status_code, detail="mxapi fatal error")
try:
ret = r.json()
@@ -64,33 +71,25 @@ class DjangoPostfixDovecotAPI(object):
return ret
def create_aliases(self, aliases):
return self._request('POST', '/aliases/create_bulk/', json=aliases)
return self._request("POST", "/aliases/create_bulk/", json=aliases)
def delete_aliases(self, aliases):
return self._request('DELETE', '/aliases/delete_bulk/', json=aliases)
return self._request("DELETE", "/aliases/delete_bulk/", json=aliases)
def list_aliases_regex(self, regex):
""" Limit aliases by regular expression and domain name
"""Limit aliases by regular expression and domain name
:param regex: Regular expression filtering out aliases
"""
params = {
'domain__name': settings.NEUF_EMAIL_DOMAIN_NAME,
'source__iregex': regex
"domain__name": settings.NEUF_EMAIL_DOMAIN_NAME,
"source__iregex": regex,
}
return self._request(
'GET',
'/aliases/',
params=params
)
return self._request("GET", "/aliases/", params=params)
def list_domains(self, name=None):
params = {}
if name is not None:
params['name'] = name
params["name"] = name
return self._request(
'GET',
'/domains/',
params=params
)
return self._request("GET", "/domains/", params=params)
+7 -7
View File
@@ -17,14 +17,14 @@ class LDAPEmailBackend(LDAPBackend):
the user's DN and ldap_user.attrs contains all of their LDAP attributes.
"""
model = self.get_user_model()
username_field = getattr(model, 'USERNAME_FIELD', 'username')
username_field = getattr(model, "USERNAME_FIELD", "username")
kwargs = {
username_field + '__iexact': ldap_user.attrs['uid'][0],
'defaults': {
username_field: ldap_user.attrs['uid'][0].lower(),
'email': email
}
username_field + "__iexact": ldap_user.attrs["uid"][0],
"defaults": {
username_field: ldap_user.attrs["uid"][0].lower(),
"email": email,
},
}
return model.objects.get_or_create(**kwargs)
return model.objects.get_or_create(**kwargs)
View File
Whitespace-only changes.
Whitespace-only changes.
@@ -1,28 +0,0 @@
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group
import requests
from mailadmin.models import OrgUnit
class Command(BaseCommand):
help = 'Adds groups and OrgUnits from inside.studentersamfundet.no.'
def handle(self, *args, **options):
r = requests.get('https://inside.studentersamfundet.no/api/groups.php')
res = r.json()
if 'results' not in res:
self.stderr.write('Error: No groups, bailing...')
groups = res['results']
for g in groups:
if g['posix_group'] != "":
obj, created = Group.objects.get_or_create(name=g['posix_group'])
ou_obj, ou_created = OrgUnit.objects.get_or_create(name=g['division_name'], inside_id=g['division_id'])
if 'styret' in g['posix_group']:
ou_obj.admin_groups.add(obj)
else:
ou_obj.member_groups.add(obj)
self.stdout.write('Done.')
+26 -17
View File
@@ -1,30 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
("auth", "0001_initial"),
]
operations = [
migrations.CreateModel(
name='OrgUnit',
name="OrgUnit",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=500)),
('prefix', models.CharField(max_length=256, null=True, blank=True)),
('posix_group', models.CharField(max_length=256, null=True, blank=True)),
('is_active', models.BooleanField(default=True)),
('created', models.DateTimeField(auto_now_add=True)),
('update', models.DateTimeField(auto_now=True)),
('groups', models.ManyToManyField(to='auth.Group', null=True, blank=True)),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("name", models.CharField(max_length=500)),
("prefix", models.CharField(max_length=256, null=True, blank=True)),
(
"posix_group",
models.CharField(max_length=256, null=True, blank=True),
),
("is_active", models.BooleanField(default=True)),
("created", models.DateTimeField(auto_now_add=True)),
("update", models.DateTimeField(auto_now=True)),
(
"groups",
models.ManyToManyField(to="auth.Group", null=True, blank=True),
),
],
options={
},
options={},
bases=(models.Model,),
),
]
@@ -1,19 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mailadmin', '0001_initial'),
("mailadmin", "0001_initial"),
]
operations = [
migrations.AddField(
model_name='orgunit',
name='inside_id',
model_name="orgunit",
name="inside_id",
field=models.IntegerField(null=True, blank=True),
preserve_default=True,
),
+15 -19
View File
@@ -1,39 +1,35 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
('mailadmin', '0002_orgunit_inside_id'),
("auth", "0001_initial"),
("mailadmin", "0002_orgunit_inside_id"),
]
operations = [
migrations.AlterModelOptions(
name='orgunit',
options={'ordering': ['name']},
name="orgunit",
options={"ordering": ["name"]},
),
migrations.RemoveField(
model_name='orgunit',
name='groups',
model_name="orgunit",
name="groups",
),
migrations.RemoveField(
model_name='orgunit',
name='posix_group',
model_name="orgunit",
name="posix_group",
),
migrations.AddField(
model_name='orgunit',
name='admin_groups',
field=models.ManyToManyField(related_name='admin_orgunits', null=True, to='auth.Group', blank=True),
model_name="orgunit",
name="admin_groups",
field=models.ManyToManyField(related_name="admin_orgunits", null=True, to="auth.Group", blank=True),
preserve_default=True,
),
migrations.AddField(
model_name='orgunit',
name='member_groups',
field=models.ManyToManyField(related_name='member_orgunits', null=True, to='auth.Group', blank=True),
model_name="orgunit",
name="member_groups",
field=models.ManyToManyField(related_name="member_orgunits", null=True, to="auth.Group", blank=True),
preserve_default=True,
),
]
+24 -14
View File
@@ -1,8 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.db.models.deletion
from django.db import migrations, models
def normalize_prefixes(apps, schema_editor):
@@ -19,26 +16,39 @@ def noop(*args, **kwargs):
class Migration(migrations.Migration):
dependencies = [
('mailadmin', '0003_auto_20141105_2153'),
("mailadmin", "0003_auto_20141105_2153"),
]
operations = [
migrations.CreateModel(
name='Prefix',
name="Prefix",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=500, null=True, blank=True)),
('orgunit', models.ForeignKey(related_name='prefixes', to='mailadmin.OrgUnit', on_delete=django.db.models.deletion.CASCADE)),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("name", models.CharField(max_length=500, null=True, blank=True)),
(
"orgunit",
models.ForeignKey(
related_name="prefixes",
to="mailadmin.OrgUnit",
on_delete=django.db.models.deletion.CASCADE,
),
),
],
options={
},
options={},
bases=(models.Model,),
),
migrations.RunPython(normalize_prefixes, noop),
migrations.RemoveField(
model_name='orgunit',
name='prefix',
model_name="orgunit",
name="prefix",
),
]
@@ -1,18 +1,14 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('mailadmin', '0004_auto_20150125_1401'),
("mailadmin", "0004_auto_20150125_1401"),
]
operations = [
migrations.AlterModelOptions(
name='prefix',
options={'verbose_name_plural': 'prefixes'},
name="prefix",
options={"verbose_name_plural": "prefixes"},
),
]
@@ -1,25 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.1 on 2016-01-10 15:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mailadmin', '0005_auto_20150125_1940'),
("mailadmin", "0005_auto_20150125_1940"),
]
operations = [
migrations.AlterField(
model_name='orgunit',
name='admin_groups',
field=models.ManyToManyField(blank=True, related_name='admin_orgunits', to='auth.Group'),
model_name="orgunit",
name="admin_groups",
field=models.ManyToManyField(blank=True, related_name="admin_orgunits", to="auth.Group"),
),
migrations.AlterField(
model_name='orgunit',
name='member_groups',
field=models.ManyToManyField(blank=True, related_name='member_orgunits', to='auth.Group'),
model_name="orgunit",
name="member_groups",
field=models.ManyToManyField(blank=True, related_name="member_orgunits", to="auth.Group"),
),
]
+8 -9
View File
@@ -8,31 +8,30 @@ class OrgUnit(models.Model):
name = models.CharField(max_length=500)
is_active = models.BooleanField(default=True)
admin_groups = models.ManyToManyField(Group, blank=True, related_name='admin_orgunits')
member_groups = models.ManyToManyField(Group, blank=True, related_name='member_orgunits')
admin_groups = models.ManyToManyField(Group, blank=True, related_name="admin_orgunits")
member_groups = models.ManyToManyField(Group, blank=True, related_name="member_orgunits")
created = models.DateTimeField(auto_now_add=True)
update = models.DateTimeField(auto_now=True)
inside_id = models.IntegerField(null=True, blank=True)
class Meta:
ordering = ['name']
ordering = ["name"]
class PrefixQueryset(models.QuerySet):
def as_regex(self):
prefixes = self.values_list('name', flat=True)
return '|'.join(['^{0}-|^{0}@'.format(p) for p in prefixes])
prefixes = self.values_list("name", flat=True)
return "|".join([f"^{p}-|^{p}@" for p in prefixes])
class Prefix(models.Model):
def __str__(self):
return self.name or ''
return self.name or ""
name = models.CharField(max_length=500, blank=True, null=True)
orgunit = models.ForeignKey('mailadmin.OrgUnit', models.CASCADE, related_name='prefixes')
orgunit = models.ForeignKey("mailadmin.OrgUnit", models.CASCADE, related_name="prefixes")
objects = PrefixQueryset.as_manager()
class Meta:
verbose_name_plural = 'prefixes'
verbose_name_plural = "prefixes"
+9 -9
View File
@@ -1,17 +1,21 @@
from rest_framework import permissions
import re
from rest_framework import permissions
from mailadmin.serializers import AliasSerializer
class SourcePrefixOwner(permissions.BasePermission):
""" Ref: http://www.django-rest-framework.org/api-guide/permissions """
"""Ref: http://www.django-rest-framework.org/api-guide/permissions"""
def has_permission(self, request, view):
assert hasattr(view, 'user_prefixes')
assert hasattr(view, "user_prefixes")
# Read permissions (GET, HEAD or OPTIONS) are allowed for all authenticated request
if request.method in permissions.SAFE_METHODS or request.method not in ['DELETE', 'POST']:
if request.method in permissions.SAFE_METHODS or request.method not in [
"DELETE",
"POST",
]:
return True
if request.user.is_superuser:
@@ -29,8 +33,4 @@ class SourcePrefixOwner(permissions.BasePermission):
# Prepare regular expression
regexp = my_prefixes.as_regex()
# All source adresses should match the prefix regexp
for alias in serializer.initial_data:
if re.search(regexp, alias.get('source', '')) is None:
return False
return True
return all(re.search(regexp, alias.get("source", "")) is not None for alias in serializer.initial_data)
+18 -12
View File
@@ -1,15 +1,15 @@
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import Group, User
from rest_framework import serializers
from rest_framework.relations import SlugRelatedField
from mailadmin.models import OrgUnit
from rest_framework.relations import SlugRelatedField
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = ['id', 'name']
fields = ["id", "name"]
class UserSerializer(serializers.ModelSerializer):
@@ -17,21 +17,27 @@ class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
exclude = ['password', 'user_permissions']
exclude = ["password", "user_permissions"]
class OrgUnitSerializer(serializers.ModelSerializer):
admin_groups = GroupSerializer(many=True)
member_groups = GroupSerializer(many=True)
prefixes = SlugRelatedField(
many=True,
read_only=True,
slug_field='name')
prefixes = SlugRelatedField(many=True, read_only=True, slug_field="name")
class Meta:
model = OrgUnit
fields = ['id', 'admin_groups', 'member_groups', 'prefixes', 'inside_id', 'name', 'is_active',
'created', 'update']
fields = [
"id",
"admin_groups",
"member_groups",
"prefixes",
"inside_id",
"name",
"is_active",
"created",
"update",
]
class AliasSerializer(serializers.Serializer):
@@ -42,8 +48,8 @@ class AliasSerializer(serializers.Serializer):
def validate_source(self, value):
_domain_name = settings.NEUF_EMAIL_DOMAIN_NAME
domain_part = value.split('@')[1]
domain_part = value.split("@")[1]
if domain_part != _domain_name:
raise serializers.ValidationError("source '{0}' is not in domain {1}".format(value, _domain_name))
raise serializers.ValidationError(f"source '{value}' is not in domain {_domain_name}")
return value
+16
View File
@@ -0,0 +1,16 @@
from http import HTTPStatus
import pytest
from django.contrib.auth.models import User
from django.urls import reverse
@pytest.fixture
def user(db):
return User.objects.create(username="admin")
def test_open_lists(user, client):
client.force_login(user=user)
response = client.get(reverse("lists"))
assert response.status_code == HTTPStatus.OK
-15
View File
@@ -1,15 +0,0 @@
from django.contrib.auth.models import User
from django.test import TestCase
# import unittest
from django.urls import reverse
class FrontendTest(TestCase):
def setUp(self):
self.user = User.objects.create(username='admin')
self.client.force_login(user=self.user)
def test_open_lists(self):
response = self.client.get(reverse('lists'))
self.assertEqual(response.status_code, 200)
+19 -11
View File
@@ -1,23 +1,31 @@
from django.urls import path
from rest_framework.routers import DefaultRouter
from mailadmin.views import MyUserViewSet, MyOrgUnitViewSet, OrgUnitViewSet, AliasesView, get_domains, index, lists, \
AliasesDeleteView
from mailadmin.views import (
AliasesDeleteView,
AliasesView,
MyOrgUnitViewSet,
MyUserViewSet,
OrgUnitViewSet,
get_domains,
index,
lists,
)
urlpatterns = [
path('', index, name='index'),
path('lists/', lists, name='lists'),
path("", index, name="index"),
path("lists/", lists, name="lists"),
]
# API
router = DefaultRouter()
router.register(r'api/me', MyUserViewSet, basename='me')
router.register(r'api/orgunits', MyOrgUnitViewSet, basename='ous')
router.register(r'api/orgunits/all', OrgUnitViewSet, basename='allous')
router.register(r"api/me", MyUserViewSet, basename="me")
router.register(r"api/orgunits", MyOrgUnitViewSet, basename="ous")
router.register(r"api/orgunits/all", OrgUnitViewSet, basename="allous")
urlpatterns += router.urls
urlpatterns += [
path('api/aliases/', AliasesView.as_view(), name='aliases'),
path('api/aliases/delete/', AliasesDeleteView.as_view(), name='aliases-delete'),
path('api/domains/', get_domains, name='email-domains'),
]
path("api/aliases/", AliasesView.as_view(), name="aliases"),
path("api/aliases/delete/", AliasesDeleteView.as_view(), name="aliases-delete"),
path("api/domains/", get_domains, name="email-domains"),
]
+25 -21
View File
@@ -1,21 +1,21 @@
import logging
from django.conf import settings
from django.contrib.auth import login, logout as auth_logout
from django.contrib.auth import login
from django.contrib.auth import logout as auth_logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User
from django.shortcuts import render, redirect
import logging
from rest_framework import viewsets, views
from django.shortcuts import redirect, render
from rest_framework import status, views, viewsets
from rest_framework.decorators import api_view
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from mailadmin.api import DjangoPostfixDovecotAPI
from mailadmin.models import OrgUnit, Prefix
from mailadmin.serializers import UserSerializer, OrgUnitSerializer, AliasSerializer
from mailadmin.permissions import SourcePrefixOwner
from mailadmin.serializers import AliasSerializer, OrgUnitSerializer, UserSerializer
logger = logging.getLogger(__name__)
@@ -26,14 +26,13 @@ def index(request):
if form.is_valid():
# Log the user in.
login(request, form.get_user())
return redirect('lists')
return redirect("lists")
elif request.user.is_authenticated:
return redirect("lists")
else:
if request.user.is_authenticated:
return redirect('lists')
else:
form = AuthenticationForm(request)
form = AuthenticationForm(request)
return render(request, "index.html", {'form': form})
return render(request, "index.html", {"form": form})
@login_required
@@ -43,19 +42,22 @@ def lists(request):
def logout(request):
auth_logout(request)
return redirect('index')
return redirect("index")
class BaseAliasesView(views.APIView):
_api = DjangoPostfixDovecotAPI()
permission_classes = (IsAuthenticated, SourcePrefixOwner,)
permission_classes = (
IsAuthenticated,
SourcePrefixOwner,
)
def _user_groups(self, request):
group_filter = request.query_params.get('groups', None)
group_filter = request.query_params.get("groups", None)
if group_filter is None:
user_groups = request.user.groups.all()
else:
group_filter = group_filter.split(',') if ',' in group_filter else [group_filter]
group_filter = group_filter.split(",") if "," in group_filter else [group_filter]
user_groups = request.user.groups.filter(pk__in=group_filter)
return user_groups
@@ -71,14 +73,15 @@ class BaseAliasesView(views.APIView):
class AliasesView(BaseAliasesView):
""" List and create aliases from django-postfix-dovecot-api """
"""List and create aliases from django-postfix-dovecot-api"""
def get(self, request):
user_prefixes = self.user_prefixes(request)
# Prepare regular expression
regex = user_prefixes.as_regex()
if len(user_prefixes) == 0:
return Response({'result': 'No results'}) # Need at least one
return Response({"result": "No results"}) # Need at least one
return Response(self._api.list_aliases_regex(regex))
@@ -91,7 +94,8 @@ class AliasesView(BaseAliasesView):
class AliasesDeleteView(BaseAliasesView):
""" Delete aliases (using POST) from django-postfix-dovecot-api """
"""Delete aliases (using POST) from django-postfix-dovecot-api"""
def post(self, request):
aliases = AliasSerializer(data=request.data, many=True)
if not aliases.is_valid():
@@ -128,7 +132,7 @@ class OrgUnitViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [IsAuthenticated]
@api_view(['GET'])
@api_view(["GET"])
def get_domains(request):
api = DjangoPostfixDovecotAPI()
domains = api.list_domains(name=settings.NEUF_EMAIL_DOMAIN_NAME)
+56 -63
View File
@@ -1,4 +1,6 @@
import contextlib
from pathlib import Path
import ldap
from django_auth_ldap.config import LDAPSearch, PosixGroupType
@@ -9,7 +11,7 @@ BASE_DIR = Path(__file__).parent.parent
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'g658807^w2!kzsdb_wznxo$5xpjb2x(sfoub%l_ap77@@_zt&*'
SECRET_KEY = "g658807^w2!kzsdb_wznxo$5xpjb2x(sfoub%l_ap77@@_zt&*" # noqa: S105
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@@ -18,56 +20,55 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'bootstrap4',
'mailadmin',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"bootstrap4",
"mailadmin",
)
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',
"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 = 'mailadminsite.urls'
ROOT_URLCONF = "mailadminsite.urls"
WSGI_APPLICATION = 'mailadminsite.wsgi.application'
WSGI_APPLICATION = "mailadminsite.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR.joinpath('db.sqlite3'),
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR.joinpath("db.sqlite3"),
}
}
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.request',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.debug",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.request",
],
},
},
@@ -75,35 +76,35 @@ TEMPLATES = [
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
LANGUAGE_CODE = 'nb'
TIME_ZONE = 'Europe/Oslo'
LANGUAGE_CODE = "nb"
TIME_ZONE = "Europe/Oslo"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR.joinpath('staticfiles')
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR.joinpath("staticfiles")
LOGIN_REDIRECT_URL = '/lists/'
LOGIN_REDIRECT_URL = "/lists/"
# Django Postfix Dovecot
DPD_API_URL = 'http://localhost:8080/api'
DPD_API_URL = "http://localhost:8080/api"
DPD_API_USERNAME = ""
DPD_API_PASSWORD = ""
NEUF_EMAIL_DOMAIN_NAME = 'stry.no'
NEUF_EMAIL_DOMAIN_NAME = "stry.no"
AUTHENTICATION_BACKENDS = (
'mailadmin.backends.LDAPEmailBackend',
'mailadmin.backends.LDAPUsernameBackend',
'django.contrib.auth.backends.ModelBackend',
"mailadmin.backends.LDAPEmailBackend",
"mailadmin.backends.LDAPUsernameBackend",
"django.contrib.auth.backends.ModelBackend",
)
# LDAP server URI and BIND_DN, same as db-settings
AUTH_LDAP_U_SERVER_URI = 'ldap://localhost/'
AUTH_LDAP_U_BIND_DN = 'cn=admin,dc=neuf,dc=no'
AUTH_LDAP_U_BIND_PASSWORD = 'toor'
AUTH_LDAP_U_SERVER_URI = "ldap://localhost/"
AUTH_LDAP_U_BIND_DN = "cn=admin,dc=neuf,dc=no"
AUTH_LDAP_U_BIND_PASSWORD = "toor" # noqa: S105
# Basic user auth
AUTH_LDAP_U_USER_SEARCH = LDAPSearch("ou=People,dc=neuf,dc=no", ldap.SCOPE_ONELEVEL, "(uid=%(user)s)")
@@ -116,7 +117,7 @@ AUTH_LDAP_U_MIRROR_GROUPS = True
AUTH_LDAP_U_USER_FLAGS_BY_GROUP = {
"is_active": "cn=dns-aktiv,ou=Groups,dc=neuf,dc=no",
"is_staff": "cn=edb,ou=Groups,dc=neuf,dc=no",
"is_superuser": "cn=edbadmin,ou=Groups,dc=neuf,dc=no"
"is_superuser": "cn=edbadmin,ou=Groups,dc=neuf,dc=no",
}
# User attribute mappings
AUTH_LDAP_U_USER_ATTR_MAP = {
@@ -125,9 +126,7 @@ AUTH_LDAP_U_USER_ATTR_MAP = {
"email": "mail",
}
# User profile attribute mappings
AUTH_LDAP_U_PROFILE_ATTR_MAP = {
"home_directory": "homeDirectory"
}
AUTH_LDAP_U_PROFILE_ATTR_MAP = {"home_directory": "homeDirectory"}
# Allways update the django user object on authentication.
AUTH_LDAP_U_ALWAYS_UPDATE_USER = True
@@ -146,16 +145,10 @@ AUTH_LDAP_E_USER_ATTR_MAP = AUTH_LDAP_U_USER_ATTR_MAP
AUTH_LDAP_E_PROFILE_ATTR_MAP = AUTH_LDAP_U_PROFILE_ATTR_MAP
AUTH_LDAP_E_ALWAYS_UPDATE_USER = AUTH_LDAP_U_ALWAYS_UPDATE_USER
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
)
}
REST_FRAMEWORK = {"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",)}
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Local settings
try:
from .local_settings import *
except ImportError:
pass
with contextlib.suppress(ImportError):
from .local_settings import * # noqa: F403
+6 -6
View File
@@ -1,18 +1,18 @@
from django.urls import include, path, re_path
from django.contrib import admin
from django.contrib.auth.views import LoginView
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path, re_path
from rest_framework import urls as rest_framework_urls
from mailadmin import urls as mailadmin_urls
from mailadmin.views import logout
urlpatterns = [
path('', include(mailadmin_urls)),
re_path(r'^admin/', admin.site.urls),
path('api-auth/', include(rest_framework_urls, namespace='rest_framework')),
path('accounts/login/', LoginView.as_view(), name='login'),
path('accounts/logout/', logout, name='logout')
path("", include(mailadmin_urls)),
re_path(r"^admin/", admin.site.urls),
path("api-auth/", include(rest_framework_urls, namespace="rest_framework")),
path("accounts/login/", LoginView.as_view(), name="login"),
path("accounts/logout/", logout, name="logout"),
]
urlpatterns += staticfiles_urlpatterns()
+2
View File
@@ -8,7 +8,9 @@ https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mailadminsite.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
+33
View File
@@ -17,3 +17,36 @@ dependencies = [
"requests==2.*",
"sentry-sdk==1.*",
]
[dependency-groups]
dev = [
"pytest>=8.3.5",
"pytest-django>=4.11.1",
"ruff>=0.11.12",
]
[tool.ruff]
line-length = 120
target-version = "py310"
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"D", # pydocstyle: Allow undocumented code
"ANN", # flake8-annotations: Allow untyped code
"TD", "FIX", # Allow TODOs and FIXMEs
"FBT002", # Allow boolean args
"G004", # Allow logging using f-string
"EM101", "EM102", # Allow string in exceptions (prioritice code readability vs exception time readability)
"TRY003", # Allow long exception messages
"E501", # Allow long lines
"COM812", "ISC001", # Avoid conflicting with ruff formatter
"RUF012" # FIXME: Fixy fix
]
[tool.ruff.lint.per-file-ignores]
# Testing code can deviate a bit more
"**/tests.py" = ["S101", "S106", "PLR2004"]
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "mailadminsite.settings"
Generated
+102
View File
@@ -127,6 +127,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "cryptography"
version = "45.0.3"
@@ -295,6 +304,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]]
name = "invoke"
version = "2.2.0"
@@ -323,6 +341,13 @@ dependencies = [
{ name = "sentry-sdk" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-django" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "django", specifier = "==4.2.*" },
@@ -339,6 +364,13 @@ requires-dist = [
{ name = "sentry-sdk", specifier = "==1.*" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=8.3.5" },
{ name = "pytest-django", specifier = ">=4.11.1" },
{ name = "ruff", specifier = ">=0.11.12" },
]
[[package]]
name = "markdown"
version = "3.8"
@@ -357,6 +389,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/29/01/e80141f1cd0459e4c9a5dd309dee135bbae41d6c6c121252fdd853001a8a/mysqlclient-2.2.7-cp313-cp313-win_amd64.whl", hash = "sha256:201a6faa301011dd07bca6b651fe5aaa546d7c9a5426835a06c3172e1056a3c5", size = 208000, upload-time = "2025-01-10T11:56:32.293Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "paramiko"
version = "3.5.1"
@@ -371,6 +412,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/15/f8/c7bd0ef12954a81a1d3cea60a13946bd9a49a0036a5927770c461eade7ae/paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61", size = 227298, upload-time = "2025-02-04T02:37:57.672Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.1"
@@ -421,6 +471,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" },
]
[[package]]
name = "pytest"
version = "8.3.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
]
[[package]]
name = "pytest-django"
version = "4.11.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202, upload-time = "2025-04-03T18:56:09.338Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" },
]
[[package]]
name = "python-ldap"
version = "3.4.4"
@@ -446,6 +523,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
]
[[package]]
name = "ruff"
version = "0.11.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" },
{ url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" },
{ url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" },
{ url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" },
{ url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" },
{ url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" },
{ url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" },
{ url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" },
{ url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" },
{ url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" },
{ url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" },
{ url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" },
{ url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" },
{ url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" },
{ url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" },
{ url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" },
]
[[package]]
name = "sentry-sdk"
version = "1.45.1"