-
{% trans 'Please confirm deletion' %}.
-
{% blocktrans %}This operation is destructive, cannot be undone and may require some minutes.{% endblocktrans %}
-
{% blocktrans %}Are you sure you want to permanently remove all objects ?{% endblocktrans %}
-
-
-
-
-
-{% endblock %}
+
+{% endblock content %}
diff --git a/easyaudit/tests/test_app/admin.py b/easyaudit/tests/test_app/admin.py
deleted file mode 100644
index 13be29d..0000000
--- a/easyaudit/tests/test_app/admin.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.contrib import admin
-
-# Register your models here.
diff --git a/easyaudit/tests/test_app/backup_apps.py b/easyaudit/tests/test_app/backup_apps.py
deleted file mode 100644
index 4e43254..0000000
--- a/easyaudit/tests/test_app/backup_apps.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.apps import AppConfig
-
-
-class TestAppConfig(AppConfig):
- name = 'test_app'
diff --git a/easyaudit/tests/test_app/migrations/0001_initial.py b/easyaudit/tests/test_app/migrations/0001_initial.py
deleted file mode 100644
index 6895e0f..0000000
--- a/easyaudit/tests/test_app/migrations/0001_initial.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.2 on 2017-07-25 15:56
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ]
-
- operations = [
- migrations.CreateModel(
- name='TestForeignKey',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=50)),
- ],
- ),
- migrations.CreateModel(
- name='TestM2M',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=50)),
- ],
- ),
- migrations.CreateModel(
- name='TestModel',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(default=b'test data', max_length=50)),
- ],
- ),
- migrations.AddField(
- model_name='testm2m',
- name='test_m2m',
- field=models.ManyToManyField(to='test_app.TestModel'),
- ),
- migrations.AddField(
- model_name='testforeignkey',
- name='test_fk',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_app.TestModel'),
- ),
- ]
diff --git a/easyaudit/tests/test_app/migrations/0002_auto_20180220_1533.py b/easyaudit/tests/test_app/migrations/0002_auto_20180220_1533.py
deleted file mode 100644
index 52c6742..0000000
--- a/easyaudit/tests/test_app/migrations/0002_auto_20180220_1533.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.8 on 2018-02-20 15:33
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('test_app', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='testmodel',
- name='name',
- field=models.CharField(default='test data', max_length=50),
- ),
- ]
diff --git a/easyaudit/tests/test_app/models.py b/easyaudit/tests/test_app/models.py
deleted file mode 100644
index 519306c..0000000
--- a/easyaudit/tests/test_app/models.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import uuid
-
-from django.db import models
-
-
-class TestModel(models.Model):
- name = models.CharField(max_length=50, default='test data')
-
-
-class TestForeignKey(models.Model):
- name = models.CharField(max_length=50)
- test_fk = models.ForeignKey(TestModel, on_delete=models.CASCADE)
-
-
-class TestM2M(models.Model):
- name = models.CharField(max_length=50)
- test_m2m = models.ManyToManyField(TestModel)
-
-
-class TestUUIDModel(models.Model):
- id = models.UUIDField(
- primary_key=True, unique=True, editable=False, default=uuid.uuid4
- )
- name = models.CharField(max_length=50, default='test data')
-
-
-class TestUUIDForeignKey(models.Model):
- id = models.UUIDField(
- primary_key=True, unique=True, editable=False, default=uuid.uuid4
- )
- name = models.CharField(max_length=50)
- test_fk = models.ForeignKey(TestUUIDModel, on_delete=models.CASCADE)
-
-
-class TestUUIDM2M(models.Model):
- id = models.UUIDField(
- primary_key=True, unique=True, editable=False, default=uuid.uuid4
- )
- name = models.CharField(max_length=50)
- test_m2m = models.ManyToManyField(TestUUIDModel)
-
-
-class TestBigIntModel(models.Model):
- id = models.BigAutoField(primary_key=True)
- name = models.CharField(max_length=50, default='test data')
-
-
-class TestBigIntForeignKey(models.Model):
- id = models.BigAutoField(primary_key=True)
- name = models.CharField(max_length=50)
- test_fk = models.ForeignKey(TestBigIntModel, on_delete=models.CASCADE)
-
-
-class TestBigIntM2M(models.Model):
- id = models.BigAutoField(primary_key=True)
- name = models.CharField(max_length=50)
- test_m2m = models.ManyToManyField(TestBigIntModel)
diff --git a/easyaudit/tests/test_app/tests.py b/easyaudit/tests/test_app/tests.py
deleted file mode 100644
index 4ef76c3..0000000
--- a/easyaudit/tests/test_app/tests.py
+++ /dev/null
@@ -1,288 +0,0 @@
-# -*- coding: utf-8 -*-
-import json
-import re
-from unittest import skip, skipIf, mock
-
-import django
-
-asgi_views_supported = django.VERSION >= (3, 1)
-if asgi_views_supported:
- from asgiref.sync import sync_to_async
-from django.test import TestCase, override_settings, tag, TransactionTestCase, SimpleTestCase
-
-from django.urls import reverse, reverse_lazy
-
-from django.contrib.auth.models import User
-from django.contrib.contenttypes.models import ContentType
-import bs4
-from test_app.models import (
- TestModel, TestForeignKey, TestM2M,
- TestBigIntModel, TestBigIntForeignKey, TestBigIntM2M,
- TestUUIDModel, TestUUIDForeignKey, TestUUIDM2M
-)
-from easyaudit.models import CRUDEvent, RequestEvent
-from easyaudit.middleware.easyaudit import set_current_user, clear_request
-
-
-class WithUserInfoMixin:
- def setUp(self):
- self.username = 'joe@example.com'
- self.email = 'joe@example.com'
- self.password = 'password'
-
-
-class TestDjangoCompat(SimpleTestCase):
-
- def test_model_state(self):
- """Ensures models have the internal `_state` object."""
- inst = TestModel()
- self.assertTrue(hasattr(inst, '_state'))
-
-
-@override_settings(TEST=True)
-class TestAuditModels(TestCase):
- Model = TestModel
- FKModel = TestForeignKey
- M2MModel = TestM2M
-
- def test_create_model(self):
- obj = self.Model.objects.create()
- crud_event_qs = CRUDEvent.objects.filter(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj))
- self.assertEqual(1, crud_event_qs.count())
- crud_event = crud_event_qs[0]
- data = json.loads(crud_event.object_json_repr)[0]
- self.assertEqual(data['fields']['name'], obj.name)
-
- def test_fk_model(self):
- obj = self.Model.objects.create()
- obj_fk = self.FKModel(name='test', test_fk=obj)
- obj_fk.save()
- crud_event = CRUDEvent.objects.filter(object_id=obj_fk.id, content_type=ContentType.objects.get_for_model(obj_fk))[0]
- data = json.loads(crud_event.object_json_repr)[0]
- self.assertEqual(str(data['fields']['test_fk']), str(obj.id))
-
- def test_m2m_model(self):
- obj = self.Model.objects.create()
- obj_m2m = self.M2MModel(name='test')
- obj_m2m.save()
- obj_m2m.test_m2m.add(obj)
- crud_event = CRUDEvent.objects.filter(object_id=obj_m2m.id, content_type=ContentType.objects.get_for_model(obj_m2m))[0]
- data = json.loads(crud_event.object_json_repr)[0]
- self.assertEqual([str(d) for d in data['fields']['test_m2m']], [str(obj.id)])
-
- def test_m2m_clear(self):
- obj = self.Model.objects.create()
- obj_m2m = self.M2MModel(name='test')
- obj_m2m.save()
- obj_m2m.test_m2m.add(obj)
- obj_m2m.test_m2m.clear()
- crud_event = CRUDEvent.objects.filter(object_id=obj_m2m.id, content_type=ContentType.objects.get_for_model(obj_m2m))[0]
- data = json.loads(crud_event.object_json_repr)[0]
- self.assertEqual([str(d) for d in data['fields']['test_m2m']], [])
-
- @override_settings(DJANGO_EASY_AUDIT_CRUD_EVENT_NO_CHANGED_FIELDS_SKIP=True)
- def test_update_skip_no_changed_fields(self):
- obj = self.Model.objects.create()
- crud_event_qs = CRUDEvent.objects.filter(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj))
- self.assertEqual(1, crud_event_qs.count())
- obj.name = 'changed name'
- obj.save()
- self.assertEqual(2, crud_event_qs.count())
- last_change = crud_event_qs.first()
- self.assertIn('name', last_change.changed_fields)
-
- def test_update(self):
- obj = self.Model.objects.create()
- crud_event_qs = CRUDEvent.objects.filter(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj))
- self.assertEqual(1, crud_event_qs.count())
- obj.name = 'changed name'
- obj.save()
- self.assertEqual(2, crud_event_qs.count())
- last_change = crud_event_qs.first()
- self.assertIn('name', last_change.changed_fields)
-
- @override_settings(DJANGO_EASY_AUDIT_CRUD_EVENT_NO_CHANGED_FIELDS_SKIP=True)
- def test_fake_update_skip_no_changed_fields(self):
- obj = self.Model.objects.create()
- crud_event_qs = CRUDEvent.objects.filter(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj))
- obj.save()
- self.assertEqual(1, crud_event_qs.count())
-
- def test_fake_update(self):
- obj = self.Model.objects.create()
- crud_event_qs = CRUDEvent.objects.filter(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj))
- obj.save()
- self.assertEqual(2, crud_event_qs.count())
-
- def test_delete(self):
- obj = self.Model.objects.create()
- crud_event_qs = CRUDEvent.objects.filter(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj))
- self.assertEqual(1, crud_event_qs.count())
-
- obj_id = obj.pk
- obj.delete()
- crud_event_qs = CRUDEvent.objects.filter(object_id=obj_id, content_type=ContentType.objects.get_for_model(obj))
- self.assertEqual(2, crud_event_qs.count())
-
- @mock.patch('easyaudit.signals.model_signals.audit_logger')
- def test_propagate_exceptions(self, mocked_audit_logger):
- mocked_audit_logger.crud.side_effect = ValueError
-
- # By default, it should catch exceptions
- _ = self.Model.objects.create()
-
- with override_settings(DJANGO_EASY_AUDIT_PROPAGATE_EXCEPTIONS=True):
- with self.assertRaises(ValueError):
- _ = self.Model.objects.create()
-
-
-class TestAuditUUIDModels(TestAuditModels):
- Model = TestUUIDModel
- FKModel = TestUUIDForeignKey
- M2MModel = TestUUIDM2M
-
-
-class TestAuditBigIntModels(TestAuditModels):
- Model = TestBigIntModel
- FKModel = TestBigIntForeignKey
- M2MModel = TestBigIntM2M
-
-
-@override_settings(TEST=True)
-class TestMiddleware(WithUserInfoMixin, TestCase):
-
- def test_middleware_logged_in(self):
- user = User.objects.create_user(self.username, self.email, self.password)
- self.client.login(username=self.username, password=self.password)
- create_obj_url = reverse("test_app:create-obj")
- self.client.post(create_obj_url)
- self.assertEqual(TestModel.objects.count(), 1)
- obj = TestModel.objects.all()[0]
- crud_event = CRUDEvent.objects.filter(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj))[0]
- self.assertEqual(crud_event.user, user)
-
- def test_middleware_not_logged_in(self):
- create_obj_url = reverse("test_app:create-obj")
- self.client.post(create_obj_url)
- self.assertEqual(TestModel.objects.count(), 1)
- obj = TestModel.objects.all()[0]
- crud_event = CRUDEvent.objects.filter(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj))[0]
- self.assertEqual(crud_event.user, None)
-
- def test_manual_set_user(self):
- user = User.objects.create_user(self.username, self.email, self.password)
-
- # set user/request
- set_current_user(user)
- obj = TestModel.objects.create()
- self.assertEqual(obj.id, 1)
- crud_event_qs = CRUDEvent.objects.filter(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj))
- self.assertEqual(crud_event_qs.count(), 1)
- crud_event = crud_event_qs[0]
- self.assertEqual(crud_event.user, user)
-
- # clear request
- clear_request()
- obj = TestModel.objects.create()
- self.assertEqual(obj.id, 2)
- crud_event_qs = CRUDEvent.objects.filter(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj))
- self.assertEqual(crud_event_qs.count(), 1)
- crud_event = crud_event_qs[0]
- self.assertEqual(crud_event.user, None)
-
- @skip("Test may need a rewrite but the library logic has been rolled back.")
- def test_middleware_logged_in_user_in_request(self):
- user = User.objects.create_user(self.username, self.email, self.password)
- self.client.force_login(user)
- create_obj_url = reverse("test_app:create-obj")
- self.client.post(create_obj_url)
- self.assertEqual(TestModel.objects.count(), 1)
- obj = TestModel.objects.all()[0]
- crud_event = CRUDEvent.objects.filter(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj))[0]
- self.assertEqual(crud_event.user, user)
-
-
-@tag("asgi")
-@override_settings(TEST=True)
-@skipIf(not asgi_views_supported, "Testing ASGI is easier with Django 3.1")
-class TestASGIRequestEvent(WithUserInfoMixin, TransactionTestCase):
-
- async def test_login(self):
- user = await sync_to_async(User.objects.create_user)(self.username, self.email, self.password)
- await sync_to_async(self.async_client.login)(username=self.username, password=self.password)
- self.assertEqual((await sync_to_async(RequestEvent.objects.count)()), 0)
- resp = await self.async_client.get(reverse_lazy("test_app:index"))
- self.assertEqual(resp.status_code, 200)
- assert (await sync_to_async(RequestEvent.objects.get)(user=user))
-
- async def test_remote_addr_default(self):
- self.assertEqual((await sync_to_async(RequestEvent.objects.count)()), 0)
- resp = await self.async_client.request(
- method='GET', path=str(reverse_lazy("test_app:index")),
- server=('127.0.0.1', '80'),
- scheme='http',
- headers=[(b'host', b'testserver')],
- query_string='',
- )
- self.assertEqual(resp.status_code, 200)
- r = await sync_to_async(RequestEvent.objects.get)(url=reverse_lazy("test_app:index"))
- i = await sync_to_async(getattr)(r, 'remote_ip')
- self.assertEqual(i, '127.0.0.1')
-
- async def test_remote_addr_another(self):
- self.assertEqual((await sync_to_async(RequestEvent.objects.count)()), 0)
- resp = await self.async_client.request(
- method='GET', path=str(reverse_lazy("test_app:index")),
- server=('127.0.0.1', '80'),
- client=('10.0.0.1', 111),
- scheme='http',
- headers=[(b'host', b'testserver')],
- query_string='',
- )
- self.assertEqual(resp.status_code, 200)
- r = await sync_to_async(RequestEvent.objects.get)(url=reverse_lazy("test_app:index"))
- i = await sync_to_async(getattr)(r, 'remote_ip')
- self.assertEqual(i, '10.0.0.1')
-
-
-@override_settings(TEST=True)
-class TestWSGIRequestEvent(WithUserInfoMixin, TestCase):
-
- def test_login(self):
- user = User.objects.create_user(self.username, self.email, self.password)
- self.client.login(username=self.username, password=self.password)
- self.assertEqual(RequestEvent.objects.count(), 0)
- resp = self.client.get(reverse_lazy("test_app:index"))
- self.assertEqual(resp.status_code, 200)
- assert RequestEvent.objects.get(user=user)
-
-
-@override_settings(TEST=True)
-class TestAuditAdmin(WithUserInfoMixin, TestCase):
-
- def _list_filters(self, content):
- """
- Extract filters from response content;
- example:
-
-
-
Filter
- By method
- ...
- By datetime
- ...
-
-
- returns:
- ['method', 'datetime', ]
- """
- html = str(bs4.BeautifulSoup(content, features="html.parser").find(id="changelist-filter"))
- filters = re.findall('
\s*By\s*(.*?)\s*
', html)
- return filters
-
- def test_request_event_admin_no_users(self):
- User.objects.create_superuser(self.username, self.email, self.password)
- self.client.login(username=self.username, password=self.password)
- response = self.client.get(reverse('admin:easyaudit_requestevent_changelist'))
- self.assertEqual(200, response.status_code)
- filters = self._list_filters(response.content)
diff --git a/easyaudit/tests/test_app/views.py b/easyaudit/tests/test_app/views.py
deleted file mode 100644
index 0195b67..0000000
--- a/easyaudit/tests/test_app/views.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from datetime import datetime
-
-from django.http import HttpResponse
-from test_app.models import TestModel, TestUUIDModel, TestBigIntModel
-
-
-def create_obj(Model):
- return Model.objects.create()
-
-
-def update_obj(Model, pk, name):
- tm = Model.objects.get(pk=pk)
- tm.name = name
- tm.save()
- return tm
-
-
-def create_obj_view(request):
- obj = create_obj(TestModel)
- return HttpResponse(obj.id)
-
-
-def index(request):
- return HttpResponse()
-
-
-def update_obj_view(request):
- name = datetime.now().isoformat()
- return HttpResponse(update_obj(
- TestModel, request.GET['id'], name
- ).id)
-
-
-def create_uuid_obj_view(request):
- return HttpResponse(create_obj(TestUUIDModel).id)
-
-
-def update_uuid_obj_view(request):
- name = datetime.now().isoformat()
- return HttpResponse(update_obj(
- TestUUIDModel, request.GET['id'], name
- ).id)
-
-
-def create_big_obj_view(request):
- return HttpResponse(create_obj(TestBigIntModel).id)
-
-
-def update_big_obj_view(request):
- name = datetime.now().isoformat()
- return HttpResponse(update_obj(
- TestBigIntModel, request.GET['id'], name
- ).id)
diff --git a/easyaudit/tests/test_project/__init__.py b/easyaudit/tests/test_project/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/easyaudit/tests/test_project/settings.py b/easyaudit/tests/test_project/settings.py
deleted file mode 100644
index f4c2549..0000000
--- a/easyaudit/tests/test_project/settings.py
+++ /dev/null
@@ -1,126 +0,0 @@
-"""
-Django settings for test_project project.
-
-Generated by 'django-admin startproject' using Django 1.11.2.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/1.11/topics/settings/
-
-For the full list of settings and their values, see
-https://docs.djangoproject.com/en/1.11/ref/settings/
-"""
-
-import os
-
-# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
-BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-
-
-# Quick-start development settings - unsuitable for production
-# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
-
-# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = 'ox!t9_6#yvbpd3y9m$hk0))4k#&@c2^k8sc6mkuslpye8ija0p'
-
-# SECURITY WARNING: don't run with debug turned on in production!
-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',
- 'easyaudit',
- 'test_app',
-]
-
-MIDDLEWARE = [
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
- 'easyaudit.middleware.easyaudit.EasyAuditMiddleware',
-]
-
-ROOT_URLCONF = 'test_project.urls'
-
-TEMPLATES = [
- {
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [],
- '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 = 'test_project.wsgi.application'
-ASGI_APPLICATION = 'test_project.asgi.application'
-
-
-# Database
-# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
-
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
- }
-}
-
-
-# Password validation
-# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
-
-AUTH_PASSWORD_VALIDATORS = [
- {
- 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
- },
- {
- 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
- },
-]
-
-
-# Internationalization
-# https://docs.djangoproject.com/en/1.11/topics/i18n/
-
-LANGUAGE_CODE = 'en-us'
-
-TIME_ZONE = 'UTC'
-
-USE_I18N = True
-
-USE_L10N = True
-
-USE_TZ = True
-
-
-# Static files (CSS, JavaScript, Images)
-# https://docs.djangoproject.com/en/1.11/howto/static-files/
-
-STATIC_URL = '/static/'
-
-DJANGO_EASY_AUDIT_REQUEST_EVENT_LIST_FILTER = ['method', 'datetime', ]
diff --git a/easyaudit/utils.py b/easyaudit/utils.py
index b66b11d..806567f 100644
--- a/easyaudit/utils.py
+++ b/easyaudit/utils.py
@@ -1,5 +1,3 @@
-from __future__ import unicode_literals
-
import datetime as dt
from django.conf import settings
@@ -10,8 +8,8 @@
def get_field_value(obj, field):
- """
- Gets the value of a given model instance field.
+ """Get the value of a given model instance field.
+
:param obj: The model instance.
:type obj: Model
:param field: The field you want to find the value of.
@@ -38,8 +36,8 @@ def get_field_value(obj, field):
def model_delta(old_model, new_model):
- """
- Provides delta/difference between two models
+ """Provide delta/difference between two models.
+
:param old: The old state of the model instance.
:type old: Model
:param new: The new state of the model instance.
@@ -49,15 +47,13 @@ def model_delta(old_model, new_model):
as value.
:rtype: dict
"""
-
delta = {}
fields = new_model._meta.fields
for field in fields:
old_value = get_field_value(old_model, field)
new_value = get_field_value(new_model, field)
if old_value != new_value:
- delta[field.name] = [smart_str(old_value),
- smart_str(new_value)]
+ delta[field.name] = [smart_str(old_value), smart_str(new_value)]
if len(delta) == 0:
delta = None
@@ -66,8 +62,8 @@ def model_delta(old_model, new_model):
def get_m2m_field_name(model, instance):
- """
- Finds M2M field name on instance
+ """Find M2M field name on instance.
+
Called from m2m_changed signal
:param model: m2m_changed signal model.
:type model: Model
@@ -79,11 +75,12 @@ def get_m2m_field_name(model, instance):
for x in model._meta.related_objects:
if x.related_model().__class__ == instance.__class__:
return x.remote_field.name
+ return None
def should_propagate_exceptions():
- """
- Should Django Easy Audit propagate signal handler exceptions.
+ """Whether Django Easy Audit should propagate signal handler exceptions.
+
:rtype: bool
"""
- return getattr(settings, 'DJANGO_EASY_AUDIT_PROPAGATE_EXCEPTIONS', False)
+ return getattr(settings, "DJANGO_EASY_AUDIT_PROPAGATE_EXCEPTIONS", False)
diff --git a/easyaudit/views.py b/easyaudit/views.py
deleted file mode 100644
index 91ea44a..0000000
--- a/easyaudit/views.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.shortcuts import render
-
-# Create your views here.
diff --git a/easyaudit/tests/manage.py b/manage.py
similarity index 69%
rename from easyaudit/tests/manage.py
rename to manage.py
index 0fc36a3..dc935d6 100755
--- a/easyaudit/tests/manage.py
+++ b/manage.py
@@ -3,7 +3,7 @@
import sys
if __name__ == "__main__":
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
from django.core.management import execute_from_command_line
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..f19928b
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,823 @@
+# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
+
+[[package]]
+name = "asgiref"
+version = "3.7.2"
+description = "ASGI specs, helper code, and adapters"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"},
+ {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""}
+
+[package.extras]
+tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
+
+[[package]]
+name = "backports-zoneinfo"
+version = "0.2.1"
+description = "Backport of the standard library zoneinfo module"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"},
+ {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"},
+]
+
+[package.extras]
+tzdata = ["tzdata"]
+
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+description = "Validate configuration and produce human readable error messages."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
+ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
+]
+
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+ {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "coverage"
+version = "7.4.4"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"},
+ {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"},
+ {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"},
+ {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"},
+ {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"},
+ {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"},
+ {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"},
+ {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"},
+ {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"},
+ {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"},
+ {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"},
+ {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"},
+ {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"},
+ {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"},
+ {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"},
+ {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"},
+ {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"},
+ {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"},
+ {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"},
+ {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"},
+ {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"},
+ {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"},
+ {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"},
+ {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"},
+ {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"},
+ {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"},
+ {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"},
+ {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"},
+ {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"},
+ {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"},
+ {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"},
+ {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"},
+ {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"},
+ {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"},
+ {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"},
+ {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"},
+ {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"},
+ {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"},
+ {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"},
+ {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"},
+ {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"},
+ {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"},
+ {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"},
+ {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"},
+ {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"},
+ {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"},
+ {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"},
+ {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"},
+ {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"},
+ {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"},
+ {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"},
+ {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"},
+]
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "cssbeautifier"
+version = "1.15.1"
+description = "CSS unobfuscator and beautifier."
+optional = false
+python-versions = "*"
+files = [
+ {file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"},
+]
+
+[package.dependencies]
+editorconfig = ">=0.12.2"
+jsbeautifier = "*"
+six = ">=1.13.0"
+
+[[package]]
+name = "distlib"
+version = "0.3.8"
+description = "Distribution utilities"
+optional = false
+python-versions = "*"
+files = [
+ {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
+ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
+]
+
+[[package]]
+name = "django"
+version = "4.2.11"
+description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "Django-4.2.11-py3-none-any.whl", hash = "sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3"},
+ {file = "Django-4.2.11.tar.gz", hash = "sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4"},
+]
+
+[package.dependencies]
+asgiref = ">=3.6.0,<4"
+"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""}
+sqlparse = ">=0.3.1"
+tzdata = {version = "*", markers = "sys_platform == \"win32\""}
+
+[package.extras]
+argon2 = ["argon2-cffi (>=19.1.0)"]
+bcrypt = ["bcrypt"]
+
+[[package]]
+name = "djlint"
+version = "1.34.1"
+description = "HTML Template Linter and Formatter"
+optional = false
+python-versions = ">=3.8.0,<4.0.0"
+files = [
+ {file = "djlint-1.34.1-py3-none-any.whl", hash = "sha256:96ff1c464fb6f061130ebc88663a2ea524d7ec51f4b56221a2b3f0320a3cfce8"},
+ {file = "djlint-1.34.1.tar.gz", hash = "sha256:db93fa008d19eaadb0454edf1704931d14469d48508daba2df9941111f408346"},
+]
+
+[package.dependencies]
+click = ">=8.0.1,<9.0.0"
+colorama = ">=0.4.4,<0.5.0"
+cssbeautifier = ">=1.14.4,<2.0.0"
+html-tag-names = ">=0.1.2,<0.2.0"
+html-void-elements = ">=0.1.0,<0.2.0"
+jsbeautifier = ">=1.14.4,<2.0.0"
+json5 = ">=0.9.11,<0.10.0"
+pathspec = ">=0.12.0,<0.13.0"
+PyYAML = ">=6.0,<7.0"
+regex = ">=2023.0.0,<2024.0.0"
+tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
+tqdm = ">=4.62.2,<5.0.0"
+
+[[package]]
+name = "editorconfig"
+version = "0.12.4"
+description = "EditorConfig File Locator and Interpreter for Python"
+optional = false
+python-versions = "*"
+files = [
+ {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.0"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
+ {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "filelock"
+version = "3.13.1"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
+ {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
+typing = ["typing-extensions (>=4.8)"]
+
+[[package]]
+name = "html-tag-names"
+version = "0.1.2"
+description = "List of known HTML tag names"
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+ {file = "html-tag-names-0.1.2.tar.gz", hash = "sha256:04924aca48770f36b5a41c27e4d917062507be05118acb0ba869c97389084297"},
+ {file = "html_tag_names-0.1.2-py3-none-any.whl", hash = "sha256:eeb69ef21078486b615241f0393a72b41352c5219ee648e7c61f5632d26f0420"},
+]
+
+[[package]]
+name = "html-void-elements"
+version = "0.1.0"
+description = "List of HTML void tag names."
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+ {file = "html-void-elements-0.1.0.tar.gz", hash = "sha256:931b88f84cd606fee0b582c28fcd00e41d7149421fb673e1e1abd2f0c4f231f0"},
+ {file = "html_void_elements-0.1.0-py3-none-any.whl", hash = "sha256:784cf39db03cdeb017320d9301009f8f3480f9d7b254d0974272e80e0cb5e0d2"},
+]
+
+[[package]]
+name = "identify"
+version = "2.5.35"
+description = "File identification library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"},
+ {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"},
+]
+
+[package.extras]
+license = ["ukkonen"]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "jsbeautifier"
+version = "1.15.1"
+description = "JavaScript unobfuscator and beautifier."
+optional = false
+python-versions = "*"
+files = [
+ {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"},
+]
+
+[package.dependencies]
+editorconfig = ">=0.12.2"
+six = ">=1.13.0"
+
+[[package]]
+name = "json5"
+version = "0.9.24"
+description = "A Python implementation of the JSON5 data format."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "json5-0.9.24-py3-none-any.whl", hash = "sha256:4ca101fd5c7cb47960c055ef8f4d0e31e15a7c6c48c3b6f1473fc83b6c462a13"},
+ {file = "json5-0.9.24.tar.gz", hash = "sha256:0c638399421da959a20952782800e5c1a78c14e08e1dc9738fa10d8ec14d58c8"},
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.8.0"
+description = "Node.js virtual environment builder"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+files = [
+ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
+ {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
+]
+
+[package.dependencies]
+setuptools = "*"
+
+[[package]]
+name = "packaging"
+version = "24.0"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
+ {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.2.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"},
+ {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
+
+[[package]]
+name = "pluggy"
+version = "1.4.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
+ {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pre-commit"
+version = "3.5.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"},
+ {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
+[[package]]
+name = "pre-commit"
+version = "3.6.2"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"},
+ {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
+[[package]]
+name = "pytest"
+version = "7.4.4"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
+ {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.23.5.post1"
+description = "Pytest support for asyncio"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pytest-asyncio-0.23.5.post1.tar.gz", hash = "sha256:b9a8806bea78c21276bc34321bbf234ba1b2ea5b30d9f0ce0f2dea45e4685813"},
+ {file = "pytest_asyncio-0.23.5.post1-py3-none-any.whl", hash = "sha256:30f54d27774e79ac409778889880242b0403d09cabd65b727ce90fe92dd5d80e"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0,<9"
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
+
+[[package]]
+name = "pytest-cov"
+version = "4.1.0"
+description = "Pytest plugin for measuring coverage."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
+ {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
+]
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
+[[package]]
+name = "pytest-django"
+version = "4.8.0"
+description = "A Django plugin for pytest."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"},
+ {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0"
+
+[package.extras]
+docs = ["sphinx", "sphinx-rtd-theme"]
+testing = ["Django", "django-configurations (>=2.0)"]
+
+[[package]]
+name = "pytest-ruff"
+version = "0.2.1"
+description = "pytest plugin to check ruff requirements."
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+ {file = "pytest_ruff-0.2.1-py3-none-any.whl", hash = "sha256:f586bbd7978cb5782b673c8e55fa069d83430139931b918bd72232ba3f71eb67"},
+ {file = "pytest_ruff-0.2.1.tar.gz", hash = "sha256:078ad696bfa347b466991ed4f9cc5ec807f5a171d7f06091660d8f16ba03a5dc"},
+]
+
+[package.dependencies]
+ruff = ">=0.0.242"
+
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+ {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
+[[package]]
+name = "regex"
+version = "2023.12.25"
+description = "Alternative regular expression module, to replace re."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"},
+ {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"},
+ {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"},
+ {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"},
+ {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"},
+ {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"},
+ {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"},
+ {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"},
+ {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"},
+ {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"},
+ {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"},
+ {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"},
+ {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"},
+ {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"},
+ {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"},
+ {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"},
+ {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"},
+ {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"},
+ {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"},
+ {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"},
+ {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"},
+ {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"},
+ {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"},
+ {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"},
+ {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"},
+ {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"},
+ {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"},
+ {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"},
+ {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"},
+ {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"},
+ {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"},
+ {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"},
+ {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"},
+ {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"},
+ {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"},
+ {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"},
+ {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"},
+ {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"},
+ {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"},
+ {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"},
+ {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"},
+ {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"},
+ {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"},
+ {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"},
+ {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"},
+ {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"},
+ {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"},
+ {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"},
+ {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"},
+ {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"},
+ {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"},
+ {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"},
+ {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"},
+ {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"},
+ {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"},
+ {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"},
+ {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"},
+ {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"},
+ {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"},
+ {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"},
+ {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"},
+ {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"},
+ {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"},
+ {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"},
+ {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"},
+ {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"},
+ {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"},
+ {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"},
+ {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"},
+ {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"},
+ {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"},
+ {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"},
+ {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"},
+ {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"},
+ {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"},
+ {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"},
+ {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"},
+ {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"},
+ {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"},
+ {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"},
+ {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"},
+ {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"},
+ {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"},
+ {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"},
+ {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"},
+ {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"},
+ {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"},
+ {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"},
+ {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"},
+ {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"},
+ {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"},
+ {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"},
+ {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"},
+]
+
+[[package]]
+name = "ruff"
+version = "0.1.15"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"},
+ {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"},
+ {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"},
+ {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"},
+ {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"},
+ {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"},
+]
+
+[[package]]
+name = "setuptools"
+version = "69.2.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"},
+ {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+
+[[package]]
+name = "sqlparse"
+version = "0.4.4"
+description = "A non-validating SQL parser."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"},
+ {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"},
+]
+
+[package.extras]
+dev = ["build", "flake8"]
+doc = ["sphinx"]
+test = ["pytest", "pytest-cov"]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "tqdm"
+version = "4.66.2"
+description = "Fast, Extensible Progress Meter"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
+ {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"]
+notebook = ["ipywidgets (>=6)"]
+slack = ["slack-sdk"]
+telegram = ["requests"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.10.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"},
+ {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"},
+]
+
+[[package]]
+name = "tzdata"
+version = "2024.1"
+description = "Provider of IANA time zone data"
+optional = false
+python-versions = ">=2"
+files = [
+ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
+ {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.25.1"
+description = "Virtual Python Environment builder"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"},
+ {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+platformdirs = ">=3.9.1,<5"
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.8"
+content-hash = "7983f8fe6297b702a07fc4a9cf119a89cafca32b93b2ea2c497342be3011a120"
diff --git a/poetry.toml b/poetry.toml
new file mode 100644
index 0000000..ab1033b
--- /dev/null
+++ b/poetry.toml
@@ -0,0 +1,2 @@
+[virtualenvs]
+in-project = true
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..827d8f3
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,109 @@
+[tool.poetry]
+name = "django-easy-audit"
+version = "1.3.6-a2"
+description = "Yet another Django audit log app, hopefully the simplest one."
+license = "GPL3"
+authors = ["Natán Calzolari
"]
+readme = "README.md"
+homepage = "https://github.com/soynatan/django-easy-audit"
+repository = "https://github.com/soynatan/django-easy-audit"
+documentation = "https://github.com/soynatan/django-easy-audit/wiki"
+classifiers = [
+ "Environment :: Plugins",
+ "Framework :: Django :: 4.2",
+ "Framework :: Django :: 5.0",
+ "Framework :: Django",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+]
+packages = [{include = "easyaudit"}]
+
+[tool.poetry.dependencies]
+python = "^3.8"
+django = "^4.2"
+
+[tool.poetry.group.dev.dependencies]
+djlint = "^1.34.1"
+pre-commit = [
+ {version = "~3.5", python = "<3.9"},
+ {version = "^3.5", python = ">=3.9"},
+]
+ruff = "^0.1.11"
+
+[tool.poetry.group.test.dependencies]
+pytest = "^7.4.4"
+pytest-asyncio = "^0.23.3"
+pytest-cov = "^4.1.0"
+pytest-django = "^4.7.0"
+pytest-ruff = "^0.2.1"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+addopts = [
+ "--ds=tests.settings", # Forces pytest-django to use test settings
+ "--ruff",
+ "--ruff-format",
+ "--ignore-glob='*/models.py'",
+]
+
+[tool.ruff]
+extend-exclude = ["migrations"]
+ignore = [
+ "D1", # Missing docstrings
+ "D203", # Docstrings on class definitions not preceded by a blank line
+ "D213", # Multi-line docstring summary should start at the second line
+ "D407", # Dashed underline after doc section (not compatible with google style)
+ "DJ008", # Model does not define `__str__` method
+ "RUF012", # Mutable default values in class attributes
+]
+line-length = 92
+select = [ # https://docs.astral.sh/ruff/rules
+ "F", # pyflakes
+ "E", # pycodestyle
+ "W", # pycodestyle
+ "C90", # mccabe
+ "I", # isort
+ "N", # pep8-naming
+ "D", # pydocstyle
+ "UP", # pyupgrade
+ "B", # flake8-bugbear
+ "S", # flake8-bandit
+ "A", # flake8-builtins
+ "C4", # flake8-comprehensions
+ "DTZ", # flake8-datetimez
+ "DJ", # flake8-django
+ "ISC", # flake8-implicit-str-concat
+ "EXE", # flake8-executable
+ "PT", # flake8-pytest-style
+ "Q", # flake8-quotes
+ "RET", # flake8-return
+ "TCH", # flake8-type-checking
+ "SIM", # flake8-simplify
+ "T20", # flake8-print
+ "TID", # flake8-tidy-imports
+ "ERA", # eradicate
+ "PL", # pylint
+ "RUF", # ruff
+]
+show-fixes = true
+target-version = "py38"
+
+[tool.ruff.lint.flake8-pytest-style]
+fixture-parentheses = false
+mark-parentheses = false
+
+[tool.ruff.per-file-ignores]
+"**/test_*.py" = ["PLR2004", "S101", "S106"]
diff --git a/setup.py b/setup.py
deleted file mode 100644
index a32a505..0000000
--- a/setup.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# -*- encoding: utf-8 -*-
-import os
-from setuptools import find_packages, setup
-
-with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
- README = readme.read()
-
-# allow setup.py to be run from any path
-os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
-
-setup(
- name='django-easy-audit',
- version='1.3.6',
- packages=find_packages(),
- include_package_data=True,
- install_requires=[
- "beautifulsoup4",
- "django>=4.2"
- ],
- python_requires=">=3.8",
- license='GPL3',
- description='Yet another Django audit log app, hopefully the simplest one.',
- long_description=README,
- url='https://github.com/soynatan/django-easy-audit',
- author='Natán Calzolari',
- author_email='natancalzolari@gmail.com',
- classifiers=[
- 'Environment :: Plugins',
- 'Framework :: Django',
- "Framework :: Django :: 4.2",
- "Framework :: Django :: 5.0",
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3 :: Only',
- 'Programming Language :: Python :: 3.8',
- 'Programming Language :: Python :: 3.9',
- 'Programming Language :: Python :: 3.10',
- 'Programming Language :: Python :: 3.11',
- 'Programming Language :: Python :: 3.12',
- 'Topic :: Software Development :: Libraries :: Python Modules',
- ],
-)
diff --git a/easyaudit/tests/test_project/asgi.py b/tests/asgi.py
similarity index 71%
rename from easyaudit/tests/test_project/asgi.py
rename to tests/asgi.py
index 13c5888..d681775 100644
--- a/easyaudit/tests/test_project/asgi.py
+++ b/tests/asgi.py
@@ -1,5 +1,4 @@
-"""
-ASGI config for test_projject project.
+"""ASGI config for test_projject project.
It exposes the ASGI callable as a module-level variable named ``application``.
@@ -11,6 +10,6 @@
from django.core.asgi import get_asgi_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
application = get_asgi_application()
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..646ab85
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,53 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import pytest
+from asgiref.sync import sync_to_async
+
+if TYPE_CHECKING:
+ from django.contrib.auth.models import User
+ from pytest_django.fixtures import SettingsWrapper
+
+
+@pytest.fixture(autouse=True)
+def test_settings(settings: SettingsWrapper) -> SettingsWrapper:
+ settings.TEST = True
+
+ return settings
+
+
+@pytest.fixture
+def no_changed_fields_skip(settings: SettingsWrapper) -> SettingsWrapper:
+ settings.DJANGO_EASY_AUDIT_CRUD_EVENT_NO_CHANGED_FIELDS_SKIP = True
+
+ return settings
+
+
+@pytest.fixture
+def username() -> str:
+ return "joe@example.com"
+
+
+@pytest.fixture
+def password() -> str:
+ return "password"
+
+
+@pytest.fixture
+def email() -> str:
+ return "joe@example.com"
+
+
+@pytest.fixture
+def user(django_user_model: User, username: str, password: str, email: str) -> User:
+ return django_user_model.objects.create_user(username, email, password)
+
+
+@pytest.fixture
+async def async_user(
+ django_user_model: User, username: str, email: str, password: str
+) -> User:
+ return await sync_to_async(django_user_model.objects.create_user)(
+ username, email, password
+ )
diff --git a/tests/settings.py b/tests/settings.py
new file mode 100644
index 0000000..5343512
--- /dev/null
+++ b/tests/settings.py
@@ -0,0 +1,129 @@
+"""Django settings for tests project.
+
+Generated by 'django-admin startproject' using Django 1.11.2.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.11/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.11/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = Path(__file__).parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "ox!t9_6#yvbpd3y9m$hk0))4k#&@c2^k8sc6mkuslpye8ija0p" # noqa: S105
+
+# SECURITY WARNING: don't run with debug turned on in production!
+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",
+ "easyaudit",
+ "tests.test_app",
+]
+
+MIDDLEWARE = [
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+ "easyaudit.middleware.easyaudit.EasyAuditMiddleware",
+]
+
+ROOT_URLCONF = "tests.urls"
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "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 = "tests.wsgi.application"
+ASGI_APPLICATION = "tests.asgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
+
+DATABASES = {
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": BASE_DIR / "db.sqlite3",
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.11/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+TIME_ZONE = "UTC"
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.11/howto/static-files/
+
+STATIC_URL = "/static/"
+
+DJANGO_EASY_AUDIT_REQUEST_EVENT_LIST_FILTER = [
+ "method",
+ "datetime",
+]
+DJANGO_EASY_AUDIT_PROPAGATE_EXCEPTIONS = True
diff --git a/easyaudit/tests/test_app/__init__.py b/tests/test_app/__init__.py
similarity index 100%
rename from easyaudit/tests/test_app/__init__.py
rename to tests/test_app/__init__.py
diff --git a/tests/test_app/apps.py b/tests/test_app/apps.py
new file mode 100644
index 0000000..f7adde9
--- /dev/null
+++ b/tests/test_app/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class TestAppConfig(AppConfig):
+ name = "tests.test_app"
+ default_auto_field = "django.db.models.AutoField"
diff --git a/easyaudit/tests/test_app/migrations/0003_testbigintforeignkey_testbigintm2m_testbigintmodel_testuuidforeignkey_testuuidm2m_testuuidmodel.py b/tests/test_app/migrations/0001_initial.py
similarity index 57%
rename from easyaudit/tests/test_app/migrations/0003_testbigintforeignkey_testbigintm2m_testbigintmodel_testuuidforeignkey_testuuidm2m_testuuidmodel.py
rename to tests/test_app/migrations/0001_initial.py
index cba9cec..51cd148 100644
--- a/easyaudit/tests/test_app/migrations/0003_testbigintforeignkey_testbigintm2m_testbigintmodel_testuuidforeignkey_testuuidm2m_testuuidmodel.py
+++ b/tests/test_app/migrations/0001_initial.py
@@ -1,61 +1,85 @@
-# Generated by Django 3.0.6 on 2020-05-14 17:28
+# Generated by Django 5.0.1 on 2024-01-05 00:49
-from django.db import migrations, models
import django.db.models.deletion
import uuid
+from django.db import migrations, models
class Migration(migrations.Migration):
+ initial = True
+
dependencies = [
- ('test_app', '0002_auto_20180220_1533'),
]
operations = [
migrations.CreateModel(
- name='TestBigIntModel',
+ name='BigIntModel',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(default='test data', max_length=50)),
],
),
migrations.CreateModel(
- name='TestUUIDModel',
+ name='Model',
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
+ ('id', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(default='test data', max_length=50)),
],
),
migrations.CreateModel(
- name='TestUUIDM2M',
+ name='UUIDModel',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
- ('name', models.CharField(max_length=50)),
- ('test_m2m', models.ManyToManyField(to='test_app.TestUUIDModel')),
+ ('name', models.CharField(default='test data', max_length=50)),
],
),
migrations.CreateModel(
- name='TestUUIDForeignKey',
+ name='BigIntM2MModel',
fields=[
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
+ ('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=50)),
- ('test_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_app.TestUUIDModel')),
+ ('test_m2m', models.ManyToManyField(to='test_app.bigintmodel')),
],
),
migrations.CreateModel(
- name='TestBigIntM2M',
+ name='BigIntForeignKeyModel',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=50)),
- ('test_m2m', models.ManyToManyField(to='test_app.TestBigIntModel')),
+ ('test_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_app.bigintmodel')),
],
),
migrations.CreateModel(
- name='TestBigIntForeignKey',
+ name='M2MModel',
fields=[
- ('id', models.BigAutoField(primary_key=True, serialize=False)),
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50)),
+ ('test_m2m', models.ManyToManyField(to='test_app.model')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ForeignKeyModel',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=50)),
+ ('test_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_app.model')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='UUIDM2MModel',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
+ ('name', models.CharField(max_length=50)),
+ ('test_m2m', models.ManyToManyField(to='test_app.uuidmodel')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='UUIDForeignKeyModel',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=50)),
- ('test_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_app.TestBigIntModel')),
+ ('test_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_app.uuidmodel')),
],
),
]
diff --git a/easyaudit/tests/test_app/migrations/__init__.py b/tests/test_app/migrations/__init__.py
similarity index 100%
rename from easyaudit/tests/test_app/migrations/__init__.py
rename to tests/test_app/migrations/__init__.py
diff --git a/tests/test_app/models.py b/tests/test_app/models.py
new file mode 100644
index 0000000..959c9da
--- /dev/null
+++ b/tests/test_app/models.py
@@ -0,0 +1,53 @@
+# ruff: noqa: A003
+import uuid
+
+from django.db import models
+
+
+class Model(models.Model):
+ id = models.AutoField(primary_key=True)
+ name = models.CharField(max_length=50, default="test data")
+
+
+class ForeignKeyModel(models.Model):
+ name = models.CharField(max_length=50)
+ test_fk = models.ForeignKey(Model, on_delete=models.CASCADE)
+
+
+class M2MModel(models.Model):
+ name = models.CharField(max_length=50)
+ test_m2m = models.ManyToManyField(Model)
+
+
+class UUIDModel(models.Model):
+ id = models.UUIDField(primary_key=True, unique=True, editable=False, default=uuid.uuid4)
+ name = models.CharField(max_length=50, default="test data")
+
+
+class UUIDForeignKeyModel(models.Model):
+ id = models.UUIDField(primary_key=True, unique=True, editable=False, default=uuid.uuid4)
+ name = models.CharField(max_length=50)
+ test_fk = models.ForeignKey(UUIDModel, on_delete=models.CASCADE)
+
+
+class UUIDM2MModel(models.Model):
+ id = models.UUIDField(primary_key=True, unique=True, editable=False, default=uuid.uuid4)
+ name = models.CharField(max_length=50)
+ test_m2m = models.ManyToManyField(UUIDModel)
+
+
+class BigIntModel(models.Model):
+ id = models.BigAutoField(primary_key=True)
+ name = models.CharField(max_length=50, default="test data")
+
+
+class BigIntForeignKeyModel(models.Model):
+ id = models.BigAutoField(primary_key=True)
+ name = models.CharField(max_length=50)
+ test_fk = models.ForeignKey(BigIntModel, on_delete=models.CASCADE)
+
+
+class BigIntM2MModel(models.Model):
+ id = models.BigAutoField(primary_key=True)
+ name = models.CharField(max_length=50)
+ test_m2m = models.ManyToManyField(BigIntModel)
diff --git a/easyaudit/tests/test_app/urls.py b/tests/test_app/urls.py
similarity index 90%
rename from easyaudit/tests/test_app/urls.py
rename to tests/test_app/urls.py
index c1bcb00..d401255 100644
--- a/easyaudit/tests/test_app/urls.py
+++ b/tests/test_app/urls.py
@@ -1,16 +1,15 @@
from django.urls import re_path
-from test_app import views
-app_name = 'test_easyaudit'
+from tests.test_app import views
+
+app_name = "test_easyaudit"
urlpatterns = [
re_path("index", views.index, name="index"),
re_path("create-obj", views.create_obj_view, name="create-obj"),
re_path("update-obj", views.update_obj_view, name="update-obj"),
-
re_path("create-uuid-obj", views.create_uuid_obj_view, name="create-uuid-obj"),
re_path("update-uuid-obj", views.update_uuid_obj_view, name="update-uuid-obj"),
-
re_path("create-big-obj", views.create_big_obj_view, name="create-big-obj"),
re_path("update-big-obj", views.update_big_obj_view, name="update-big-obj"),
]
diff --git a/tests/test_app/views.py b/tests/test_app/views.py
new file mode 100644
index 0000000..c65267a
--- /dev/null
+++ b/tests/test_app/views.py
@@ -0,0 +1,48 @@
+from datetime import datetime, timezone
+
+from django.http import HttpResponse
+
+from tests.test_app.models import BigIntModel, Model, UUIDModel
+
+
+def create_obj(model):
+ return model.objects.create()
+
+
+def update_obj(model, pk, name):
+ tm = model.objects.get(pk=pk)
+ tm.name = name
+ tm.save()
+ return tm
+
+
+def create_obj_view(request):
+ obj = create_obj(Model)
+ return HttpResponse(obj.id)
+
+
+def index(request):
+ return HttpResponse()
+
+
+def update_obj_view(request):
+ name = datetime.now(timezone.utc).isoformat()
+ return HttpResponse(update_obj(Model, request.GET["id"], name).id)
+
+
+def create_uuid_obj_view(request):
+ return HttpResponse(create_obj(UUIDModel).id)
+
+
+def update_uuid_obj_view(request):
+ name = datetime.now(timezone.utc).isoformat()
+ return HttpResponse(update_obj(UUIDModel, request.GET["id"], name).id)
+
+
+def create_big_obj_view(request):
+ return HttpResponse(create_obj(BigIntModel).id)
+
+
+def update_big_obj_view(request):
+ name = datetime.now(timezone.utc).isoformat()
+ return HttpResponse(update_obj(BigIntModel, request.GET["id"], name).id)
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000..b938fd2
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,371 @@
+import json
+
+import pytest
+from asgiref.sync import sync_to_async
+from django.contrib.contenttypes.models import ContentType
+from django.core import management
+from django.urls import reverse
+from django.utils.version import get_version
+from pytest_django.asserts import assertInHTML
+
+from easyaudit.middleware.easyaudit import clear_request, set_current_user
+from easyaudit.models import CRUDEvent, RequestEvent
+from tests.test_app.models import (
+ BigIntForeignKeyModel,
+ BigIntM2MModel,
+ BigIntModel,
+ ForeignKeyModel,
+ M2MModel,
+ Model,
+ UUIDForeignKeyModel,
+ UUIDM2MModel,
+ UUIDModel,
+)
+
+
+@pytest.mark.django_db
+def test_no_migrations(capsys: pytest.CaptureFixture):
+ management.call_command("makemigrations", dry_run=True)
+
+ captured = capsys.readouterr().out
+ assert "No changes detected" in captured
+
+
+def test_no_issues(capsys: pytest.CaptureFixture):
+ management.call_command("check", fail_level="WARNING")
+
+ captured: str = capsys.readouterr().out
+ assert "System check identified no issues" in captured
+
+
+@pytest.mark.parametrize(
+ "model",
+ [
+ BigIntForeignKeyModel,
+ BigIntM2MModel,
+ BigIntModel,
+ ForeignKeyModel,
+ M2MModel,
+ Model,
+ UUIDForeignKeyModel,
+ UUIDM2MModel,
+ UUIDModel,
+ ],
+)
+class TestDjangoCompat:
+ def test_model_state(self, model):
+ """Ensures models have the internal `_state` object."""
+ model_instances = model()
+ assert hasattr(model_instances, "_state")
+
+
+@pytest.mark.django_db
+class TestAuditModels:
+ @pytest.fixture
+ def model(self):
+ return Model
+
+ @pytest.fixture
+ def fk_model(self):
+ return ForeignKeyModel
+
+ @pytest.fixture
+ def m2m_model(self):
+ return M2MModel
+
+ @pytest.fixture
+ def _audit_logger(self, monkeypatch):
+ def _crud(*args, **kwargs):
+ raise ValueError("Test exception")
+
+ monkeypatch.setattr("easyaudit.signals.crud_flows.audit_logger.crud", _crud)
+
+ def test_create_model(self, model):
+ obj = model.objects.create()
+ crud_event_qs = CRUDEvent.objects.filter(
+ object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)
+ )
+ assert crud_event_qs.count() == 1
+
+ crud_event = crud_event_qs.first()
+ data = json.loads(crud_event.object_json_repr)[0]
+ assert data["fields"]["name"] == obj.name
+
+ def test_fk_model(self, model, fk_model):
+ obj = model.objects.create()
+ obj_fk = fk_model(name="test", test_fk=obj)
+ obj_fk.save()
+
+ crud_event = CRUDEvent.objects.filter(
+ object_id=obj_fk.id, content_type=ContentType.objects.get_for_model(obj_fk)
+ ).first()
+ data = json.loads(crud_event.object_json_repr)[0]
+ assert str(data["fields"]["test_fk"]) == str(obj.id)
+
+ def test_m2m_model(self, model, m2m_model):
+ obj = model.objects.create()
+ obj_m2m = m2m_model(name="test")
+ obj_m2m.save()
+ obj_m2m.test_m2m.add(obj)
+
+ crud_event = CRUDEvent.objects.filter(
+ object_id=obj_m2m.id,
+ content_type=ContentType.objects.get_for_model(obj_m2m),
+ ).first()
+ data = json.loads(crud_event.object_json_repr)[0]
+ assert [str(d) for d in data["fields"]["test_m2m"]] == [str(obj.id)]
+
+ def test_m2m_clear(self, model, m2m_model):
+ obj = model.objects.create()
+ obj_m2m = m2m_model(name="test")
+ obj_m2m.save()
+ obj_m2m.test_m2m.add(obj)
+ obj_m2m.test_m2m.clear()
+
+ crud_event = CRUDEvent.objects.filter(
+ object_id=obj_m2m.id,
+ content_type=ContentType.objects.get_for_model(obj_m2m),
+ ).first()
+ data = json.loads(crud_event.object_json_repr)[0]
+ assert [str(d) for d in data["fields"]["test_m2m"]] == []
+
+ @pytest.mark.usefixtures("no_changed_fields_skip")
+ def test_update_skip_no_changed_fields(self, model):
+ obj = model.objects.create()
+ crud_event_qs = CRUDEvent.objects.filter(
+ object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)
+ )
+ assert crud_event_qs.count() == 1
+
+ obj.name = "changed name"
+ obj.save()
+ assert crud_event_qs.count() == 2
+
+ last_change = crud_event_qs.first()
+ assert "name" in last_change.changed_fields
+
+ def test_update(self, model):
+ obj = model.objects.create()
+ crud_event_qs = CRUDEvent.objects.filter(
+ object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)
+ )
+ assert crud_event_qs.count() == 1
+
+ obj.name = "changed name"
+ obj.save()
+ assert crud_event_qs.count() == 2
+
+ last_change = crud_event_qs.first()
+ assert "name" in last_change.changed_fields
+
+ @pytest.mark.usefixtures("no_changed_fields_skip")
+ def test_fake_update_skip_no_changed_fields(self, model):
+ obj = model.objects.create()
+ crud_event_qs = CRUDEvent.objects.filter(
+ object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)
+ )
+ obj.save()
+ assert crud_event_qs.count() == 1
+
+ def test_fake_update(self, model):
+ obj = model.objects.create()
+ crud_event_qs = CRUDEvent.objects.filter(
+ object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)
+ )
+ obj.save()
+ assert crud_event_qs.count() == 2
+
+ def test_delete(self, model):
+ obj = model.objects.create()
+ crud_event_qs = CRUDEvent.objects.filter(
+ object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)
+ )
+ assert crud_event_qs.count() == 1
+
+ obj_id = obj.pk
+ obj.delete()
+ crud_event_qs = CRUDEvent.objects.filter(
+ object_id=obj_id, content_type=ContentType.objects.get_for_model(obj)
+ )
+ assert crud_event_qs.count() == 2
+
+ @pytest.mark.usefixtures("_audit_logger")
+ def test_propagate_exceptions(self, model, settings):
+ with pytest.raises(ValueError, match="Test exception"):
+ model.objects.create()
+
+ settings.DJANGO_EASY_AUDIT_PROPAGATE_EXCEPTIONS = False
+ try:
+ model.objects.create()
+ except ValueError:
+ pytest.fail("Unexpected ValueError")
+
+
+class TestAuditUUIDModels(TestAuditModels):
+ @pytest.fixture
+ def model(self):
+ return UUIDModel
+
+ @pytest.fixture
+ def fk_model(self):
+ return UUIDForeignKeyModel
+
+ @pytest.fixture
+ def m2m_model(self):
+ return UUIDM2MModel
+
+
+class TestAuditBigIntModels(TestAuditModels):
+ @pytest.fixture
+ def model(self):
+ return BigIntModel
+
+ @pytest.fixture
+ def fk_model(self):
+ return BigIntForeignKeyModel
+
+ @pytest.fixture
+ def m2m_model(self):
+ return BigIntM2MModel
+
+
+@pytest.mark.django_db
+class TestMiddleware:
+ def test_middleware_logged_in(self, user, client, username, password):
+ client.login(username=username, password=password)
+ client.post(reverse("test_app:create-obj"))
+ assert Model.objects.count() == 1
+
+ obj = Model.objects.all().first()
+ crud_event = CRUDEvent.objects.filter(
+ object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)
+ ).first()
+ assert crud_event.user == user
+
+ def test_middleware_not_logged_in(self, client):
+ create_obj_url = reverse("test_app:create-obj")
+ client.post(create_obj_url)
+ assert Model.objects.count() == 1
+
+ obj = Model.objects.all().first()
+ crud_event = CRUDEvent.objects.filter(
+ object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)
+ ).first()
+ assert crud_event.user is None
+
+ def test_manual_set_user(self, django_user_model, username, email, password):
+ user = django_user_model.objects.create_user(username, email, password)
+ set_current_user(user)
+ obj = Model.objects.create()
+ assert obj.id == 1
+
+ crud_event_qs = CRUDEvent.objects.filter(
+ object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)
+ )
+ assert crud_event_qs.count() == 1
+
+ crud_event = crud_event_qs.first()
+ assert crud_event.user == user
+
+ clear_request()
+ obj = Model.objects.create()
+ assert obj.id == 2
+
+ crud_event_qs = CRUDEvent.objects.filter(
+ object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)
+ )
+ assert crud_event_qs.count() == 1
+
+ crud_event = crud_event_qs.first()
+ assert crud_event.user is None
+
+ def test_middleware_logged_in_user_in_request(self, user, client):
+ client.force_login(user)
+ create_obj_url = reverse("test_app:create-obj")
+ client.post(create_obj_url)
+ assert Model.objects.count() == 1
+
+ obj = Model.objects.first()
+ crud_event = CRUDEvent.objects.filter(
+ object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)
+ ).first()
+ assert crud_event.user == user
+
+
+@pytest.mark.asyncio
+@pytest.mark.django_db(transaction=True)
+class TestASGIRequestEvent:
+ async def test_login(self, async_user, async_client, username, password):
+ await sync_to_async(async_client.login)(username=username, password=password)
+ assert await sync_to_async(RequestEvent.objects.count)() == 0
+
+ resp = await async_client.get(reverse("test_app:index"))
+ assert resp.status_code == 200
+
+ qs = await sync_to_async(RequestEvent.objects.filter)(user=async_user)
+ assert await sync_to_async(qs.exists)()
+
+ async def test_remote_addr_default(self, async_client):
+ assert await sync_to_async(RequestEvent.objects.count)() == 0
+
+ resp = await async_client.request(
+ method="GET",
+ path=str(reverse("test_app:index")),
+ server=("127.0.0.1", "80"),
+ scheme="http",
+ headers=[(b"host", b"testserver")],
+ query_string="",
+ )
+ assert resp.status_code == 200
+
+ event = await sync_to_async(RequestEvent.objects.get)(url=reverse("test_app:index"))
+ assert event.remote_ip == "127.0.0.1"
+
+ async def test_remote_addr_another(self, async_client):
+ assert await sync_to_async(RequestEvent.objects.count)() == 0
+
+ resp = await async_client.request(
+ method="GET",
+ path=str(reverse("test_app:index")),
+ server=("127.0.0.1", "80"),
+ client=("10.0.0.1", 111),
+ scheme="http",
+ headers=[(b"host", b"testserver")],
+ query_string="",
+ )
+ assert resp.status_code == 200
+
+ event = await sync_to_async(RequestEvent.objects.get)(url=reverse("test_app:index"))
+ assert event.remote_ip == "10.0.0.1"
+
+
+@pytest.mark.django_db
+class TestWSGIRequestEvent:
+ def test_login(self, user, client, username, password):
+ client.login(username=username, password=password)
+ assert RequestEvent.objects.count() == 0
+
+ resp = client.get(reverse("test_app:index"))
+ assert resp.status_code == 200
+
+ assert RequestEvent.objects.get(user=user)
+
+
+@pytest.mark.django_db
+class TestAuditAdmin:
+ @pytest.fixture
+ def tag_name(self):
+ return "summary" if get_version() >= "4.1" else "h3"
+
+ def test_request_event_admin_no_users(self, admin_client, settings, tag_name):
+ response = admin_client.get(reverse("admin:easyaudit_requestevent_changelist"))
+ assert response.status_code == 200
+
+ decoded_content = response.content.decode()
+ for f in settings.DJANGO_EASY_AUDIT_REQUEST_EVENT_LIST_FILTER:
+ assertInHTML(
+ f"<{tag_name}>"
+ f"By {RequestEvent._meta.get_field(f).verbose_name}"
+ f"{tag_name}>",
+ decoded_content,
+ )
diff --git a/easyaudit/tests/test_project/urls.py b/tests/urls.py
similarity index 80%
rename from easyaudit/tests/test_project/urls.py
rename to tests/urls.py
index 6a34c4b..01b85bb 100644
--- a/easyaudit/tests/test_project/urls.py
+++ b/tests/urls.py
@@ -1,8 +1,10 @@
-"""test_project URL Configuration
+"""tests URL Configuration.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.11/topics/http/urls/
-Examples:
+
+Examples
+--------
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
@@ -12,11 +14,12 @@
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+
"""
-from django.urls import include, re_path
from django.contrib import admin
+from django.urls import include, re_path
urlpatterns = [
- re_path(r'^admin/', admin.site.urls),
- re_path(r'^test_app/', include('test_app.urls', namespace="test_app")),
+ re_path(r"^admin/", admin.site.urls),
+ re_path(r"^test_app/", include("tests.test_app.urls", namespace="test_app")),
]
diff --git a/easyaudit/tests/test_project/wsgi.py b/tests/wsgi.py
similarity index 71%
rename from easyaudit/tests/test_project/wsgi.py
rename to tests/wsgi.py
index 4c57701..3fd028f 100644
--- a/easyaudit/tests/test_project/wsgi.py
+++ b/tests/wsgi.py
@@ -1,5 +1,4 @@
-"""
-WSGI config for test_project project.
+"""WSGI config for tests project.
It exposes the WSGI callable as a module-level variable named ``application``.
@@ -11,6 +10,6 @@
from django.core.wsgi import get_wsgi_application
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
application = get_wsgi_application()